Code Monkey home page Code Monkey logo

exploring-es2016-es2017's People

Contributors

chenxiaochun avatar

Stargazers

 avatar

Watchers

 avatar  avatar

exploring-es2016-es2017's Issues

字符串操作方法:padStart() 和 padEnd()

综述

在 ECMAScript2017 中添加了两个新的字符串方法:

'x'.padStart(5, 'ab')
"ababx"
'x'.padEnd(5, 'ab')
"xabab"

两个方法的使用场景:

  • 用等宽字体显示表格式的数据
  • 给一个文件名或者 URL 添加顺序号,比如:file001.txt
  • 对 console 进行对齐输出
  • 打印拥有固定位数的十六进制数或者二进制数

String.prototype.padStart(maxLength, fillString=' ')

此方法的功能是使用 fillString 给被处理字符串添加前缀,直到被处理字符串的长度正好等于 maxLength 为止。

'x'.padStart(5, 'ab')
"ababx"

如有必要,为了长度能恰好的等于 maxLength,可能也只截取应用 fillString 的一部分字符片段。

'x'.padStart(4, 'ab')
"abax"

如果被处理字符串的长度大于等于 maxLength,那么原样返回。

'abcd'.padStart(2, '#')
"abcd"

如果 maxLength 和 fillString 的长度是相同的,那么就会截取 fillString 的一部分,然后与被处理字符合成一个长度正好等于 maxLength 的新字符串。

'abc'.padStart(10, '0123456789')
"0123456abc"

如果省略了 fillString,则默认为空格。

'x'.padStart(3)
"  x"

一个padStart()方法的简单实现

String.prototype.padStart = function (maxLength, fillString=' ') {
    let str = String(this);
    if (str.length >= maxLength) {
        return str;
    }

    fillString = String(fillString);
    if (fillString.length === 0) {
        fillString = ' ';
    }

    let fillLen = maxLength - str.length;
    let timesToRepeat = Math.ceil(fillLen / fillString.length);
    let truncatedStringFiller = fillString
        .repeat(timesToRepeat)
        .slice(0, fillLen);
    return truncatedStringFiller + str;
};

String.prototype.padEnd(maxLength, fillString=' ')

padEnd()padStart()的工作方式类似,只不过后者是用来插入前缀字符,前者用来插入后缀字符。

示例1:

'x'.padEnd(5, 'ab')
'xabab'

示例2:

'x'.padEnd(4, 'ab')
'xaba'

示例3:

'abcd'.padEnd(2, '#')
'abcd'

示例4:

'abc'.padEnd(10, '0123456789')
'abc0123456'

示例5:

'x'.padEnd(3)
'x  '

共享内存与原子操作

在ES2107中引入了两个新的对象:SharedArrayBufferatomics

atomics直译为“原子学”。它提供了若干静态方法用于对SharedArrayBuffer进行原子上的操作。

并行与并发

这两个词语的意思非常相近,很容易让人混淆,所以很多地方混用它们好像也不是太大的问题。对于它们也存在很多的定义,但我是这样理解使用它们的:

并行

指的是多个计算任务同时发生,但是在给定的时刻只会执行其中一项任务。它又分成两种模式:

  1. 数据并行,指的是相同的代码在并行上被执行多次,但它是基于相同的数据集在不同的实例上进行操作。例如:分布式计算就是一个数据并行的设计实例。
  2. 任务并行,指的是不同的代码被并行执行。例如:Web worker以及Unix的生成进程模式。

并发

指的是在一个给定的时间周期内执行多个任务,并且没有先后顺序。所以,我们使用的电脑就是并发性质的,它可以在一个 CPU 时钟内执行多个任务,例如:播放音乐、接受键盘指令、刷新屏幕等。

js并行性的发展历史

众所周知,js是运行在一个单线程上。如果一项任务需要异步执行,浏览器通常是开启一个单独的线程来运行,之后再将运行结果以回调函数的方式返回给之前的线程。

之后的Web worker才赋予了浏览器真正意义上的并行任务特性。它们是比较重量级的进程,每一个worker都有自己的全局环境。默认情况下,worker之间不会共享任何资源,后来才逐渐形成了它们之间的通信机制:

  1. 你只能发送和接收字符串。
  2. 结构化克隆机制被引进之后,一些数据的副本才能被发送和接收了,例如:JSON数据、数组、正则表达式、Blob对象、ImageData对象。而且它还能正确地处理对象之间的循环引用。但是,它无法处理error对象、函数对象、DOM节点等。
  3. worker之间传送数据过程中,发送方不能在接收方访问数据时,也同时去访问数据。

再一个应用领域是基于WebGL的图形计算,可以把你的输入数据逐像素的转换成另外一张图像。

第三个是SIMD,它是比较底层的一种应用方式。它能让你在同一时刻同时对若干整数或者浮点数进行运算(例如:加法或者乘法)。

共享数组缓冲区

共享数组缓冲区是并发性抽象中一个比较重要、比较高等的构建模块。它们可以让你在多个worker或者主线程之间共享SharedArrayBuffer的字节数据。它主要有两处好处:

  1. 你可以更快速的在worker之间共享数据
  2. postMessage()相比,协调worker会变得更简单快捷

创建和发送一个共享数组缓存区

创建一个共享数组缓存区和创建一个普通数组缓存区的方式是一样的,都是通过在构造函数里指定缓存区的大小(单位:字节)。使用缓存区用来在worker之间共享数据。如果是你自己本地使用,那就可以再用类型化的数组再包裹一层即可。

// main.js

const worker = new Worker('worker.js');

// To be shared
const sharedBuffer = new SharedArrayBuffer(10 * Int32Array.BYTES_PER_ELEMENT); // 10 elements

// Share sharedBuffer with the worker
worker.postMessage({sharedBuffer}); // clone

// Local only
const sharedArray = new Int32Array(sharedBuffer); // (B)

注意:共享数组缓存区的正确共享方式就是克隆它。但是有的引擎实现的还是旧api,所以可能需要手动转换一下。而且正在转换中的数据,是不可以访问它的。

worker.postMessage({sharedBuffer}, [sharedBuffer]);

接收一个共享数组缓存区

首先把共享数组缓存区提取出来,然后用类型数组包裹一次就可以使用了

// worker.js

self.addEventListener('message', function (event) {
    const {sharedBuffer} = event.data;
    const sharedArray = new Int32Array(sharedBuffer); // (A)

    console.log(sharedArray)
    // ···
});
//输出结果:

Int32Array(10) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

安全访问共享数据的方式:Atomics

幂运算符

幂运算符是 ES7 中的一个新特性。

6 ** 2
36

它与 Math.pow(x, y)的计算结果一样的。

Math.pow(6, 2)
36

你也可以这样使用:

let num = 3;
num **= 2;
console.log(num);
9

函数参数的尾部逗号

现在声明函数的时候,可以在参数尾部添加逗号了。

function foo(
    param1,
    param2,
) {}

同理,调用函数时也可以这样用:

foo(
    'abc',
    'def',
);

在一个对象中使用会被忽略:

let obj = {
    first: 'Jane',
    last: 'Doe',
};

for(o in obj){
    console.log(o)
}
'first'
'last'

同样,在数组中使用也会被忽略:

let arr = [
    'red',
    'green',
    'blue',
];
console.log(arr.length);
3

使用函数尾部参数语法有两个好处:

  1. 维护代码时更简单,当修改的代码是最后一个参数位置时,不需要再增删最后那个逗号。
  2. 增加一个参数,在版本控制系统中查看代码时,会发现仅仅是添加参数的位置有改动记录。
foo(
    'abc',
);

改成:

foo(
    'abc',
    'def',
);

Async函数

综述

现在以下几种async函数的用法都是可行的。

Async函数声明:

async function foo(){}

Async函数表达式:

const foo = async function(){}

Async方法定义:

let obj = {async foo(){}}

Async箭头函数:

const foo = async () => {}

Async函数通常返回的是 Promise:

async function asyncFunc() {
    return 123;
}

asyncFunc()
.then(x => console.log(x));
123
async function asyncFunc() {
    throw new Error('Problem!');
}

asyncFunc()
.catch(err => console.log(err));
Error: Problem!

只有操作符await可以放在 Async 函数里用来处理返回的 Promise 对象结果。所以await的处理结果随着 Promise 的状态不同而不同。

简单处理一个异步结果:

async function asyncFunc() {
    const result = await otherAsyncFunc();
    console.log(result);
}

// 等价于:

function asyncFunc() {
    return otherAsyncFunc()
    .then(result => {
        console.log(result);
    });
}

顺序处理多个异步结果:

async function asyncFunc() {
    const result1 = await otherAsyncFunc1();
    console.log(result1);
    const result2 = await otherAsyncFunc2();
    console.log(result2);
}

// 等价于:

function asyncFunc() {
    return otherAsyncFunc1()
    .then(result1 => {
        console.log(result1);
        return otherAsyncFunc2();
    })
    .then(result2 => {
        console.log(result2);
    });
}

平行处理多个异步结果:

async function asyncFunc() {
    const [result1, result2] = await Promise.all([
        otherAsyncFunc1(),
        otherAsyncFunc2(),
    ]);
    console.log(result1, result2);
}

// 等价于:

function asyncFunc() {
    return Promise.all([
        otherAsyncFunc1(),
        otherAsyncFunc2(),
    ])
    .then([result1, result2] => {
        console.log(result1, result2);
    });
}

处理错误异常:

async function asyncFunc() {
    try {
        await otherAsyncFunc();
    } catch (err) {
        console.error(err);
    }
}

// 等价于:

function asyncFunc() {
    return otherAsyncFunc()
    .catch(err => {
        console.error(err);
    });
}

理解async函数

在我解释async函数之前,我想通过把Promisesgenerator结合起来,用看起来像同步代码的方式去模拟异步方式。

对于处理获取一次性结果的异步函数,Promises是目前最流行的方式。下面是一个使用fetch方法获取文件的示例:

function fetchJson(url) {
    return fetch(url)
    .then(request => request.text())
    .then(text => {
        return JSON.parse(text);
    })
    .catch(error => {
        console.log(`ERROR: ${error.stack}`);
    });
}
fetchJson('http://example.com/some_file.json')
.then(obj => console.log(obj));

co是一个基于Promisesgenerator的实现,也能够让你以书写同步代码的方式来实现上面的示例。

const fetchJson = co.wrap(function* (url) {
    try {
        let request = yield fetch(url);
        let text = yield request.text();
        return JSON.parse(text);
    }
    catch (error) {
        console.log(`ERROR: ${error.stack}`);
    }
});

cogenerator回调函数中每次检测到一个带有yield操作符的方法,就会产生一个Promise对象,co会先暂停回调代码的执行,直到Promise对象的状态发生变化再继续执行。无论Promise的状态为resolved或者rejectedyield都会将相应的结果值返回。

详细说明一下async函数的执行过程:

  1. async函数在开始执行的时候,通常都是返回一个Promise对象
  2. 函数体被执行之后,你可以使用return或者throw直接完成执行过程。也可以使用await暂时完成执行过程,然后根据情况再继续执行
  3. 最终返回一个Promise对象
  4. thencatch中的callback只有在当前所有代码执行完毕之后,才会被执行。从下面的输出结果可以看出函数asyncFunc的返回值等到所有代码包括循环逻辑都执行完毕之后,才最终得以被输出
async function asyncFunc() {
    console.log('asyncFunc()'); // (A)
    return 'abc';
}
asyncFunc().
then(x => console.log(`Resolved: ${x}`)); // (B)
console.log('main');

for(let i=0; i<5; i++){
    console.log(i)
}
asyncFunc()
main
0
1
2
3
4
Resolved: abc

使用returnResolve一个async函数状态,是一种很标准的操作方式。这意味着你可以:

  1. 直接返回一个非Promise对象类型的值,作为Resolve状态的参数值
  2. 返回的Promise对象代表了当前async的函数状态

使用await的若干贴示

使用async函数最常犯的一个错误就是忘记添加await关键字,在下面示例中value被指向了一个Promise对象,但它可能并不是你想要的结果:

async function asyncFunc() {
    const value = otherAsyncFunc(); // missing `await`!
    ···
}

await可以感知到后面跟的异步函数是否返回了结果值,然后它就可以告诉调用者当前的异步函数是否已经执行完毕了。例如在下面的示例中,await可以确保step1()执行完之前,不会招待foo()的剩余逻辑代码:

async function foo() {
    await step1(); // (A)
    ···
}

有时候你仅仅是想触发一个异步函数计算,并不想知道它会何时完成。在下面示例中,我们并不关心写文件的操作何时完成,只要它们是按正确的顺序执行就可以了。最后一行的await只是为了确保关闭写文件的操作能被成功执行即可。

async function asyncFunc() {
    const writer = openFile('someFile.txt');
    writer.write('hello'); // don’t wait
    writer.write('world'); // don’t wait
    await writer.close(); // wait for file to close
}

多个await异步函数是顺序执行的关系,想要它们同时执行就得使用Promise.all()了:

async function foo() {
    const result1 = await asyncFunc1();
    const result2 = await asyncFunc2();
}
async function foo() {
    const [result1, result2] = await Promise.all([
        asyncFunc1(),
        asyncFunc2(),
    ]);
}

async在回调函数中的应用

有一个需要知道的限制是,await操作符只会影响async函数的直接作用域环境。因此,你不能在async函数内的回调函数中使用await,这也会让那些基于callback的方法变得非常难于理解。

Array.prototype.map()

下面的示例是想下载若干url并返回其内容:

async function downloadContent(urls) {
    return urls.map(url => {
        // Wrong syntax!
        const content = await httpGet(url);
        return content;
    });
}

像上面这种在普通的箭头函数中使用await根本无法运行,会抛出语法错误。那我们应该怎么用,像下面这样吗:

async function downloadContent(urls) {
    return urls.map(async (url) => {
        const content = await httpGet(url);
        return content;
    });
}

function httpGet(url){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            if(url.indexOf('a') > -1){
                resolve('a.com');    
            }else{
                resolve('b.com');
            }
        }, 2000)  
    });
}

const contents = downloadContent(['http://a.com', 'http://b.com']);
console.log(contents);
contents.then((url) => {
    console.log(url);
});

输出结果:

2017-09-03 9 59 57

你会发现代码中有两个问题:

  1. 返回值是一个包含两个Promise对象的数组,并不是我们期望的包含resolve返回值的数组
  2. await只能暂停箭头回调函数里的httpGet()map本身的回调函数执行完成之后,并不能影响外层的downloadContent ()也执行完成

我们用Promise.all()来修复这两个问题,把返回的 包含两个Promise对象的数组 转换成 包含两个数组元素的Promise对象,看如下示例:

async function downloadContent(urls) {
    const promiseArray = urls.map(url => httpGet(url));
    return await Promise.all(promiseArray);
}

function httpGet(url){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            if(url.indexOf('a') > -1){
                resolve('a.com');    
            }else{
                resolve('b.com');
            }
        }, 2000)  
    });
}

const contents = downloadContent(['http://a.com', 'http://b.com']);
console.log(contents);
contents.then((url) => {
    console.log(url);
});

输出结果:

2017-09-03 10 14 42

OK,现在输出结果是正确的了。但是这段代码还是有一点点低效的地方需要改进,downloadContent ()函数里首先用await展开了Promise.all()的返回结果,后面又用return包装了一次,其实我们用return直接返回Promise.all()即可:

async function downloadContent(urls) {
    const promiseArray = urls.map(url => httpGet(url));
    return Promise.all(promiseArray);
}

Array.prototype.forEach()

我们这次换成forEach()方法来模拟输出若干文件内容。很显然,下面的示例会抛出语法错误,因为你不能在普通的箭头函数中直接使用await

async function logContent(urls) {
    urls.forEach(url => {
        // Wrong syntax
        const content = await httpGet(url);
        console.log(content);
    });
}

那我把代码修改成如下:

async function logContent(urls) {
    urls.forEach(async url => {
        const content = await httpGet(url);
        console.log(content);
    });
    // Not finished here
}

这次代码倒是运行了,但是httpGet()方法返回resolve状态的操作是异步的,也就是说当forEach()方法已经返回之后,它的callback还并没有执行完成。修复此问题,只需要将代码做一下更改:

async function logContent(urls) {
    for (const url of urls) {
        const content = await httpGet(url);
        console.log(content);
    }
}

这段示例中的httpGet()执行顺序是线性的,每一次的调用必须要等待上一次执行完毕。如果想要改成并行的执行顺序,就得用Promise.all()了:

async function logContent(urls) {
    await Promise.all(urls.map(
        async url => {
            const content = await httpGet(url);
            console.log(content);            
        }));
}

map()方法创建了一个Promise对象数组。我们并不关心这几个Promise对象的履行结果,只要它们履行了即可。也就是loginContent()方法执行完成就可以了。在这个示例中除非把Promise.all()直接返回,否则此函数的结果只会包含若干`undefined。

2017-09-03 4 32 51

使用async函数的若干贴示

async函数的基础就是Promise,所以充分理解下面的示例非常重要。尤其是在那些没有使用Promise机制的老代码中使用async函数时,你可能除了直接使用Promise之外没有别的选择。

下面是一个在XMLHttpRequest中使用Promise的示例:

function httpGet(url, responseType="") {
    return new Promise(
        function (resolve, reject) {
            const request = new XMLHttpRequest();
            request.onload = function () {
                if (this.status === 200) {
                    // Success
                    resolve(this.response);
                } else {
                    // Something went wrong (404 etc.)
                    reject(new Error(this.statusText));
                }
            };
            request.onerror = function () {
                reject(new Error(
                    'XMLHttpRequest Error: '+this.statusText));
            };
            request.open('GET', url);
            xhr.responseType = responseType;
            request.send();
        });
}

XMLHttpRequest的 API 设计都是基于 callback 的。使用async函数就意味着你要在内层回调函数里使用returnthrow返回Promise对象的状态,但这肯定是不可能的。因此,在这情况中使用async的风格就是:

  • 使用Promise直接构建一个异步的基元
  • 通过async函数来使用这些基元

在一个模块或者 script 的顶级作用域中使用await,可以像下面这样:

async function main() {
    console.log(await asyncFunction());
}
main();

或者

(async function () {
    console.log(await asyncFunction());
})();

或者

(async () => {
    console.log(await asyncFunction());
})();

不用太担心那些未处理rejections,以前这种情况可能都是静默失败,不过现在大多数的现代浏览器都会抛出一个未处理的异常:

async function foo() {
    throw new Error('Problem!');
}
foo();

Object.getOwnPropertyDescriptors()

Object.getOwnPropertyDescriptors(obj)的功能返回一个包含 obj 自身所有属性的属性描述符数组。

const obj = {
    [Symbol('foo')]: 123,
    get bar() { return 'abc' },
};
console.log(Object.getOwnPropertyDescriptors(obj));

输出:

image

应用场景

复制若干属性给一个对象

从 ES6 开始,才有了一个用来复制属性的工具方法:Object.assign()。但是,这个方法只能简单的使用 get 和 set 操作去复制那些本身就是关键字的属性。这也意味着它并不能正确地复制那些非默认属性。

const source = {
    set foo(value) {
        console.log(value);
    }
};
console.log(Object.getOwnPropertyDescriptor(source, 'foo'));

image

使用Object.assign()复制 foo 到 target 对象:

const target1 = {};
Object.assign(target1, source);
console.log(Object.getOwnPropertyDescriptor(target1, 'foo'));

image

然后我们把Object.getOwnPropertyDescriptors()Object.defineProperties()组合起来却能得到我们想要的结果。

const target2 = {};
Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source));
console.log(Object.getOwnPropertyDescriptor(target2, 'foo'));

image

对象复制

对象的浅复制也可以使用Object.getOwnPropertyDescriptors()。但是,这次我们用Object.create()来尝试一下,这个方法有两个参数:

  • 第一个参数指定用来返回的对象原型
  • 第二个参数是一个类似于Object.getOwnPropertyDescriptors()方法返回的描述符属性集合
const clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj));

使用对象字面量跨对象访问

使用对象字面量的最好方式就是创建一个对象,比如下面的示例中把内置属性__proto__指向了另一个任意的对象prot

var prot = {
    value: 1
}

const obj = {
    __proto__: prot,
    foo: 123,
};

console.log(obj.value)
//正确的输出了我们所期望的值

1

可惜的是,这种用法只是在浏览器环境中才支持。更通用的做法是使用Object.create()和赋值操作。

var prot = {
    value: 1
}

const obj = Object.create(prot);
obj.foo = 123;

console.log(obj.value)
//依然可以输出正确的值

1

还有一种实现方式就是使用Object.getOwnPropertyDescriptors()

var prot = {
    value: 1
}

const obj = Object.create(
    prot,
    Object.getOwnPropertyDescriptors({
        foo: 123,
    })
);

console.log(obj.value)
//输出了正确的结果 

1

Array.prototype.includes

通过单词意思也能看出来,此方法用来判断元素是否存在于一个数组中,存在返回true,否则返回false

['a', 'b', 'c'].includes('a')
true
['a', 'b', 'c'].includes('d')
false

includes非常类似的一个方法是indexOf,比如下面两种用法实现的作用是一样的。

arr.includes(x)
arr.indexOf(x) >= 0

includes方法还可以查找NaN

[NaN].includes(NaN)
true

indexOf方法却不行。

[NaN].indexOf(NaN)
-1

includes方法不能区分+0-0

[-0].includes(+0)
true

类型化数组中也同样存在一个includes方法。

let tarr = Uint8Array.of(12, 5, 3);
console.log(tarr.includes(5));
true

FAQ

  • 为什么此方法会被命名为includes,而不是contains

contains确实是最初的选择,但是后来某些代码库(MooTools)有了同名的实现,就被放弃了。

  • 接上一问题,为什么它也没有被命名为has呢?

因为has方法用于判断键值对(Map.prototype.has),而includes通常用于判断元素(String.prototype.includes)。一个集合(Set)的所有元素是可以通过keyvalue分别查看的,这也是为什么Set中也没有includes方法了。

  • String.prototype.includes方法的工作机制是基于字符串,而不是基于字符的。可这是不是就和Array.prototype.includes方法的工作机制不一致了?如果数组的includes方法和String上的保持一致,那么数组的includes方法参数接受的应该是一个数组,而不能是一个单一元素。但是,这两个对象上的includes方法又是模仿的indexOf方法。字符是一种特定情况,字符串一般情况下都是拥有一定的长度。

Object.entries()和Object.values()

Object.entries()

let obj = { one: 1, two: 2 };
for (let [k,v] of Object.entries(obj)) {
    console.log(`${JSON.stringify(k)}: ${JSON.stringify(v)}`);
}
"one": 1
"two": 2

如果传给它的数据是key-value形式的键值对,那么返回的是每一个都包含两个元素的数组列表。Object.entries(x)的参数必须是一个对象,并且返回的是一个可权举的、转换成字符串的key属性数组。

Object.entries({ one: 1, two: 2 })
[ [ 'one', 1 ], [ 'two', 2 ] ]

如果某些keySymbol类型,则会被直接忽略。

Object.entries({ [Symbol()]: 123, foo: 'abc' });
[ [ 'foo', 'abc' ] ]

Object.entries()带给我们的是一种遍历对象属性的方式(为什么对象默认不支持遍历)。它也可以与Maps相配合,这比上面的使用方式更直白。

let map = new Map(Object.entries({
    one: 1,
    two: 2,
}));
console.log(JSON.stringify([...map]));
[["one",1],["two",2]]

Object.values()

它用来返回键值可枚举的、可转换成字符串类型的属性值。

Object.values({ one: 1, two: 2 })
[1, 2]

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.