diff --git a/docs/module.md b/docs/module.md index 431e3ef..8ec97ef 100644 --- a/docs/module.md +++ b/docs/module.md @@ -12,7 +12,9 @@ let { stat, exists, readFile } = require('fs'); // 等同于 let _fs = require('fs'); -let stat = _fs.stat, exists = _fs.exists, readfile = _fs.readfile; +let stat = _fs.stat; +let exists = _fs.exists; +let readfile = _fs.readfile; ``` 上面代码的实质是整体加载`fs`模块(即加载`fs`的所有方法),生成一个对象(`_fs`),然后再从这个对象上面读取3个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。 @@ -58,6 +60,8 @@ ES6 的模块自动采用严格模式,不管你有没有在模块头部加上` 上面这些限制,模块都必须遵守。由于严格模式是 ES5 引入的,不属于 ES6,所以请参阅相关 ES5 书籍,本书不再详细介绍了。 +其中,尤其需要注意`this`的限制。ES6 模块之中,顶层的`this`指向`undefined`,即不应该在顶层代码使用`this`。 + ## export 命令 模块功能主要由两个命令构成:`export`和`import`。`export`命令用于规定模块的对外接口,`import`命令用于输入其他模块提供的功能。 @@ -71,7 +75,7 @@ export var lastName = 'Jackson'; export var year = 1958; ``` -上面代码是`profile.js`文件,保存了用户信息。ES6将其视为一个模块,里面用`export`命令对外部输出了三个变量。 +上面代码是`profile.js`文件,保存了用户信息。ES6 将其视为一个模块,里面用`export`命令对外部输出了三个变量。 `export`的写法,除了像上面这样,还有另外一种。 @@ -86,7 +90,7 @@ export {firstName, lastName, year}; 上面代码在`export`命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在`var`语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。 -export命令除了输出变量,还可以输出函数或类(class)。 +`export`命令除了输出变量,还可以输出函数或类(class)。 ```javascript export function multiply(x, y) { @@ -163,7 +167,7 @@ setTimeout(() => foo = 'baz', 500); 上面代码输出变量`foo`,值为`bar`,500毫秒之后变成`baz`。 -这一点与CommonJS规范完全不同。CommonJS模块输出的是值的缓存,不存在动态更新,详见下文《ES6模块加载的实质》一节。 +这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新,详见下文《ES6模块加载的实质》一节。 最后,`export`命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的`import`命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了ES6模块的设计初衷。 @@ -399,6 +403,18 @@ export default var a = 1; 上面代码中,`export default a`的含义是将变量`a`的值赋给变量`default`。所以,最后一种写法会报错。 +同样地,因为`export default`本质是将该命令后面的值,赋给`default`变量以后再默认,所以直接将一个值写在`export default`之后。 + +```javascript +// 正确 +export default 42; + +// 报错 +export 42; +``` + +上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定外对接口为`default`。 + 有了`export default`命令,输入模块时就非常直观了,以输入 lodash 模块为例。 ```javascript @@ -417,20 +433,16 @@ import _, { each } from 'lodash'; export default function (obj) { // ··· } + export function each(obj, iterator, context) { // ··· } + export { each as forEach }; ``` 上面代码的最后一行的意思是,暴露出`forEach`接口,默认指向`each`接口,即`forEach`和`each`指向同一个方法。 -如果要输出默认的值,只需将值跟在`export default`之后即可。 - -```javascript -export default 42; -``` - `export default`也可以用来输出类。 ```javascript @@ -488,14 +500,20 @@ export default es6; export { default as es6 } from './someModule'; ``` -另外,ES7有一个[提案](https://github.com/leebyron/ecmascript-more-export-from),简化先输入后输出的写法,拿掉输出时的大括号。 +下面三种`import`语句,没有对应的复合写法。 ```javascript -// 现行的写法 -export {v} from 'mod'; +import * as someIdentifier from "someModule"; +import someIdentifier from "someModule"; +import someIdentifier, { namedIdentifier } from "someModule"; +``` -// 提案的写法 -export v from 'mod'; +为了做到形式的对称,现在有[提案](https://github.com/leebyron/ecmascript-export-default-from),提出补上这三种复合写法。 + +```javascript +export * as someIdentifier from "someModule"; +export someIdentifier from "someModule"; +export someIdentifier, { namedIdentifier } from "someModule"; ``` ## 模块的继承 @@ -540,9 +558,9 @@ console.log(exp(math.e)); ## ES6模块加载的实质 -ES6模块加载的机制,与CommonJS模块完全不同。CommonJS模块输出的是一个值的拷贝,而ES6模块输出的是值的引用。 +ES6 模块加载的机制,与 CommonJS 模块完全不同。CommonJS模块输出的是一个值的拷贝,而 ES6 模块输出的是值的引用。 -CommonJS模块输出的是被输出值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件`lib.js`的例子。 +CommonJS 模块输出的是被输出值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件`lib.js`的例子。 ```javascript // lib.js @@ -591,7 +609,7 @@ $ node main.js 4 ``` -ES6 模块的运行机制与 CommonJS 不一样。JS引擎对脚本静态分析的时候,遇到模块加载命令`import`,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的`import`有点像 Unix 系统的“符号连接”,原始值变了,`import`加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。 +ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令`import`,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的`import`有点像 Unix 系统的“符号连接”,原始值变了,`import`加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。 还是举上面的例子。 @@ -635,9 +653,9 @@ bar baz ``` -上面代码表明,ES6模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。 +上面代码表明,ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。 -由于ES6输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。 +由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。 ```javascript // lib.js @@ -650,7 +668,7 @@ obj.prop = 123; // OK obj = {}; // TypeError ``` -上面代码中,`main.js`从`lib.js`输入变量`obj`,可以对`obj`添加属性,但是重新赋值就会报错。因为变量`obj`指向的地址是只读的,不能重新赋值,这就好比`main.js`创造了一个名为`obj`的const变量。 +上面代码中,`main.js`从`lib.js`输入变量`obj`,可以对`obj`添加属性,但是重新赋值就会报错。因为变量`obj`指向的地址是只读的,不能重新赋值,这就好比`main.js`创造了一个名为`obj`的`const`变量。 最后,`export`通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。 @@ -685,7 +703,7 @@ import './x'; import './y'; ``` -现在执行`main.js`,输出的是1。 +现在执行`main.js`,输出的是`1`。 ```bash $ babel-node main.js @@ -712,6 +730,184 @@ $ babel-node main.js - 该脚本内部的顶层变量,都只在该脚本内部有效,外部不可见。 - 该脚本内部的顶层的`this`关键字,返回`undefined`,而不是指向`window`。 +## Node 的加载处理 + +### 概述 + +Node 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。 + +在静态分析阶段,一个模块脚本只要有一行`import`或`export`语句,Node 就会认为该脚本为 ES6 模块,否则就为 CommonJS 模块。如果不输出任何接口,但是希望被 Node 认为是 ES6 模块,可以在脚本中加一行语句。 + +```javascript +export {}; +``` + +上面的命令并不是输出一个空对象,而是不输出任何接口的 ES6 标准写法。 + +如何不指定绝对路径,Node 加载 ES6 模块会依次寻找以下脚本,与`require()`的规则一致。 + +```javascript +import './foo'; +// 依次寻找 +// ./foo.js +// ./foo/package.json +// ./foo/index.js + +import 'baz'; +// 依次寻找 +// ./node_modules/baz.js +// ./node_modules/baz/package.json +// ./node_modules/baz/index.js +// 寻找上一级目录 +// ../node_modules/baz.js +// ../node_modules/baz/package.json +// ../node_modules/baz/index.js +// 再上一级目录 +``` + +ES6 模块之中,顶层的`this`指向`undefined`;CommonJS 模块的顶层`this`指向当前模块,这是两者的一个重大差异。 + +### import 命令加载 CommonJS 模块 + +Node 采用 CommonJS 模块格式,模块的输出都定义在`module.exports`这个属性上面。在 Node 环境中,使用`import`命令加载 CommonJS 模块,Node 会自动将`module.exports`属性,当作模块的默认输出,即等同于`export default`。 + +下面是一个 CommonJS 模块。 + +```javascript +// a.js +module.exports = { + foo: 'hello', + bar: 'world' +}; + +// 等同于 +export default { + foo: 'hello', + bar: 'world' +}; +``` + +`import`命令加载上面的模块,`module.exports`会被视为默认输出。 + +```javascript +// 写法一 +import baz from './a'; +// baz = {foo: 'hello', bar: 'world'}; + +// 写法二 +import {default as baz} from './a'; +// baz = {foo: 'hello', bar: 'world'}; +``` + +如果采用整体输入的写法(`import * as xxx from someModule`),`default`会取代`module.exports`,作为输入的接口。 + +```javascript +import * as baz from './a'; +// baz = { +// get default() {return module.exports;}, +// get foo() {return this.default.foo}.bind(baz), +// get bar() {return this.default.bar}.bind(baz) +// } +``` + +上面代码中,`this.default`取代了`module.exports`。需要注意的是,Node 会自动为`baz`添加`default`属性,通过`baz.default`拿到`module.exports`。 + +```javascript +// b.js +module.exports = null; + +// es.js +import foo from './b'; +// foo = null; + +import * as bar from './b'; +// bar = {default:null}; +``` + +上面代码中,`es.js`采用第二种写法时,要通过`bar.default`这样的写法,才能拿到`module.exports`。 + +下面是另一个例子。 + +```javascript +// c.js +module.exports = function two() { + return 2; +}; + +// es.js +import foo from './c'; +foo(); // 2 + +import * as bar from './c'; +bar.default(); // 2 +bar(); // throws, bar is not a function +``` + +上面代码中,`bar`本身是一个对象,不能当作函数调用,只能通过`bar.default`调用。 + +CommonJS 模块的输出缓存机制,在 ES6 加载方式下依然有效。 + +```javascript +// foo.js +module.exports = 123; +setTimeout(_ => module.exports = null); +``` + +上面代码中,对于加载`foo.js`的脚本,`module.exports`将一直是`123`,而不会变成`null`。 + +由于 ES6 模块是编译时确定输出接口,CommonJS 模块是运行时确定输出接口,所以采用`import`命令加载 CommonJS 模块时,不允许采用下面的写法。 + +```javascript +import {readfile} from 'fs'; +``` + +上面的写法不正确,因为`fs`是 CommonJS 格式,只有在运行时才能确定`readfile`接口,而`import`命令要求编译时就确定这个接口。解决方法就是改为整体输入。 + +```javascript +import * as express from 'express'; +const app = express.default(); + +import express from 'express'; +const app = express(); +``` + +### require 命令加载 ES6 模块 + +采用`require`命令加载 ES6 模块时,ES6 模块的所有输出接口,会成为输入对象的属性。 + +```javascript +// es.js +let foo = {bar:'my-default'}; +export default foo; +foo = null; + +// cjs.js +const es_namespace = require('./es'); +console.log(es_namespace.default); +// {bar:'my-default'} +``` + +上面代码中,`default`接口变成了`es_namespace.default`属性。另外,由于存在缓存机制,`es.js`对`foo`的重新赋值没有在模块外部反映出来。 + +下面是另一个例子。 + +```javascript +// es.js +export let foo = {bar:'my-default'}; +export {foo as bar}; +export function f() {}; +export class c {}; + +// cjs.js +const es_namespace = require('./es'); +// es_namespace = { +// get foo() {return foo;} +// get bar() {return foo;} +// get f() {return f;} +// get c() {return c;} +// } +``` + ## 循环加载 “循环加载”(circular dependency)指的是,`a`脚本的执行依赖`b`脚本,而`b`脚本的执行又依赖`a`脚本。 diff --git a/docs/object.md b/docs/object.md index ad2fdab..23dcd8b 100644 --- a/docs/object.md +++ b/docs/object.md @@ -872,27 +872,29 @@ Object.entries(obj) 除了返回值不一样,该方法的行为与`Object.values`基本一致。 -如果原对象的属性名是一个Symbol值,该属性会被省略。 +如果原对象的属性名是一个 Symbol 值,该属性会被忽略。 ```javascript Object.entries({ [Symbol()]: 123, foo: 'abc' }); // [ [ 'foo', 'abc' ] ] ``` -上面代码中,原对象有两个属性,`Object.entries`只输出属性名非Symbol值的属性。将来可能会有`Reflect.ownEntries()`方法,返回对象自身的所有属性。 +上面代码中,原对象有两个属性,`Object.entries`只输出属性名非 Symbol 值的属性。将来可能会有`Reflect.ownEntries()`方法,返回对象自身的所有属性。 `Object.entries`的基本用途是遍历对象的属性。 ```javascript let obj = { one: 1, two: 2 }; for (let [k, v] of Object.entries(obj)) { - console.log(`${JSON.stringify(k)}: ${JSON.stringify(v)}`); + console.log( + `${JSON.stringify(k)}: ${JSON.stringify(v)}` + ); } // "one": 1 // "two": 2 ``` -`Object.entries`方法的一个用处是,将对象转为真正的`Map`结构。 +`Object.entries`方法的另一个用处是,将对象转为真正的`Map`结构。 ```javascript var obj = { foo: 'bar', baz: 42 }; diff --git a/docs/reference.md b/docs/reference.md index 23d086c..20d64ae 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -209,6 +209,7 @@ - Jason Orendorff, [ES6 In Depth: Modules](https://hacks.mozilla.org/2015/08/es6-in-depth-modules/): ES6模块设计思想的介绍 - Ben Newman, [The Importance of import and export](http://benjamn.github.io/empirenode-2015/#/): ES6模块的设计思想 - ESDiscuss, [Why is "export default var a = 1;" invalid syntax?](https://esdiscuss.org/topic/why-is-export-default-var-a-1-invalid-syntax) +- Bradley Meck, [ES6 Module Interoperability](https://github.com/nodejs/node-eps/blob/master/002-es6-modules.md): 介绍 Node 如何处理 ES6 语法加载 CommonJS 模块 ## 二进制数组 diff --git a/docs/simd.md b/docs/simd.md index a151fff..65e0f64 100644 --- a/docs/simd.md +++ b/docs/simd.md @@ -2,9 +2,9 @@ ## 概述 -SIMD(发音`/sim-dee/`)是“Single Instruction/Multiple Data”的缩写,意为“单指令,多数据”。它是JavaScript操作CPU对应指令的接口,你可以看做这是一种不同的运算执行模式。与它相对的是SISD(“Single Instruction/Single Data”),即“单指令,单数据”。 +SIMD(发音`/sim-dee/`)是“Single Instruction/Multiple Data”的缩写,意为“单指令,多数据”。它是 JavaScript 操作 CPU 对应指令的接口,你可以看做这是一种不同的运算执行模式。与它相对的是 SISD(“Single Instruction/Single Data”),即“单指令,单数据”。 -SIMD的含义是使用一个指令,完成多个数据的运算;SISD的含义是使用一个指令,完成单个数据的运算,这是JavaScript的默认运算模式。显而易见,SIMD的执行效率要高于SISD,所以被广泛用于3D图形运算、物理模拟等运算量超大的项目之中。 +SIMD 的含义是使用一个指令,完成多个数据的运算;SISD 的含义是使用一个指令,完成单个数据的运算,这是 JavaScript 的默认运算模式。显而易见,SIMD 的执行效率要高于 SISD,所以被广泛用于3D图形运算、物理模拟等运算量超大的项目之中。 为了理解SIMD,请看下面的例子。 @@ -22,7 +22,7 @@ c // Array[6, 8, 10, 12] 上面代码中,数组`a`和`b`的对应成员相加,结果放入数组`c`。它的运算模式是依次处理每个数组成员,一共有四个数组成员,所以需要运算4次。 -如果采用SIMD模式,只要运算一次就够了。 +如果采用 SIMD 模式,只要运算一次就够了。 ```javascript var a = SIMD.Float32x4(1, 2, 3, 4); @@ -32,22 +32,22 @@ var c = SIMD.Float32x4.add(a, b); // Float32x4[6, 8, 10, 12] 上面代码之中,数组`a`和`b`的四个成员的各自相加,只用一条指令就完成了。因此,速度比上一种写法提高了4倍。 -一次SIMD运算,可以处理多个数据,这些数据被称为“通道”(lane)。上面代码中,一次运算了四个数据,因此就是四个通道。 +一次 SIMD 运算,可以处理多个数据,这些数据被称为“通道”(lane)。上面代码中,一次运算了四个数据,因此就是四个通道。 -SIMD通常用于矢量运算。 +SIMD 通常用于矢量运算。 ```javascript v + w = 〈v1, …, vn〉+ 〈w1, …, wn〉 = 〈v1+w1, …, vn+wn〉 ``` -上面代码中,`v`和`w`是两个多元矢量。它们的加运算,在SIMD下是一个指令、而不是n个指令完成的,这就大大提高了效率。这对于3D动画、图像处理、信号处理、数值处理、加密等运算是非常重要的。比如,Canvas的`getImageData()`会将图像文件读成一个二进制数组,SIMD就很适合对于这种数组的处理。 +上面代码中,`v`和`w`是两个多元矢量。它们的加运算,在 SIMD 下是一个指令、而不是 n 个指令完成的,这就大大提高了效率。这对于3D动画、图像处理、信号处理、数值处理、加密等运算是非常重要的。比如,Canvas的`getImageData()`会将图像文件读成一个二进制数组,SIMD 就很适合对于这种数组的处理。 -总得来说,SIMD是数据并行处理(parallelism)的一种手段,可以加速一些运算密集型操作的速度。 +总的来说,SIMD 是数据并行处理(parallelism)的一种手段,可以加速一些运算密集型操作的速度。将来与 WebAssembly 结合以后,可以让 JavaScript 达到二进制代码的运行速度。 ## 数据类型 -SIMD提供12种数据类型,总长度都是128个二进制位。 +SIMD 提供12种数据类型,总长度都是128个二进制位。 - Float32x4:四个32位浮点数 - Float64x2:两个64位浮点数 @@ -71,7 +71,7 @@ SIMD提供12种数据类型,总长度都是128个二进制位。 - 无符号的整数(Uint,比如1) - 布尔值(Bool,包含`true`和`false`两种值) -每种SIMD的数据类型都是一个函数方法,可以传入参数,生成对应的值。 +每种 SIMD 的数据类型都是一个函数方法,可以传入参数,生成对应的值。 ```javascript var a = SIMD.Float32x4(1.0, 2.0, 3.0, 4.0); @@ -712,5 +712,5 @@ function average(list) { } ``` -上面代码先是每隔四位,将所有的值读入一个SIMD,然后立刻累加。然后,得到累加值四个通道的总和,再除以`n`就可以了。 +上面代码先是每隔四位,将所有的值读入一个 SIMD,然后立刻累加。然后,得到累加值四个通道的总和,再除以`n`就可以了。 diff --git a/sidebar.md b/sidebar.md index 48d5dfa..42bf83e 100644 --- a/sidebar.md +++ b/sidebar.md @@ -7,7 +7,7 @@ ## 目录 1. [前言](#README) 1. [ECMAScript 6简介](#docs/intro) -1. [let和const命令](#docs/let) +1. [let 和 const 命令](#docs/let) 1. [变量的解构赋值](#docs/destructuring) 1. [字符串的扩展](#docs/string) 1. [正则的扩展](#docs/regex) @@ -16,13 +16,13 @@ 1. [函数的扩展](#docs/function) 1. [对象的扩展](#docs/object) 1. [Symbol](#docs/symbol) -1. [Set和Map数据结构](#docs/set-map) +1. [Set 和 Map 数据结构](#docs/set-map) 1. [Proxy](#docs/proxy) 1. [Reflect](#docs/reflect) -1. [Iterator和for...of循环](#docs/iterator) -1. [Generator函数](#docs/generator) -1. [Promise对象](#docs/promise) -1. [异步操作和Async函数](#docs/async) +1. [Promise 对象](#docs/promise) +1. [Iterator 和 for...of 循环](#docs/iterator) +1. [Generator 函数](#docs/generator) +1. [异步操作和 Async 函数](#docs/async) 1. [Class](#docs/class) 1. [Decorator](#docs/decorator) 1. [Module](#docs/module)