- 第1章 ECMAScript 6简介
- 第2章 let和const命令
- 第3章 变量的解构赋值
- 第4章 字符串的扩展
- 第5章 正则的扩展
- 第6章 数值的扩展
- 第7章 数组的扩展
- 第8章 函数的扩展
- 第9章 对象的扩展
- 第10章 Symbol
- 第11章 Proxy和Reflect
- 第12章 二进制数组
- 第13章 Set和Map数据结构
- 第14章 Iterator和for…of循环
- 第15章 Generator 函数
- 第16章 Promise对象
- 第17章 异步操作和Async函数
- 第18章 Class
- 第19章 修饰器
- 第20章 Module
第1章 ECMAScript 6简介
Babel转码器
.babelrc配置文件
放在项目根目录下,用来配置转码规则和插件,如:
{
"presets": [
"es2015",
"react",
"stage-2"
],
"plugins": []
}
转码规则需要单独安装,如: $ npm install –save-dev babel-preset-es2015
babel-cli
用于命令行执行转码,如: $ babel example.js –out-file compiled.js $ babel src –out-dir lib
babel-node
是babel-cli自带的一个命令,无需单独安装,可以直接运行ES6代码: $ babel-node es6.js # 替代node
babel-register
该模块会改写require命令,每当使用require加载.js、.jsx、.es和.es6后缀名的文件,就会先用Babel进行转码: require(“babel-register”); require(“./index.js”);
babel-core
babel-core提供babel的API,有些代码需要通过调用Babel的API才能进行转码,如:
var es6Code = 'let x = n => n + 1';
var es5Code = require('babel-core')
.transform(es6Code, {
presets: ['es2015']
})
.code;
babel-polyfill
Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。如果也想转换这些对象,需要使用babel-polyfill: import ‘babel-polyfill’; // 或者 require(‘babel-polyfill’);
babel用于浏览器环境
<script src="node_modules/babel-core/browser.js"></script>
<script type="text/babel">
// ES6 code
</script>
直接在浏览器中进行转码性能太差,可以配合browserify在服务器端把代码转换为浏览器可以直接执行的代码: $ npm install –save-dev babelify babel-preset-es2015 $ browserify script.js -o bundle.js -t [ babelify –presets [ es2015 ] ]
可以在package.json中进行配置,这样就不用每次都在命令行输入参数了:
{
"browserify": {
"transform": [["babelify", { "presets": ["es2015"] }]]
}
}
Traceur
(略)
第2章 let和const命令
let
// 使用let声明的变量只在let命令所在的代码块内有效
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
// 使用let声明的变量不会“变量提升”,变量一定要在声明后使用
console.log(foo); // 输出undefined
console.log(bar); // 报错ReferenceError
var foo = 2; // 会变量提升,即脚本开始运行时变量foo就已存在,但没有值
let bar = 2;
// 只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响(const同理)
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError (暂时性死区)
let tmp;
}
// let不允许在相同作用域内,重复声明同一个变量
function () {
let a = 10;
var a = 1; // 报错
let a = 1; // 报错
}
块级作用域
ES5只有全局作用域和函数作用域,没有块级作用域,这会造成很多不合理的场景,比如:
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = "hello world"; // 变量提升,会覆盖外层tmp变量
}
}
f(); // undefined
let为JavaScript新增了块级作用域,使得获得广泛应用的立即执行匿名函数(IIFE)不再必要了。
ES5标准规定不允许在块级作用域中声明函数,但是很多浏览器没有遵守这个规定。ES6明确允许在块级作用域中声明函数:
// ES6严格模式
'use strict';
if (true) {
function f() {}
}
// 不报错
并且ES6规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句:
// 函数声明语句
{
let a = 'secret';
function f() {
return a;
}
}
// 函数表达式
{
let a = 'secret';
let f = function () {
return a;
};
}
const
const声明的变量不得改变值,一旦声明,就必须立即初始化:
const foo;
// SyntaxError: Missing initializer in const declaration
对于复合类型的变量,变量名不指向数据,而是指向数据所在的地址。const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变:
const foo = {};
foo.prop = 123;
foo.prop
// 123
foo = {}; // TypeError: "foo" is read-only
如果真的想将对象冻结,应该使用Object.freeze
方法:
// 彻底冻结一个对象(包括对象的属性)
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, value) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
ES6一共有6种声明变量的方法
:var、function、let、const、import、class
全局对象的属性
全局对象是最顶层的对象,在浏览器环境指的是window对象,在Node.js指的是global对象。 ES5之中,全局对象的属性与全局变量是等价的,未声明的全局变量,自动成为全局对象window的属性。 从ES6开始,全局变量将逐步与全局对象的属性脱钩,let命令、const命令、class命令声明的全局变量,不属于全局对象的属性:
var a = 1;
// 如果在Node的REPL环境,可以写成global.a
// 或者采用通用方法,写成this.a
window.a // 1
let b = 1;
window.b // undefined
第3章 变量的解构赋值
数组的解构赋值
var [a, b, c] = [1, 2, 3];
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
// 支持不完全结构
let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
// 允许指定默认值
var [foo = true] = [];
foo // true
[x, y = 'b'] = ['a']; // x='a', y='b'
[x, y = 'b'] = ['a', undefined]; // x='a', y='b'
// ES6内部使用严格相等运算符(===),判断一个位置是否有值
var [x = 1] = [undefined];
x // 1
var [x = 1] = [null];
x // null
如果等号的右边不是可遍历
的结构,那么将会报错:
// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
如果默认值是一个表达式,那么这个表达式是惰性求值的:
// 因为x能取到值,所以函数f根本不会执行
function f() {
console.log('aaa');
}
let [x = f()] = [1];
对象的解构赋值
对象的属性没有次序,变量必须与属性同名,才能取到正确的值:
var { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
var { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined
对象的解构赋值是下面形式的简写:
var { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
即对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。因此,如果变量名与属性名不一致,可以写成下面这样:
var { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'
解构也可以用于嵌套结构的对象:
var node = {
loc: {
start: {
line: 1,
column: 5
}
}
};
var { loc: { start: { line }} } = node;
line // 1
loc // error: loc is undefined
start // error: start is undefined
只有line是变量,loc和start都是模式,不会被赋值。
如果要将一个已经声明的变量用于解构赋值,必须使用():
var x;
{x} = {x: 1};
// SyntaxError: syntax error
上面代码的写法会报错,因为JavaScript引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免JavaScript将其解释为代码块
,才能解决这个问题。
({x} = {x: 1});
字符串的解构赋值
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
let {length : len} = 'hello';
len // 5
数值和布尔值的解构赋值
如果等号右边是数值和布尔值,则会先转为对象
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
解构赋值的规则是,只要等号右边的值不是对象,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错
。
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
函数参数的解构赋值
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]
undefined就会触发函数参数的默认值:
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]
圆括号问题
建议只要有可能,就不要在模式中放置圆括号。 (不要自找麻烦)
用途
遍历Map结构:
for (let [key, value] of map) {
console.log(key + " is " + value);
}
for (let [key] of map) {
// ...
}
for (let [,value] of map) {
// ...
}
输入模块的指定方法:
const { SourceMapConsumer, SourceNode } = require("source-map");
第4章 字符串的扩展
JavaScript允许采用\uxxxx形式表示一个字符
,其中“xxxx”表示字符的码点
。但是,这种表示法只限于\u0000——\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表达。
"\uD842\uDFB7"
// "𠮷"
"\u20BB7" // 超过0xFFFF,ES5会理解成“\u20BB+7”,\u20BB是一个不可打印字符,所以只会显示一个空格
// " 7"
ES6对Unicode字符的表示做出了改进,只要将码点放入大括号,就能正确解读4字节字符:
"\u{20BB7}"
// "𠮷"
因此ES6共有6种方法可以表示一个字符:
'\z' === 'z' // true
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true
'\u{7A}' === 'z' // true
字符串的遍历器接口
ES6为字符串添加了遍历器接口,使得字符串可以被for…of循环遍历:
for (let codePoint of 'foo') {
console.log(codePoint)
}
// "f"
// "o"
// "o"
模板字符串
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
// 普通字符串
`In JavaScript '\n' is a line-feed.`
// 多行字符串
`In JavaScript this is
not legal.`
console.log(`string text line 1
string text line 2`);
// 字符串中嵌入变量
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
标签模板
模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串:
alert`123`
// 等同于
alert(123)
标签模板其实不是模板,而是函数调用的一种特殊形式。 如果模板字符里面有变量,会将模板字符串先处理成多个参数,再调用函数:
var a = 5;
var b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);
API
codePointAt() // 能够正确处理4个字节储存的字符,返回一个字符的码点 String.fromCodePoint() // 用于从码点返回对应字符 at() // 可以识别Unicode编号大于0xFFFF的字符,返回正确的字符 normalize() // 用来将字符的不同表示方法统一为同样的形式,这称为Unicode正规化(字符合成) includes() // 返回布尔值,表示是否找到了参数字符串。 startsWith() // 返回布尔值,表示参数字符串是否在源字符串的头部。 endsWith() // 返回布尔值,表示参数字符串是否在源字符串的尾部。 repeat() // 返回一个新字符串,表示将原字符串重复n次 padStart() // 头部补全 padEnd() // 尾部补全 String.raw() // 往往用来充当模板字符串的处理函数,返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,对应于替换变量后的模板字符串。
第5章 正则的扩展
构造函数的改变
ES6改变了ES5中RegExp的构造函数的行为:构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。
new RegExp(/abc/ig, 'i').flags
// "i"
字符串的正则方法
字符串对象共有4个方法,可以使用正则表达式:match()、replace()、search()和split()。
u修饰符
ES6对正则表达式添加了u修饰符,用来正确处理大于\uFFFF的Unicode字符
/^\uD83D/u.test('\uD83D\uDC2A')
// false
/^\uD83D/.test('\uD83D\uDC2A')
// true
y修饰符
ES6为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符,y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义:
var s = 'aaa_aa_a';
var r1 = /a+/g;
var r2 = /a+/y;
r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]
r1.exec(s) // ["aa"]
r2.exec(s) // null
sticky属性
ES6的正则对象多了sticky属性,表示是否设置了y修饰符
flags属性
新增了flags属性,会返回正则表达式的修饰符
// ES5的source属性
// 返回正则表达式的正文
/abc/ig.source
// "abc"
// ES6的flags属性
// 返回正则表达式的修饰符
/abc/ig.flags
// 'gi'
RegExp.escape()
ES7,(略)
后行断言
ES7,(略)
第6章 数值的扩展
二进制和八进制表示法
ES6提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示:
0b111110111 === 503 // true
0o767 === 503 // true
Number.isFinite()
用来检查一个数值是否为有限的(finite):
Number.isFinite(15); // true
Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite(-Infinity); // false
Number.isFinite('foo'); // false
Number.isFinite('15'); // false
Number.isFinite(true); // false
Number.isNaN()
用来检查一个值是否为NaN:
Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9/NaN) // true
Number.isNaN('true'/0) // true
Number.isNaN('true'/'true') // true
Number.parseInt(), Number.parseFloat()
ES6将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。
Number.parseInt === parseInt // true
Number.parseFloat === parseFloat // true
Number.isInteger()
用来判断一个值是否为整数,在JavaScript内部,整数和浮点数是同样的储存方法,所以3和3.0被视为同一个值:
Number.isInteger(25) // true
Number.isInteger(25.0) // true
Number.isInteger(25.1) // false
Number.isInteger("15") // false
Number.isInteger(true) // false
Number.EPSILON
新增的一个极小的常量:
Number.EPSILON
// 2.220446049250313e-16
Number.EPSILON.toFixed(20)
// '0.00000000000000022204'
Number.isSafeInteger()
JavaScript能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值。ES6引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。 实际使用这个函数时,需要注意。验证运算结果是否落在安全整数的范围内,不要只验证运算结果,而要同时验证参与运算的每个值。
Number.isSafeInteger(9007199254740993)
// false
Number.isSafeInteger(990)
// true
Number.isSafeInteger(9007199254740993 - 990)
// true
9007199254740993 - 990
// 返回结果 9007199254740002
// 正确答案应该是 9007199254740003
Math对象的扩展
Math.trunc() // 用于去除一个数的小数部分,返回整数部分 Math.sign() // 用来判断一个数到底是正数、负数、还是零 Math.cbrt() // 用于计算一个数的立方根 Math.clz32() // 返回一个数的32位无符号整数形式有多少个前导0 Math.imul() // 返回两个数以32位带符号整数形式相乘的结果,返回的也是一个32位的带符号整数 Math.fround() // 返回一个数的单精度浮点数形式 Math.hypot() // 返回所有参数的平方和的平方根 (新增若干对数、指数、三角函数方法,略)
指数运算符
ES7新增了一个指数运算符(**)
2 ** 2 // 4
2 ** 3 // 8
第7章 数组的扩展
Array.from()
用于将两类对象转为真正的数组:
- 类似数组的对象(array-like object)
- 可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map,只要是部署了Iterator接口的数据结构,Array.from都能将其转为数组) ```javascript let arrayLike = { ‘0’: ‘a’, ‘1’: ‘b’, ‘2’: ‘c’, length: 3 };
// ES5的写法 var arr1 = [].slice.call(arrayLike); // [‘a’, ‘b’, ‘c’]
// ES6的写法 let arr2 = Array.from(arrayLike); // [‘a’, ‘b’, ‘c’]
扩展运算符(...)也可以将某些数据结构转为数组:
```javascript
// arguments对象
function foo() {
var args = [...arguments];
}
// NodeList对象
[...document.querySelectorAll('div')]
扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。Array.from方法则是还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换
。
Array.from({ length: 3 });
// [ undefined, undefined, undefined ]
Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组:
Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);
Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]
如果map函数里面用到了this关键字,还可以传入Array.from的第三个参数,用来绑定this。
Array.of()
用于将一组值,转换为数组:
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]
这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异(只有当参数个数不少于2个时,Array()才会返回由参数组成的新数组。参数个数只有一个时,实际上是指定数组的长度):
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
数组实例的copyWithin()
在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。 Array.prototype.copyWithin(target, start = 0, end = this.length)
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]
数组实例的find()和findIndex()
find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
[1, 4, -5, 10].find((n) => n < 0)
// -5
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。 这两个方法都可以发现NaN,弥补了数组的IndexOf方法的不足。
[NaN].indexOf(NaN)
// -1
[NaN].findIndex(y => Object.is(NaN, y))
// 0
数组实例的fill()
使用给定值,填充一个数组
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
数组实例的entries(),keys()和values()
ES6提供三个新的方法——entries(),keys()和values()——用于遍历数组。它们都返回一个遍历器对象,可以用for…of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
数组实例的includes()
返回一个布尔值,表示某个数组是否包含给定的值:
[1, 2, 3].includes(2); // true
[1, 2, 3].includes(4); // false
[1, 2, NaN].includes(NaN); // true
数组的空位
数组的空位指,数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位:
Array(3) // [, , ,]
注意,空位不是undefined,一个位置的值等于undefined,依然是有值的:
0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false
ES5对空位的处理很不一致,大多数情况下会忽略空位:
// forEach方法
[,'a'].forEach((x,i) => console.log(i)); // 1
// filter方法
['a',,'b'].filter(x => true) // ['a','b']
// every方法
[,'a'].every(x => x==='a') // true
// some方法
[,'a'].some(x => x !== 'a') // false
// map方法
[,'a'].map(x => 1) // [,1]
// join方法
[,'a',undefined,null].join('#') // "#a##"
// toString方法
[,'a',undefined,null].toString() // ",a,,"
ES6则是明确将空位转为undefined。 由于空位的处理规则非常不统一,所以建议避免出现空位。
第8章 函数的扩展
函数参数的默认值
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
如果传入undefined,将触发参数等于默认值,null则没有这个效果:
function foo(x = 5, y = 6) {
console.log(x, y);
}
foo(undefined, null)
// 5 null
函数的length属性
指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
rest参数
ES6引入rest参数(形式为“…变量名”),用于获取函数的多余参数。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
rest参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。 函数的length属性,不包括rest参数。
扩展运算符
将一个数组转为用逗号分隔的参数序列。
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
[...'hello']
// [ "h", "e", "l", "l", "o" ]
扩展运算符内部调用的是数据结构的Iterator接口
,因此只要具有Iterator接口的对象,都可以使用扩展运算符。
由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了:
function f(x, y, z) {
// ...
}
f.apply(null, args); // ES5的写法
f(...args); // ES6的写法
name属性
返回该函数的函数名:
function foo() {}
foo.name // "foo"
如果将一个匿名函数赋值给一个变量,ES5的name属性,会返回空字符串,而ES6的name属性会返回实际的函数名:
var func1 = function () {};
// ES5
func1.name // ""
// ES6
func1.name // "func1"
如果将一个具名函数赋值给一个变量,则ES5和ES6的name属性都返回这个具名函数原本的名字。
Function构造函数返回的函数实例,name属性的值为“anonymous”。
(new Function).name // "anonymous"
bind返回的函数,name属性值会加上“bound ”前缀。
function foo() {};
foo.bind({}).name // "bound foo"
(function(){}).bind({}).name // "bound "
箭头函数
var f = v => v;
var f = () => 5;
var sum = (num1, num2) => num1 + num2;
var sum = (num1, num2) => { return num1 + num2; }
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号:
var getTempItem = id => ({ id: id, name: "Temp" });
箭头函数有几个使用注意点:
- 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。 ```javascript function foo() { setTimeout(() => { console.log(‘id:’, this.id); }, 100); }
var id = 21;
foo.call({ id: 42 }); // id: 42
2. 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
3. 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。
4. 不可以使用yield命令,因此箭头函数不能用作Generator函数。
箭头函数中this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。箭头函数转成ES5的代码如下:
```javascript
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。
function foo() {
setTimeout(() => {
console.log('args:', arguments);
}, 100);
}
foo(2, 4, 6, 8)
// args: [2, 4, 6, 8]
由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。
函数绑定
ES7提出了“函数绑定”(function bind)运算符,用来取代call、apply、bind调用。函数绑定运算符是并排的两个双冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。
foo::bar;
// 等同于
bar.bind(foo);
foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);
const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
return obj::hasOwnProperty(key);
}
尾调用优化
尾调用(Tail Call)指某个函数的最后一步操作是返回另一个函数的调用。
function f(x){
return g(x);
}
不属于尾调用的情况:
// 情况一
function f(x){
let y = g(x); // 调用后还有操作
return y;
}
// 情况二
function f(x){
return g(x) + 1; // 调用后还有操作
}
// 情况三
function f(x){
g(x); // 相当于return undefined
}
尾调用优化
:函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。所有的调用帧,就形成一个“调用栈”(call stack)。尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3); // 由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除 f(x) 的调用帧,只保留 g(3) 的调用帧。
“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。
尾递归:如果尾调用自身,就称为尾递归。递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。 尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身,做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。:
// 阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n)
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1); // 不属于尾调用
}
factorial(5) // 120
阶乘函数 factorial 需要用到一个中间变量 total ,那就把这个中间变量改写成函数的参数,改写成尾递归:
// 复杂度 O(1)
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6也是如此,第一次明确规定,所有ECMAScript的实现,都必须部署“尾调用优化”。这就是说,在ES6中,只要使用尾递归,就不会发生栈溢出,相对节省内存
。
ES6的尾调用优化只在严格模式下开启,正常模式是无效的。这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈:func.arguments、func.caller尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
函数参数的尾逗号
ES7有一个提案,允许函数的最后一个参数有尾逗号(trailing comma),这样,当以后需要再添加新参数时,这一行代码在版本控制中不会有改变:
function clownsEverywhere(
param1,
param2,
) { /* ... */ }
第9章 对象的扩展
属性的简洁表示法
ES6允许直接写入变量和函数,作为对象的属性和方法(允许在对象之中,只写属性名,不写属性值。这时,属性值等于属性名所代表的变量的值):
var birth = '2000/01/01';
var Person = {
name: '张三',
//等同于birth: birth
birth,
// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }
};
属性名表达式
ES6允许字面量定义对象时,用表达式作为属性名(或方法名),表达式必须放在方括号内:
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123,
['h'+'ello']() {
return 'hi';
}
};
属性名表达式与简洁表示法,不能同时使用:
// 报错
var foo = 'bar';
var bar = 'abc';
var baz = { [foo] };
// 正确
var foo = 'bar';
var baz = { [foo]: 'abc'};
方法的name属性
var person = {
sayName() {
console.log(this.name);
},
get firstName() {
return "Nicholas";
}
};
person.sayName.name // "sayName"
person.firstName.name // "get firstName"
有两种特殊情况:bind方法创造的函数,name属性返回“bound”加上原函数的名字;Function构造函数创造的函数,name属性返回“anonymous”。
(new Function()).name // "anonymous"
var doSomething = function() {
// ...
};
doSomething.bind().name // "bound doSomething"
如果对象的方法是一个Symbol值,那么name属性返回的是这个Symbol值的描述:
const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
[key1]() {},
[key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""
Object.is()
相等运算符(==)和严格相等运算符(===)都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。ES6提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是+0不等于-0,二是NaN等于自身:
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
Object.assign()
Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target):
var target = { a: 1 };
var source1 = { b: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。 Object.assign拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。 属性名为Symbol值的属性,也会被Object.assign拷贝。
Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
// { a: 'b', Symbol(c): 'd' }
Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
对于嵌套的对象,一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加:
var target = { a: { b: 'c', d: 'e' } }
var source = { a: { b: 'hello' } }
Object.assign(target, source)
// { a: { b: 'hello' } }
Object.assign可以用来处理数组,但是会把数组视为对象:
Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
属性的可枚举性
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor
方法可以获取该属性的描述对象。
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
// }
ES5有三个操作会忽略enumerable为false的属性。
- for…in循环:只遍历对象自身的和继承的可枚举的属性
- Object.keys():返回对象自身的所有可枚举的属性的键名
- JSON.stringify():只串行化对象自身的可枚举的属性
ES6新增了一个操作Object.assign(),会忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。
这四个操作之中,只有for…in会返回继承的属性。
另外,ES6规定,所有Class的原型的方法都是不可枚举的。
Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable // false
属性的遍历
ES6一共有5种方法可以遍历对象的属性。
-
for...in
for…in循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)。 -
Object.keys(obj)
Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)。 -
Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)。 -
Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有Symbol属性。 -
Reflect.ownKeys(obj)
Reflect.ownKeys返回一个数组,包含对象自身的所有属性,不管是属性名是Symbol或字符串,也不管是否可枚举。
以上的5种方法遍历对象的属性,都遵守同样的属性遍历的次序规则:
首先遍历所有属性名为数值
的属性,按照数字排序。
其次遍历所有属性名为字符串
的属性,按照生成时间排序。
最后遍历所有属性名为Symbol值
的属性,按照生成时间排序。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
__proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()
proto__属性用来读取或设置当前对象的prototype对象,在实现上,__proto__调用的是Object.prototype.__proto。如果一个对象本身部署了__proto__属性,则该属性的值就是对象的原型。
// es6的写法
var obj = {
method: function() { ... }
};
obj.__proto__ = someOtherObj;
// es5的写法
var obj = Object.create(someOtherObj);
obj.method = function() { ... };
无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。
Object.values(),Object.entries()
ES7有一个提案,引入了跟Object.keys配套的Object.values和Object.entries。 (略)
对象的扩展运算符
ES7有一个提案,将Rest解构赋值/扩展运算符(…)引入对象
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
Rest解构赋值不会拷贝继承自原型对象的属性:
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let o3 = { ...o2 };
o3 // { b: 2 }
扩展运算符(…)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中:
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }
这等同于使用Object.assign方法。
Object.getOwnPropertyDescriptors()
ES7有一个提案,提出了Object.getOwnPropertyDescriptors方法,返回指定对象所有自身属性(非继承属性)的描述对象。
可以用来实现Mixin(混入)模式:
let mix = (object) => ({
with: (...mixins) => mixins.reduce(
(c, mixin) => Object.create(
c, Object.getOwnPropertyDescriptors(mixin)
), object)
});
// multiple mixins example
let a = {a: 'a'};
let b = {b: 'b'};
let c = {c: 'c'};
let d = mix(c).with(a, b);
第10章 Symbol
ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。它是JavaScript语言的第七种数据类型
,前六种是:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
Symbol值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型
,一种是原来就有的字符串,另一种就是新增的Symbol类型。凡是属性名属于Symbol类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象。也就是说,由于Symbol值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型
。
Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
let s = Symbol();
typeof s
// "symbol"
var s1 = Symbol('foo');
var s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
// 没有参数的情况
var s1 = Symbol();
var s2 = Symbol();
s1 === s2 // false
// 有参数的情况
var s1 = Symbol("foo");
var s2 = Symbol("foo");
s1 === s2 // false
Symbol值不能与其他类型的值进行运算,但是,Symbol值可以显式转为字符串,也可以转为布尔值,但是不能转为数值。:
var sym = Symbol('My symbol');
"your symbol is " + sym
// TypeError: can't convert symbol to string
`your symbol is ${sym}`
// TypeError: can't convert symbol to string
var sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
var sym = Symbol();
Boolean(sym) // true
!sym // false
if (sym) {
// ...
}
Number(sym) // TypeError
sym + 2 // TypeError
作为属性名的Symbol
由于每一个Symbol值都是不相等的,这意味着Symbol值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性:
var mySymbol = Symbol();
// 第一种写法
var a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
var a = {
[mySymbol]: 'Hello!' // 在对象的内部,使用Symbol值定义属性时,Symbol值必须放在方括号之中
};
// 第三种写法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
Symbol值作为对象属性名时,不能用点运算符:
var mySymbol = Symbol();
var a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
属性名的遍历
Symbol作为属性名,该属性不会出现在for…in、for…of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有Symbol属性名。
另一个新的API,Reflect.ownKeys方法可以返回所有类型的键名,包括常规键名和Symbol键名。
Symbol.for(),Symbol.keyFor()
Symbol.for方法接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建并返回一个以该字符串为名称的Symbol值。
var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
s1 === s2 // true
Symbol.for为Symbol值登记的名字,是全局环境的,可以在不同的iframe或service worker中取到同一个值。
Symbol.keyFor方法返回一个已登记的Symbol类型值的key:
var s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
var s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
由于以Symbol值作为名称的属性,不会被常规方法遍历得到,可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法:
var size = Symbol('size');
class Collection {
constructor() {
this[size] = 0;
}
add(item) {
this[this[size]] = item;
this[size]++;
}
static sizeOf(instance) {
return instance[size];
}
}
var x = new Collection();
Collection.sizeOf(x) // 0
x.add('foo');
Collection.sizeOf(x) // 1
Object.keys(x) // ['0']
Object.getOwnPropertyNames(x) // ['0']
Object.getOwnPropertySymbols(x) // [Symbol(size)]
使用Symbol实现模块的 Singleton 模式
// mod.js
const FOO_KEY = Symbol.for('foo');
function A() {
this.foo = 'hello';
}
if (!global[FOO_KEY]) {
global[FOO_KEY] = new A();
}
module.exports = global[FOO_KEY];
上面代码中,可以保证global[FOO_KEY]不会被其他脚本改写。
内置的Symbol值
ES6还提供了11个内置的Symbol值,指向语言内部使用的方法:
-
Symbol.hasInstance:foo instanceof Foo在语言内部,实际调用的是FooSymbol.hasInstance
-
Symbol.isConcatSpreadable:表示该对象使用Array.prototype.concat()时,是否可以展开 ```javascript let arr1 = [‘c’, ‘d’]; [‘a’, ‘b’].concat(arr1, ‘e’) // [‘a’, ‘b’, ‘c’, ‘d’, ‘e’] arr1[Symbol.isConcatSpreadable] // undefined
let arr2 = [‘c’, ‘d’]; arr2[Symbol.isConcatSpreadable] = false; [‘a’, ‘b’].concat(arr2, ‘e’) // [‘a’, ‘b’, [‘c’,’d’], ‘e’]
3. Symbol.species:指向一个方法。该对象作为构造函数创造实例时,会调用这个方法。即如果this.constructor[Symbol.species]存在,就会使用这个属性作为构造函数,来创造新的实例对象。
4. Symbol.match :指向一个函数。当执行str.match(myObject)时,如果该属性存在,会调用它,返回该方法的返回值
5. Symbol.replace:指向一个方法,当该对象被String.prototype.replace方法调用时,会返回该方法的返回值。
6. Symbol.search:指向一个方法,当该对象被String.prototype.search方法调用时,会返回该方法的返回值。
7. Symbol.split:指向一个方法,当该对象被String.prototype.split方法调用时,会返回该方法的返回值。
8. Symbol.iterator:指向该对象的默认遍历器方法
```javascript
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
-
Symbol.toPrimitive:指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。
-
Symbol.toStringTag:指向一个方法。在该对象上面调用Object.prototype.toString方法时,如果这个属性存在,它的返回值会出现在toString方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object]或[object Array]中object后面的那个字符串。ES6新增内置对象的Symbol.toStringTag属性值如下: JSON[Symbol.toStringTag]:’JSON’ Math[Symbol.toStringTag]:’Math’ Module对象M[Symbol.toStringTag]:’Module’ ArrayBuffer.prototype[Symbol.toStringTag]:’ArrayBuffer’ DataView.prototype[Symbol.toStringTag]:’DataView’ Map.prototype[Symbol.toStringTag]:’Map’ Promise.prototype[Symbol.toStringTag]:’Promise’ Set.prototype[Symbol.toStringTag]:’Set’ %TypedArray%.prototype[Symbol.toStringTag]:’Uint8Array’等 WeakMap.prototype[Symbol.toStringTag]:’WeakMap’ WeakSet.prototype[Symbol.toStringTag]:’WeakSet’ %MapIteratorPrototype%[Symbol.toStringTag]:’Map Iterator’ %SetIteratorPrototype%[Symbol.toStringTag]:’Set Iterator’ %StringIteratorPrototype%[Symbol.toStringTag]:’String Iterator’ Symbol.prototype[Symbol.toStringTag]:’Symbol’ Generator.prototype[Symbol.toStringTag]:’Generator’ GeneratorFunction.prototype[Symbol.toStringTag]:’GeneratorFunction’
-
Symbol.unscopables:指向一个对象。该对象指定了使用with关键字时,哪些属性会被with环境排除。
第11章 Proxy和Reflect
Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。ES6原生提供Proxy构造函数,用来生成Proxy实例: var proxy = new Proxy(target, handler); target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
Proxy支持的拦截操作
对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。
-
get(target, propKey, receiver) 拦截对象属性的读取,比如proxy.foo和proxy[‘foo’]。 参数receiver是一个对象,可选。
-
set(target, propKey, value, receiver) 拦截对象属性的设置,比如proxy.foo = v或proxy[‘foo’] = v,返回一个布尔值。
-
has(target, propKey) 拦截propKey in proxy的操作,以及对象的hasOwnProperty方法,返回一个布尔值。
-
deleteProperty(target, propKey) 拦截delete proxy[propKey]的操作,返回一个布尔值。
-
ownKeys(target) 拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy),返回一个数组。该方法返回对象所有自身的属性,而Object.keys()仅返回对象可遍历的属性。
-
getOwnPropertyDescriptor(target, propKey) 拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
-
defineProperty(target, propKey, propDesc) 拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
-
preventExtensions(target) 拦截Object.preventExtensions(proxy),返回一个布尔值。
-
getPrototypeOf(target) 拦截Object.getPrototypeOf(proxy),返回一个对象。
-
isExtensible(target) 拦截Object.isExtensible(proxy),返回一个布尔值。
-
setPrototypeOf(target, proto) 拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。 如果目标对象是函数,那么还有两种额外操作可以拦截。
-
apply(target, object, args) 拦截Proxy实例作为函数调用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。
-
construct(target, args) 拦截Proxy实例作为构造函数调用的操作,比如new proxy(…args)。
Proxy.revocable()
Proxy.revocable方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例:
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
Reflect
Reflect对象的设计目的有这样几个:
-
将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。
-
修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
-
让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
-
Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就
让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础
。也就是说,不管Proxy怎么修改默认行为,总可以在Reflect上获取默认行为
:var loggedObj = new Proxy(obj, { get(target, name) { console.log('get', target, name); return Reflect.get(target, name); }, deleteProperty(target, name) { console.log('delete' + name); return Reflect.deleteProperty(target, name); }, has(target, name) { console.log('has' + name); return Reflect.has(target, name); } });
上面代码中,每一个Proxy对象的拦截操作(get、delete、has),内部都调用对应的Reflect方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。
Reflect对象的方法
Reflect.apply(target,thisArg,args)
Reflect.construct(target,args) 等同于new target(…args),这提供了一种不使用new,来调用构造函数的方法。
Reflect.get(target,name,receiver) 查找并返回target对象的name属性,如果没有该属性,则返回undefined。 如果name属性部署了读取函数,则读取函数的this绑定receiver。
var obj = {
get foo() { return this.bar(); },
bar: function() { ... }
};
// 下面语句会让 this.bar()
// 变成调用 wrapper.bar()
Reflect.get(obj, "foo", wrapper);
Reflect.set(target,name,value,receiver)
Reflect.defineProperty(target,name,desc)
Reflect.deleteProperty(target,name)
Reflect.has(target,name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
读取对象的__proto__属性,对应Object.getPrototypeOf(obj)
Reflect.setPrototypeOf(target, prototype)
第12章 二进制数组
二进制数组允许开发者以数组下标的形式,直接操作内存
,使得开发者有可能通过JavaScript与操作系统的原生接口进行二进制通信
。
二进制数组由三类对象组成
-
ArrayBuffer
对象:代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。 -
TypedArray
视图:是一组不同类型视图的统称,共包括9种类型的视图,比如Uint8Array(无符号8位整数)数组视图, Int16Array(16位整数)数组视图, Float32Array(32位浮点数)数组视图等等。 -
DataView
视图:可以自定义复合格式的视图,比如第一个字节是Uint8(无符号8位整数)、第二、三个字节是Int16(16位整数)、第四个字节开始是Float32(32位浮点数)等等,此外还可以自定义字节序。
即,ArrayBuffer对象代表原始的二进制数据,TypedArray视图用来读写简单类型的二进制数据,DataView视图用来读写复杂类型的二进制数据。
注意,二进制数组并不是真正的数组,而是类似数组的对象。
ArrayBuffer对象
ArrayBuffer对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写
,视图的作用是以指定格式解读二进制数据:
var buf = new ArrayBuffer(32);
var dataView = new DataView(buf);
dataView.getUint8(0) // 0
TypedArray视图的构造函数,除了接受ArrayBuffer实例作为参数,还可以接受普通数组作为参数,直接分配内存生成底层的ArrayBuffer实例,并同时完成对这段内存的赋值:
var typedArray = new Uint8Array([0,1,2]);
typedArray.length // 3
typedArray[0] = 5;
typedArray // [5, 1, 2]
ArrayBuffer实例的byteLength属性
,返回所分配的内存区域的字节长度
:
var buffer = new ArrayBuffer(32);
buffer.byteLength
// 32
ArrayBuffer实例的slice方法
,可以将内存区域的一部分,拷贝生成一个新的ArrayBuffer对象:
var buffer = new ArrayBuffer(8);
var newBuffer = buffer.slice(0, 3); // 拷贝buffer对象的前3个字节(从0开始,到第3个字节前面结束)
除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。
ArrayBuffer的静态方法isView
,返回一个布尔值,表示参数是否为ArrayBuffer的视图实例:
var buffer = new ArrayBuffer(8);
ArrayBuffer.isView(buffer) // false
var v = new Int32Array(buffer);
ArrayBuffer.isView(v) // true
TypedArray视图
普通数组与TypedArray数组的差异主要在以下方面:
- TypedArray数组的所有成员,都是同一种类型。
- TypedArray数组的成员是连续的,不会有空位。
- TypedArray数组成员的默认值为0。比如,new Array(10)返回一个普通数组,里面没有任何成员,只是10个空位;new Uint8Array(10)返回一个TypedArray数组,里面10个成员都是0。
- TypedArray数组只是一层视图,
本身不储存数据
,它的数据都储存在底层的ArrayBuffer对象之中,要获取底层对象必须使用buffer属性。
视图可以不通过ArrayBuffer对象,直接分配内存而生成:
var f64a = new Float64Array(8);
f64a[0] = 10;
f64a[1] = 20;
f64a[2] = f64a[0] + f64a[1];
普通数组的操作方法和属性,对TypedArray数组完全适用,除了concat方法。
TypedArray数组只能处理小端字节序!DataView对象,可以设定字节序。
不同的视图类型,所能容纳的数值范围是确定的。超出这个范围,就会出现溢出。TypedArray数组(除了Uint8ClampedArray)的溢出处理规则,简单来说,就是抛弃溢出的位,然后按照视图类型进行解释。
var uint8 = new Uint8Array(1);
uint8[0] = 256;
uint8[0] // 0
uint8[0] = -1;
uint8[0] // 255
Uint8ClampedArray规定,凡是发生正向溢出,该值一律等于当前数据类型的最大值,即255;如果发生负向溢出,该值一律等于当前数据类型的最小值,即0。
buffer属性,返回整段内存区域对应的ArrayBuffer对象。该属性为只读属性。 set方法用于复制数组(普通数组或TypedArray数组),也就是将一段内容完全复制到另一段内存。 subarray方法是对于TypedArray数组的一部分,再建立一个新的视图。 (其他方法,略)
DataView视图
在设计目的上,ArrayBuffer对象的各种TypedArray视图,是用来向网卡、声卡之类的本机设备传送数据,所以使用本机的字节序就可以了;而DataView视图的设计目的,是用来处理网络设备传来的数据,所以大端字节序或小端字节序是可以自行设定的。
DataView实例提供8个方法读取内存: getInt8:读取1个字节,返回一个8位整数。 getUint8:读取1个字节,返回一个无符号的8位整数。 getInt16:读取2个字节,返回一个16位整数。 getUint16:读取2个字节,返回一个无符号的16位整数。 getInt32:读取4个字节,返回一个32位整数。 getUint32:读取4个字节,返回一个无符号的32位整数。 getFloat32:读取4个字节,返回一个32位浮点数。 getFloat64:读取8个字节,返回一个64位浮点数。 如果一次读取两个或两个以上字节,就必须明确数据的存储方式,到底是小端字节序还是大端字节序。默认情况下,DataView的get方法使用大端字节序解读数据,如果需要使用小端字节序解读,必须在get方法的第二个参数指定true。
第13章 Set和Map数据结构
Set
可以利用Set去除数组重复成员:
// 去除数组的重复成员
[...new Set(array)]
Set的属性: Set.prototype.constructor:构造函数,默认就是Set函数。 Set.prototype.size:返回Set实例的成员总数。
Set的操作方法: add(value):添加某个值,返回Set结构本身。 delete(value):删除某个值,返回一个布尔值,表示删除是否成功。 has(value):返回一个布尔值,表示该值是否为Set的成员。 clear():清除所有成员,没有返回值。
Set的遍历方法: keys():返回键名的遍历器,由于Set结构没有键名,只有键值(或者说键名和键值是同一个值),所以key方法和value方法的行为完全一致。 values():返回键值的遍历器 entries():返回键值对的遍历器 forEach():使用回调函数遍历每个成员
let set = new Set([1, 2, 3]);
set.forEach((value, key) => console.log(value * 2) )
// 2
// 4
// 6
Set的遍历顺序就是插入顺序
。
使用Set可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference):
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);
// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}
// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}
// 差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}
WeakSet
WeakSet与Set有两个区别:
- WeakSet的成员只能是对象,而不能是其他类型的值。
- WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于WeakSet之中。这个特点意味着,无法引用WeakSet的成员(WeakSet没有size属性),因此WeakSet是不可遍历的。 WeakSet的一个用处,是储存DOM节点,而不用担心这些节点从文档移除时,会引发内存泄漏。
Map
Map数据结构类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键:
var m = new Map();
var o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
只有对同一个对象的引用,Map结构才将其视为同一个键,Map的键实际上是跟内存地址绑定的。
Map的操作方法和遍历方法与Set类似,略。
如果所有Map的键都是字符串,它可以转为对象:
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
Map转为JSON要区分两种情况。一种情况是,Map的键名都是字符串,这时可以选择转为对象JSON:
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'
另一种情况是,Map的键名有非字符串,这时可以选择转为数组JSON:
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
WeakMap
WeakMap结构与Map结构基本类似,唯一的区别是它只接受对象作为键名(null除外),不接受其他类型的值作为键名,而且键名所指向的对象,不计入垃圾回收机制。 WeakMap的设计目的在于,键名是对象的弱引用(垃圾回收机制不将该引用考虑在内),所以其所对应的对象可能会被自动回收。当对象被回收后,WeakMap自动移除对应的键值对。典型应用是,一个对应DOM元素的WeakMap结构,当某个DOM元素被清除,其所对应的WeakMap记录就会自动被移除。基本上,WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。 WeakMap与Map在API上的区别主要是两个,一是没有遍历操作(即没有key()、values()和entries()方法),也没有size属性;二是无法清空,即不支持clear方法。这与WeakMap的键不被计入引用、被垃圾回收机制忽略有关。因此,WeakMap只有四个方法可用:get()、set()、has()、delete()。
WeakMap应用的典型场合就是DOM节点作为键名:
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
myWeakmap.set(myElement, {timesClicked: 0});
myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
myWeakmap.set(myElement, logoData);
}, false);
第14章 Iterator和for…of循环
Iterator的作用有三个:
- 为各种数据结构,提供一个统一的、简便的访问接口;
- 使得数据结构的成员能够按某种次序排列;
- ES6创造了一种新的遍历命令for…of循环,Iterator接口主要供for…of消费。
Iterator的遍历过程:
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
- 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
- 不断调用指针对象的next方法,直到它指向数据结构的结束位置。 每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。
由于Iterator只是把接口规格加到数据结构之上,所以,遍历器与它所遍历的那个数据结构,实际上是分开的,完全可以写出没有对应数据结构的遍历器对象,或者说用遍历器对象模拟出数据结构:
// 无限运行的遍历器对象
var it = idMaker();
it.next().value // '0'
it.next().value // '1'
it.next().value // '2'
// ...
function idMaker() {
var index = 0;
return {
next: function() {
return {value: index++, done: false};
}
};
}
数据结构的默认Iterator接口
ES6规定,默认的Iterator接口部署在数据结构的Symbol.iterator属性。一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。调用Symbol.iterator方法,就会得到当前数据结构默认的遍历器生成函数。Symbol.iterator本身是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为Symbol的特殊值,所以要放在方括号内。一个对象如果要有可被for…of循环调用的Iterator接口,就必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上的对象具有该方法也可):
class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}
[Symbol.iterator]() { return this; }
next() {
var value = this.value;
if (value < this.stop) {
this.value++;
return {done: false, value: value};
} else {
return {done: true, value: undefined};
}
}
}
function range(start, stop) {
return new RangeIterator(start, stop);
}
for (var value of range(0, 3)) {
console.log(value);
}
对象(Object)之所以没有默认部署Iterator接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。
对于类似数组的对象(存在数值键名和length属性),部署Iterator接口,有一个简便方法,就是Symbol.iterator方法直接引用数组的Iterator接口:
let iterable = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
console.log(item); // 'a', 'b', 'c'
}
注意,普通对象部署数组的Symbol.iterator方法,并无效果:
let iterable = {
a: 'a',
b: 'b',
c: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
console.log(item); // undefined, undefined, undefined
}
默认调用Iterator接口(即Symbol.iterator方法)的场合
- 解构赋值
- 扩展运算符
- yield*
- 由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口,如: for…of Array.from() Map(), Set(), WeakMap(), WeakSet()(比如new Map([[‘a’,1],[‘b’,2]])) Promise.all() Promise.race()
字符串的Iterator接口
字符串是一个类似数组的对象,也原生具有Iterator接口。
var someString = "hi";
typeof someString[Symbol.iterator]
// "function"
var iterator = someString[Symbol.iterator]();
iterator.next() // { value: "h", done: false }
iterator.next() // { value: "i", done: false }
iterator.next() // { value: undefined, done: true }
Iterator接口与Generator函数
let obj = {
* [Symbol.iterator]() {
yield 'hello';
yield 'world';
}
};
for (let x of obj) {
console.log(x);
}
// hello
// world
遍历器对象的return(),throw()
遍历器对象除了具有next方法,还可以具有return方法和throw方法。如果自己写遍历器对象生成函数,那么next方法是必须部署的,return方法和throw方法是否部署是可选的。 return方法的使用场合是,如果for…of循环提前退出(通常是因为出错,或者有break语句或continue语句),就会调用return方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法。
function readLinesSync(file) {
return {
next() {
if (file.isAtEndOfFile()) {
file.close();
return { done: true };
}
},
return() {
file.close();
return { done: true };
},
};
}
for (let line of readLinesSync(fileName)) {
console.log(x); // 触发return()方法
break;
}
throw方法主要是配合Generator函数使用,一般的遍历器对象用不到这个方法。
for…of循环
一个数据结构只要部署了Symbol.iterator属性,就被视为具有iterator接口,就可以用for…of循环遍历它的成员。
第15章 Generator 函数
调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object)。必须调用遍历器对象的next方法,使得指针移向下一个状态,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield语句(或return语句)为止。换言之,Generator函数是分段执行的,yield语句是暂停执行的标记,而next方法可以恢复执行。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
yield语句
遍历器对象的next方法的运行逻辑如下:
- 遇到yield语句,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
- 下一次调用next方法时,再继续往下执行,直到遇到下一个yield语句。
- 如果没有再遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
- 如果该函数没有return语句,则返回的对象的value属性值为undefined。
yield语句与return语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行
,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield语句。正常函数只能返回一个值,因为只能执行一次return;Generator函数可以返回一系列的值,因为可以有任意多个yield。从另一个角度看,也可以说Generator生成了一系列的值。
由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator接口(此时不再需要调用next方法):
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
next方法的参数
yield句本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。
function* f() {
for(var i=0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
通过next方法的参数,就有办法在Generator函数开始运行之后,继续向函数体内部注入值。也就是说,可以在Generator函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
注意,由于next方法的参数表示上一个yield语句的返回值,所以第一次使用next方法时,不能带有参数。V8引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。
Generator.prototype.throw()
Generator函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在Generator函数体内捕获。
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b
上面代码中,遍历器对象i连续抛出两个错误。第一个错误被Generator函数体内的catch语句捕获。i第二次抛出错误,由于Generator函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了Generator函数体,被函数体外的catch语句捕获。
注意,不要混淆遍历器对象的throw方法和全局的throw命令
。上面代码的错误,是用遍历器对象的throw方法抛出的,而不是用throw命令抛出的。后者只能被函数体外的catch语句捕获。
如果Generator函数内部没有部署try…catch代码块,那么throw方法抛出的错误,将被外部try…catch代码块捕获:
var g = function* () {
while (true) {
yield;
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 外部捕获 a
throw方法被捕获以后,会附带执行下一条yield语句。也就是说,会附带执行一次next方法:
var gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}
var g = gen();
g.next() // a
g.throw() // b
g.next() // c
Generator函数体内抛出的错误,也可以被函数体外的catch捕获:
function *foo() {
var x = yield 3;
var y = x.toUpperCase();
yield y;
}
var it = foo();
it.next(); // { value:3, done:false }
try {
it.next(42);
} catch (err) {
console.log(err);
}
一旦Generator执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next方法,将返回一个value属性等于undefined、done属性等于true的对象,即JavaScript引擎认为这个Generator已经运行结束了。
Generator.prototype.return()
Generator函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历Generator函数。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
如果Generator函数内部有try…finally代码块,那么return方法会推迟到finally代码块执行完再执行:
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers()
g.next() // { done: false, value: 1 }
g.next() // { done: false, value: 2 }
g.return(7) // { done: false, value: 4 }
g.next() // { done: false, value: 5 }
g.next() // { done: true, value: 7 }
yield*语句
如果在Generater函数内部,调用另一个Generator函数,默认情况下是没有效果的。yield*语句,用来在一个Generator函数里面执行另一个Generator函数。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
yield后面的Generator函数(没有return语句时),不过是for…of的一种简写形式,完全可以用后者替代前者。有return语句时,则需要用var value = yield iterator的形式获取return语句的值。实际上,任何数据结构只要有Iterator接口,就可以被yield*遍历。
作为对象属性的Generator函数
如果一个对象的属性是Generator函数,可以简写成下面的形式:
let obj = {
* myGeneratorMethod() {
···
}
};
Generator函数的this
(略)
Generator与协程
一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。它与普通的线程很相似,都有自己的执行上下文、可以分享全局变量。它们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停状态。此外,普通的线程是抢先式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。 从实现上看,在内存中,子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行。
Generator函数是ECMAScript 6对协程的实现,但属于不完全实现。Generator函数被称为“半协程”(semi-coroutine),意思是只有Generator函数的调用者,才能将程序的执行权还给Generator函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。 如果将Generator函数当作协程,完全可以将多个需要互相协作的任务写成Generator函数,它们之间使用yield语句交换控制权。
Generator的应用
(略)
第16章 Promise对象
ES6原生提供了Promise对象。 Promise对象有以下两个特点。
- 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称Fulfilled)和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。 有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。 Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。 Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject: ```javascript var promise = new Promise(function(resolve, reject) { // … some code
if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } });
resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从Pending变为Resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从Pending变为Rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise实例生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数:
```javascript
promise.then(function(value) {
// success
}, function(error) {
// failure
});
Promise新建后就会立即执行,然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以“Resolved”最后输出。
reject函数的参数通常是Error对象的实例,表示抛出的错误;resolve函数的参数除了正常的值以外,还可能是另一个Promise实例,表示异步操作的结果有可能是一个值,也有可能是另一个异步操作:
var p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
var p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail
这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是Pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是Resolved或者Rejected,那么p2的回调函数将会立刻执行.
Promise.prototype.then()
它的作用是为Promise实例添加状态改变时的回调函数。then方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法:
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
Promise.prototype.catch()
Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。
getJSON("/posts.json").then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。 建议总是使用catch方法,而不使用then方法的第二个参数。
跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数,Promise对象抛出的错误不会传递到外层代码,即不会有任何反应。
Promise.all()
Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。 var p = Promise.all([p1, p2, p3]); // p1、p2、p3都是Promise对象的实例,如果不是,就会先调用Promise.resolve方法,将参数转为Promise实例,再进一步处理
p的状态由p1、p2、p3决定,分成两种情况:
- 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
- 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。 ```javascript // 生成一个Promise对象的数组 var promises = [2, 3, 5, 7, 11, 13].map(function (id) { return getJSON(“/post/” + id + “.json”); });
Promise.all(promises).then(function (posts) { // … }).catch(function(reason){ // … });
### Promise.race()
Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例:
var p = Promise.race([p1,p2,p3]);
只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的回调函数。
```javascript
// 处理超时
var p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
])
p.then(response => console.log(response))
p.catch(error => console.log(error))
Promise.resolve()
将现有对象转为Promise对象。参数分成四种情况:
-
参数是一个Promise实例 如果参数是Promise实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
-
参数是一个thenable对象 thenable对象指的是具有then方法的对象,Promise.resolve会将这个对象转为Promise对象,然后就立即执行thenable对象的then方法。
-
参数不是具有then方法的对象,或根本就不是对象 Promise.resolve返回一个新的Promise对象,状态为Resolved。
-
不带有任何参数 直接返回一个Resolved状态的Promise对象。所以,如果希望得到一个Promise对象,比较方便的方法就是直接调用Promise.resolve方法。
需要注意的是,立即resolve的Promise对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时:
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one
// two
// three
Promise.reject()
Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected。它的参数用法与Promise.resolve方法完全一致。
done()
(略)
finally()
(略)
Generator函数与Promise的结合
(略)
第17章 异步操作和Async函数
回调函数、Promise
(略)
Generator函数执行异步任务
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
Thunk函数
var x = 1;
function f(m){
return m * 2;
}
f(x + 5)
传值调用
:即在进入函数体之前,就计算x + 5的值(等于6),再将这个值传入函数f 。C语言就采用这种策略。
f(x + 5)
// 传值调用时,等同于
f(6)
传名调用
:即直接将表达式x + 5传入函数体,只在用到它的时候求值。Haskell语言采用这种策略。
f(x + 5)
// 传名调用时,等同于
(x + 5) * 2
编译器的”传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做Thunk函数:
function f(m){
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};
function f(thunk){
return thunk() * 2;
}
凡是用到原参数的地方,对Thunk函数求值即可。这就是Thunk函数的定义,它是”传名调用”的一种实现策略,用来替换某个表达式。
JavaScript语言的Thunk函数
在JavaScript语言中,Thunk函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本)
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
var Thunk = function (fileName){
return function (callback){
return fs.readFile(fileName, callback);
};
};
任何函数,只要参数有回调函数,就能写成Thunk函数的形式:
// Thunk函数转换器
var Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
Thunkify模块
$ npm install thunkify
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
// ...
});
Generator 函数的流程管理
ES6有了Generator函数,Thunk函数现在可以用于Generator函数的自动流程管理:
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
function* g() {
// ...
}
run(g);
co模块
async函数
async函数就是Generator函数的语法糖。
var fs = require('fs');
var readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
写成async函数:
var asyncReadFile = async function (){
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
async函数对 Generator 函数的改进,体现在以下四点。
-
内置执行器。Generator函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。 var result = asyncReadFile(); 上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像Generator函数,需要调用next方法,或者用co模块,才能得到真正执行,得到最后结果。
-
更好的语义。async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
-
更广的适用性。 co模块约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面,可以是Promise对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
-
返回值是Promise。async函数的返回值是Promise对象,这比Generator函数的返回值是Iterator对象方便多了。可以用then方法指定下一步的操作。
进一步说,async函数完全可以看作多个异步操作,包装成的一个Promise对象,而await命令就是内部then命令的语法糖。
async函数语法
- async函数返回一个Promise对象 async函数内部return语句返回的值,会成为then方法回调函数的参数: ```javascript async function f() { return ‘hello world’; }
f().then(v => console.log(v)) // “hello world”
async函数内部抛出错误,会导致返回的Promise对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。
2. async函数返回的Promise对象,必须等到内部所有await命令的Promise对象执行完,才会发生状态改变。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
3. 正常情况下,await命令后面是一个Promise对象。如果不是,会被转成一个立即resolve的Promise对象。
4. 如果await后面的异步操作出错,那么等同于async函数返回的Promise对象被reject。
### async函数的实现
async 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里:
```javascript
async function fn(args){
// ...
}
// 等同于
function fn(args){
return spawn(function*() {
// ...
});
}
所有的async函数都可以写成上面的第二种形式,其中的 spawn 函数就是自动执行器。
(略)
第18章 Class
基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。ES6的类,完全可以看作构造函数的另一种写法:
class Point {
// ...
}
typeof Point // "function"
Point === Point.prototype.constructor // true
使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。
构造函数的prototype属性,在ES6的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。
class Point {
constructor(){
// ...
}
toString(){
// ...
}
toValue(){
// ...
}
}
// 等同于
Point.prototype = {
toString(){},
toValue(){}
};
在类的实例上面调用方法,其实就是调用原型上的方法:
class B {}
let b = new B();
b.constructor === B.prototype.constructor // true
prototype对象的constructor属性,直接指向“类”的本身,这与ES5的行为是一致的:
Point.prototype.constructor === Point // true
类的内部所有定义的方法,都是不可枚举的(non-enumerable)。这一点与ES5的行为不一致:
// ES 6
class Point {
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
// ES 5
var Point = function (x, y) {
// ...
};
Point.prototype.toString = function() {
// ...
};
Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
constructor方法
constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。 类的构造函数,不使用new是没法调用的,会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。
类的实例对象
与ES5一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var point = new Point(2, 3);
point.toString() // (2, 3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false !!!
point.__proto__.hasOwnProperty('toString') // true
与ES5一样,类的所有实例共享一个原型对象:
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__
//true
不存在变量提升:
new Foo(); // ReferenceError
class Foo {}
与函数一样,类也可以使用表达式的形式定义:
const MyClass = class {
getClassName() {
return Me.name;
}
};
ES6的类不提供私有方法,可以利用Symbol来模拟:
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
// 公有方法
foo(baz) {
this[bar](baz);
}
// 私有方法
[bar](baz) {
return this[snaf] = baz;
}
// ...
};
this的指向
类的方法内部如果含有this,它默认指向类的实例,单独使用方法时容易出错:
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
应该使用箭头函数:
class Logger {
constructor() {
this.printName = (name = 'there') => {
this.print(`Hello ${name}`);
};
}
// ...
}
类和模块的内部,默认就是严格模式。
Class的继承
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。
类的prototype属性和__proto__属性
Class作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
- 子类的__proto__属性,表示
构造函数的继承
,总是指向父类。 - 子类prototype属性的__proto__属性,表示
方法的继承
,总是指向父类的prototype属性。 ```javascript class A { }
class B extends A { }
B.proto === A // true B.prototype.proto === A.prototype // true
这样的结果是因为,类的继承是按照下面的模式实现的:
```javascript
class A {
}
class B {
}
// B的实例继承A的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B继承A的静态属性
Object.setPrototypeOf(B, A);
这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(__proto__属性)是父类(A);作为一个构造函数,子类(B)的原型(prototype属性)是父类的实例。
Extends 的继承目标
只要是一个有prototype属性的函数,就能被继承。由于函数都有prototype属性(除了Function.prototype函数),因此可以是任意函数。
子类继承Object类:
class A extends Object {
}
A.__proto__ === Object // true
A.prototype.__proto__ === Object.prototype // true
这种情况下,A其实就是构造函数Object的复制,A的实例就是Object的实例。
不存在任何继承:
class A {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true
这种情况下,A作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承Funciton.prototype。但是,A调用后返回一个空对象(即Object实例),所以A.prototype.__proto__指向构造函数(Object)的prototype属性。
子类继承null:
class A extends null {
}
A.__proto__ === Function.prototype // true
A.prototype.__proto__ === undefined // true
Object.getPrototypeOf()
Object.getPrototypeOf方法可以用来从子类上获取父类。
Object.getPrototypeOf(ColorPoint) === Point
// true
因此,可以使用这个方法判断,一个类是否继承了另一个类。
super关键字
super这个关键字,有两种用法,含义不同。
- 作为函数调用时(即super(…args)),super代表父类的构造函数。
- 作为对象调用时(即super.prop或super.method()),super代表父类。注意,此时super即可以引用父类实例的属性和方法,也可以引用父类的静态方法。
实例的__proto__属性
子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。
原生构造函数的继承
ECMAScript的原生构造函数大致有下面这些: Boolean() Number() String() Array() Date() Function() RegExp() Error() Object() ES6允许继承原生构造函数定义子类,因为ES6是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。
Class的取值函数(getter)和存值函数(setter)
与ES5一样,在Class内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
Class的静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
Class的静态属性和实例属性
ES6明确规定,Class内部只有静态方法,没有静态属性。 目前只有这种方法可行:
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
new.target属性
new是从构造函数生成实例的命令。ES6为new命令引入了一个new.target属性,(在构造函数中)返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。需要注意的是,子类继承父类时,new.target会返回子类。
Mixin模式的实现
(略)
第19章 修饰器
修饰器(Decorator)是一个函数,用来修改类的行为。这是ES7的一个提案。修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码:
function testable(target) {
target.isTestable = true;
}
@testable
class MyTestableClass {}
console.log(MyTestableClass.isTestable) // true
修饰器本质就是编译时执行的函数。修饰器函数的第一个参数,就是所要修饰的目标类。
可以在修饰器外面再封装一层函数来为修饰器增加参数:
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true
@testable(false)
class MyClass {}
MyClass.isTestable // false
方法的修饰
修饰器函数一共可以接受三个参数,第一个参数是所要修饰的目标对象,第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象。
class Person {
@nonenumerable
get kidCount() { return this.children.length; }
}
function nonenumerable(target, name, descriptor) {
descriptor.enumerable = false;
return descriptor;
}
为什么修饰器不能用于函数?
修饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。
var counter = 0;
var add = function () {
counter++;
};
@add
function foo() {
}
因为函数提升,使得实际执行的代码是下面这样:
var counter;
var add;
@add
function foo() {
}
counter = 0;
add = function () {
counter++;
};
core-decorators.js
(略)
使用修饰器实现自动发布事件
(略)
Mixin
(略)
Trait
(略)
第20章 Module
在ES6之前,社区制定了一些模块加载方案,最主要的有CommonJS和AMD两种。前者用于服务器,后者用于浏览器。ES6在语言规格的层面上,实现了模块功能,而且实现得相当简单,完全可以取代现有的CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。
ES6模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS和AMD模块,都只能在运行时确定这些东西
。比如,CommonJS模块就是对象,输入时必须查找对象属性。
// CommonJS模块
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat, exists = _fs.exists, readfile = _fs.readfile;
上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取3个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6模块不是对象,而是通过export命令显式指定输出的代码,输入时也采用静态命令的形式。
// ES6模块
import { stat, exists, readFile } from 'fs';
上面代码的实质是从fs模块加载3个方法,其他方法不加载。这种加载称为“编译时加载”,即ES6可以在编译时就完成模块加载,效率要比CommonJS模块的加载方式高
。由于ES6模块是编译时加载,使得静态分析成为可能。
浏览器使用ES6模块的语法如下:
<script type="module" src="foo.js"></script>
ES6的模块自动采用严格模式
严格模式主要有以下限制: 变量必须声明后再使用 函数的参数不能有同名属性,否则报错 不能使用with语句 不能对只读属性赋值,否则报错 不能使用前缀0表示八进制数,否则报错 不能删除不可删除的属性,否则报错 不能删除变量delete prop,会报错,只能删除属性delete global[prop] eval不会在它的外层作用域引入变量 eval和arguments不能被重新赋值 arguments不会自动反映函数参数的变化 不能使用arguments.callee 不能使用arguments.caller 禁止this指向全局对象 不能使用fn.caller和fn.arguments获取函数调用的堆栈 增加了保留字(比如protected、static和interface)
export命令
export命令用于规定模块的对外接口。一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。
需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系:
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。
export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值
。
这一点与CommonJS规范完全不同。CommonJS模块输出的是值的缓存,不存在动态更新。
export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了ES6模块的设计初衷。
import命令
import命令接受一个对象(用大括号表示),里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块对外接口的名称相同。如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。
注意,import命令具有提升效果,会提升到整个模块的头部,首先执行:
foo(); // 不会报错,因为import的执行早于foo的调用
import { foo } from 'my_module';
模块的整体加载
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
export default命令
// export-default.js
export default function () {
console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'
export default命令用于指定模块的默认输出。本质上,export default就是输出一个叫做default的变量或方法,然后系统允许导入时为它取任意名字。
export default也可以用来输出类:
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass';
let o = new MyClass();
模块的继承
export * from 'circle'; // 继承了circle模块
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
export *命令会忽略circle模块的default方法。然后,上面代码又输出了自定义的e变量和默认方法。
ES6模块加载的实质
CommonJS模块输出的是一个值的拷贝,而ES6模块输出的是值的引用。 ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个动态的只读引用。等到真的需要用到时,再到模块里面去取值,换句话说,ES6的输入有点像Unix系统的“符号连接”,原始值变了,import输入的值也会跟着变。因此,ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
export通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。
// mod.js
function C() {
this.sum = 0;
this.add = function () {
this.sum += 1;
};
this.show = function () {
console.log(this.sum);
};
}
export let c = new C();
// x.js
import {c} from './mod';
c.add();
// y.js
import {c} from './mod';
c.show();
// main.js
import './x';
import './y';
$ babel-node main.js 1
循环加载
CommonJS模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。
ES6处理“循环加载”与CommonJS有本质的不同。ES6模块是动态引用,如果使用import从一个模块加载变量(即import foo from ‘foo’),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。(即,可能会由于循环加载导致取到的值为undefined)