本文介绍: 简单速通一遍es6

《阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版》 – 书栈网 · BookStack

1. 声明变量方法

var function let const import class
前面两个是es5的,后面是es6的,let const 的好处是块变量:只在声明所在的块级作用域内有效;
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。

2. 取顶层对象

JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。

  • 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window
  • 浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self
  • Node 里面,顶层对象是global,但其他环境都不支持。

同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this变量,但是有局限性。

  • 全局环境中,this会返回顶层对象。但是,Node 模块和 ES6 模块中,this返回的是当前模块。
  • 函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined
  • 不管是严格模式,还是普通模式,new Function('return this')(),总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么evalnew Function这些方法都可能无法使用。

综上所述,很难找到一种方法,可以在所有情况下,都取到顶层对象。下面是两种勉强可以使用的方法。

  1. // 方法一

  2. (typeof window !== 'undefined'

  3. ? window

  4. : (typeof process === 'object' &&

  5.   `typeof require === 'function' &&`
    
  6.   `typeof global === 'object')`
    
  7.  `? global`
    
  8.  `: this);`
    
  9. // 方法二

  10. var getGlobal = function () {

  11. if (typeof self !== 'undefined') { return self; }

  12. if (typeof window !== 'undefined') { return window; }

  13. if (typeof global !== 'undefined') { return global; }

  14. throw new Error('unable to locate global object');

  15. };

es6采用的方式是globalThis,都可以从它拿到顶层对象

3. 变量解构

数组字符串解构按顺序

  1. let [a, b, c] = [1, 2, 3];
  2. const [a, b, c, d, e] = 'hello';

对象解构按对象名

  1. let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
  2. let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };

数值和布尔值解构

  1. let {toString: s} = 123;

  2. s === Number.prototype.toString // true

  3. let {toString: s} = true;

  4. s === Boolean.prototype.toString // true

  5. let { prop: x } = undefined; // TypeError

  6. let { prop: y } = null; // TypeError

函数解构

  1. [1, undefined, 3].map((x = 'yes') => x);
  2. // [ 1, 'yes', 3 ]

结构默认值:

  1. var {x = 3} = {};

  2. x // 3

  3. var { message: msg = 'Something went wrong' } = {};

  4. msg // "Something went wrong"

这里要注意:

  1. // 错误的写法
  2. let x;
  3. {x} = {x: 1};
  4. // SyntaxError: syntax error

上面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。

  1. // 正确的写法
  2. let x;
  3. ({x} = {x: 1});

不能使用圆括号

(1)变量声明语句
(2)函数参数
(3)赋值语句的模式

4. 字符串拓展

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

  1. $('#result').append(`
  2. There are <b>${basket.count}</b> items
  3. in your basket, <em>${basket.onSale}</em>
  4. are on sale!
  5. `);

模板字符串前后的空格和换行可以使用trim()来处理
` this is a template string `.trim()

“标签模板”的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。

  1. let a = 5;

  2. let b = 10;

  3. tag`Hello ${ a + b } world ${ a * b }`;

  4. // 等同于

  5. tag(['Hello ', ' world ', ''], 15, 50);

5. 字符串新增方法

ES6 提供了String.fromCodePoint()方法,可以识别大于0xFFFF的字符,弥补了String.fromCharCode()方法的不足。在作用上,正好与下面的codePointAt()方法相反。

  1. String.fromCodePoint(0x20BB7)
  2. // "𠮷"
  3. String.fromCodePoint(0x78, 0x1f680, 0x79) === 'xuD83DuDE80y'
  4. // true

ES6 还为原生的 String 对象,提供了一个raw()方法。该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法。

  1. // `foo${1 + 2}bar`
  2. // 等同于
  3. String.raw({ raw: ['foo', 'bar'] }, 1 + 2) // "foo3bar"

语调符号和重音符号。为了表示它们,Unicode 提供了两种方法。一种是直接提供带重音符号的字符,比如Ǒ(u01D1)。另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符,比如O(u004F)和ˇ(u030C)合成Ǒ(u004Fu030C)。

  1. 'u01D1'==='u004Fu030C' //false

  2. 'u01D1'.length // 1

  3. 'u004Fu030C'.length // 2

  4. 'u01D1'.normalize() === 'u004Fu030C'.normalize()

  5. // true

传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。和后面两个和python一致

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
  1. let s = 'Hello world!';

  2. s.startsWith('world', 6) // true

  3. s.endsWith('Hello', 5) // true

  4. s.includes('Hello', 6) // false

repeat方法返回一个新字符串,表示将原字符串重复n次。小数会向下取整,负数或者Infinity会报错

  1. 'x'.repeat(3) // "xxx"
  2. 'hello'.repeat(2) // "hellohello"
  3. 'na'.repeat(0) // ""

ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。

  1. 'x'.padStart(5, 'ab') // 'ababx'

  2. 'x'.padStart(4, 'ab') // 'abax'

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

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

ES2019 对字符串实例新增了trimStart()trimEnd()这两个方法。它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。浏览器还部署了额外的两个方法,trimLeft()trimStart()的别名,trimRight()trimEnd()的别名。

  1. const s = ' abc ';

  2. s.trim() // "abc"

  3. s.trimStart() // "abc "

  4. s.trimEnd() // " abc"

matchAll()方法返回一个正则表达式在当前字符串的所有匹配,详见《正则的扩展》的一章。

6. 正则的拓展

感觉和现在的python差不多,这里要注意是exec进行调用

  1. const RE_OPT_A = /^(?<as>a+)?$/;

  2. const matchObj = RE_OPT_A.exec('');

  3. matchObj.groups.as // undefined

  4. '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]

7. 数值的拓展

ES6 在Number对象上,新提供了Number.isFinite()Number.isNaN()两个方法。它们与传统的全局方法isFinite()isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,Number.isFinite()对于非数值一律返回falseNumber.isNaN()只有对于NaN才返回true,非NaN一律返回false

ES6 将全局方法parseInt()parseFloat(),移植到Number对象上面,行为完全保持不变。其功能是解析string转化为int或者float

  1. Number.parseInt('12.34') // 12
  2. Number.parseFloat('123.45#') // 123.45

Number.isInteger()用来判断一个数值是否为整数。

  1. Number.isInteger(5E-324) // false
  2. Number.isInteger(5E-325) // true
    如果一个数值的绝对值小于Number.MIN_VALUE(5E-324)会被自动转为 0,上面代码中,5E-325由于值太小,会被自动转为0,因此返回true

ES6 在Number对象上面,新增一个极小的常量Number.EPSILON。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。Number.EPSILON实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。

  1. 0.1 + 0.2 === 0.3 // false

JavaScript 能够准确表示的整数范围在-2^532^53之间(不含两个端点),超过这个范围,无法精确表示这个值。ES6 引入了Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。

JavaScript 所有数字都保存成 64 位浮点数,这给数值的表示带来了两大限制。一是数值的精度只能到 53 个二进制位(相当于 16 个十进制位),大于这个范围的整数,JavaScript 是无法精确表示的,这使得 JavaScript 不适合进行科学和金融方面的精确计算。二是大于或等于2的1024次方的数值,JavaScript 无法表示,会返回Infinity

  1. // 超过 53 个二进制位的数值,无法保持精度

  2. Math.pow(2, 53) === Math.pow(2, 53) + 1 // true

  3. // 超过 2 的 1024 次方的数值,无法表示

  4. Math.pow(2, 1024) // Infinity

8. 函数的拓展

如果有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined。ES2017 允许函数的最后一个参数有尾逗号(trailing comma)。

  1. // 例一

  2. function f(x = 1, y) {

  3. return [x, y];

  4. }

  5. f() // [1, undefined]

  6. f(2) // [2, undefined]

  7. f(, 1) // 报错

  8. f(undefined, 1) // [1, 1]

  9. // 例二

  10. function f(x, y = 5, z) {

  11. return [x, y, z];

  12. }

  13. f() // [undefined, 5, undefined]

  14. f(1) // [1, 5, undefined]

  15. f(1, ,2) // 报错

  16. f(1, undefined, 2) // [1, 5, 2]

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。

  1. (function (a) {}).length // 1
  2. (function (a = 5) {}).length // 0
  3. (function (a, b, c = 5) {}).length // 2

如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。

  1. (function (a = 0, b, c) {}).length // 0
  2. (function (a, b = 1, c) {}).length // 1

ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。下面是一个 rest 参数代替arguments变量的例子。

  1. // arguments变量的写法

  2. function sortNumbers() {

  3. return Array.prototype.slice.call(arguments).sort();

  4. }

  5. // rest参数的写法

  6. const sortNumbers = (...numbers) => numbers.sort();

arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。下面是一个利用 rest 参数改写数组push方法的例子。

注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

  1. function push(array, ...items) {

  2. items.forEach(function(item) {

  3. array.push(item);

  4. console.log(item);

  5. });

  6. }

  7. var a = [];

  8. push(a, 1, 2, 3)

ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。

  1. function doSomething(a, b) {
  2. 'use strict';
  3. // code
  4. }

函数的name属性,返回该函数的函数名。

  1. const bar = function baz() {};
  2. bar.name // "baz"

箭头函数

ES6 允许使用“箭头”(=>)定义函数。

  1. var f = v => v;

  2. // 等同于

  3. var f = function (v) {

  4. return v;

  5. };

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

  1. var f = () => 5;

  2. // 等同于

  3. var f = function () { return 5 };

  4. var sum = (num1, num2) => num1 + num2;

  5. // 等同于

  6. var sum = function(num1, num2) {

  7. return num1 + num2;

  8. };

ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言(比如 Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。

9. 数组的拓展

扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。

  1. console.log(...[1, 2, 3])

  2. // 1 2 3

  3. console.log(1, ...[2, 3, 4], 5)

  4. // 1 2 3 4 5

复制数组

  1. const a1 = [1, 2];
  2. // 写法一
  3. const a2 = [...a1];
  4. // 写法二
  5. const [...a2] = a1;

合并数组

  1. const arr1 = ['a', 'b'];

  2. const arr2 = ['c'];

  3. const arr3 = ['d', 'e'];

  4. // ES5 的合并数组

  5. arr1.concat(arr2, arr3);

  6. // [ 'a', 'b', 'c', 'd', 'e' ]

  7. // ES6 的合并数组

  8. [...arr1, ...arr2, ...arr3]

  9. // [ 'a', 'b', 'c', 'd', 'e' ]

两种转化数组的方式,array.from() array.of()

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
Array.of方法用于将一组值,转换为数组。

数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined

  1. [1, 4, -5, 10].find((n) => n < 0)
  2. // -5

上面代码找出数组中第一个小于 0 的成员。

  1. [1, 5, 10, 15].find(function(value, index, arr) {
  2. return value > 9;
  3. }) // 10

数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1

  1. [1, 5, 10, 15].findIndex(function(value, index, arr) {
  2. return value > 9;
  3. }) // 2

fill方法使用给定值,填充一个数组。

  1. ['a', 'b', 'c'].fill(7)

  2. // [7, 7, 7]

  3. new Array(3).fill(7)

  4. // [7, 7, 7]

数组也是键值对组成的['a', 'b'] 相当于 {0: 'a', 1: 'b'},其键就是序号,其值是本身;

entries()keys()values()——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

数组的成员有时还是数组,Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

  1. [1, 2, [3, [4, 5]]].flat()

  2. // [1, 2, 3, [4, 5]]

  3. [1, 2, [3, [4, 5]]].flat(2)

  4. // [1, 2, 3, 4, 5]

flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。

  1. // 相当于 [[2, 4], [3, 6], [4, 8]].flat()
  2. [2, 3, 4].flatMap((x) => [x, x * 2])
  3. // [2, 4, 3, 6, 4, 8]

10. 对象的扩展

变量foo直接写在大括号里面。这时,属性名就是变量名, 属性值就是变量值。下面是另一个例子。

  1. let birth = '2000/01/01';

  2. const Person = {

  3. name: '张三',

  4. //等同于birth: birth

  5. birth,

  6. // 等同于hello: function ()...

  7. hello() { console.log('我的名字是', this.name); }

  8. };

ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。

  1. let lastWord = 'last word';

  2. const a = {

  3. 'first word': 'hello',

  4. [lastWord]: 'world'

  5. };

  6. a['first word'] // "hello"

  7. a[lastWord] // "world"

  8. a['last word'] // "world"

注意,属性名表达式如果是一个对象{},默认情况下会自动将对象转为字符串[object Object],这一点要特别小心。

我们知道,this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。

三种super的用法都会报错,因为对于 JavaScript 引擎来说,这里的super都没有用在对象的方法之中。第一种写法是super用在属性里面,第二种和第三种写法是super用在一个函数里面,然后赋值给foo属性。目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。

  1. // 报错

  2. const obj = {

  3. foo: super.foo

  4. }

  5. // 报错

  6. const obj = {

  7. foo: () => super.foo

  8. }

  9. // 报错

  10. const obj = {

  11. foo: function () {

  12. return super.foo

  13. }

  14. }

对象的拓展运算符

  1. let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
  2. let { x, ...y, ...z } = someObject; // 句法错误
  3. let { ...x, y, z } = someObject; // 句法错误
  4. let { x, ...y, ...z } = someObject; // 句法错误

注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。

  1. let obj = { a: { b: 1 } };
  2. let { ...x } = obj;
  3. obj.a.b = 2;
  4. x.a.b // 2

ES2020 引入了“链判断运算符”(optional chaining operator)?.

  1. const firstName = (message

  2. && message.body

  3. && message.body.user

  4. && message.body.user.firstName) || 'default';

  5. const firstName = message?.body?.user?.firstName || 'default';

  6. const fooValue = myForm.querySelector('input[name=foo]')?.value

相等运算符(==)和严格相等运算符(===)ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

  1. Object.is('foo', 'foo')

  2. // true

  3. Object.is({}, {})

  4. // false

  5. +0 === -0 //true

  6. NaN === NaN // false

  7. Object.is(+0, -0) // false

  8. Object.is(NaN, NaN) // true

Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

  1. const obj1 = {a: {b: 1}};

  2. const obj2 = Object.assign({}, obj1);

  3. obj1.a.b = 2;

  4. obj2.a.b // 2

对于这种嵌套的对象,一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加。

  1. const target = { a: { b: 'c', d: 'e' } }
  2. const source = { a: { b: 'hello' } }
  3. Object.assign(target, source)
  4. // { a: { b: 'hello' } }

Object.assign可以用来处理数组,但是会把数组视为对象。

Object.assign只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。

  1. const source = {

  2. get foo() { return 1 }

  3. };

  4. const target = {};

  5. Object.assign(target, source)

  6. // { foo: 1 }

ES2017 引入了Object.getOwnPropertyDescriptors()方法,返回指定对象所有自身属性(非继承属性)的描述对象。

  1. const obj = {

  2. foo: 123,

  3. get bar() { return 'abc' }

  4. };

  5. Object.getOwnPropertyDescriptors(obj)

  6. // { foo:

  7. // { value: 123,

  8. // writable: true,

  9. // enumerable: true,

  10. // configurable: true },

  11. // bar:

  12. // { get: [Function: get bar],

  13. // set: undefined,

  14. // enumerable: true,

  15. // 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()的逆操作,用于将一个键值对数组转为对象。

12. Symbol

ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。

但是,它也不是私有属性,有一个Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

  1. const obj = {};

  2. const foo = Symbol('foo');

  3. obj[foo] = 'bar';

  4. for (let i in obj) {

  5. console.log(i); // 无输出

  6. }

  7. Object.getOwnPropertyNames(obj) // []

  8. Object.getOwnPropertySymbols(obj) // [Symbol(foo)]

13. Set 和 Map 数据结构

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 转为数组最方便的方法,就是使用扩展运算符(...)。

  1. const myMap = new Map()
  2. .set(true, 7)
  3. .set({foo: 3}, ['abc']);
  4. [...myMap]
  5. // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]

(2)数组 转为 Map

将数组传入 Map 构造函数,就可以转为 Map。

  1. new Map([
  2. [true, 7],
  3. [{foo: 3}, ['abc']]
  4. ])
  5. // Map {
  6. // true => 7,
  7. // Object {foo: 3} => ['abc']
  8. // }

(3)Map 转为对象

如果所有 Map 的键都是字符串,它可以无损地转为对象。

  1. function strMapToObj(strMap) {

  2. let obj = Object.create(null);

  3. for (let [k,v] of strMap) {

  4. `obj[k] = v;`
    
  5. }

  6. return obj;

  7. }

  8. const myMap = new Map()

  9. .set('yes', true)

  10. .set('no', false);

  11. strMapToObj(myMap)

  12. // { yes: true, no: false }

如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。

(4)对象转为 Map

对象转为 Map 可以通过Object.entries()

  1. let obj = {"a":1, "b":2};
  2. let map = new Map(Object.entries(obj));

此外,也可以自己实现一个转换函数。

  1. function objToStrMap(obj) {

  2. let strMap = new Map();

  3. for (let k of Object.keys(obj)) {

  4. `strMap.set(k, obj[k]);`
    
  5. }

  6. return strMap;

  7. }

  8. objToStrMap({yes: true, no: false})

  9. // Map {"yes" => true, "no" => false}

(5)Map 转为 JSON

Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。

  1. function strMapToJson(strMap) {

  2. return JSON.stringify(strMapToObj(strMap));

  3. }

  4. let myMap = new Map().set('yes', true).set('no', false);

  5. strMapToJson(myMap)

  6. // '{"yes":true,"no":false}'

另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。

  1. function mapToArrayJson(map) {

  2. return JSON.stringify([...map]);

  3. }

  4. let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);

  5. mapToArrayJson(myMap)

  6. // '[[true,7],[{"foo":3},["abc"]]]'

(6)JSON 转为 Map

JSON 转为 Map,正常情况下,所有键名都是字符串。

  1. function jsonToStrMap(jsonStr) {

  2. return objToStrMap(JSON.parse(jsonStr));

  3. }

  4. jsonToStrMap('{"yes": true, "no": false}')

  5. // Map {'yes' => true, 'no' => false}

但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。

  1. function jsonToMap(jsonStr) {

  2. return new Map(JSON.parse(jsonStr));

  3. }

  4. jsonToMap('[[true,7],[{"foo":3},["abc"]]]')

  5. // Map {true => 7, Object {foo: 3} => ['abc']}

WeakMap结构与Map结构类似,也是用于生成键值对的集合。WeakMapMap的区别有两点。首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。

和weakset一样

14. Proxy

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

下面是 Proxy 支持的拦截操作一览,一共 13 种。

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而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)
  1. var person = {

  2. name: "张三"

  3. };

  4. var proxy = new Proxy(person, {

  5. get: function(target, propKey) {

  6. if (propKey in target) {

  7. return target[propKey];

  8. } else {

  9. throw new ReferenceError("Prop name "" + propKey + "" does not exist.");

  10. }

  11. }

  12. });

  13. proxy.name // "张三"

  14. proxy.age // 抛出一个错误

Proxy.revocable()方法返回一个可取消的 Proxy 实例。

Proxy.revocable()方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。

Proxy.revocable()的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。

15. Reflect

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。

(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。

(2) 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false

(3) 让Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。

(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

Reflect对象一共有 13 个静态方法。

  • Reflect.apply(target, thisArg, args)
  • Reflect.construct(target, args)
  • Reflect.get(target, name, receiver)
  • 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)
  • Reflect.setPrototypeOf(target, prototype)

上面这些方法的作用,大部分与Object对象的同名方法的作用都是相同的,而且它与Proxy对象的方法是一一对应的。下面是对它们的解释。

16. Promise

Promise对象有以下两个特点。

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

我们可以将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化。

  1. const preloadImage = function (path) {
  2. return new Promise(function (resolve, reject) {
  3. const image = new Image();
  4. image.onload = resolve;
  5. image.onerror = reject;
  6. image.src = path;
  7. });
  8. };

同时要注意:

  1. setTimeout(function () {

  2. console.log('three');

  3. }, 0);

  4. Promise.resolve().then(function () {

  5. console.log('two');

  6. });

  7. console.log('one');

  8. // one

  9. // two

  10. // three

上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log('one')则是立即执行,因此最先输出。

实际开发中,经常遇到一种情况:不知道或者不想区分,函数f是同步函数还是异步操作,但是想用 Promise 来处理它。因为这样就可以不管f是否包含异步操作,都用then方法指定下一步流程,用catch方法处理f抛出的错误。一般就会采用下面的写法。

由于Promise.try为所有操作提供了统一的处理机制,所以如果想用then方法管理流程,最好都用Promise.try包装一下,其中一点就是可以更好地管理异常。

  1. Promise.try(() => database.users.get({id: userId}))
  2. .then(...)
  3. .catch(...)

基本操作

ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。

下面代码创造了一个Promise实例。

  1. const promise = new Promise(function(resolve, reject) {

  2. // ... some code

  3. if (/* 异步操作成功 */){

  4. `resolve(value);`
    
  5. } else {

  6. `reject(error);`
    
  7. }

  8. });

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

  1. promise.then(function(value) {
  2. // success
  3. }, function(error) {
  4. // failure
  5. });

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

  1. getJSON("/post/1.json").then(
  2. post => getJSON(post.commentURL)
  3. ).then(
  4. comments => console.log("resolved: ", comments),
  5. err => console.log("rejected: ", err)
  6. );

Promise.prototype.catch()方法是.then(null, rejection).then(undefined, rejection)的别名,用于指定发生错误时的回调函数。

  1. getJSON('/posts.json').then(function(posts) {
  2. // ...
  3. }).catch(function(error) {
  4. // 处理 getJSON 和 前一个回调函数运行时发生的错误
  5. console.log('发生错误!', error);
  6. });

上面代码中,getJSON()方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then()方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。

  1. p.then((val) => console.log('fulfilled:', val))

  2. .catch((err) => console.log('rejected', err));

  3. // 等同于

  4. p.then((val) => console.log('fulfilled:', val))

  5. .then(null, (err) => console.log("rejected:", err));

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

  1. promise
  2. .then(result => {···})
  3. .catch(error => {···})
  4. .finally(() => {···});

上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。

  1. server.listen(port)
  2. .then(function () {
  3. `// ...`
    
  4. })
  5. .finally(server.stop);

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

  1. const p = Promise.all([p1, p2, p3]);

上面代码中,Promise.all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

p的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

复制代码

  1. const p = Promise.race([p1, p2, p3]);

上面代码中,只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

Promise.race()方法的参数与Promise.all()方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve()方法,将参数转为 Promise 实例,再进一步处理。

下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject,否则变为resolve

  1. const p = Promise.race([

  2. fetch('/resource-that-may-take-a-while'),

  3. new Promise(function (resolve, reject) {

  4. `setTimeout(() => reject(new Error('request timeout')), 5000)`
    
  5. })

  6. ]);

  7. p

  8. .then(console.log)

  9. .catch(console.error);

上面代码中,如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。

Promise.allSettled()

Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。该方法由 ES2020 引入。

  1. const promises = [

  2. fetch('/api-1'),

  3. fetch('/api-2'),

  4. fetch('/api-3'),

  5. ];

  6. await Promise.allSettled(promises);

  7. removeLoadingIndicator();

上面代码对服务器发出三个请求,等到三个请求都结束,不管请求成功还是失败,加载的滚动图标就会消失。

该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()的 Promise 实例。

  1. const resolved = Promise.resolve(42);

  2. const rejected = Promise.reject(-1);

  3. const allSettledPromise = Promise.allSettled([resolved, rejected]);

  4. allSettledPromise.then(function (results) {

  5. console.log(results);

  6. });

  7. // [

  8. // { status: 'fulfilled', value: 42 },

  9. // { status: 'rejected', reason: -1 }

  10. // ]

上面代码中,Promise.allSettled()的返回值allSettledPromise,状态只可能变成fulfilled。它的监听函数接收到的参数是数组results。该数组的每个成员都是一个对象,对应传入Promise.allSettled()的两个 Promise 实例。每个对象都有status属性,该属性的值只可能是字符串fulfilled或字符串rejectedfulfilled时,对象有value属性,rejected时有reason属性,对应两种状态的返回值。

下面是返回值用法的例子。

  1. const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];

  2. const results = await Promise.allSettled(promises);

  3. // 过滤出成功的请求

  4. const successfulPromises = results.filter(p => p.status === 'fulfilled');

  5. // 过滤出失败的请求,并输出原因

  6. const errors = results

  7. .filter(p => p.status === 'rejected')

  8. .map(p => p.reason);

有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,Promise.allSettled()方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。Promise.all()方法无法做到这一点。

  1. const urls = [ /* ... */ ];

  2. const requests = urls.map(x => fetch(x));

  3. try {

  4. await Promise.all(requests);

  5. console.log('所有请求都成功。');

  6. } catch {

  7. console.log('至少一个请求失败,其他请求可能还没结束。');

  8. }

上面代码中,Promise.all()无法确定所有请求都结束。想要达到这个目的,写起来很麻烦,有了Promise.allSettled(),这就很容易了。

Promise.any()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。该方法目前是一个第三阶段的提案 。

Promise.any()Promise.race()方法很像,只有一点不同,就是不会因为某个 Promise 变成rejected状态而结束。

  1. var resolved = Promise.resolve(42);

  2. var rejected = Promise.reject(-1);

  3. var alsoRejected = Promise.reject(Infinity);

  4. Promise.any([resolved, rejected, alsoRejected]).then(function (result) {

  5. console.log(result); // 42

  6. });

  7. Promise.any([rejected, alsoRejected]).catch(function (results) {

  8. console.log(results); // [-1, Infinity]

  9. });

17. Iterator 和 for … of 循环

扩展运算符

只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。

  1. let arr = [...iterable];

yield

yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。

  1. let generator = function* () {

  2. yield 1;

  3. yield* [2,3,4];

  4. yield 5;

  5. };

  6. var iterator = generator();

  7. iterator.next() // { value: 1, done: false }

  8. iterator.next() // { value: 2, done: false }

  9. iterator.next() // { value: 3, done: false }

  10. iterator.next() // { value: 4, done: false }

  11. iterator.next() // { value: 5, done: false }

  12. iterator.next() // { value: undefined, done: true }

其他场合

由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。

  • for…of
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]])
  • Promise.all()
  • Promise.race()

generator 函数

  1. let myIterable = {

  2. [Symbol.iterator]: function* () {

  3. yield 1;

  4. yield 2;

  5. yield 3;

  6. }

  7. }

  8. [...myIterable] // [1, 2, 3]

  9. // 或者采用下面的简洁写法

  10. let obj = {

  11. * [Symbol.iterator]() {

  12. yield 'hello';

  13. yield 'world';

  14. }

  15. };

  16. for (let x of obj) {

  17. console.log(x);

  18. }

  19. // "hello"

  20. // "world"

遍历器对象除了具有next方法,还可以具有return方法和throw方法。如果你自己写遍历器对象生成函数,那么next方法是必须部署的,return方法和throw方法是否部署是可选的。

return方法的使用场合是,如果for...of循环提前退出(通常是因为出错,或者有break语句),就会调用return方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法。

  1. function readLinesSync(file) {

  2. return {

  3. `[Symbol.iterator]() {`
    
  4.   `return {`
    
  5.     `next() {`
    
  6.       `return { done: false };`
    
  7.     `},`
    
  8.     `return() {`
    
  9.       `file.close();`
    
  10.       `return { done: true };`
    
  11.     `}`
    
  12.   `};`
    
  13. `},`
    
  14. };

  15. }
    上面代码中,函数readLinesSync接受一个文件对象作为参数,返回一个遍历器对象,其中除了next方法,还部署了return方法。下面的两种情况,都会触发执行return方法。

  16. // 情况一

  17. for (let line of readLinesSync(fileName)) {

  18. console.log(line);

  19. break;

  20. }

  21. // 情况二

  22. for (let line of readLinesSync(fileName)) {

  23. console.log(line);

  24. throw new Error();

  25. }

18. Generator 函数

函数的写法如下:

  1. 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)的语法功能。

复制代码

  1. function* gen() {
  2. yield 123 + 456;
  3. }

上面代码中,yield后面的表达式123 + 456,不会立即求值,只会在next方法将指针移到这一句时,才会求值。

yield表达式与return语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式。正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历(英语中,generator 这个词是“生成器”的意思)。

Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。

  1. function* f() {

  2. console.log('执行了!')

  3. }

  4. var generator = f();

  5. setTimeout(function () {

  6. generator.next()

  7. }, 2000);

上面代码中,函数f如果是普通函数,在为变量generator赋值时就会执行。但是,函数f是一个 Generator 函数,就变成只有调用next方法时,函数f才会执行。

另外需要注意,yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。

Generator 是实现状态机的最佳结构。比如,下面的clock函数就是一个状态机。

  1. var ticking = true;
  2. var clock = function() {
  3. if (ticking)
  4. `console.log('Tick!');`
    
  5. else
  6. `console.log('Tock!');`
    
  7. ticking = !ticking;
  8. }

上面代码的clock函数一共有两种状态(TickTock),每运行一次,就改变一次状态。这个函数如果用 Generator 实现,就是下面这样。

  1. var clock = function* () {
  2. while (true) {
  3. `console.log('Tick!');`
    
  4. `yield;`
    
  5. `console.log('Tock!');`
    
  6. `yield;`
    
  7. }
  8. };

18. Generator 函数的语法 – 应用 – 《阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版》 – 书栈网 · BookStack

generator 控制流

  1. scheduler(longRunningTask(initialValue));

  2. function scheduler(task) {

  3. var taskObj = task.next(task.value);

  4. // 如果Generator函数未结束,就继续调用

  5. if (!taskObj.done) {

  6. task.value = taskObj.value

  7. scheduler(task);

  8. }

  9. }

  10. let steps = [step1Func, step2Func, step3Func];

  11. function* iterateSteps(steps){

  12. for (var i=0; i< steps.length; i++){

  13. var step = steps[i];

  14. yield step();

  15. }

  16. }

for … of 无法遍历return 对象

for...of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。

  1. function* foo() {

  2. yield 1;

  3. yield 2;

  4. yield 3;

  5. yield 4;

  6. yield 5;

  7. return 6;

  8. }

  9. for (let v of foo()) {

  10. console.log(v);

  11. }

  12. // 1 2 3 4 5

上面代码使用for...of循环,依次显示 5 个yield表达式的值。这里需要注意,一旦next方法的返回对象的done属性为truefor...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。

Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数。

  1. function* numbers () {
  2. yield 1;
  3. try {
  4. yield 2;
  5. yield 3;
  6. } finally {
  7. yield 4;
  8. yield 5;
  9. }
  10. yield 6;
  11. }
  12. var g = numbers();
  13. g.next() // { value: 1, done: false }
  14. g.next() // { value: 2, done: false }
  15. g.return(7) // { value: 4, done: false }
  16. g.next() // { value: 5, done: false }
  17. g.next() // { value: 7, done: true }

上面代码中,调用return()方法后,就开始执行finally代码块,不执行try里面剩下的代码了,然后等到finally代码块执行完,再返回return()方法指定的返回值。

next()throw()return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。

next()是将yield表达式替换成一个值。

  1. const g = function* (x, y) {

  2. let result = yield x + y;

  3. return result;

  4. };

  5. const gen = g(1, 2);

  6. gen.next(); // Object {value: 3, done: false}

  7. gen.next(1); // Object {value: 1, done: true}

  8. // 相当于将 let result = yield x + y

  9. // 替换成 let result = 1;

上面代码中,第二个next(1)方法就相当于将yield表达式替换成一个值1。如果next方法没有参数,就相当于替换成undefined

throw()是将yield表达式替换成一个throw语句。

  1. gen.throw(new Error('出错了')); // Uncaught Error: 出错了
  2. // 相当于将 let result = yield x + y
  3. // 替换成 let result = throw(new Error('出错了'));

return()是将yield表达式替换成一个return语句。

  1. gen.return(2); // Object {value: 2, done: true}
  2. // 相当于将 let result = yield x + y
  3. // 替换成 let result = return 2;

yield*表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。

yield*命令可以很方便地取出嵌套数组的所有成员。

复制代码

  1. function* iterTree(tree) {

  2. if (Array.isArray(tree)) {

  3. `for(let i=0; i < tree.length; i++) {`
    
  4.   `yield* iterTree(tree[i]);`
    
  5. `}`
    
  6. } else {

  7. `yield tree;`
    
  8. }

  9. }

  10. const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];

  11. for(let x of iterTree(tree)) {

  12. console.log(x);

  13. }

  14. // a

  15. // b

  16. // c

  17. // d

  18. // e

由于扩展运算符...默认调用 Iterator 接口,所以上面这个函数也可以用于嵌套数组的平铺。

  1. [...iterTree(tree)] // ["a", "b", "c", "d", "e"]

this 结合 generator :

  1. function* F() {

  2. this.a = 1;

  3. yield this.b = 2;

  4. yield this.c = 3;

  5. }

  6. var f = F.call(F.prototype);

  7. f.next(); // Object {value: 2, done: false}

  8. f.next(); // Object {value: 3, done: false}

  9. f.next(); // Object {value: undefined, done: true}

  10. f.a // 1

  11. f.b // 2

  12. f.c // 3

19. Generator 函数的异步应用

Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。

下面看看如何使用 Generator 函数,执行一个真实的异步任务。

  1. var fetch = require('node-fetch');

  2. function* gen(){

  3. var url = 'https://api.github.com/users/github';

  4. var result = yield fetch(url);

  5. console.log(result.bio);

  6. }

上面代码中,Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了yield命令。

执行这段代码的方法如下。

  1. var g = gen();

  2. var result = g.next();

  3. result.value.then(function(data){

  4. return data.json();

  5. }).then(function(data){

  6. g.next(data);

  7. });

上面代码中,首先执行 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 执行器。

  1. function run(fn) {

  2. var gen = fn();

  3. function next(err, data) {

  4. `var result = gen.next(data);`
    
  5. `if (result.done) return;`
    
  6. `result.value(next);`
    
  7. }

  8. next();

  9. }

  10. function* g() {

  11. // ...

  12. }

  13. run(g);

上面代码的run函数,就是一个 Generator 函数的自动执行器。内部的next函数就是 Thunk 的回调函数。next函数先将指针移到 Generator 函数的下一步(gen.next方法),然后判断 Generator 函数是否结束(result.done属性),如果没结束,就将next函数再传入 Thunk 函数(result.value属性),否则就直接退出。

co 模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator 函数的自动执行。

下面是一个 Generator 函数,用于依次读取两个文件。

  1. var gen = function* () {
  2. var f1 = yield readFile('/etc/fstab');
  3. var f2 = yield readFile('/etc/shells');
  4. console.log(f1.toString());
  5. console.log(f2.toString());
  6. };

co 模块可以让你不用编写 Generator 函数的执行器。

  1. var co = require('co');
  2. co(gen);

co就是把对象转化为promise对象如何层层then

20. async 函数

async 函数是什么?一句话,它就是 Generator 函数的语法糖。

前文有一个 Generator 函数,依次读取两个文件。

  1. const fs = require('fs');

  2. const readFile = function (fileName) {

  3. return new Promise(function (resolve, reject) {

  4. `fs.readFile(fileName, function(error, data) {`
    
  5.   `if (error) return reject(error);`
    
  6.   `resolve(data);`
    
  7. `});`
    
  8. });

  9. };

  10. const gen = function* () {

  11. const f1 = yield readFile('/etc/fstab');

  12. const f2 = yield readFile('/etc/shells');

  13. console.log(f1.toString());

  14. console.log(f2.toString());

  15. };

上面代码的函数gen可以写成async函数,就是下面这样。

  1. const asyncReadFile = async function () {
  2. const f1 = await readFile('/etc/fstab');
  3. const f2 = await readFile('/etc/shells');
  4. console.log(f1.toString());
  5. console.log(f2.toString());
  6. };

一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。

ad
async函数对 Generator 函数的改进,体现在以下四点。

(1)内置执行器。

Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

  1. asyncReadFile();

上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。

(2)更好的语义。

asyncawait,比起星号和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 函数有多种使用形式。

  1. // 函数声明

  2. async function foo() {}

  3. // 函数表达式

  4. const foo = async function () {};

  5. // 对象的方法

  6. let obj = { async foo() {} };

  7. obj.foo().then(...)

  8. // Class 的方法

  9. class Storage {

  10. constructor() {

  11. this.cachePromise = caches.open('avatars');

  12. }

  13. async getAvatar(name) {

  14. const cache = await this.cachePromise;

  15. return cache.match(`/avatars/${name}.jpg`);

  16. }

  17. }

  18. const storage = new Storage();

  19. storage.getAvatar('jake').then(…);

  20. // 箭头函数

  21. const foo = async () => {};

Promise 对象的状态变化

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

下面是一个例子。

  1. async function getTitle(url) {
  2. let response = await fetch(url);
  3. let html = await response.text();
  4. return html.match(/<title>([sS]+)</title>/i)[1];
  5. }
  6. getTitle('https://tc39.github.io/ecma262/').then(console.log)
  7. // "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 函数和自动执行器,包装在一个函数里。

  1. async function fn(args) {

  2. // ...

  3. }

  4. // 等同于

  5. function fn(args) {

  6. return spawn(function* () {

  7. `// ...`
    
  8. });

  9. }

所有的async函数都可以写成上面的第二种形式,其中的spawn函数就是自动执行器。

下面给出spawn函数的实现,基本就是前文自动执行器的翻版。

复制代码

  1. function spawn(genF) {
  2. return new Promise(function(resolve, reject) {
  3. `const gen = genF();`
    
  4. `function step(nextF) {`
    
  5.   `let next;`
    
  6.   `try {`
    
  7.     `next = nextF();`
    
  8.   `} catch(e) {`
    
  9.     `return reject(e);`
    
  10.   `}`
    
  11.   `if(next.done) {`
    
  12.     `return resolve(next.value);`
    
  13.   `}`
    
  14.   `Promise.resolve(next.value).then(function(v) {`
    
  15.     `step(function() { return gen.next(v); });`
    
  16.   `}, function(e) {`
    
  17.     `step(function() { return gen.throw(e); });`
    
  18.   `});`
    
  19. `}`
    
  20. `step(function() { return gen.next(undefined); });`
    
  21. });
  22. }

三种异步的比较

我们通过一个例子,来看 async 函数与 Promise、Generator 函数的比较。

假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。

首先是 Promise 的写法。

  1. function chainAnimationsPromise(elem, animations) {

  2. // 变量ret用来保存上一个动画的返回值

  3. let ret = null;

  4. // 新建一个空的Promise

  5. let p = Promise.resolve();

  6. // 使用then方法,添加所有动画

  7. for(let anim of animations) {

  8. `p = p.then(function(val) {`
    
  9.   `ret = val;`
    
  10.   `return anim(elem);`
    
  11. `});`
    
  12. }

  13. // 返回一个部署了错误捕捉机制的Promise

  14. return p.catch(function(e) {

  15. `/* 忽略错误,继续执行 */`
    
  16. }).then(function() {

  17. `return ret;`
    
  18. });

  19. }

虽然 Promise 的写法比回调函数的写法大大改进,但是一眼看上去,代码完全都是 Promise 的 API(thencatch等等),操作本身的语义反而不容易看出来。

接着是 Generator 函数的写法。

  1. function chainAnimationsGenerator(elem, animations) {

  2. return spawn(function*() {

  3. `let ret = null;`
    
  4. `try {`
    
  5.   `for(let anim of animations) {`
    
  6.     `ret = yield anim(elem);`
    
  7.   `}`
    
  8. `} catch(e) {`
    
  9.   `/* 忽略错误,继续执行 */`
    
  10. `}`
    
  11. `return ret;`
    
  12. });

  13. }

上面代码使用 Generator 函数遍历了每个动画,语义比 Promise 写法更清晰,用户定义的操作全部都出现在spawn函数的内部。这个写法的问题在于,必须有一个任务运行器,自动执行 Generator 函数,上面代码的spawn函数就是自动执行器,它返回一个 Promise 对象,而且必须保证yield语句后面的表达式,必须返回一个 Promise。

最后是 async 函数的写法。

  1. async function chainAnimationsAsync(elem, animations) {
  2. let ret = null;
  3. try {
  4. `for(let anim of animations) {`
    
  5.   `ret = await anim(elem);`
    
  6. `}`
    
  7. } catch(e) {
  8. `/* 忽略错误,继续执行 */`
    
  9. }
  10. return ret;
  11. }

可以看到 Async 函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。它将 Generator 写法中的自动执行器,改在语言层面提供,不暴露给用户,因此代码量最少。如果使用 Generator 写法,自动执行器需要用户自己提供。

顺序完成异步操作

  1. async function logInOrder(urls) {

  2. // 并发读取远程URL

  3. const textPromises = urls.map(async url => {

  4. const response = await fetch(url);

  5. return response.text();

  6. });

  7. // 按次序输出

  8. for (const textPromise of textPromises) {

  9. console.log(await textPromise);

  10. }

  11. }

顶层await

  1. // awaiting.js
  2. let output;
  3. export default (async function main() {
  4. const dynamic = await import(someMission);
  5. const data = await fetch(url);
  6. output = someProcess(dynamic.default, data);
  7. })();
  8. export { output };

21. Class 的基本语法

原始:

  1. function Point(x, y) {

  2. this.x = x;

  3. this.y = y;

  4. }

  5. Point.prototype.toString = function () {

  6. return '(' + this.x + ', ' + this.y + ')';

  7. };

  8. var p = new Point(1, 2);

es6改进后:

  1. class Point {

  2. constructor(x, y) {

  3. this.x = x;

  4. this.y = y;

  5. }

  6. toString() {

  7. return '(' + this.x + ', ' + this.y + ')';

  8. }

  9. }

上面代码定义了一个“类”,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。

类的数据类型就是函数,类本身就指向构造函数。

  1. class Bar {

  2. doStuff() {

  3. console.log('stuff');

  4. }

  5. }

  6. var b = new Bar();

  7. b.doStuff() // "stuff"

构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。

  1. class Point {

  2. constructor() {

  3. // ...

  4. }

  5. toString() {

  6. // ...

  7. }

  8. toValue() {

  9. // ...

  10. }

  11. }

  12. // 等同于

  13. Point.prototype = {

  14. constructor() {},

  15. toString() {},

  16. toValue() {},

  17. };

prototype对象的constructor属性,直接指向“类”的本身

  1. Point.prototype.constructor === Point // true

类是有函数构造的q

  1. //定义类

  2. class Point {

  3. constructor(x, y) {

  4. this.x = x;

  5. this.y = y;

  6. }

  7. toString() {

  8. return '(' + this.x + ', ' + this.y + ')';

  9. }

  10. }

  11. var point = new Point(2, 3);

  12. point.toString() // (2, 3)

  13. point.hasOwnProperty('x') // true

  14. point.hasOwnProperty('y') // true

  15. point.hasOwnProperty('toString') // false

  16. point.__proto__.hasOwnProperty('toString') // true

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

  1. class Foo {

  2. static classMethod() {

  3. `return 'hello';`
    
  4. }

  5. }

  6. Foo.classMethod() // 'hello'

  7. var foo = new Foo();

  8. foo.classMethod()

  9. // TypeError: foo.classMethod is not a function

Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

  1. class Foo {

  2. static bar() {

  3. `this.baz();`
    
  4. }

  5. static baz() {

  6. `console.log('hello');`
    
  7. }

  8. baz() {

  9. `console.log('world');`
    
  10. }

  11. }

  12. Foo.bar() // hello
    上面代码中,静态方法bar调用了this.baz,这里的this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz。另外,从这个例子还可以看出,静态方法可以与非静态方法重名。

  13. class Foo {

  14. static classMethod() {

  15. `return 'hello';`
    
  16. }

  17. }

  18. class Bar extends Foo {

  19. }

  20. Bar.classMethod() // 'hello'

实例属性的新写法:可以不使用constructor, 而是直接写在顶层

  1. class IncreasingCounter {

  2. constructor() {

  3. `this._count = 0;`
    
  4. }

  5. get value() {

  6. `console.log('Getting the current value!');`
    
  7. `return this._count;`
    
  8. }

  9. increment() {

  10. `this._count++;`
    
  11. }

  12. }

  13. class IncreasingCounter {

  14. _count = 0;

  15. get value() {

  16. `console.log('Getting the current value!');`
    
  17. `return this._count;`
    
  18. }

  19. increment() {

  20. `this._count++;`
    
  21. }

  22. }

静态属性

  1. class Foo {

  2. }

  3. Foo.prop = 1;

  4. Foo.prop // 1

  5. class MyClass {

  6. static myStaticProp = 42;

  7. constructor() {

  8. `console.log(MyClass.myStaticProp); // 42`
    
  9. }

  10. }

可以在constructor定义属性的前面添加static 设置静态属性

私有属性和私有方法,外部不能访问

  1. class Foo {
  2. #a;
  3. #b;
  4. constructor(a, b) {
  5. `this.#a = a;`
    
  6. `this.#b = b;`
    
  7. }
  8. #sum() {
  9. `return #a + #b;`
    
  10. }
  11. printSum() {
  12. `console.log(this.#sum());`
    
  13. }
  14. }

new是从构造函数生成实例对象的命令。ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。

  1. function Person(name) {

  2. if (new.target !== undefined) {

  3. `this.name = name;`
    
  4. } else {

  5. `throw new Error('必须使用 new 命令生成实例');`
    
  6. }

  7. }

  8. // 另一种写法

  9. function Person(name) {

  10. if (new.target === Person) {

  11. `this.name = name;`
    
  12. } else {

  13. `throw new Error('必须使用 new 命令生成实例');`
    
  14. }

  15. }

  16. var person = new Person('张三'); // 正确

  17. var notAPerson = Person.call(person, '张三'); // 报错

new.target 是用来检测是否是由new构成的,区别于call构成

new.target会返回子类。

用法:

  1. class Shape {

  2. constructor() {

  3. `if (new.target === Shape) {`
    
  4.   `throw new Error('本类不能实例化');`
    
  5. `}`
    
  6. }

  7. }

  8. class Rectangle extends Shape {

  9. constructor(length, width) {

  10. `super();`
    
  11. `// ...`
    
  12. }

  13. }

  14. var x = new Shape(); // 报错

  15. var y = new Rectangle(3, 4); // 正确

22. Class 的继承

  1. class Point {

  2. }

  3. class ColorPoint extends Point {

  4. }

子类在constructor中必须使用super() 可以调用父类的constructor

  1. class ColorPoint extends Point {

  2. constructor(x, y, color) {

  3. `super(x, y); // 调用父类的constructor(x, y)`
    
  4. `this.color = color;`
    
  5. }

  6. toString() {

  7. `return this.color + ' ' + super.toString(); // 调用父类的toString()`
    
  8. }

  9. }

如果子类没有定义constructor方法,这个方法会被默认添加;

  1. class ColorPoint extends Point {

  2. }

  3. // 等同于

  4. class ColorPoint extends Point {

  5. constructor(...args) {

  6. `super(...args);`
    
  7. }

  8. }

同时需要注意的是只有在使用super后才可以使用this关键字

  1. let cp = new ColorPoint(25, 8, 'green');

  2. cp instanceof ColorPoint // true

  3. cp instanceof Point // true

实例对象cp同时是子类和父类ColorPointPoint两个类的实例

Object.getPrototypeOf方法可以用来从子类上获取父类。

super() 只能放在 constructor中

  1. class A {

  2. p() {

  3. `return 2;`
    
  4. }

  5. }

  6. class B extends A {

  7. constructor() {

  8. `super();`
    
  9. `console.log(super.p()); // 2`
    
  10. }

  11. }

  12. let b = new B();

上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()

  1. class A {

  2. constructor() {

  3. `this.x = 1;`
    
  4. }

  5. print() {

  6. `console.log(this.x);`
    
  7. }

  8. }

  9. class B extends A {

  10. constructor() {

  11. `super();`
    
  12. `this.x = 2;`
    
  13. }

  14. m() {

  15. `super.print();`
    
  16. }

  17. }

  18. let b = new B();

  19. b.m() // 2

上面代码中,super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()内部的this指向子类B的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)

由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。

  1. class A {

  2. constructor() {

  3. `this.x = 1;`
    
  4. }

  5. }

  6. class B extends A {

  7. constructor() {

  8. `super();`
    
  9. `this.x = 2;`
    
  10. `super.x = 3;`
    
  11. `console.log(super.x); // undefined`
    
  12. `console.log(this.x); // 3`
    
  13. }

  14. }

  15. let b = new B();

上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined

如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。

  1. class Parent {

  2. static myMethod(msg) {

  3. `console.log('static', msg);`
    
  4. }

  5. myMethod(msg) {

  6. `console.log('instance', msg);`
    
  7. }

  8. }

  9. class Child extends Parent {

  10. static myMethod(msg) {

  11. `super.myMethod(msg);`
    
  12. }

  13. myMethod(msg) {

  14. `super.myMethod(msg);`
    
  15. }

  16. }

  17. Child.myMethod(1); // static 1

  18. var child = new Child();

  19. child.myMethod(2); // instance 2

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

  1. class A {

  2. }

  3. class B extends A {

  4. }

  5. B.__proto__ === A // true

  6. B.prototype.__proto__ === A.prototype // true

继承原生构造函数:

  • Boolean()
  • Number()
  • String()
  • Array()
  • Date()
  • Function()
  • RegExp()
  • Error()
  • Object()
  1. class MyArray extends Array {

  2. constructor(...args) {

  3. `super(...args);`
    
  4. }

  5. }

  6. var arr = new MyArray();

  7. arr[0] = 12;

  8. arr.length // 1

  9. arr.length = 0;

  10. arr[0] // undefined

Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。

  1. const a = {
  2. a: 'a'
  3. };
  4. const b = {
  5. b: 'b'
  6. };
  7. const c = {...a, ...b}; // {a: 'a', b: 'b'}

23. Module的语法

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

  1. // ES6模块
  2. import { stat, exists, readFile } from 'fs';

下面是import()的一些适用场合。

(1)按需加载。

import()可以在需要的时候,再加载某个模块。

  1. button.addEventListener('click', event => {
  2. import('./dialogBox.js')
  3. .then(dialogBox => {
  4. `dialogBox.open();`
    
  5. })
  6. .catch(error => {
  7. `/* Error handling */`
    
  8. })
  9. });

上面代码中,import()方法放在click事件的监听函数之中,只有用户点击了按钮,才会加载这个模块。

(2)条件加载

import()可以放在if代码块,根据不同的情况,加载不同的模块。

  1. if (condition) {
  2. import('moduleA').then(...);
  3. } else {
  4. import('moduleB').then(...);
  5. }

上面代码中,如果满足条件,就加载模块 A,否则加载模块 B。

(3)动态的模块路径

import()允许模块路径动态生成。

  1. import(f())
  2. .then(...);

上面代码中,根据函数f的返回结果,加载不同的模块。

注意点

import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。

  1. import('./myModule.js')
  2. .then(({export1, export2}) => {
  3. // ...·
  4. });

上面代码中,export1export2都是myModule.js的输出接口,可以解构获得。

如果模块有default输出接口,可以用参数直接获得。

  1. import('./myModule.js')
  2. .then(myModule => {
  3. console.log(myModule.default);
  4. });

上面的代码也可以使用具名输入的形式。

  1. import('./myModule.js')
  2. .then(({default: theDefault}) => {
  3. console.log(theDefault);
  4. });

如果想同时加载多个模块,可以采用下面的写法。

  1. Promise.all([
  2. import('./module1.js'),
  3. import('./module2.js'),
  4. import('./module3.js'),
  5. ])
  6. .then(([module1, module2, module3]) => {
  7. ···
  8. });

import()也可以用在 async 函数之中。

  1. async function main() {
  2. const myModule = await import('./myModule.js');
  3. const {export1, export2} = await import('./myModule.js');
  4. const [module1, module2, module3] =
  5. `await Promise.all([`
    
  6.   `import('./module1.js'),`
    
  7.   `import('./module2.js'),`
    
  8.   `import('./module3.js'),`
    
  9. `]);`
    
  10. }
  11. main();
严格模式:

ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

严格模式主要有以下限制。

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用with语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
  • eval不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protectedstaticinterface

上面这些限制,模块都必须遵守。由于严格模式是 ES5 引入的,不属于 ES6,所以请参阅相关 ES5 书籍,本书不再详细介绍了。

其中,尤其需要注意this的限制。ES6 模块之中,顶层的this指向undefined,即不应该在顶层代码使用this

export:

模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。

  1. // profile.js
  2. export var firstName = 'Michael';
  3. export var lastName = 'Jackson';
  4. export var year = 1958;

上面代码是profile.js文件,保存了用户信息。ES6 将其视为一个模块,里面用export命令对外部输出了三个变量。

export的写法,除了像上面这样,还有另外一种。

  1. // profile.js

  2. var firstName = 'Michael';

  3. var lastName = 'Jackson';

  4. var year = 1958;

  5. export { firstName, lastName, year };

  6. function v1() { ... }

  7. function v2() { ... }

  8. export {

  9. v1 as streamV1,

  10. v2 as streamV2,

  11. v2 as streamLatestVersion

  12. };

  13. // 写法一

  14. export var m = 1;

  15. // 写法二

  16. var m = 1;

  17. export {m};

  18. // 写法三

  19. var n = 1;

  20. export {n as m};

  21. // 报错

  22. function f() {}

  23. export f;

  24. // 正确

  25. export function f() {};

  26. // 正确

  27. function f() {}

  28. export {f};

另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

最后,export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。

  1. function foo() {

  2. export default 'bar' // SyntaxError

  3. }

  4. foo()
    上面代码中,export语句放在函数之中,结果报错。

  5. // main.js

  6. import { firstName, lastName, year } from './profile.js';

  7. function setName(element) {

  8. element.textContent = firstName + ' ' + lastName;

  9. }

如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

  1. import { lastName as surname } from './profile.js';

import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。

  1. import {a} from './xxx.js'

  2. a = {}; // Syntax Error : 'a' is read-only;

上面代码中,脚本加载了变量a,对其重新赋值就会报错,因为a是一个只读的接口。但是,如果a是一个对象,改写a的属性是允许的。

  1. import {a} from './xxx.js'

  2. a.foo = 'hello'; // 合法操作

由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

  1. // 报错

  2. import { 'f' + 'oo' } from 'my_module';

  3. // 报错

  4. let module = 'my_module';

  5. import { foo } from module;

  6. // 报错

  7. if (x === 1) {

  8. import { foo } from 'module1';

  9. } else {

  10. import { foo } from 'module2';

  11. }

  12. import * as circle from './circle';

  13. console.log('圆面积:' + circle.area(4));

  14. console.log('圆周长:' + circle.circumference(14));

注意,模块整体加载所在的那个对象(上例是circle),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。

  1. import * as circle from './circle';

  2. // 下面两行都是不允许的

  3. circle.foo = 'hello';

  4. circle.area = function () {};

export default:

  1. // modules.js

  2. function add(x, y) {

  3. return x * y;

  4. }

  5. export {add as default};

  6. // 等同于

  7. // export default add;

  8. // app.js

  9. import { default as foo } from 'modules';

  10. // 等同于

  11. // import foo from 'modules';

有了export default命令,输入模块时就非常直观了,以输入 lodash 模块为例。

  1. import _ from 'lodash';

如果想在一条import语句中,同时输入默认方法和其他接口,可以写成下面这样。

  1. import _, { each, forEach } from 'lodash';

export 与 import 的复合写法

  1. export { foo, bar } from 'my_module';

  2. // 可以简单理解为

  3. import { foo, bar } from 'my_module';

  4. export { foo, bar };

上面代码中,exportimport语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar

  1. // 接口改名

  2. export { foo as myFoo } from 'my_module';

  3. // 整体输出

  4. export * from 'my_module';

  5. // 默认接口

  6. export { default } from 'foo';

具名接口改为默认接口的写法如下。

  1. export { es6 as default } from './someModule';

  2. // 等同于

  3. import { es6 } from './someModule';

  4. export default es6;

模块的继承:

  1. // circleplus.js

  2. export * from 'circle';

  3. export var e = 2.71828182846;

  4. export default function(x) {

  5. return Math.exp(x);

  6. }

上面代码中的export *,表示再输出circle模块的所有属性和方法。注意,export *命令会忽略circle模块的default方法。然后,上面代码又输出了自定义的e变量和默认方法。

如果要使用的常量非常多,可以建一个专门的constants目录,将各种常量写在不同的文件里面,保存在该目录下。

  1. // constants/db.js

  2. export const db = {

  3. url: 'http://my.couchdbserver.local:5984',

  4. admin_username: 'admin',

  5. admin_password: 'admin password'

  6. };

  7. // constants/user.js

  8. export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];

24. Module 的加载实现

HTML 网页中,浏览器通过<script>标签加载 JavaScript 脚本。

  1. <!-- 页面内嵌的脚本 -->

  2. <script type="application/javascript">

  3. // module code

  4. </script>

  5. <!-- 外部脚本 -->

  6. <script type="application/javascript" src="path/to/myModule.js">

  7. </script>

上面代码中,由于浏览器脚本的默认语言是 JavaScript,因此type="application/javascript"可以省略。

默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。

如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载,下面就是两种异步加载的语法。

  1. <script src="path/to/myModule.js" defer></script>
  2. <script src="path/to/myModule.js" async></script>

deferasync的区别是:defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer是“渲染完再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。

加载规则

浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性。

  1. <script type="module" src="./foo.js"></script>

上面代码在网页中插入一个模块foo.js,由于type属性设为module,所以浏览器知道这是一个 ES6 模块。

浏览器对于带有type="module"<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。

  1. <script type="module">
  2. import $ from "./jquery/src/jquery.js";
  3. $('#message').text('Hi from jQuery!');
  4. </script>

对于外部的模块脚本(上例是foo.js),有几点需要注意。

  • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
  • 模块脚本自动采用严格模式,不管有没有声明use strict
  • 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。
  • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。
  • 同一个模块如果加载多次,将只执行一次。

下面是一个示例模块。

  1. import utils from 'https://example.com/js/utils.js';

  2. const x = 1;

  3. console.log(x === window.x); //false

  4. console.log(this === undefined); // true

利用顶层的this等于undefined这个语法点,可以侦测当前代码是否在 ES6 模块之中。

  1. const isNotModuleScript = this !== undefined;

讨论 Node.js 加载 ES6 模块之前,必须了解 ES6 模块与 CommonJS 模块完全不同。

它们有两个重大差异。

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件lib.js的例子。

  1. // lib.js
  2. var counter = 3;
  3. function incCounter() {
  4. counter++;
  5. }
  6. module.exports = {
  7. counter: counter,
  8. incCounter: incCounter,
  9. };

上面代码输出内部变量counter和改写这个变量的内部方法incCounter。然后,在main.js里面加载这个模块。

  1. // main.js

  2. var mod = require('./lib');

  3. console.log(mod.counter); // 3

  4. mod.incCounter();

  5. console.log(mod.counter); // 3

上面代码说明,lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。

Node.js 加载

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

  1. {
  2. "type": "module"
  3. }

一旦设置了以后,该目录里面的 JS 脚本,就被解释用 ES6 模块。

  1. # 解释成 ES6 模块
  2. $ 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

25. 编程风格

尽量不要使用var,而是使用let和const,在let和const之间优选使用const

静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。

使用数组成员对变量赋值时,优先使用解构赋值。

单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。

对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法。

使用扩展运算符(…)拷贝数组。

使用 Array.from 方法,将类似数组的对象转为数组。

立即执行函数可以写成箭头函数的形式。

那些使用匿名函数当作参数的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了 this。

注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要key: value的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。

  1. let map = new Map(arr);

  2. for (let key of map.keys()) {

  3. console.log(key);

  4. }

  5. for (let value of map.values()) {

  6. console.log(value);

  7. }

  8. for (let item of map.entries()) {

  9. console.log(item[0], item[1]);

  10. }

总是用 Class,取代需要 prototype 的操作。因为 Class 的写法更简洁,更易于理解。

使用extends实现继承,因为这样更简单,不会有破坏instanceof运算的危险。

首先,Module 语法是 JavaScript 模块的标准写法,坚持使用这种写法。使用import取代require

使用export取代module.exports

如果模块只有一个输出值,就使用export default,如果模块有多个输出值,就不使用export defaultexport default与普通的export不要同时使用。

不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default)。

  1. // bad

  2. import * as myObject from './importModule';

  3. // good

  4. import myObject from './importModule';

如果模块默认输出一个函数,函数名的首字母应该小写。

如果模块默认输出一个对象,对象名的首字母应该大写。

语法规则和代码风格的检查工具

ESLint 是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。

首先,安装 ESLint。

  1. $ npm i -g eslint

然后,安装 Airbnb 语法规则,以及 import、a11y、react 插件。

  1. $ npm i -g eslint-config-airbnb
  2. $ npm i -g eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react

最后,在项目的根目录下新建一个.eslintrc文件,配置 ESLint。

  1. {
  2. "extends": "eslint-config-airbnb"
  3. }

26. 读懂规格

  1. Let O be ToObject(this value).
  2. ReturnIfAbrupt(O).
  3. Let len be ToLength(Get(O, "length")).
  4. ReturnIfAbrupt(len).
  5. If IsCallable(callbackfn) is false, throw a TypeError exception.
  6. If thisArg was supplied, let T be thisArg; else let T be undefined.
  7. Let A be ArraySpeciesCreate(O, len).
  8. ReturnIfAbrupt(A).
  9. Let k be 0.
  10. Repeat, while k < len
    1. Let Pk be ToString(k).
    2. Let kPresent be HasProperty(O, Pk).
    3. ReturnIfAbrupt(kPresent).
    4. If kPresent is true, then
      1. Let kValue be Get(O, Pk).
      2. ReturnIfAbrupt(kValue).
      3. Let mappedValue be Call(callbackfn, T, «kValue, k, O»).
      4. ReturnIfAbrupt(mappedValue).
      5. Let status be CreateDataPropertyOrThrow (A, Pk, mappedValue).
      6. ReturnIfAbrupt(status).
    5. Increase k by 1.
  11. Return A.

翻译如下。

  1. 得到当前数组的this对象
  2. 如果报错就返回
  3. 求出当前数组的length属性
  4. 如果报错就返回
  5. 如果 map 方法的参数callbackfn不可执行,就报错
  6. 如果 map 方法的参数之中,指定了this,就让T等于该参数,否则Tundefined
  7. 生成一个新的数组A,跟当前数组的length属性保持一致
  8. 如果报错就返回
  9. 设定k等于 0
  10. 只要k小于当前数组的length属性,就重复下面步骤
    1. 设定Pk等于ToString(k),即将K转为字符串
    2. 设定kPresent等于HasProperty(O, Pk),即求当前数组有没有指定属性
    3. 如果报错就返回
    4. 如果kPresent等于true,则进行下面步骤
      1. 设定kValue等于Get(O, Pk),取出当前数组的指定属性
      2. 如果报错就返回
      3. 设定mappedValue等于Call(callbackfn, T, «kValue, k, O»),即执行回调函数
      4. 如果报错就返回
      5. 设定status等于CreateDataPropertyOrThrow (A, Pk, mappedValue),即将回调函数的值放入A数组的指定位置
      6. 如果报错就返回
    5. k增加 1
  11. 返回A

仔细查看上面的算法,可以发现,当处理一个全是空位的数组时,前面步骤都没有问题。进入第 10 步中第 2 步时,kPresent会报错,因为空位对应的属性名,对于数组来说是不存在的,因此就会返回,不会进行后面的步骤。

27. 异步遍历器

将异步操作包装成 Thunk 函数或者 Promise 对象,即next()方法返回值的value属性是一个 Thunk 函数或者 Promise 对象,等待以后返回真正的值,而done属性则还是同步产生的。

  1. function idMaker() {

  2. let index = 0;

  3. return {

  4. `next: function() {`
    
  5.   `return {`
    
  6.     `value: new Promise(resolve => setTimeout(() => resolve(index++), 1000)),`
    
  7.     `done: false`
    
  8.   `};`
    
  9. `}`
    
  10. };

  11. }

  12. const it = idMaker();

  13. it.next().value.then(o => console.log(o)) // 1

  14. it.next().value.then(o => console.log(o)) // 2

  15. it.next().value.then(o => console.log(o)) // 3

  16. // ...

上面代码中,value属性的返回值是一个 Promise 对象,用来放置异步操作。但是这样写很麻烦,不太符合直觉,语义也比较绕。

asyncIterator是一个异步遍历器,调用next方法以后,返回一个 Promise 对象。因此,可以使用then方法指定,这个 Promise 对象的状态变为resolve以后的回调函数。回调函数的参数,则是一个具有valuedone两个属性的对象,这个跟同步遍历器是一样的。

我们知道,一个对象的同步遍历器的接口,部署在Symbol.iterator属性上面。同样地,对象的异步遍历器接口,部署在Symbol.asyncIterator属性上面。不管是什么样的对象,只要它的Symbol.asyncIterator属性有值,就表示应该对它进行异步遍历。

  1. const asyncIterable = createAsyncIterable(['a', 'b']);

  2. const asyncIterator = asyncIterable[Symbol.asyncIterator]();

  3. asyncIterator

  4. .next()

  5. .then(iterResult1 => {

  6. console.log(iterResult1); // { value: 'a', done: false }

  7. return asyncIterator.next();

  8. })

  9. .then(iterResult2 => {

  10. console.log(iterResult2); // { value: 'b', done: false }

  11. return asyncIterator.next();

  12. })

  13. .then(iterResult3 => {

  14. console.log(iterResult3); // { value: undefined, done: true }

  15. });

  16. async function f() {

  17. const asyncIterable = createAsyncIterable(['a', 'b']);

  18. const asyncIterator = asyncIterable[Symbol.asyncIterator]();

  19. console.log(await asyncIterator.next());

  20. // { value: 'a', done: false }

  21. console.log(await asyncIterator.next());

  22. // { value: 'b', done: false }

  23. console.log(await asyncIterator.next());

  24. // { value: undefined, done: true }

  25. }

面代码中,next方法用await处理以后,就不必使用then方法了。整个流程已经很接近同步处理了。

注意,异步遍历器的next方法是可以连续调用的,不必等到上一步产生的 Promise 对象resolve以后再调用。这种情况下,next方法会累积起来,自动按照每一步的顺序运行下去。下面是一个例子,把所有的next方法放在Promise.all方法里面。

  1. const asyncIterable = createAsyncIterable(['a', 'b']);

  2. const asyncIterator = asyncIterable[Symbol.asyncIterator]();

  3. const [{value: v1}, {value: v2}] = await Promise.all([

  4. asyncIterator.next(), asyncIterator.next()

  5. ]);

  6. console.log(v1, v2); // a b

另一种用法是一次性调用所有的next方法,然后await最后一步操作。

  1. async function runner() {

  2. const writer = openFile('someFile.txt');

  3. writer.next('hello');

  4. writer.next('world');

  5. await writer.return();

  6. }

  7. runner();

createAsyncIterable()返回一个拥有异步遍历器接口的对象,for...of循环自动调用这个对象的异步遍历器的next方法,会得到一个 Promise 对象。await用来处理这个 Promise 对象,一旦resolve,就把得到的值(x)传入for...of的循环体。

for await...of循环的一个用途,是部署了 asyncIterable 操作的异步接口,可以直接放入这个循环。

  1. let body = '';

  2. async function f() {

  3. for await(const data of req) body += data;

  4. const parsed = JSON.parse(body);

  5. console.log('got', parsed);

  6. }

上面代码中,req是一个 asyncIterable 对象,用来异步读取数据。可以看到,使用for await...of循环以后,代码会非常简洁。

如果next方法返回的 Promise 对象被rejectfor await...of就会报错,要用try...catch捕捉。

  1. async function () {
  2. try {
  3. `for await (const x of createRejectingIterable()) {`
    
  4.   `console.log(x);`
    
  5. `}`
    
  6. } catch (e) {
  7. `console.error(e);`
    
  8. }
  9. }

注意,for await...of循环也可以用于同步遍历器。

  1. (async function () {
  2. for await (const x of ['a', 'b']) {
  3. `console.log(x);`
    
  4. }
  5. })();
  6. // a
  7. // b

异步遍历器的设计目的之一,就是 Generator 函数处理同步操作和异步操作时,能够使用同一套接口。

  1. // 同步 Generator 函数

  2. function* map(iterable, func) {

  3. const iter = iterable[Symbol.iterator]();

  4. while (true) {

  5. `const {value, done} = iter.next();`
    
  6. `if (done) break;`
    
  7. `yield func(value);`
    
  8. }

  9. }

  10. // 异步 Generator 函数

  11. async function* map(iterable, func) {

  12. const iter = iterable[Symbol.asyncIterator]();

  13. while (true) {

  14. `const {value, done} = await iter.next();`
    
  15. `if (done) break;`
    
  16. `yield func(value);`
    
  17. }

  18. }

yield*语句也可以跟一个异步遍历器。

  1. async function* gen1() {

  2. yield 'a';

  3. yield 'b';

  4. return 2;

  5. }

  6. async function* gen2() {

  7. // result 最终会等于 2

  8. const result = yield* gen1();

  9. }

28. ArrayBuffer

二进制数组由三类对象组成。

(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也是一个构造函数,可以分配一段可以存放数据的连续内存区域。

  1. const buf = new ArrayBuffer(32);

上面代码生成了一段 32 字节的内存区域,每个字节的值默认都是 0。可以看到,ArrayBuffer构造函数的参数是所需要的内存大小(单位字节)。

为了读写这段内容,需要为它指定视图。DataView视图的创建,需要提供ArrayBuffer对象实例作为参数。

  1. const buf = new ArrayBuffer(32);
  2. const dataView = new DataView(buf);
  3. dataView.getUint8(0) // 0

上面代码对一段 32 字节的内存,建立DataView视图,然后以不带符号的 8 位整数格式,从头读取 8 位二进制数据,结果得到 0,因为原始内存的ArrayBuffer对象,默认所有位都是 0。

另一种TypedArray视图,与DataView视图的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的数据格式。

  1. const buffer = new ArrayBuffer(12);

  2. const x1 = new Int32Array(buffer);

  3. x1[0] = 1;

  4. const x2 = new Uint8Array(buffer);

  5. x2[0] = 2;

  6. x1[0] // 2

TypedArray视图的构造函数,除了接受ArrayBuffer实例作为参数,还可以接受普通数组作为参数,直接分配内存生成底层的ArrayBuffer实例,并同时完成对这段内存的赋值。

  1. const typedArray = new Uint8Array([0,1,2]);

  2. typedArray.length // 3

  3. typedArray[0] = 5;

  4. typedArray // [5, 1, 2]

ArrayBuffer实例的byteLength属性,返回所分配的内存区域的字节长度。

  1. const buffer = new ArrayBuffer(32);
  2. buffer.byteLength
  3. // 32

ArrayBuffer实例有一个slice方法,允许将内存区域的一部分,拷贝生成一个新的ArrayBuffer对象。

  1. const buffer = new ArrayBuffer(8);
  2. const newBuffer = buffer.slice(0, 3);

ArrayBuffer有一个静态方法isView,返回一个布尔值,表示参数是否为ArrayBuffer的视图实例。这个方法大致相当于判断参数,是否为TypedArray实例或DataView实例。

  1. const buffer = new ArrayBuffer(8);

  2. ArrayBuffer.isView(buffer) // false

  3. const v = new Int32Array(buffer);

  4. 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()

复合视图:

由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做“复合视图”。

  1. const buffer = new ArrayBuffer(24);

  2. const idView = new Uint32Array(buffer, 0, 1);

  3. const usernameView = new Uint8Array(buffer, 4, 16);

  4. const amountDueView = new Float32Array(buffer, 20, 1);

DataView视图本身也是构造函数,接受一个ArrayBuffer对象作为参数,生成视图。

  1. new DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);

下面是一个例子。

  1. const buffer = new ArrayBuffer(24);
  2. 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属性默认为textXMLHttpRequest第二版XHR2允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(responseType)设为arraybuffer;如果不知道,就设为blob

  1. let xhr = new XMLHttpRequest();

  2. xhr.open('GET', someUrl);

  3. xhr.responseType = 'arraybuffer';

  4. xhr.onload = function () {

  5. let arrayBuffer = xhr.response;

  6. // ···

  7. };

  8. xhr.send();

网页Canvas元素输出的二进制像素数据,就是 TypedArray 数组。

  1. const canvas = document.getElementById('myCanvas');

  2. const ctx = canvas.getContext('2d');

  3. const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

  4. const uint8ClampedArray = imageData.data;

ES2017 引入SharedArrayBuffer,允许 Worker 线程与主线程共享同一块内存。SharedArrayBuffer的 API 与ArrayBuffer一模一样,唯一的区别是后者无法共享数据。

  1. // 主线程

  2. // 新建 1KB 共享内存

  3. const sharedBuffer = new SharedArrayBuffer(1024);

  4. // 主线程将共享内存的地址发送出去

  5. w.postMessage(sharedBuffer);

  6. // 在共享内存上建立视图,供写入数据

  7. const sharedArray = new Int32Array(sharedBuffer);

上面代码中,postMessage方法的参数是SharedArrayBuffer对象。

Worker 线程从事件的data属性上面取到数据。

  1. // Worker 线程

  2. onmessage = function (ev) {

  3. // 主线程共享的数据,就是 1KB 的共享内存

  4. const sharedBuffer = ev.data;

  5. // 在共享内存上建立视图,方便读写

  6. const sharedArray = new Int32Array(sharedBuffer);

  7. // ...

  8. };

共享内存也可以在 Worker 线程创建,发给主线程。

SharedArrayBufferArrayBuffer一样,本身是无法读写的,必须在上面建立视图,然后通过视图读写。

  1. // 分配 10 万个 32 位整数占据的内存空间

  2. const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000);

  3. // 建立 32 位整数视图

  4. const ia = new Int32Array(sab); // ia.length == 100000

  5. // 新建一个质数生成器

  6. const primes = new PrimeGenerator();

  7. // 将 10 万个质数,写入这段内存空间

  8. for ( let i=0 ; i < ia.length ; i++ )

  9. ia[i] = primes.next();

  10. // 向 Worker 线程发送这段共享内存

  11. w.postMessage(ia);

Worker 线程收到数据后的处理如下。

  1. // Worker 线程
  2. let ia;
  3. onmessage = function (ev) {
  4. ia = ev.data;
  5. console.log(ia.length); // 100000
  6. console.log(ia[37]); // 输出 163,因为这是第38个质数
  7. };

多线程共享内存,最大的问题就是如何防止两个线程同时修改某个地址,或者说,当一个线程修改共享内存以后,必须有一个机制让其他线程同步。SharedArrayBuffer API 提供Atomics对象,保证所有共享内存的操作都是“原子性”的,并且可以在所有线程内同步。

共享内存上面的某些运算是不能被打断的,即不能在运算过程中,让其他线程改写内存上面的值。Atomics 对象提供了一些运算方法,防止数据被改写。

  1. Atomics.add(sharedArray, index, value)

Atomics.add用于将value加到sharedArray[index],返回sharedArray[index]旧的值。

  1. Atomics.sub(sharedArray, index, value)

Atomics.sub用于将valuesharedArray[index]减去,返回sharedArray[index]旧的值。

  1. Atomics.and(sharedArray, index, value)

Atomics.and用于将valuesharedArray[index]进行位运算and,放入sharedArray[index],并返回旧的值。

  1. Atomics.or(sharedArray, index, value)

Atomics.or用于将valuesharedArray[index]进行位运算or,放入sharedArray[index],并返回旧的值。

  1. Atomics.xor(sharedArray, index, value)

Atomic.xor用于将vaulesharedArray[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 里面原来那个值是否发生变化(即被其他线程改写过)。如果没有改写过,就将它写回原来的位置,否则读取新的值,再重头进行一次操作。

29. 最新提案

do 表达式

  1. // 等同于 <表达式>

  2. do { <表达式>; }

  3. // 等同于 <语句>

  4. do { <语句> }

do表达式的好处是可以封装多个语句,让程序更加模块化,就像乐高积木那样一块块拼装起来。

  1. let x = do {
  2. if (foo()) { f() }
  3. else if (bar()) { g() }
  4. else { h() }
  5. };

开发者使用一个模块时,有时需要知道模板本身的一些信息(比如模块的路径)。现在有一个提案,为 import 命令添加了一个元属性import.meta,返回当前模块的元信息。

import.meta只能在模块内部使用,如果在模块外部使用会报错。

这个属性返回一个对象,该对象的各种属性就是当前运行的脚本的元信息。具体包含哪些属性,标准没有规定,由各个运行环境自行决定。一般来说,import.meta至少会有下面两个属性。

(1)import.meta.url

import.meta.url返回当前模块的 URL 路径。举例来说,当前模块主文件的路径是https://foo.com/main.jsimport.meta.url就返回这个路径。如果模块里面还有一个数据文件data.txt,那么就可以用下面的代码,获取这个数据文件的路径。

  1. 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是浏览器特有的元属性,返回加载模块的那个<script>元素,相当于document.currentScript属性。

  1. // HTML 代码为

  2. // <script type="module" src="my-module.js" data-foo="abc"></script>

  3. // my-module.js 内部执行下面的代码

  4. import.meta.scriptElement.dataset.foo

  5. // "abc"

函数的部分执行有一些特别注意的地方。

(1)函数的部分执行是基于原函数的。如果原函数发生变化,部分执行生成的新函数也会立即反映这种变化。

(2)如果预先提供的那个值是一个表达式,那么这个表达式并不会在定义时求值,而是在每次调用时求值。

(3)如果新函数的参数多于占位符的数量,那么多余的参数将被忽略。

(4)...只会被采集一次,如果函数的部分执行使用了多个...,那么每个...的值都将相同。

JavaScript 的管道是一个运算符,写作|>。它的左边是一个表达式,右边是一个函数。管道运算符把左边表达式的值,传入右边的函数进行求值。

  1. x |> f
  2. // 等同于
  3. f(x)

数值分割:

  1. 123_00 === 12_300 // true

  2. 12345_00 === 123_4500 // true

  3. 12345_00 === 1_234_500 // true

数值分隔符有几个使用注意点。

  • 不能在数值的最前面(leading)或最后面(trailing)。
  • 不能两个或两个以上的分隔符连在一起。
  • 小数点的前后不能有分隔符。
  • 科学计数法里面,表示指数的eE前后不能有分隔符。

Math.sign()用来判断一个值的正负,但是如果参数是-0,它会返回-0

  1. Math.sign(-0) // -0

  2. Math.signbit(2) //false

  3. Math.signbit(-2) //true

  4. Math.signbit(0) //false

  5. Math.signbit(-0) //true

双冒号运算符

箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(callapplybind)。但是,箭头函数并不适用于所有场合,所以现在有一个提案,提出了“函数绑定”(function bind)运算符,用来取代callapplybind调用。

函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。

  1. foo::bar;

  2. // 等同于

  3. bar.bind(foo);

  4. foo::bar(...arguments);

  5. // 等同于

  6. bar.apply(foo, arguments);

  7. const hasOwnProperty = Object.prototype.hasOwnProperty;

  8. function hasOwn(obj, key) {

  9. return obj::hasOwnProperty(key);

  10. }

如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。

  1. var method = obj::obj.foo;

  2. // 等同于

  3. var method = ::obj.foo;

  4. let log = ::console.log;

  5. // 等同于

  6. var log = console.log.bind(console);

如果双冒号运算符的运算结果,还是一个对象,就可以采用链式写法。

  1. import { map, takeWhile, forEach } from "iterlib";

  2. getPlayers()

  3. ::map(x => x.character())

  4. ::takeWhile(x => x.strength > 100)

  5. ::forEach(x => console.log(x));

30. Decorator

装饰器可以用来装饰整个类。

  1. function testable(isTestable) {

  2. return function(target) {

  3. `target.isTestable = isTestable;`
    
  4. }

  5. }

  6. @testable(true)

  7. class MyTestableClass {}

  8. MyTestableClass.isTestable // true

  9. @testable(false)

  10. class MyClass {}

  11. MyClass.isTestable // false

上面代码中,@testable就是一个装饰器。它修改了MyTestableClass这个类的行为,为它加上了静态属性isTestabletestable函数的参数targetMyTestableClass类本身。

装饰器不仅可以装饰类,还可以装饰类的属性。

  1. class Person {
  2. @readonly
  3. name() { return `${this.first} ${this.last}` }
  4. }

上面代码中,装饰器readonly用来装饰“类”的name方法。

装饰器函数readonly一共可以接受三个参数。

  1. function readonly(target, name, descriptor){

  2. // descriptor对象原来的值如下

  3. // {

  4. // value: specifiedFunction,

  5. // enumerable: false,

  6. // configurable: true,

  7. // writable: true

  8. // };

  9. descriptor.writable = false;

  10. return descriptor;

  11. }

  12. readonly(Person.prototype, 'name', descriptor);

  13. // 类似于

  14. Object.defineProperty(Person.prototype, 'name', descriptor);

装饰器第一个参数是类的原型对象,上例是Person.prototype,装饰器的本意是要“装饰”类的实例,但是这个时候实例还没生成,所以只能去装饰原型(这不同于类的装饰,那种情况时target参数指的是类本身);第二个参数是所要装饰的属性名,第三个参数是该属性的描述对象。

另外,上面代码说明,装饰器(readonly)会修改属性的描述对象(descriptor),然后被修改的描述对象再用来定义属性。

装饰器只适用于类和类的方法,并不适用于函数

core-decorators.js

core-decorators.js是一个第三方模块,提供了几个常见的装饰器,通过它可以更好地理解装饰器。

autobind装饰器使得方法中的this对象,绑定原始对象。

readonly装饰器使得属性或方法不可写。

override装饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。

deprecatedeprecated装饰器在控制台显示一条警告,表示该方法将废除。

suppressWarnings装饰器抑制deprecated装饰器导致的console.warn()调用。但是,异步代码发出的调用除外。

在装饰器的基础上,可以实现Mixin模式。所谓Mixin模式,就是对象继承的一种替代方案,中文译为“混入”(mix in),意为在一个对象之中混入另外一个对象的方法。

方法一:

  1. const Foo = {

  2. foo() { console.log('foo') }

  3. };

  4. class MyClass {}

  5. Object.assign(MyClass.prototype, Foo);

  6. let obj = new MyClass();

  7. obj.foo() // 'foo'

方法二:
部署一个通用脚本mixins.js,将 Mixin 写成一个装饰器。

  1. export function mixins(...list) {
  2. return function (target) {
  3. `Object.assign(target.prototype, ...list);`
    
  4. };
  5. }

然后,就可以使用上面这个装饰器,为类“混入”各种方法。

  1. import { mixins } from './mixins';

  2. const Foo = {

  3. foo() { console.log('foo') }

  4. };

  5. @mixins(Foo)

  6. class MyClass {}

  7. let obj = new MyClass();

  8. obj.foo() // "foo"

Trait 也是一种装饰器,效果与 Mixin 类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等。

下面采用traits-decorator这个第三方模块作为例子。这个模块提供的traits装饰器,不仅可以接受对象,还可以接受 ES6 类作为参数。

  1. import { traits } from 'traits-decorator';

  2. class TFoo {

  3. foo() { console.log('foo') }

  4. }

  5. const TBar = {

  6. bar() { console.log('bar') }

  7. };

  8. @traits(TFoo, TBar)

  9. class MyClass { }

  10. let obj = new MyClass();

  11. obj.foo() // foo

  12. obj.bar() // bar

31. 参考链接

官方文件
综合介绍
let 和 const
解构赋值
字符串
正则
数值
数组
函数
对象
Symbol
Set 和 Map
Proxy 和 Reflect
Promise 对象
Iterator
Generator
异步操作和 Async 函数
Class
Decorator
Module
二进制数组
SIMD
工具

32. Mixin

JavaScript 语言的设计是单一继承,即子类只能继承一个父类,不允许继承多个父类。这种设计保证了对象继承的层次结构是树状的,而不是复杂的网状结构

但是,这大大降低了编程的灵活性。因为实际开发中,有时不可避免,子类需要继承多个父类。举例来说,“猫”可以继承“哺乳类动物”,也可以继承“宠物”。

这里使用mixin和trait解决

33. SIMD

SIMD(发音/sim-dee/)是“Single Instruction/Multiple Data”的缩写,意为“单指令,多数据”。它是 JavaScript 操作 CPU 对应指令的接口,你可以看做这是一种不同的运算执行模式。与它相对的是 SISD(“Single Instruction/Single Data”),即“单指令,单数据”。

SIMD 的含义是使用一个指令,完成多个数据的运算;SISD 的含义是使用一个指令,完成单个数据的运算,这是 JavaScript 的默认运算模式。显而易见,SIMD 的执行效率要高于 SISD,所以被广泛用于 3D 图形运算、物理模拟等运算量超大的项目之中。

总的来说,SIMD 是数据并行处理(parallelism)的一种手段,可以加速一些运算密集型操作的速度。将来与 WebAssembly 结合以后,可以让 JavaScript 达到二进制代码的运行速度。

34. 函数式编程

柯里化(currying)指的是将一个多参数的函数拆分成一系列函数,每个拆分后的函数都只接受一个参数(unary)。

  1. function add (a) {

  2. return function (b) {

  3. `return a + b;`
    
  4. }

  5. }

  6. // 或者采用箭头函数写法

  7. const add = x => y => x + y;

  8. const f = add(1);

  9. f(1) // 2

函数合成(function composition)指的是,将多个函数合成一个函数。

  1. const compose = f => g => x => f(g(x));

  2. const f = compose (x => x * 4) (x => x + 3);

  3. f(2) // 20

参数倒置(flip)指的是改变函数前两个参数的顺序。

  1. let f = {};

  2. f.flip =

  3. fn =>

  4. `(a, b, ...args) => fn(b, a, ...args.reverse());`
    
  5. var divide = (a, b) => a / b;

  6. var flip = f.flip(divide);

  7. flip(10, 5) // 0.5

  8. flip(1, 10) // 10

  9. var three = (a, b, c) => [a, b, c];

  10. var flip = f.flip(three);

  11. flip(1, 2, 3); // => [2, 1, 3]

执行边界(until)指的是函数执行到满足条件为止。

  1. let f = {};

  2. f.until = (condition, f) =>

  3. (...args) => {

  4. `var r = f.apply(null, args);`
    
  5. `return condition(r) ? r : f.until(condition, f)(r);`
    
  6. };

  7. let condition = x => x > 100;

  8. let inc = x => x + 1;

  9. let until = f.until(condition, inc);

  10. until(0) // 101

  11. condition = x => x === 5;

  12. until = f.until(condition, inc);

  13. until(3) // 5

Mateo Gianolio, Haskell in ES6: Part 1

next()throw()return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。

原文地址:https://blog.csdn.net/m0_72947390/article/details/135681654

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。

如若转载,请注明出处:http://www.7code.cn/show_60390.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注