《阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版》 - 书栈网 · BookStack
var function let const import class
前面两个是es5的,后面是es6的,let const 的好处是块变量:只在声明所在的块级作用域内有效;
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。
window
,但 Node 和 Web Worker 没有window
。self
也指向顶层对象,但是 Node 没有self
。global
,但其他环境都不支持。同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this
变量,但是有局限性。
this
会返回顶层对象。但是,Node 模块和 ES6 模块中,this
返回的是当前模块。this
,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this
会指向顶层对象。但是,严格模式下,这时this
会返回undefined
。new Function('return this')()
,总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么eval
、new Function
这些方法都可能无法使用。综上所述,很难找到一种方法,可以在所有情况下,都取到顶层对象。下面是两种勉强可以使用的方法。
// 方法一
(typeof window !== 'undefined'
? window
: (typeof process === 'object' &&
`typeof require === 'function' &&`
`typeof global === 'object')`
`? global`
`: this);`
// 方法二
var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};
es6采用的方式是globalThis
,都可以从它拿到顶层对象
数组字符串解构按顺序
let [a, b, c] = [1, 2, 3];
const [a, b, c, d, e] = 'hello';
对象解构按对象名
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
数值和布尔值解构
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
函数解构
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]
结构默认值:
var {x = 3} = {};
x // 3
var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"
这里要注意:
// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error
上面代码的写法会报错,因为 JavaScript 引擎会将{x}
理解成一个代码块,从而发生语法错误。
// 正确的写法
let x;
({x} = {x: 1});
不能使用圆括号
(1)变量声明语句
(2)函数参数
(3)赋值语句的模式
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
$('#result').append(`
There are ${basket.count} items
in your basket, ${basket.onSale}
are on sale!
`);
模板字符串前后的空格和换行可以使用trim()
来处理
` this is a template string `.trim()
“标签模板”的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。
let a = 5;
let b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);
ES6 提供了String.fromCodePoint()
方法,可以识别大于0xFFFF
的字符,弥补了String.fromCharCode()
方法的不足。在作用上,正好与下面的codePointAt()
方法相反。
String.fromCodePoint(0x20BB7)
// ""
String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'
// true
ES6 还为原生的 String 对象,提供了一个raw()
方法。该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法。
// `foo${1 + 2}bar`
// 等同于
String.raw({ raw: ['foo', 'bar'] }, 1 + 2) // "foo3bar"
语调符号和重音符号。为了表示它们,Unicode 提供了两种方法。一种是直接提供带重音符号的字符,比如Ǒ
(\u01D1)。另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符,比如O
(\u004F)和ˇ
(\u030C)合成Ǒ
(\u004F\u030C)。
'\u01D1'==='\u004F\u030C' //false
'\u01D1'.length // 1
'\u004F\u030C'.length // 2
'\u01D1'.normalize() === '\u004F\u030C'.normalize()
// true
传统上,JavaScript 只有indexOf
方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。和后面两个和python一致
let s = 'Hello world!';
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false
repeat
方法返回一个新字符串,表示将原字符串重复n
次。小数会向下取整,负数或者Infinity
会报错
'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""
ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()
用于头部补全,padEnd()
用于尾部补全。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
ES2019 对字符串实例新增了trimStart()
和trimEnd()
这两个方法。它们的行为与trim()
一致,trimStart()
消除字符串头部的空格,trimEnd()
消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。浏览器还部署了额外的两个方法,trimLeft()
是trimStart()
的别名,trimRight()
是trimEnd()
的别名。
const s = ' abc ';
s.trim() // "abc"
s.trimStart() // "abc "
s.trimEnd() // " abc"
matchAll()
方法返回一个正则表达式在当前字符串的所有匹配,详见《正则的扩展》的一章。
感觉和现在的python差不多,这里要注意是exec进行调用
const RE_OPT_A = /^(?
const matchObj = RE_OPT_A.exec('');
matchObj.groups.as // undefined
'as' in matchObj.groups // true
字符串对象共有 4 个方法,可以使用正则表达式:match()
、replace()
、search()
和split()
。
ES6 将这 4 个方法,在语言内部全部调用RegExp
的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp
对象上。
String.prototype.match
调用 RegExp.prototype[Symbol.match]
String.prototype.replace
调用 RegExp.prototype[Symbol.replace]
String.prototype.search
调用 RegExp.prototype[Symbol.search]
String.prototype.split
调用 RegExp.prototype[Symbol.split]
ES6 在Number
对象上,新提供了Number.isFinite()
和Number.isNaN()
两个方法。它们与传统的全局方法isFinite()
和isNaN()
的区别在于,传统方法先调用Number()
将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,Number.isFinite()
对于非数值一律返回false
, Number.isNaN()
只有对于NaN
才返回true
,非NaN
一律返回false
。
ES6 将全局方法parseInt()
和parseFloat()
,移植到Number
对象上面,行为完全保持不变。其功能是解析string转化为int或者float
Number.parseInt('12.34') // 12
Number.parseFloat('123.45#') // 123.45
Number.isInteger()
用来判断一个数值是否为整数。
Number.isInteger(5E-324) // false
Number.isInteger(5E-325) // true
Number.MIN_VALUE
(5E-324)会被自动转为 0,上面代码中,5E-325
由于值太小,会被自动转为0,因此返回true
。ES6 在Number
对象上面,新增一个极小的常量Number.EPSILON
。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。Number.EPSILON
实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。
0.1 + 0.2 === 0.3 // false
JavaScript 能够准确表示的整数范围在-2^53
到2^53
之间(不含两个端点),超过这个范围,无法精确表示这个值。ES6 引入了Number.MAX_SAFE_INTEGER
和Number.MIN_SAFE_INTEGER
这两个常量,用来表示这个范围的上下限。Number.isSafeInteger()
则是用来判断一个整数是否落在这个范围之内。
JavaScript 所有数字都保存成 64 位浮点数,这给数值的表示带来了两大限制。一是数值的精度只能到 53 个二进制位(相当于 16 个十进制位),大于这个范围的整数,JavaScript 是无法精确表示的,这使得 JavaScript 不适合进行科学和金融方面的精确计算。二是大于或等于2的1024次方的数值,JavaScript 无法表示,会返回Infinity
。
// 超过 53 个二进制位的数值,无法保持精度
Math.pow(2, 53) === Math.pow(2, 53) + 1 // true
// 超过 2 的 1024 次方的数值,无法表示
Math.pow(2, 1024) // Infinity
如果有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined
。ES2017 允许函数的最后一个参数有尾逗号(trailing comma)。
// 例一
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1]
// 例二
function f(x, y = 5, z) {
return [x, y, z];
}
f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 报错
f(1, undefined, 2) // [1, 5, 2]
指定了默认值以后,函数的length
属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length
属性将失真。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
如果设置了默认值的参数不是尾参数,那么length
属性也不再计入后面的参数了。
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
ES6 引入 rest 参数(形式为...变量名
),用于获取函数的多余参数,这样就不需要使用arguments
对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。下面是一个 rest 参数代替arguments
变量的例子。
// arguments变量的写法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort();
}
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
arguments
对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.prototype.slice.call
先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。下面是一个利用 rest 参数改写数组push
方法的例子。
注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
console.log(item);
});
}
var a = [];
push(a, 1, 2, 3)
ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。
function doSomething(a, b) {
'use strict';
// code
}
函数的name
属性,返回该函数的函数名。
const bar = function baz() {};
bar.name // "baz"
箭头函数
ES6 允许使用“箭头”(=>
)定义函数。
var f = v => v;
// 等同于
var f = function (v) {
return v;
};
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言(比如 Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。
扩展运算符(spread)是三个点(...
)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
复制数组
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;
合并数组
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];
// ES5 的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]
// ES6 的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
两种转化数组的方式,array.from() array.of()
Array.from
方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
Array.of
方法用于将一组值,转换为数组。
数组实例的find
方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true
的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined
。
[1, 4, -5, 10].find((n) => n < 0)
// -5
上面代码找出数组中第一个小于 0 的成员。
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
数组实例的findIndex
方法的用法与find
方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1
。
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
fill
方法使用给定值,填充一个数组。
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
数组也是键值对组成的['a', 'b'] 相当于 {0: 'a', 1: 'b'}
,其键就是序号,其值是本身;
entries()
,keys()
和values()
——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用for...of
循环进行遍历,唯一的区别是keys()
是对键名的遍历、values()
是对键值的遍历,entries()
是对键值对的遍历。
数组的成员有时还是数组,Array.prototype.flat()
用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。
[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]
[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]
flatMap()
方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()
),然后对返回值组成的数组执行flat()
方法。该方法返回一个新数组,不改变原数组。
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
变量foo
直接写在大括号里面。这时,属性名就是变量名, 属性值就是变量值。下面是另一个例子。
let birth = '2000/01/01';
const Person = {
name: '张三',
//等同于birth: birth
birth,
// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }
};
ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。
let lastWord = 'last word';
const a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
注意,属性名表达式如果是一个对象{},默认情况下会自动将对象转为字符串[object Object]
,这一点要特别小心。
我们知道,this
关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super
,指向当前对象的原型对象。
三种super
的用法都会报错,因为对于 JavaScript 引擎来说,这里的super
都没有用在对象的方法之中。第一种写法是super
用在属性里面,第二种和第三种写法是super
用在一个函数里面,然后赋值给foo
属性。目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。
// 报错
const obj = {
foo: super.foo
}
// 报错
const obj = {
foo: () => super.foo
}
// 报错
const obj = {
foo: function () {
return super.foo
}
}
对象的拓展运算符
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
let { x, ...y, ...z } = someObject; // 句法错误
let { ...x, y, z } = someObject; // 句法错误
let { x, ...y, ...z } = someObject; // 句法错误
注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。
let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2
ES2020 引入了“链判断运算符”(optional chaining operator)?.
const firstName = (message
&& message.body
&& message.body.user
&& message.body.user.firstName) || 'default';
const firstName = message?.body?.user?.firstName || 'default';
const fooValue = myForm.querySelector('input[name=foo]')?.value
相等运算符(==
)和严格相等运算符(===
)ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is
就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
Object.assign
方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
Object.assign
方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
const obj1 = {a: {b: 1}};
const obj2 = Object.assign({}, obj1);
obj1.a.b = 2;
obj2.a.b // 2
对于这种嵌套的对象,一旦遇到同名属性,Object.assign
的处理方法是替换,而不是添加。
const target = { a: { b: 'c', d: 'e' } }
const source = { a: { b: 'hello' } }
Object.assign(target, source)
// { a: { b: 'hello' } }
Object.assign
可以用来处理数组,但是会把数组视为对象。
Object.assign
只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。
const source = {
get foo() { return 1 }
};
const target = {};
Object.assign(target, source)
// { foo: 1 }
ES2017 引入了Object.getOwnPropertyDescriptors()
方法,返回指定对象所有自身属性(非继承属性)的描述对象。
const obj = {
foo: 123,
get bar() { return 'abc' }
};
Object.getOwnPropertyDescriptors(obj)
// { foo:
// { value: 123,
// writable: true,
// enumerable: true,
// configurable: true },
// bar:
// { get: [Function: get bar],
// set: undefined,
// enumerable: true,
// configurable: true } }
__proto__
属性(前后各两个下划线),用来读取或设置当前对象的原型对象(prototype)。目前,所有浏览器(包括 IE11)都部署了这个属性。但是不推荐使用,实现上,__proto__
调用的是Object.prototype.__proto__
,具体实现如下。
Object.setPrototypeOf
方法的作用与__proto__
相同,用来设置一个对象的原型对象(prototype),返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。
Object.getPrototypeOf()
该方法与Object.setPrototypeOf
方法配套,用于读取一个对象的原型对象。
Object.keys(),Object.values(),Object.entries()
作为遍历一个对象的补充手段,供for...of
循环使用。
Object.fromEntries()
方法是Object.entries()
的逆操作,用于将一个键值对数组转为对象。
ES6 引入了一种新的原始数据类型Symbol
,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined
、null
、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...in
、for...of
循环中,也不会被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
返回。
但是,它也不是私有属性,有一个Object.getOwnPropertySymbols()
方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
const obj = {};
const foo = Symbol('foo');
obj[foo] = 'bar';
for (let i in obj) {
console.log(i); // 无输出
}
Object.getOwnPropertyNames(obj) // []
Object.getOwnPropertySymbols(obj) // [Symbol(foo)]
Set 结构的实例有以下属性。
Set.prototype.constructor
:构造函数,默认就是Set
函数。Set.prototype.size
:返回Set
实例的成员总数。Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。
Set.prototype.add(value)
:添加某个值,返回 Set 结构本身。Set.prototype.delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。Set.prototype.has(value)
:返回一个布尔值,表示该值是否为Set
的成员。Set.prototype.clear()
:清除所有成员,没有返回值。keys
方法、values
方法、entries
方法返回的都是遍历器对象(详见《Iterator 对象》一章)。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys
方法和values
方法的行为完全一致。
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。首先,WeakSet 的成员只能是对象,而不能是其他类型的值。其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
Map是JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
Map 结构的实例有以下属性和操作方法。
size
属性返回 Map 结构的成员总数。
set
方法设置键名key
对应的键值为value
,然后返回整个 Map 结构。如果key
已经有值,则键值会被更新,否则就新生成该键。
set
方法返回的是当前的Map
对象,因此可以采用链式写法。
get
方法读取key
对应的键值,如果找不到key
,返回undefined
。
has
方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
delete
方法删除某个键,返回true
。如果删除失败,返回false
。
clear
方法清除所有成员,没有返回值。
Map.prototype.keys()
:返回键名的遍历器。Map.prototype.values()
:返回键值的遍历器。Map.prototype.entries()
:返回所有成员的遍历器。Map.prototype.forEach()
:遍历 Map 的所有成员。(1)Map 转为数组
前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(...
)。
const myMap = new Map()
.set(true, 7)
.set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
(2)数组 转为 Map
将数组传入 Map 构造函数,就可以转为 Map。
new Map([
[true, 7],
[{foo: 3}, ['abc']]
])
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }
(3)Map 转为对象
如果所有 Map 的键都是字符串,它可以无损地转为对象。
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
`obj[k] = v;`
}
return obj;
}
const myMap = new Map()
.set('yes', true)
.set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
(4)对象转为 Map
对象转为 Map 可以通过Object.entries()
。
let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));
此外,也可以自己实现一个转换函数。
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
`strMap.set(k, obj[k]);`
}
return strMap;
}
objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}
(5)Map 转为 JSON
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"]]]'
(6)JSON 转为 Map
JSON 转为 Map,正常情况下,所有键名都是字符串。
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
WeakMap
结构与Map
结构类似,也是用于生成键值对的集合。WeakMap
与Map
的区别有两点。首先,WeakMap
只接受对象作为键名(null
除外),不接受其他类型的值作为键名。其次,WeakMap
的键名所指向的对象,不计入垃圾回收机制。
和weakset一样
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
下面是 Proxy 支持的拦截操作一览,一共 13 种。
proxy.foo
和proxy['foo']
。proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值。propKey in proxy
的操作,返回一个布尔值。delete proxy[propKey]
的操作,返回一个布尔值。Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。Object.preventExtensions(proxy)
,返回一个布尔值。Object.getPrototypeOf(proxy)
,返回一个对象。Object.isExtensible(proxy)
,返回一个布尔值。Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。new proxy(...args)
。var person = {
name: "张三"
};
var proxy = new Proxy(person, {
get: function(target, propKey) {
if (propKey in target) {
return target[propKey];
} else {
throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
}
}
});
proxy.name // "张三"
proxy.age // 抛出一个错误
Proxy.revocable()
方法返回一个可取消的 Proxy 实例。
Proxy.revocable()
方法返回一个对象,该对象的proxy
属性是Proxy
实例,revoke
属性是一个函数,可以取消Proxy
实例。上面代码中,当执行revoke
函数之后,再访问Proxy
实例,就会抛出一个错误。
Proxy.revocable()
的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
Reflect
对象与Proxy
对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect
对象的设计目的有这样几个。
(1) 将Object
对象的一些明显属于语言内部的方法(比如Object.defineProperty
),放到Reflect
对象上。现阶段,某些方法同时在Object
和Reflect
对象上部署,未来的新方法将只部署在Reflect
对象上。也就是说,从Reflect
对象上可以拿到语言内部的方法。
(2) 修改某些Object
方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)
在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)
则会返回false
。
(3) 让Object
操作都变成函数行为。某些Object
操作是命令式,比如name in obj
和delete obj[name]
,而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
让它们变成了函数行为。
(4)Reflect
对象的方法与Proxy
对象的方法一一对应,只要是Proxy
对象的方法,就能在Reflect
对象上找到对应的方法。这就让Proxy
对象可以方便地调用对应的Reflect
方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy
怎么修改默认行为,你总可以在Reflect
上获取默认行为。
Reflect
对象一共有 13 个静态方法。
上面这些方法的作用,大部分与Object
对象的同名方法的作用都是相同的,而且它与Proxy
对象的方法是一一对应的。下面是对它们的解释。
Promise
对象有以下两个特点。
(1)对象的状态不受外界影响。Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise
这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
我们可以将图片的加载写成一个Promise
,一旦加载完成,Promise
的状态就发生变化。
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
同时要注意:
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one
// two
// three
上面代码中,setTimeout(fn, 0)
在下一轮“事件循环”开始时执行,Promise.resolve()
在本轮“事件循环”结束时执行,console.log('one')
则是立即执行,因此最先输出。
实际开发中,经常遇到一种情况:不知道或者不想区分,函数f
是同步函数还是异步操作,但是想用 Promise 来处理它。因为这样就可以不管f
是否包含异步操作,都用then
方法指定下一步流程,用catch
方法处理f
抛出的错误。一般就会采用下面的写法。
由于Promise.try
为所有操作提供了统一的处理机制,所以如果想用then
方法管理流程,最好都用Promise.try
包装一下,其中一点就是可以更好地管理异常。
Promise.try(() => database.users.get({id: userId}))
.then(...)
.catch(...)
基本操作
ES6 规定,Promise
对象是一个构造函数,用来生成Promise
实例。
下面代码创造了一个Promise
实例。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
`resolve(value);`
} else {
`reject(error);`
}
});
Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve
函数的作用是,将Promise
对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject
函数的作用是,将Promise
对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise
实例生成以后,可以用then
方法分别指定resolved
状态和rejected
状态的回调函数。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
then
方法可以接受两个回调函数作为参数。第一个回调函数是Promise
对象的状态变为resolved
时调用,第二个回调函数是Promise
对象的状态变为rejected
时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise
对象传出的值作为参数。
getJSON("/post/1.json").then(
post => getJSON(post.commentURL)
).then(
comments => console.log("resolved: ", comments),
err => console.log("rejected: ", err)
);
Promise.prototype.catch()
方法是.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
上面代码中,getJSON()
方法返回一个 Promise 对象,如果该对象状态变为resolved
,则会调用then()
方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected
,就会调用catch()
方法指定的回调函数,处理这个错误。另外,then()
方法指定的回调函数,如果运行中抛出错误,也会被catch()
方法捕获。
p.then((val) => console.log('fulfilled:', val))
.catch((err) => console.log('rejected', err));
// 等同于
p.then((val) => console.log('fulfilled:', val))
.then(null, (err) => console.log("rejected:", err));
finally()
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代码中,不管promise
最后的状态,在执行完then
或catch
指定的回调函数以后,都会执行finally
方法指定的回调函数。
下面是一个例子,服务器使用 Promise 处理请求,然后使用finally
方法关掉服务器。
server.listen(port)
.then(function () {
`// ...`
})
.finally(server.stop);
finally
方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled
还是rejected
。这表明,finally
方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
Promise.all()
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all()
方法接受一个数组作为参数,p1
、p2
、p3
都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve
方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()
方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。
p
的状态由p1
、p2
、p3
决定,分成两种情况。
(1)只有p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。
(2)只要p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
Promise.race()
方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
复制代码
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数。
Promise.race()
方法的参数与Promise.all()
方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve()
方法,将参数转为 Promise 实例,再进一步处理。
下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject
,否则变为resolve
。
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
`setTimeout(() => reject(new Error('request timeout')), 5000)`
})
]);
p
.then(console.log)
.catch(console.error);
上面代码中,如果 5 秒之内fetch
方法无法返回结果,变量p
的状态就会变为rejected
,从而触发catch
方法指定的回调函数。
Promise.allSettled()
Promise.allSettled()
方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled
还是rejected
,包装实例才会结束。该方法由 ES2020 引入。
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];
await Promise.allSettled(promises);
removeLoadingIndicator();
上面代码对服务器发出三个请求,等到三个请求都结束,不管请求成功还是失败,加载的滚动图标就会消失。
该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled
,不会变成rejected
。状态变成fulfilled
后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()
的 Promise 实例。
const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
console.log(results);
});
// [
// { status: 'fulfilled', value: 42 },
// { status: 'rejected', reason: -1 }
// ]
上面代码中,Promise.allSettled()
的返回值allSettledPromise
,状态只可能变成fulfilled
。它的监听函数接收到的参数是数组results
。该数组的每个成员都是一个对象,对应传入Promise.allSettled()
的两个 Promise 实例。每个对象都有status
属性,该属性的值只可能是字符串fulfilled
或字符串rejected
。fulfilled
时,对象有value
属性,rejected
时有reason
属性,对应两种状态的返回值。
下面是返回值用法的例子。
const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);
// 过滤出成功的请求
const successfulPromises = results.filter(p => p.status === 'fulfilled');
// 过滤出失败的请求,并输出原因
const errors = results
.filter(p => p.status === 'rejected')
.map(p => p.reason);
有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,Promise.allSettled()
方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。Promise.all()
方法无法做到这一点。
const urls = [ /* ... */ ];
const requests = urls.map(x => fetch(x));
try {
await Promise.all(requests);
console.log('所有请求都成功。');
} catch {
console.log('至少一个请求失败,其他请求可能还没结束。');
}
上面代码中,Promise.all()
无法确定所有请求都结束。想要达到这个目的,写起来很麻烦,有了Promise.allSettled()
,这就很容易了。
Promise.any()
方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成fulfilled
状态,包装实例就会变成fulfilled
状态;如果所有参数实例都变成rejected
状态,包装实例就会变成rejected
状态。该方法目前是一个第三阶段的提案 。
Promise.any()
跟Promise.race()
方法很像,只有一点不同,就是不会因为某个 Promise 变成rejected
状态而结束。
var resolved = Promise.resolve(42);
var rejected = Promise.reject(-1);
var alsoRejected = Promise.reject(Infinity);
Promise.any([resolved, rejected, alsoRejected]).then(function (result) {
console.log(result); // 42
});
Promise.any([rejected, alsoRejected]).catch(function (results) {
console.log(results); // [-1, Infinity]
});
扩展运算符
只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。
let arr = [...iterable];
yield
yield*
后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
var iterator = generator();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }
其他场合
由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。
new Map([['a',1],['b',2]])
)generator 函数
let myIterable = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
}
}
[...myIterable] // [1, 2, 3]
// 或者采用下面的简洁写法
let obj = {
* [Symbol.iterator]() {
yield 'hello';
yield 'world';
}
};
for (let x of obj) {
console.log(x);
}
// "hello"
// "world"
遍历器对象除了具有next
方法,还可以具有return
方法和throw
方法。如果你自己写遍历器对象生成函数,那么next
方法是必须部署的,return
方法和throw
方法是否部署是可选的。
return
方法的使用场合是,如果for...of
循环提前退出(通常是因为出错,或者有break
语句),就会调用return
方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return
方法。
function readLinesSync(file) {
return {
`[Symbol.iterator]() {`
`return {`
`next() {`
`return { done: false };`
`},`
`return() {`
`file.close();`
`return { done: true };`
`}`
`};`
`},`
};
}
上面代码中,函数readLinesSync
接受一个文件对象作为参数,返回一个遍历器对象,其中除了next
方法,还部署了return
方法。下面的两种情况,都会触发执行return
方法。
// 情况一
for (let line of readLinesSync(fileName)) {
console.log(line);
break;
}
// 情况二
for (let line of readLinesSync(fileName)) {
console.log(line);
throw new Error();
}
函数的写法如下:
function* foo(x, y) { ··· }
yield 表达式
由于 Generator 函数返回的遍历器对象,只有调用next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield
表达式就是暂停标志。
遍历器对象的next
方法的运行逻辑如下。
(1)遇到yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回的对象的value
属性值。
(2)下一次调用next
方法时,再继续往下执行,直到遇到下一个yield
表达式。
(3)如果没有再遇到新的yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return
语句后面的表达式的值,作为返回的对象的value
属性值。
(4)如果该函数没有return
语句,则返回的对象的value
属性值为undefined
。
需要注意的是,yield
表达式后面的表达式,只有当调用next
方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
复制代码
function* gen() {
yield 123 + 456;
}
上面代码中,yield
后面的表达式123 + 456
,不会立即求值,只会在next
方法将指针移到这一句时,才会求值。
yield
表达式与return
语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield
,函数暂停执行,下一次再从该位置继续向后执行,而return
语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return
语句,但是可以执行多次(或者说多个)yield
表达式。正常函数只能返回一个值,因为只能执行一次return
;Generator 函数可以返回一系列的值,因为可以有任意多个yield
。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历(英语中,generator 这个词是“生成器”的意思)。
Generator 函数可以不用yield
表达式,这时就变成了一个单纯的暂缓执行函数。
function* f() {
console.log('执行了!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
上面代码中,函数f
如果是普通函数,在为变量generator
赋值时就会执行。但是,函数f
是一个 Generator 函数,就变成只有调用next
方法时,函数f
才会执行。
另外需要注意,yield
表达式只能用在 Generator 函数里面,用在其他地方都会报错。
Generator 是实现状态机的最佳结构。比如,下面的clock
函数就是一个状态机。
var ticking = true;
var clock = function() {
if (ticking)
`console.log('Tick!');`
else
`console.log('Tock!');`
ticking = !ticking;
}
上面代码的clock
函数一共有两种状态(Tick
和Tock
),每运行一次,就改变一次状态。这个函数如果用 Generator 实现,就是下面这样。
var clock = function* () {
while (true) {
`console.log('Tick!');`
`yield;`
`console.log('Tock!');`
`yield;`
}
};
18. Generator 函数的语法 - 应用 - 《阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版》 - 书栈网 · BookStack
generator 控制流
scheduler(longRunningTask(initialValue));
function scheduler(task) {
var taskObj = task.next(task.value);
// 如果Generator函数未结束,就继续调用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}
let steps = [step1Func, step2Func, step3Func];
function* iterateSteps(steps){
for (var i=0; i< steps.length; i++){
var step = steps[i];
yield step();
}
}
for … of 无法遍历return 对象
for...of
循环可以自动遍历 Generator 函数运行时生成的Iterator
对象,且此时不再需要调用next
方法。
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
上面代码使用for...of
循环,依次显示 5 个yield
表达式的值。这里需要注意,一旦next
方法的返回对象的done
属性为true
,for...of
循环就会中止,且不包含该返回对象,所以上面代码的return
语句返回的6
,不包括在for...of
循环之中。
Generator 函数返回的遍历器对象,还有一个return
方法,可以返回给定的值,并且终结遍历 Generator 函数。
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
上面代码中,调用return()
方法后,就开始执行finally
代码块,不执行try
里面剩下的代码了,然后等到finally
代码块执行完,再返回return()
方法指定的返回值。
next()
、throw()
、return()
这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield
表达式。
next()
是将yield
表达式替换成一个值。
const g = function* (x, y) {
let result = yield x + y;
return result;
};
const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}
gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;
上面代码中,第二个next(1)
方法就相当于将yield
表达式替换成一个值1
。如果next
方法没有参数,就相当于替换成undefined
。
throw()
是将yield
表达式替换成一个throw
语句。
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));
return()
是将yield
表达式替换成一个return
语句。
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
yield*
表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。
yield*
命令可以很方便地取出嵌套数组的所有成员。
复制代码
function* iterTree(tree) {
if (Array.isArray(tree)) {
`for(let i=0; i < tree.length; i++) {`
`yield* iterTree(tree[i]);`
`}`
} else {
`yield tree;`
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
console.log(x);
}
// a
// b
// c
// d
// e
由于扩展运算符...
默认调用 Iterator 接口,所以上面这个函数也可以用于嵌套数组的平铺。
[...iterTree(tree)] // ["a", "b", "c", "d", "e"]
this 结合 generator :
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。
下面看看如何使用 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);
}
上面代码中,Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了yield
命令。
执行这段代码的方法如下。
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
上面代码中,首先执行 Generator 函数,获取遍历器对象,然后使用next
方法(第二行),执行异步任务的第一阶段。由于Fetch
模块返回的是一个 Promise 对象,因此要用then
方法调用下一个next
方法。
Thunk 函数是自动执行 Generator 函数的一种方法。
传值调用和传名调用
”传值调用”(call by value),即在进入函数体之前,就计算x + 5
的值(等于 6),再将这个值传入函数f
。C 语言就采用这种策略。
“传名调用”(call by name),即直接将表达式x + 5
传入函数体,只在用到它的时候求值。Haskell 语言采用这种策略。
Thunk 函数真正的威力,在于可以自动执行 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);
上面代码的run
函数,就是一个 Generator 函数的自动执行器。内部的next
函数就是 Thunk 的回调函数。next
函数先将指针移到 Generator 函数的下一步(gen.next
方法),然后判断 Generator 函数是否结束(result.done
属性),如果没结束,就将next
函数再传入 Thunk 函数(result.value
属性),否则就直接退出。
co 模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator 函数的自动执行。
下面是一个 Generator 函数,用于依次读取两个文件。
var gen = function* () {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
co 模块可以让你不用编写 Generator 函数的执行器。
var co = require('co');
co(gen);
co就是把对象转化为promise对象如何层层then
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
前文有一个 Generator 函数,依次读取两个文件。
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
`fs.readFile(fileName, function(error, data) {`
`if (error) return reject(error);`
`resolve(data);`
`});`
});
};
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
上面代码的函数gen
可以写成async
函数,就是下面这样。
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
一比较就会发现,async
函数就是将 Generator 函数的星号(*
)替换成async
,将yield
替换成await
,仅此而已。
ad
async
函数对 Generator 函数的改进,体现在以下四点。
(1)内置执行器。
Generator 函数的执行必须靠执行器,所以才有了co
模块,而async
函数自带执行器。也就是说,async
函数的执行,与普通函数一模一样,只要一行。
asyncReadFile();
上面的代码调用了asyncReadFile
函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next
方法,或者用co
模块,才能真正执行,得到最后结果。
(2)更好的语义。
async
和await
,比起星号和yield
,语义更清楚了。async
表示函数里有异步操作,await
表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。
co
模块约定,yield
命令后面只能是 Thunk 函数或 Promise 对象,而async
函数的await
命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
(4)返回值是 Promise。
async
函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then
方法指定下一步的操作。
进一步说,async
函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await
命令就是内部then
命令的语法糖。
async 函数有多种使用形式。
// 函数声明
async function foo() {}
// 函数表达式
const foo = async function () {};
// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)
// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jake').then(…);
// 箭头函数
const foo = async () => {};
Promise 对象的状态变化
async
函数返回的 Promise 对象,必须等到内部所有await
命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return
语句或者抛出错误。也就是说,只有async
函数内部的异步操作执行完,才会执行then
方法指定的回调函数。
下面是一个例子。
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
上面代码中,函数getTitle
内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then
方法里面的console.log
。
await 命令
正常情况下,await
命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。另一种情况是,await
命令后面是一个thenable
对象(即定义了then
方法的对象),那么await
会将其等同于 Promise 对象。
任何一个await
语句后面的 Promise 对象变为reject
状态,那么整个async
函数都会中断执行。
另一种方法是await
后面的 Promise 对象再跟一个catch
方法,处理前面可能出现的错误。
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
`// ...`
});
}
所有的async
函数都可以写成上面的第二种形式,其中的spawn
函数就是自动执行器。
下面给出spawn
函数的实现,基本就是前文自动执行器的翻版。
复制代码
function spawn(genF) {
return new Promise(function(resolve, reject) {
`const gen = genF();`
`function step(nextF) {`
`let next;`
`try {`
`next = nextF();`
`} catch(e) {`
`return reject(e);`
`}`
`if(next.done) {`
`return resolve(next.value);`
`}`
`Promise.resolve(next.value).then(function(v) {`
`step(function() { return gen.next(v); });`
`}, function(e) {`
`step(function() { return gen.throw(e); });`
`});`
`}`
`step(function() { return gen.next(undefined); });`
});
}
三种异步的比较
我们通过一个例子,来看 async 函数与 Promise、Generator 函数的比较。
假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。
首先是 Promise 的写法。
function chainAnimationsPromise(elem, animations) {
// 变量ret用来保存上一个动画的返回值
let ret = null;
// 新建一个空的Promise
let p = Promise.resolve();
// 使用then方法,添加所有动画
for(let anim of animations) {
`p = p.then(function(val) {`
`ret = val;`
`return anim(elem);`
`});`
}
// 返回一个部署了错误捕捉机制的Promise
return p.catch(function(e) {
`/* 忽略错误,继续执行 */`
}).then(function() {
`return ret;`
});
}
虽然 Promise 的写法比回调函数的写法大大改进,但是一眼看上去,代码完全都是 Promise 的 API(then
、catch
等等),操作本身的语义反而不容易看出来。
接着是 Generator 函数的写法。
function chainAnimationsGenerator(elem, animations) {
return spawn(function*() {
`let ret = null;`
`try {`
`for(let anim of animations) {`
`ret = yield anim(elem);`
`}`
`} catch(e) {`
`/* 忽略错误,继续执行 */`
`}`
`return ret;`
});
}
上面代码使用 Generator 函数遍历了每个动画,语义比 Promise 写法更清晰,用户定义的操作全部都出现在spawn
函数的内部。这个写法的问题在于,必须有一个任务运行器,自动执行 Generator 函数,上面代码的spawn
函数就是自动执行器,它返回一个 Promise 对象,而且必须保证yield
语句后面的表达式,必须返回一个 Promise。
最后是 async 函数的写法。
async function chainAnimationsAsync(elem, animations) {
let ret = null;
try {
`for(let anim of animations) {`
`ret = await anim(elem);`
`}`
} catch(e) {
`/* 忽略错误,继续执行 */`
}
return ret;
}
可以看到 Async 函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。它将 Generator 写法中的自动执行器,改在语言层面提供,不暴露给用户,因此代码量最少。如果使用 Generator 写法,自动执行器需要用户自己提供。
顺序完成异步操作
async function logInOrder(urls) {
// 并发读取远程URL
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
顶层await
// awaiting.js
let output;
export default (async function main() {
const dynamic = await import(someMission);
const data = await fetch(url);
output = someProcess(dynamic.default, data);
})();
export { output };
原始:
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
es6改进后:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
上面代码定义了一个“类”,定义“类”的方法的时候,前面不需要加上function
这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。
类的数据类型就是函数,类本身就指向构造函数。
class Bar {
doStuff() {
console.log('stuff');
}
}
var b = new Bar();
b.doStuff() // "stuff"
构造函数的prototype
属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype
属性上面。
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
prototype
对象的constructor
属性,直接指向“类”的本身
Point.prototype.constructor === Point // true
类是有函数构造的q
//定义类
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
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Foo {
static classMethod() {
`return 'hello';`
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
Foo
类的classMethod
方法前有static
关键字,表明该方法是一个静态方法,可以直接在Foo
类上调用(Foo.classMethod()
),而不是在Foo
类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。
class Foo {
static bar() {
`this.baz();`
}
static baz() {
`console.log('hello');`
}
baz() {
`console.log('world');`
}
}
Foo.bar() // hello
上面代码中,静态方法bar
调用了this.baz
,这里的this
指的是Foo
类,而不是Foo
的实例,等同于调用Foo.baz
。另外,从这个例子还可以看出,静态方法可以与非静态方法重名。
class Foo {
static classMethod() {
`return 'hello';`
}
}
class Bar extends Foo {
}
Bar.classMethod() // 'hello'
实例属性的新写法:可以不使用constructor, 而是直接写在顶层
class IncreasingCounter {
constructor() {
`this._count = 0;`
}
get value() {
`console.log('Getting the current value!');`
`return this._count;`
}
increment() {
`this._count++;`
}
}
class IncreasingCounter {
_count = 0;
get value() {
`console.log('Getting the current value!');`
`return this._count;`
}
increment() {
`this._count++;`
}
}
静态属性
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
class MyClass {
static myStaticProp = 42;
constructor() {
`console.log(MyClass.myStaticProp); // 42`
}
}
可以在constructor定义属性的前面添加static 设置静态属性
私有属性和私有方法,外部不能访问
class Foo {
#a;
#b;
constructor(a, b) {
`this.#a = a;`
`this.#b = b;`
}
#sum() {
`return #a + #b;`
}
printSum() {
`console.log(this.#sum());`
}
}
new
是从构造函数生成实例对象的命令。ES6 为new
命令引入了一个new.target
属性,该属性一般用在构造函数之中,返回new
命令作用于的那个构造函数。如果构造函数不是通过new
命令或Reflect.construct()
调用的,new.target
会返回undefined
,因此这个属性可以用来确定构造函数是怎么调用的。
function Person(name) {
if (new.target !== undefined) {
`this.name = name;`
} else {
`throw new Error('必须使用 new 命令生成实例');`
}
}
// 另一种写法
function Person(name) {
if (new.target === Person) {
`this.name = name;`
} else {
`throw new Error('必须使用 new 命令生成实例');`
}
}
var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三'); // 报错
new.target 是用来检测是否是由new构成的,区别于call构成
new.target
会返回子类。
用法:
class Shape {
constructor() {
`if (new.target === Shape) {`
`throw new Error('本类不能实例化');`
`}`
}
}
class Rectangle extends Shape {
constructor(length, width) {
`super();`
`// ...`
}
}
var x = new Shape(); // 报错
var y = new Rectangle(3, 4); // 正确
class Point {
}
class ColorPoint extends Point {
}
子类在constructor中必须使用super() 可以调用父类的constructor
class ColorPoint extends Point {
constructor(x, y, color) {
`super(x, y); // 调用父类的constructor(x, y)`
`this.color = color;`
}
toString() {
`return this.color + ' ' + super.toString(); // 调用父类的toString()`
}
}
如果子类没有定义constructor方法,这个方法会被默认添加;
class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
`super(...args);`
}
}
同时需要注意的是只有在使用super后才可以使用this关键字
let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true
实例对象cp
同时是子类和父类ColorPoint
和Point
两个类的实例
Object.getPrototypeOf
方法可以用来从子类上获取父类。
super() 只能放在 constructor中
class A {
p() {
`return 2;`
}
}
class B extends A {
constructor() {
`super();`
`console.log(super.p()); // 2`
}
}
let b = new B();
上面代码中,子类B
当中的super.p()
,就是将super
当作一个对象使用。这时,super
在普通方法之中,指向A.prototype
,所以super.p()
就相当于A.prototype.p()
。
class A {
constructor() {
`this.x = 1;`
}
print() {
`console.log(this.x);`
}
}
class B extends A {
constructor() {
`super();`
`this.x = 2;`
}
m() {
`super.print();`
}
}
let b = new B();
b.m() // 2
上面代码中,super.print()
虽然调用的是A.prototype.print()
,但是A.prototype.print()
内部的this
指向子类B
的实例,导致输出的是2
,而不是1
。也就是说,实际上执行的是super.print.call(this)
。
由于this
指向子类实例,所以如果通过super
对某个属性赋值,这时super
就是this
,赋值的属性会变成子类实例的属性。
class A {
constructor() {
`this.x = 1;`
}
}
class B extends A {
constructor() {
`super();`
`this.x = 2;`
`super.x = 3;`
`console.log(super.x); // undefined`
`console.log(this.x); // 3`
}
}
let b = new B();
上面代码中,super.x
赋值为3
,这时等同于对this.x
赋值为3
。而当读取super.x
的时候,读的是A.prototype.x
,所以返回undefined
。
如果super
作为对象,用在静态方法之中,这时super
将指向父类,而不是父类的原型对象。
class Parent {
static myMethod(msg) {
`console.log('static', msg);`
}
myMethod(msg) {
`console.log('instance', msg);`
}
}
class Child extends Parent {
static myMethod(msg) {
`super.myMethod(msg);`
}
myMethod(msg) {
`super.myMethod(msg);`
}
}
Child.myMethod(1); // static 1
var child = new Child();
child.myMethod(2); // instance 2
(1)子类的__proto__
属性,表示构造函数的继承,总是指向父类。
(2)子类prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。
class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
继承原生构造函数:
class MyArray extends Array {
constructor(...args) {
`super(...args);`
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined
Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。
const a = {
a: 'a'
};
const b = {
b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}
ES6 模块不是对象,而是通过export
命令显式指定输出的代码,再通过import
命令输入。
// ES6模块
import { stat, exists, readFile } from 'fs';
下面是import()
的一些适用场合。
(1)按需加载。
import()
可以在需要的时候,再加载某个模块。
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
`dialogBox.open();`
})
.catch(error => {
`/* Error handling */`
})
});
上面代码中,import()
方法放在click
事件的监听函数之中,只有用户点击了按钮,才会加载这个模块。
(2)条件加载
import()
可以放在if
代码块,根据不同的情况,加载不同的模块。
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
上面代码中,如果满足条件,就加载模块 A,否则加载模块 B。
(3)动态的模块路径
import()
允许模块路径动态生成。
import(f())
.then(...);
上面代码中,根据函数f
的返回结果,加载不同的模块。
import()
加载模块成功以后,这个模块会作为一个对象,当作then
方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。
import('./myModule.js')
.then(({export1, export2}) => {
// ...·
});
上面代码中,export1
和export2
都是myModule.js
的输出接口,可以解构获得。
如果模块有default
输出接口,可以用参数直接获得。
import('./myModule.js')
.then(myModule => {
console.log(myModule.default);
});
上面的代码也可以使用具名输入的形式。
import('./myModule.js')
.then(({default: theDefault}) => {
console.log(theDefault);
});
如果想同时加载多个模块,可以采用下面的写法。
Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
])
.then(([module1, module2, module3]) => {
···
});
import()
也可以用在 async 函数之中。
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
`await Promise.all([`
`import('./module1.js'),`
`import('./module2.js'),`
`import('./module3.js'),`
`]);`
}
main();
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";
。
严格模式主要有以下限制。
with
语句delete prop
,会报错,只能删除属性delete global[prop]
eval
不会在它的外层作用域引入变量eval
和arguments
不能被重新赋值arguments
不会自动反映函数参数的变化arguments.callee
arguments.caller
this
指向全局对象fn.caller
和fn.arguments
获取函数调用的堆栈protected
、static
和interface
)上面这些限制,模块都必须遵守。由于严格模式是 ES5 引入的,不属于 ES6,所以请参阅相关 ES5 书籍,本书不再详细介绍了。
其中,尤其需要注意this
的限制。ES6 模块之中,顶层的this
指向undefined
,即不应该在顶层代码使用this
。
export:
模块功能主要由两个命令构成:export
和import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export
关键字输出该变量。下面是一个 JS 文件,里面使用export
命令输出变量。
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
上面代码是profile.js
文件,保存了用户信息。ES6 将其视为一个模块,里面用export
命令对外部输出了三个变量。
export
的写法,除了像上面这样,还有另外一种。
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export { firstName, lastName, year };
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
// 报错
function f() {}
export f;
// 正确
export function f() {};
// 正确
function f() {}
export {f};
另外,export
语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
最后,export
命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import
命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
function foo() {
export default 'bar' // SyntaxError
}
foo()
上面代码中,export
语句放在函数之中,结果报错。
// main.js
import { firstName, lastName, year } from './profile.js';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
如果想为输入的变量重新取一个名字,import
命令要使用as
关键字,将输入的变量重命名。
import { lastName as surname } from './profile.js';
import
命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
上面代码中,脚本加载了变量a
,对其重新赋值就会报错,因为a
是一个只读的接口。但是,如果a
是一个对象,改写a
的属性是允许的。
import {a} from './xxx.js'
a.foo = 'hello'; // 合法操作
由于import
是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
注意,模块整体加载所在的那个对象(上例是circle
),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。
import * as circle from './circle';
// 下面两行都是不允许的
circle.foo = 'hello';
circle.area = function () {};
export default:
// modules.js
function add(x, y) {
return x * y;
}
export {add as default};
// 等同于
// export default add;
// app.js
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';
有了export default
命令,输入模块时就非常直观了,以输入 lodash 模块为例。
import _ from 'lodash';
如果想在一条import
语句中,同时输入默认方法和其他接口,可以写成下面这样。
import _, { each, forEach } from 'lodash';
export 与 import 的复合写法
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
上面代码中,export
和import
语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foo
和bar
实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo
和bar
。
// 接口改名
export { foo as myFoo } from 'my_module';
// 整体输出
export * from 'my_module';
// 默认接口
export { default } from 'foo';
具名接口改为默认接口的写法如下。
export { es6 as default } from './someModule';
// 等同于
import { es6 } from './someModule';
export default es6;
模块的继承:
// circleplus.js
export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
上面代码中的export *
,表示再输出circle
模块的所有属性和方法。注意,export *
命令会忽略circle
模块的default
方法。然后,上面代码又输出了自定义的e
变量和默认方法。
如果要使用的常量非常多,可以建一个专门的constants
目录,将各种常量写在不同的文件里面,保存在该目录下。
// constants/db.js
export const db = {
url: 'http://my.couchdbserver.local:5984',
admin_username: 'admin',
admin_password: 'admin password'
};
// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];
HTML 网页中,浏览器通过标签加载 JavaScript 脚本。
// module code
上面代码中,由于浏览器脚本的默认语言是 JavaScript,因此type="application/javascript"
可以省略。
默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。
如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载,下面就是两种异步加载的语法。
defer
与async
的区别是:defer
要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer
是“渲染完再执行”,async
是“下载完就执行”。另外,如果有多个defer
脚本,会按照它们在页面出现的顺序加载,而多个async
脚本是不能保证加载顺序的。
浏览器加载 ES6 模块,也使用标签,但是要加入
type="module"
属性。
上面代码在网页中插入一个模块foo.js
,由于type
属性设为module
,所以浏览器知道这是一个 ES6 模块。
浏览器对于带有type="module"
的,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了
标签的
defer
属性。
import $ from "./jquery/src/jquery.js";
$('#message').text('Hi from jQuery!');
对于外部的模块脚本(上例是foo.js
),有几点需要注意。
use strict
。import
命令加载其他模块(.js
后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export
命令输出对外接口。this
关键字返回undefined
,而不是指向window
。也就是说,在模块顶层使用this
关键字,是无意义的。下面是一个示例模块。
import utils from 'https://example.com/js/utils.js';
const x = 1;
console.log(x === window.x); //false
console.log(this === undefined); // true
利用顶层的this
等于undefined
这个语法点,可以侦测当前代码是否在 ES6 模块之中。
const isNotModuleScript = this !== undefined;
讨论 Node.js 加载 ES6 模块之前,必须了解 ES6 模块与 CommonJS 模块完全不同。
它们有两个重大差异。
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件lib.js
的例子。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
上面代码输出内部变量counter
和改写这个变量的内部方法incCounter
。然后,在main.js
里面加载这个模块。
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
上面代码说明,lib.js
模块加载以后,它的内部变化就影响不到输出的mod.counter
了。这是因为mod.counter
是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
Node.js 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。从 v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。
Node.js 要求 ES6 模块采用.mjs
后缀文件名。也就是说,只要脚本文件里面使用import
或者export
命令,那么就必须采用.mjs
后缀名。Node.js 遇到.mjs
文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"
。
如果不希望将后缀名改成.mjs
,可以在项目的package.json
文件中,指定type
字段为module
。
{
"type": "module"
}
一旦设置了以后,该目录里面的 JS 脚本,就被解释用 ES6 模块。
# 解释成 ES6 模块
$ node my-app.js
如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs
。如果没有type
字段,或者type
字段为commonjs
,则.js
脚本会被解释成 CommonJS 模块。
总结为一句话:.mjs
文件总是以 ES6 模块加载,.cjs
文件总是以 CommonJS 模块加载,.js
文件的加载取决于package.json
里面type
字段的设置。
注意,ES6 模块与 CommonJS 模块尽量不要混用。require
命令不能加载.mjs
文件,会报错,只有import
命令才可以加载.mjs
文件。反过来,.mjs
文件里面也不能使用require
命令,必须使用import
。
尽量不要使用var,而是使用let和const,在let和const之间优选使用const
静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。
使用数组成员对变量赋值时,优先使用解构赋值。
单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。
对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign
方法。
使用扩展运算符(…)拷贝数组。
使用 Array.from 方法,将类似数组的对象转为数组。
立即执行函数可以写成箭头函数的形式。
那些使用匿名函数当作参数的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了 this。
注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要key: value
的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。
let map = new Map(arr);
for (let key of map.keys()) {
console.log(key);
}
for (let value of map.values()) {
console.log(value);
}
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
总是用 Class,取代需要 prototype 的操作。因为 Class 的写法更简洁,更易于理解。
使用extends
实现继承,因为这样更简单,不会有破坏instanceof
运算的危险。
首先,Module 语法是 JavaScript 模块的标准写法,坚持使用这种写法。使用import
取代require
。
使用export
取代module.exports
。
如果模块只有一个输出值,就使用export default
,如果模块有多个输出值,就不使用export default
,export default
与普通的export
不要同时使用。
不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default)。
// bad
import * as myObject from './importModule';
// good
import myObject from './importModule';
如果模块默认输出一个函数,函数名的首字母应该小写。
如果模块默认输出一个对象,对象名的首字母应该大写。
ESLint 是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。
首先,安装 ESLint。
$ npm i -g eslint
然后,安装 Airbnb 语法规则,以及 import、a11y、react 插件。
$ npm i -g eslint-config-airbnb
$ npm i -g eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
最后,在项目的根目录下新建一个.eslintrc
文件,配置 ESLint。
{
"extends": "eslint-config-airbnb"
}
- Let
O
beToObject(this value)
.ReturnIfAbrupt(O)
.- Let
len
beToLength(Get(O, "length"))
.ReturnIfAbrupt(len)
.- If
IsCallable(callbackfn)
isfalse
, throw a TypeError exception.- If
thisArg
was supplied, letT
bethisArg
; else letT
beundefined
.- Let
A
beArraySpeciesCreate(O, len)
.ReturnIfAbrupt(A)
.- Let
k
be 0.- Repeat, while
k
<len
- Let
Pk
beToString(k)
.- Let
kPresent
beHasProperty(O, Pk)
.ReturnIfAbrupt(kPresent)
.- If
kPresent
istrue
, then
- Let
kValue
beGet(O, Pk)
.ReturnIfAbrupt(kValue)
.- Let
mappedValue
beCall(callbackfn, T, «kValue, k, O»)
.ReturnIfAbrupt(mappedValue)
.- Let
status
beCreateDataPropertyOrThrow (A, Pk, mappedValue)
.ReturnIfAbrupt(status)
.- Increase
k
by 1.- Return
A
.
翻译如下。
- 得到当前数组的
this
对象- 如果报错就返回
- 求出当前数组的
length
属性- 如果报错就返回
- 如果 map 方法的参数
callbackfn
不可执行,就报错- 如果 map 方法的参数之中,指定了
this
,就让T
等于该参数,否则T
为undefined
- 生成一个新的数组
A
,跟当前数组的length
属性保持一致- 如果报错就返回
- 设定
k
等于 0- 只要
k
小于当前数组的length
属性,就重复下面步骤
- 设定
Pk
等于ToString(k)
,即将K
转为字符串- 设定
kPresent
等于HasProperty(O, Pk)
,即求当前数组有没有指定属性- 如果报错就返回
- 如果
kPresent
等于true
,则进行下面步骤
- 设定
kValue
等于Get(O, Pk)
,取出当前数组的指定属性- 如果报错就返回
- 设定
mappedValue
等于Call(callbackfn, T, «kValue, k, O»)
,即执行回调函数- 如果报错就返回
- 设定
status
等于CreateDataPropertyOrThrow (A, Pk, mappedValue)
,即将回调函数的值放入A
数组的指定位置- 如果报错就返回
k
增加 1- 返回
A
仔细查看上面的算法,可以发现,当处理一个全是空位的数组时,前面步骤都没有问题。进入第 10 步中第 2 步时,kPresent
会报错,因为空位对应的属性名,对于数组来说是不存在的,因此就会返回,不会进行后面的步骤。
将异步操作包装成 Thunk 函数或者 Promise 对象,即next()
方法返回值的value
属性是一个 Thunk 函数或者 Promise 对象,等待以后返回真正的值,而done
属性则还是同步产生的。
function idMaker() {
let index = 0;
return {
`next: function() {`
`return {`
`value: new Promise(resolve => setTimeout(() => resolve(index++), 1000)),`
`done: false`
`};`
`}`
};
}
const it = idMaker();
it.next().value.then(o => console.log(o)) // 1
it.next().value.then(o => console.log(o)) // 2
it.next().value.then(o => console.log(o)) // 3
// ...
上面代码中,value
属性的返回值是一个 Promise 对象,用来放置异步操作。但是这样写很麻烦,不太符合直觉,语义也比较绕。
asyncIterator
是一个异步遍历器,调用next
方法以后,返回一个 Promise 对象。因此,可以使用then
方法指定,这个 Promise 对象的状态变为resolve
以后的回调函数。回调函数的参数,则是一个具有value
和done
两个属性的对象,这个跟同步遍历器是一样的。
我们知道,一个对象的同步遍历器的接口,部署在Symbol.iterator
属性上面。同样地,对象的异步遍历器接口,部署在Symbol.asyncIterator
属性上面。不管是什么样的对象,只要它的Symbol.asyncIterator
属性有值,就表示应该对它进行异步遍历。
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
asyncIterator
.next()
.then(iterResult1 => {
console.log(iterResult1); // { value: 'a', done: false }
return asyncIterator.next();
})
.then(iterResult2 => {
console.log(iterResult2); // { value: 'b', done: false }
return asyncIterator.next();
})
.then(iterResult3 => {
console.log(iterResult3); // { value: undefined, done: true }
});
async function f() {
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
console.log(await asyncIterator.next());
// { value: 'a', done: false }
console.log(await asyncIterator.next());
// { value: 'b', done: false }
console.log(await asyncIterator.next());
// { value: undefined, done: true }
}
面代码中,next
方法用await
处理以后,就不必使用then
方法了。整个流程已经很接近同步处理了。
注意,异步遍历器的next
方法是可以连续调用的,不必等到上一步产生的 Promise 对象resolve
以后再调用。这种情况下,next
方法会累积起来,自动按照每一步的顺序运行下去。下面是一个例子,把所有的next
方法放在Promise.all
方法里面。
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
const [{value: v1}, {value: v2}] = await Promise.all([
asyncIterator.next(), asyncIterator.next()
]);
console.log(v1, v2); // a b
另一种用法是一次性调用所有的next
方法,然后await
最后一步操作。
async function runner() {
const writer = openFile('someFile.txt');
writer.next('hello');
writer.next('world');
await writer.return();
}
runner();
createAsyncIterable()
返回一个拥有异步遍历器接口的对象,for...of
循环自动调用这个对象的异步遍历器的next
方法,会得到一个 Promise 对象。await
用来处理这个 Promise 对象,一旦resolve
,就把得到的值(x
)传入for...of
的循环体。
for await...of
循环的一个用途,是部署了 asyncIterable 操作的异步接口,可以直接放入这个循环。
let body = '';
async function f() {
for await(const data of req) body += data;
const parsed = JSON.parse(body);
console.log('got', parsed);
}
上面代码中,req
是一个 asyncIterable 对象,用来异步读取数据。可以看到,使用for await...of
循环以后,代码会非常简洁。
如果next
方法返回的 Promise 对象被reject
,for await...of
就会报错,要用try...catch
捕捉。
async function () {
try {
`for await (const x of createRejectingIterable()) {`
`console.log(x);`
`}`
} catch (e) {
`console.error(e);`
}
}
注意,for await...of
循环也可以用于同步遍历器。
(async function () {
for await (const x of ['a', 'b']) {
`console.log(x);`
}
})();
// a
// b
异步遍历器的设计目的之一,就是 Generator 函数处理同步操作和异步操作时,能够使用同一套接口。
// 同步 Generator 函数
function* map(iterable, func) {
const iter = iterable[Symbol.iterator]();
while (true) {
`const {value, done} = iter.next();`
`if (done) break;`
`yield func(value);`
}
}
// 异步 Generator 函数
async function* map(iterable, func) {
const iter = iterable[Symbol.asyncIterator]();
while (true) {
`const {value, done} = await iter.next();`
`if (done) break;`
`yield func(value);`
}
}
yield*
语句也可以跟一个异步遍历器。
async function* gen1() {
yield 'a';
yield 'b';
return 2;
}
async function* gen2() {
// result 最终会等于 2
const result = yield* gen1();
}
二进制数组由三类对象组成。
(1)ArrayBuffer
对象:代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。
(2)TypedArray
视图:共包括 9 种类型的视图,比如Uint8Array
(无符号 8 位整数)数组视图, Int16Array
(16 位整数)数组视图, Float32Array
(32 位浮点数)数组视图等等。
(3)DataView
视图:可以自定义复合格式的视图,比如第一个字节是 Uint8(无符号 8 位整数)、第二、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还可以自定义字节序。
简单说,ArrayBuffer
对象代表原始的二进制数据,TypedArray
视图用来读写简单类型的二进制数据,DataView
视图用来读写复杂类型的二进制数据。
TypedArray
视图支持的数据类型一共有 9 种(DataView
视图支持除Uint8C
以外的其他 8 种)。
数据类型 | 字节长度 | 含义 | 对应的 C 语言类型 |
---|---|---|---|
Int8 | 1 | 8 位带符号整数 | signed char |
Uint8 | 1 | 8 位不带符号整数 | unsigned char |
Uint8C | 1 | 8 位不带符号整数(自动过滤溢出) | unsigned char |
Int16 | 2 | 16 位带符号整数 | short |
Uint16 | 2 | 16 位不带符号整数 | unsigned short |
Int32 | 4 | 32 位带符号整数 | int |
Uint32 | 4 | 32 位不带符号的整数 | unsigned int |
Float32 | 4 | 32 位浮点数 | float |
Float64 | 8 | 64 位浮点数 | double |
ArrayBuffer
对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray
视图和DataView
视图)来读写,视图的作用是以指定格式解读二进制数据。
ArrayBuffer
也是一个构造函数,可以分配一段可以存放数据的连续内存区域。
const buf = new ArrayBuffer(32);
上面代码生成了一段 32 字节的内存区域,每个字节的值默认都是 0。可以看到,ArrayBuffer
构造函数的参数是所需要的内存大小(单位字节)。
为了读写这段内容,需要为它指定视图。DataView
视图的创建,需要提供ArrayBuffer
对象实例作为参数。
const buf = new ArrayBuffer(32);
const dataView = new DataView(buf);
dataView.getUint8(0) // 0
上面代码对一段 32 字节的内存,建立DataView
视图,然后以不带符号的 8 位整数格式,从头读取 8 位二进制数据,结果得到 0,因为原始内存的ArrayBuffer
对象,默认所有位都是 0。
另一种TypedArray
视图,与DataView
视图的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的数据格式。
const buffer = new ArrayBuffer(12);
const x1 = new Int32Array(buffer);
x1[0] = 1;
const x2 = new Uint8Array(buffer);
x2[0] = 2;
x1[0] // 2
TypedArray
视图的构造函数,除了接受ArrayBuffer
实例作为参数,还可以接受普通数组作为参数,直接分配内存生成底层的ArrayBuffer
实例,并同时完成对这段内存的赋值。
const typedArray = new Uint8Array([0,1,2]);
typedArray.length // 3
typedArray[0] = 5;
typedArray // [5, 1, 2]
ArrayBuffer
实例的byteLength
属性,返回所分配的内存区域的字节长度。
const buffer = new ArrayBuffer(32);
buffer.byteLength
// 32
ArrayBuffer
实例有一个slice
方法,允许将内存区域的一部分,拷贝生成一个新的ArrayBuffer
对象。
const buffer = new ArrayBuffer(8);
const newBuffer = buffer.slice(0, 3);
ArrayBuffer
有一个静态方法isView
,返回一个布尔值,表示参数是否为ArrayBuffer
的视图实例。这个方法大致相当于判断参数,是否为TypedArray
实例或DataView
实例。
const buffer = new ArrayBuffer(8);
ArrayBuffer.isView(buffer) // false
const v = new Int32Array(buffer);
ArrayBuffer.isView(v) // true
普通数组的操作方法和属性,对 TypedArray 数组完全适用。
TypedArray.prototype.copyWithin(target, start[, end = this.length])
TypedArray.prototype.entries()
TypedArray.prototype.every(callbackfn, thisArg?)
TypedArray.prototype.fill(value, start=0, end=this.length)
TypedArray.prototype.filter(callbackfn, thisArg?)
TypedArray.prototype.find(predicate, thisArg?)
TypedArray.prototype.findIndex(predicate, thisArg?)
TypedArray.prototype.forEach(callbackfn, thisArg?)
TypedArray.prototype.indexOf(searchElement, fromIndex=0)
TypedArray.prototype.join(separator)
TypedArray.prototype.keys()
TypedArray.prototype.lastIndexOf(searchElement, fromIndex?)
TypedArray.prototype.map(callbackfn, thisArg?)
TypedArray.prototype.reduce(callbackfn, initialValue?)
TypedArray.prototype.reduceRight(callbackfn, initialValue?)
TypedArray.prototype.reverse()
TypedArray.prototype.slice(start=0, end=this.length)
TypedArray.prototype.some(callbackfn, thisArg?)
TypedArray.prototype.sort(comparefn)
TypedArray.prototype.toLocaleString(reserved1?, reserved2?)
TypedArray.prototype.toString()
TypedArray.prototype.values()
复合视图:
由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做“复合视图”。
const buffer = new ArrayBuffer(24);
const idView = new Uint32Array(buffer, 0, 1);
const usernameView = new Uint8Array(buffer, 4, 16);
const amountDueView = new Float32Array(buffer, 20, 1);
DataView
视图本身也是构造函数,接受一个ArrayBuffer
对象作为参数,生成视图。
new DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);
下面是一个例子。
const buffer = new ArrayBuffer(24);
const dv = new DataView(buffer);
DataView
实例有以下属性,含义与TypedArray
实例的同名方法相同。
DataView.prototype.buffer
:返回对应的 ArrayBuffer 对象DataView.prototype.byteLength
:返回占据的内存字节长度DataView.prototype.byteOffset
:返回当前视图从对应的 ArrayBuffer 对象的哪个字节开始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 位浮点数。arraybuffer的应用:
传统上,服务器通过 AJAX 操作只能返回文本数据,即responseType
属性默认为text
。XMLHttpRequest
第二版XHR2
允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(responseType
)设为arraybuffer
;如果不知道,就设为blob
。
let xhr = new XMLHttpRequest();
xhr.open('GET', someUrl);
xhr.responseType = 'arraybuffer';
xhr.onload = function () {
let arrayBuffer = xhr.response;
// ···
};
xhr.send();
网页Canvas
元素输出的二进制像素数据,就是 TypedArray 数组。
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const uint8ClampedArray = imageData.data;
ES2017 引入SharedArrayBuffer
,允许 Worker 线程与主线程共享同一块内存。SharedArrayBuffer
的 API 与ArrayBuffer
一模一样,唯一的区别是后者无法共享数据。
// 主线程
// 新建 1KB 共享内存
const sharedBuffer = new SharedArrayBuffer(1024);
// 主线程将共享内存的地址发送出去
w.postMessage(sharedBuffer);
// 在共享内存上建立视图,供写入数据
const sharedArray = new Int32Array(sharedBuffer);
上面代码中,postMessage
方法的参数是SharedArrayBuffer
对象。
Worker 线程从事件的data
属性上面取到数据。
// Worker 线程
onmessage = function (ev) {
// 主线程共享的数据,就是 1KB 的共享内存
const sharedBuffer = ev.data;
// 在共享内存上建立视图,方便读写
const sharedArray = new Int32Array(sharedBuffer);
// ...
};
共享内存也可以在 Worker 线程创建,发给主线程。
SharedArrayBuffer
与ArrayBuffer
一样,本身是无法读写的,必须在上面建立视图,然后通过视图读写。
// 分配 10 万个 32 位整数占据的内存空间
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000);
// 建立 32 位整数视图
const ia = new Int32Array(sab); // ia.length == 100000
// 新建一个质数生成器
const primes = new PrimeGenerator();
// 将 10 万个质数,写入这段内存空间
for ( let i=0 ; i < ia.length ; i++ )
ia[i] = primes.next();
// 向 Worker 线程发送这段共享内存
w.postMessage(ia);
Worker 线程收到数据后的处理如下。
// Worker 线程
let ia;
onmessage = function (ev) {
ia = ev.data;
console.log(ia.length); // 100000
console.log(ia[37]); // 输出 163,因为这是第38个质数
};
多线程共享内存,最大的问题就是如何防止两个线程同时修改某个地址,或者说,当一个线程修改共享内存以后,必须有一个机制让其他线程同步。SharedArrayBuffer API 提供Atomics
对象,保证所有共享内存的操作都是“原子性”的,并且可以在所有线程内同步。
共享内存上面的某些运算是不能被打断的,即不能在运算过程中,让其他线程改写内存上面的值。Atomics 对象提供了一些运算方法,防止数据被改写。
Atomics.add(sharedArray, index, value)
Atomics.add
用于将value
加到sharedArray[index]
,返回sharedArray[index]
旧的值。
Atomics.sub(sharedArray, index, value)
Atomics.sub
用于将value
从sharedArray[index]
减去,返回sharedArray[index]
旧的值。
Atomics.and(sharedArray, index, value)
Atomics.and
用于将value
与sharedArray[index]
进行位运算and
,放入sharedArray[index]
,并返回旧的值。
Atomics.or(sharedArray, index, value)
Atomics.or
用于将value
与sharedArray[index]
进行位运算or
,放入sharedArray[index]
,并返回旧的值。
Atomics.xor(sharedArray, index, value)
Atomic.xor
用于将vaule
与sharedArray[index]
进行位运算xor
,放入sharedArray[index]
,并返回旧的值。
(5)其他方法
Atomics
对象还有以下方法。
Atomics.compareExchange(sharedArray, index, oldval, newval)
:如果sharedArray[index]
等于oldval
,就写入newval
,返回oldval
。Atomics.isLockFree(size)
:返回一个布尔值,表示Atomics
对象是否可以处理某个size
的内存锁定。如果返回false
,应用程序就需要自己来实现锁定。Atomics.compareExchange
的一个用途是,从 SharedArrayBuffer 读取一个值,然后对该值进行某个操作,操作结束以后,检查一下 SharedArrayBuffer 里面原来那个值是否发生变化(即被其他线程改写过)。如果没有改写过,就将它写回原来的位置,否则读取新的值,再重头进行一次操作。
do 表达式
// 等同于 <表达式>
do { <表达式>; }
// 等同于 <语句>
do { <语句> }
do
表达式的好处是可以封装多个语句,让程序更加模块化,就像乐高积木那样一块块拼装起来。
let x = do {
if (foo()) { f() }
else if (bar()) { g() }
else { h() }
};
开发者使用一个模块时,有时需要知道模板本身的一些信息(比如模块的路径)。现在有一个提案,为 import 命令添加了一个元属性import.meta
,返回当前模块的元信息。
import.meta
只能在模块内部使用,如果在模块外部使用会报错。
这个属性返回一个对象,该对象的各种属性就是当前运行的脚本的元信息。具体包含哪些属性,标准没有规定,由各个运行环境自行决定。一般来说,import.meta
至少会有下面两个属性。
(1)import.meta.url
import.meta.url
返回当前模块的 URL 路径。举例来说,当前模块主文件的路径是https://foo.com/main.js
,import.meta.url
就返回这个路径。如果模块里面还有一个数据文件data.txt
,那么就可以用下面的代码,获取这个数据文件的路径。
new URL('data.txt', import.meta.url)
注意,Node.js 环境中,import.meta.url
返回的总是本地路径,即是file:URL
协议的字符串,比如file:///home/user/foo.js
。
(2)import.meta.scriptElement
import.meta.scriptElement
是浏览器特有的元属性,返回加载模块的那个元素,相当于
document.currentScript
属性。
// HTML 代码为
//
// my-module.js 内部执行下面的代码
import.meta.scriptElement.dataset.foo
// "abc"
函数的部分执行有一些特别注意的地方。
(1)函数的部分执行是基于原函数的。如果原函数发生变化,部分执行生成的新函数也会立即反映这种变化。
(2)如果预先提供的那个值是一个表达式,那么这个表达式并不会在定义时求值,而是在每次调用时求值。
(3)如果新函数的参数多于占位符的数量,那么多余的参数将被忽略。
(4)...
只会被采集一次,如果函数的部分执行使用了多个...
,那么每个...
的值都将相同。
JavaScript 的管道是一个运算符,写作|>
。它的左边是一个表达式,右边是一个函数。管道运算符把左边表达式的值,传入右边的函数进行求值。
x |> f
// 等同于
f(x)
数值分割:
123_00 === 12_300 // true
12345_00 === 123_4500 // true
12345_00 === 1_234_500 // true
数值分隔符有几个使用注意点。
e
或E
前后不能有分隔符。Math.sign()
用来判断一个值的正负,但是如果参数是-0
,它会返回-0
。
Math.sign(-0) // -0
Math.signbit(2) //false
Math.signbit(-2) //true
Math.signbit(0) //false
Math.signbit(-0) //true
箭头函数可以绑定this
对象,大大减少了显式绑定this
对象的写法(call
、apply
、bind
)。但是,箭头函数并不适用于所有场合,所以现在有一个提案,提出了“函数绑定”(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);
}
如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。
var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;
let log = ::console.log;
// 等同于
var log = console.log.bind(console);
如果双冒号运算符的运算结果,还是一个对象,就可以采用链式写法。
import { map, takeWhile, forEach } from "iterlib";
getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));
装饰器可以用来装饰整个类。
function testable(isTestable) {
return function(target) {
`target.isTestable = isTestable;`
}
}
@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true
@testable(false)
class MyClass {}
MyClass.isTestable // false
上面代码中,@testable
就是一个装饰器。它修改了MyTestableClass
这个类的行为,为它加上了静态属性isTestable
。testable
函数的参数target
是MyTestableClass
类本身。
装饰器不仅可以装饰类,还可以装饰类的属性。
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}
上面代码中,装饰器readonly
用来装饰“类”的name
方法。
装饰器函数readonly
一共可以接受三个参数。
function readonly(target, name, descriptor){
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor;
}
readonly(Person.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor);
装饰器第一个参数是类的原型对象,上例是Person.prototype
,装饰器的本意是要“装饰”类的实例,但是这个时候实例还没生成,所以只能去装饰原型(这不同于类的装饰,那种情况时target
参数指的是类本身);第二个参数是所要装饰的属性名,第三个参数是该属性的描述对象。
另外,上面代码说明,装饰器(readonly)会修改属性的描述对象(descriptor),然后被修改的描述对象再用来定义属性。
装饰器只适用于类和类的方法,并不适用于函数
core-decorators.js是一个第三方模块,提供了几个常见的装饰器,通过它可以更好地理解装饰器。
autobind
装饰器使得方法中的this
对象,绑定原始对象。
readonly
装饰器使得属性或方法不可写。
override
装饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。
deprecate
或deprecated
装饰器在控制台显示一条警告,表示该方法将废除。
suppressWarnings
装饰器抑制deprecated
装饰器导致的console.warn()
调用。但是,异步代码发出的调用除外。
在装饰器的基础上,可以实现Mixin
模式。所谓Mixin
模式,就是对象继承的一种替代方案,中文译为“混入”(mix in),意为在一个对象之中混入另外一个对象的方法。
方法一:
const Foo = {
foo() { console.log('foo') }
};
class MyClass {}
Object.assign(MyClass.prototype, Foo);
let obj = new MyClass();
obj.foo() // 'foo'
方法二:
部署一个通用脚本mixins.js
,将 Mixin 写成一个装饰器。
export function mixins(...list) {
return function (target) {
`Object.assign(target.prototype, ...list);`
};
}
然后,就可以使用上面这个装饰器,为类“混入”各种方法。
import { mixins } from './mixins';
const Foo = {
foo() { console.log('foo') }
};
@mixins(Foo)
class MyClass {}
let obj = new MyClass();
obj.foo() // "foo"
Trait 也是一种装饰器,效果与 Mixin 类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等。
下面采用traits-decorator这个第三方模块作为例子。这个模块提供的traits
装饰器,不仅可以接受对象,还可以接受 ES6 类作为参数。
import { traits } from 'traits-decorator';
class TFoo {
foo() { console.log('foo') }
}
const TBar = {
bar() { console.log('bar') }
};
@traits(TFoo, TBar)
class MyClass { }
let obj = new MyClass();
obj.foo() // foo
obj.bar() // bar
global
globalThis
polyfill in universal JavaScript:如何写 globalThis 的垫片库__proto__
in ECMAScript 6Promise.all
方法的一些很好的例子JavaScript 语言的设计是单一继承,即子类只能继承一个父类,不允许继承多个父类。这种设计保证了对象继承的层次结构是树状的,而不是复杂的网状结构。
但是,这大大降低了编程的灵活性。因为实际开发中,有时不可避免,子类需要继承多个父类。举例来说,“猫”可以继承“哺乳类动物”,也可以继承“宠物”。
这里使用mixin和trait解决
SIMD(发音/sim-dee/
)是“Single Instruction/Multiple Data”的缩写,意为“单指令,多数据”。它是 JavaScript 操作 CPU 对应指令的接口,你可以看做这是一种不同的运算执行模式。与它相对的是 SISD(“Single Instruction/Single Data”),即“单指令,单数据”。
SIMD 的含义是使用一个指令,完成多个数据的运算;SISD 的含义是使用一个指令,完成单个数据的运算,这是 JavaScript 的默认运算模式。显而易见,SIMD 的执行效率要高于 SISD,所以被广泛用于 3D 图形运算、物理模拟等运算量超大的项目之中。
总的来说,SIMD 是数据并行处理(parallelism)的一种手段,可以加速一些运算密集型操作的速度。将来与 WebAssembly 结合以后,可以让 JavaScript 达到二进制代码的运行速度。
柯里化(currying)指的是将一个多参数的函数拆分成一系列函数,每个拆分后的函数都只接受一个参数(unary)。
function add (a) {
return function (b) {
`return a + b;`
}
}
// 或者采用箭头函数写法
const add = x => y => x + y;
const f = add(1);
f(1) // 2
函数合成(function composition)指的是,将多个函数合成一个函数。
const compose = f => g => x => f(g(x));
const f = compose (x => x * 4) (x => x + 3);
f(2) // 20
参数倒置(flip)指的是改变函数前两个参数的顺序。
let f = {};
f.flip =
fn =>
`(a, b, ...args) => fn(b, a, ...args.reverse());`
var divide = (a, b) => a / b;
var flip = f.flip(divide);
flip(10, 5) // 0.5
flip(1, 10) // 10
var three = (a, b, c) => [a, b, c];
var flip = f.flip(three);
flip(1, 2, 3); // => [2, 1, 3]
执行边界(until)指的是函数执行到满足条件为止。
let f = {};
f.until = (condition, f) =>
(...args) => {
`var r = f.apply(null, args);`
`return condition(r) ? r : f.until(condition, f)(r);`
};
let condition = x => x > 100;
let inc = x => x + 1;
let until = f.until(condition, inc);
until(0) // 101
condition = x => x === 5;
until = f.until(condition, inc);
until(3) // 5
Mateo Gianolio, Haskell in ES6: Part 1
next()
、throw()
、return()
这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield
表达式。