es6笔记

97 阅读52分钟

1.globalThis顶层对象

任何环境下,都能使用globalThis获取顶层对象

2.Number类型的特点

  • JavaScript 所有数字都保存成 64 位浮点数
  • 数值的精度只能到 53 个二进制位(相当于 16 个十进制位)
  • 大于或等于2的1024次方的数值,js无法表示,会返回Infinity

3.BigInt类型

3.1BigInt类型的特点

  • BigInt只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示
  • 为了与Number类型区别,BigInt类型的数据必须添加后缀n
  • BigInt同样可以使用各种进制表示,都要加上后缀n
  • BigInt与普通整数是两种值,它们之间并不相等
  • typeof运算符对于BigInt类型的数据返回'bigint'
  • BigInt可以使用负号(-),但是不能使用正号(+),因为会与asm.js冲突
  • BigInt不能与普通数值进行混合运算
  • 比较运算符(比如>)和相等运算符(==)允许BigInt与其他类型的值混合计算
  • BigInt与字符串混合运算时,会先转为字符串,再进行运算

3.2BigInt()函数

  • BigInt()函数必须有参数,而且参数必须可以正常转为数值,否则会报错
  • 参数如果是小数,也会报错

4.函数参数默认值

4.1函数参数默认值的位置

定义了默认值的参数,应该是函数的尾参数,非尾部的参数即使设置了默认值,也没法省略,否则将报错

// 例一
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]

4.2undefined或null作为默认值的区别

如果传入undefined作为参数,将触发该参数等于默认值,null则没有这个效果

function foo(x = 5, y = 6) {
  console.log(x, y);
}

foo(undefined, null)
// 5 null

4.3参数默认值对函数length属性的影响

  • 函数的length属性,指函数预期传入的参数个数
  • 函数的参数指定默认值之后,函数的length属性将失真(不包括指定了默认值的参数)
  • 如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
(function(...args) {}).length // 0
(function (a, b = 1, c) {}).length // 1

4.4默认值形成作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域,等到初始化结束,这个作用域才会消失

// 例1
var x = 1;

function f(x, y = x) {	// {let x=2; let y=x}
  console.log(y);
}

f(2) // 2

// 例2
function f(y = x) {	// {let y=x}
  let x = 2;
  console.log(y);
}

f() // ReferenceError: x is not defined

// 例3
var x = 1;

function foo(x = x) {	// {let x=x}
  // ...
}

foo() // ReferenceError: Cannot access 'x' before initialization

// 例4
var x = 1;
function foo(x, y = function() { x = 2; }) // 这个局部作用域 {let x=1;let y=function() { x = 2; }}
{
  var x = 3; 
  y();	// (参数)局部x=2
  console.log(x); // 函数作用域声明了x,x=3
}

foo() // 3

// 例5
var x = 1;
function foo(x, y = function() { x = 2; })  // 这个局部作用域 {let x=1;let y=function() { x = 2; }}
{
  x = 3; // 指向参数x
  y();	// (参数)局部x=2
  console.log(x); // 函数作用域没有声明x,其x指向参数x,x=2
}

foo() // 2

4.5rest和arguments的区别

  • arguments是伪数组,rest是真数组
  • rest参数之后不能再有其他参数
  • 函数的length属性,不包括rest参数

4.6函数内的严格模式

函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错

4.7函数的name属性

// 具名函数
function a() {}
a.name // 'a'

// 匿名函数赋值
var b = function () {}
b.name // 'b'

// 具名函数赋值
const bar = function baz() {}
bar.name // 'baz'

// Function构造函数返回的函数实例,name属性的值为anonymous
(new Function).name // 'anonymous'

// bind返回的函数,name属性值会加上bound前缀
function foo() {};
foo.bind({}).name // 'bound foo'

(function(){}).bind({}).name // 'bound'

5.箭头函数

5.1简化写法

  • 由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错

    let getTempItem = id => ({ id, name: "Temp" })
    
  • 如果箭头函数只有一行语句,且不需要返回值,可以使用void

    let fn = () => void doesNotReturn()
    
    // 等同于
    let fn = function() { doesNotReturn() }
    

5.2箭头函数没有自己的this

  • 普通函数的this指向在定义时决定

  • 箭头函数没有自己的this,它的this指向外层作用域的this,外层作用域的this指向被改变时(如call),箭头函数的this会跟随变化

    // 普通函数
    function foo() {
      function fn() {
        console.log('id:', this.id);
      }
      fn()
    }
    
    var id = 21;
    
    foo.call({ id: 42 }) // id: 21
    
    // 箭头函数
    function foo() {
      const fn =()=> {
        console.log('id:', this.id);
      }
      fn()
    }
    
    var id = 21;
    
    foo.call({ id: 42 }) // id: 42
    // 等价于
    function foo() {
      let that = this
      function fn() {
        console.log('id:', that.id);
      }
      fn()
    }
    
    var id = 21;
    
    foo.call({ id: 42 }) // id: 42
    
  • 定义对象的方法不要使用箭头函数,对象不构成单独的作用域

    globalThis.s = 21;
    
    const obj = {
      s: 42,
      m: () => console.log(this.s)
    };
    
    obj.m() // 21
    

6.尾调用

6.1定义

某个函数的最后一步是调用另一个函数(调用后不能有其它操作)

function f(x){
  return g(x);
}

不符合的情况:

// 情况一
function f(x){
  let y = g(x);
  return y;
}

// 情况二
function f(x){
  return g(x) + 1;
}

// 情况三
function f(x){
  g(x);
}

尾调用不一定出现在函数尾部,只要是最后一步操作即可

// m和n都属于尾调用
function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}

6.2尾调用优化

  • 尾调用时只保留内层函数的调用帧
  • 只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧
  • 目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持

6.3尾调用只在严格模式下生效

  • ES6 的尾调用优化只在严格模式下开启,正常模式是无效的

  • 函数内部有两个变量,可以跟踪函数的调用栈

    • func.arguments:返回调用时函数的参数
    • func.caller:返回调用当前函数的那个函数
  • 尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。

  • 严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。

function restricted() {
  'use strict';
  restricted.caller;    // 报错
  restricted.arguments; // 报错
}
restricted();

7.尾递归

7.1定义

  • 函数调用自身,称为递归。如果尾调用自身,就称为尾递归

  • ES6 第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”

  • ES6 中只要使用尾递归,就不会发生栈溢出(或者层层递归造成的超时),相对节省内存

8.数组

8.1负索引

  • js不支持数组的负索引
  • [-1]会被js理解为对象的(js中数组是特殊的对象)
  • 数组中只有键名为自然数(0,1,2...)的值才能被map遍历到

8.2at()

  • 接受一个整数作为参数,返回对应位置的成员,支持负索引(该方法适用于数组、字符串、类型数组等)

  • 如果参数位置超出了数组范围,at()返回undefined

const arr = [0, 1, 2, 3, 4]
arr.at(2) // 2
arr.at(-2) // 3
arr.at(5) // undefined
arr.at(-6) // undefined

8.3数组空位

  • 空位指数组的某一个位置没有任何值

  • 空位不是undefined,某一个位置的值等于undefined,依然是有值的

  • in运算符可以判断对应的下标是否为空位

    0 in [undefined, undefined, undefined] // true
    0 in [, , ,] // false
    
  • es6中,大部分方法会将空位处理成undefined,但map在遍历数组时会直接跳过空位

8.4数组排序sort()的稳定性

  • 排序稳定性指的是排序关键字相同的项目,排序前后的顺序不变

    // 按首字母排序
    // 稳定排序
    const arr = [
      'peach',
      'straw',
      'apple',
      'spork'
    ];
    
    const stableSorting = (s1, s2) => {
      if (s1[0] < s2[0]) return -1;
      return 1;
    };
    
    arr.sort(stableSorting)
    // ["apple", "peach", "straw", "spork"]
    
    // 不稳定排序
    const unstableSorting = (s1, s2) => {
      if (s1[0] <= s2[0]) return -1;
      return 1;
    };
    
    arr.sort(unstableSorting)
    // ["apple", "peach", "spork", "straw"]
    
  • 插入排序、合并排序、冒泡排序等都是稳定的

  • 堆排序、快速排序等是不稳定的

  • 不稳定排序的主要缺点是,多重排序时可能会产生问题

  • ES2019 明确规定,Array.prototype.sort()的默认排序算法必须稳定

9.对象

9.1属性的简洁表示法

对象中,使用简洁表示法定义的方法不能当作构造函数使用

const obj = {
  f() {
    this.foo = 'bar';
  }
};

new obj.f() // 报错

9.2属性名表达式

  • 对象的属性名和方法名都可以通过中括号语法表示(方括号内必须是一个表达式)

  • 属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object]

    const keyA = {a: 1};
    const keyB = {b: 2};
    
    const myObject = {
      [keyA]: 'valueA',
      [keyB]: 'valueB'
    };
    
    myObject // {[object Object]: "valueB"}
    

9.3方法的name属性

  • 函数的name属性,返回函数名。对象方法也是函数,因此也有name属性

  • 如果对象的方法使用了取值函数(getter)和存值函数(setter),则name属性不是在该方法上面,而是该方法的属性的描述对象的getset属性上面,返回值是方法名前加上getset

    const obj = {
      get foo() {},
      set foo(x) {},
    };
    console.log(obj.foo.name);
    // TypeError: Cannot read property 'name' of undefined
    
    const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
    console.log(descriptor);
    // {enumerable: true,configurable: true,get: ƒ foo(),set: ƒ foo(x)}
    
    console.log(descriptor.get.name); // "get foo"
    console.log(descriptor.set.name); // "set foo"
    
  • bind方法创造的函数,name属性返回bound加上原函数的名字

    const doSomething = function () {
      // ...
    };
    console.log(doSomething.bind().name); // "bound doSomething"
    
  • 如果对象的方法是一个 Symbol 值,那么name属性返回的是这个 Symbol 值的描述

    const key1 = Symbol('description');
    const key2 = Symbol();
    let obj = {
      [key1]() {},
      [key2]() {},
    };
    console.log(obj[key1].name); // "[description]"
    console.log(obj[key2].name); // ""
    

9.3属性的可枚举性

  • 对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为

  • Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象

  • 描述对象的enumerable属性,称为“可枚举性”,如果enumerablefalse,以下操作会忽略当前属性

    • for...in循环:只遍历对象自身的和继承的可枚举的属性(大多数时候,我们只关心对象自身的属性。所以,尽量不要用for...in循环,而用Object.keys()代替)
    • Object.keys():返回对象自身的所有可枚举的属性的键名
    • JSON.stringify():只串行化对象自身的可枚举的属性
    • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性
  • toStringlength属性的enumerable都是false

    Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
    // false
    
    Object.getOwnPropertyDescriptor([], 'length').enumerable
    // false
    
  • ES6 规定,所有 Class 的原型的方法都是不可枚举的

    Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
    // false
    

9.4属性的遍历

ES6 一共有 5 种方法可以遍历对象的属性:

(1)for...in

for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)

(2)Object.keys(obj)

Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名

(3)Object.getOwnPropertyNames(obj)

Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名

(4)Object.getOwnPropertySymbols(obj)

Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名

(5)Reflect.ownKeys(obj)

Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举

以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则:

  • 首先遍历所有数值键,按照数值升序排列。
  • 其次遍历所有字符串键,按照加入时间升序排列。
  • 最后遍历所有 Symbol 键,按照加入时间升序排列。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]

9.5super关键字

  • super指向当前对象的原型对象

  • super关键字表示原型对象时,只能用在对象的简写方法之中,用在其他地方都会报错

  • super.foo等同于Object.getPrototypeOf(this).foo(属性)或Object.getPrototypeOf(this).foo.call(this)(方法)

    const proto = {
      x: 'hello',
      foo() {
        console.log(this.x);
      },
    };
    
    const obj = {
      x: 'world',
      foo() {
        super.foo();
      }
    }
    
    Object.setPrototypeOf(obj, proto);
    
    obj.foo() // "world"
    

9.6拷贝对象原型的属性

// 写法一
const clone1 = {
  __proto__: Object.getPrototypeOf(obj),
  ...obj
};

// 写法二
const clone2 = Object.assign(
  Object.create(Object.getPrototypeOf(obj)),
  obj
);

// 写法三
const clone3 = Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
)

// 写法四
const clone4 = JSON.parse(JSON.stringify(obj))

9.7AggregateError 错误对象

  • 如果某个单一操作,同时引发了多个错误,需要同时抛出这些错误,那么就可以抛出一个AggregateError错误对象,把各种错误都放在这个对象里面

  • AggregateError本身是一个构造函数,用来生成AggregateError实例对象

  • AggregateError()构造函数可以接受两个参数

    • errors:数组,它的每个成员都是一个错误对象。该参数是必须的

    • message:字符串,表示 AggregateError 抛出时的提示信息。该参数是可选的

    try {
      throw new AggregateError([
      new Error('ERROR_11112'),
      new TypeError('First name must be a string'),
      new RangeError('Transaction value must be at least 1'),
      new URIError('User profile link must be https'),
    ], 'Transaction cannot be processed')
    } catch (e) {
      console.dir(e);
    }
    
  • AggregateError的实例对象有三个属性

    • name:错误名称,默认为“AggregateError”
    • message:错误的提示信息
    • errors:数组,每个成员都是一个错误对象
    try {
      throw new AggregateError([
        new Error("some error"),
      ], 'Hello');
    } catch (e) {
      console.log(e instanceof AggregateError); // true
      console.log(e.message);                   // "Hello"
      console.log(e.name);                      // "AggregateError"
      console.log(e.errors);                    // [ Error: "some error" ]
    }
    

10.对象的方法

10.1 Object.is()

  • Object.is()判断两个值是否完全相等

  • ===下,NaN不等于自身、+0等于-0Object.is()解决了这两个问题

    console.log(+0 === -0); //true
    console.log(NaN === NaN); // false
    
    console.log(Object.is(+0, -0)); // false
    console.log(Object.is(NaN, NaN)); // true
    

10.2 Object.assign()

  • Object.assign()类似于...,属于浅拷贝

  • 第一个参数是目标对象target,其它参数都作为数据源source

    Object.assign(target, source1, source2)
    
    • 如果只有一个参数targetObject.assign()会直接返回该参数
    • 如果target不是对象,则会先转成对象,然后返回
    • 由于undefinednull无法转成对象,所以如果它们作为target,就会报错
    • 如果source不是对象,则会先转成对象,如果无法转成对象,就会跳过,即undefinednull作为source时,会被跳过且不会报错
    • 字符串会以数组形式,拷贝入目标对象
  • Object.assign()可以用来处理数组,但是会把数组视为对象

    let arr = [1, 2, 3];
    Object.assign(arr, [4, 5]);
    console.log(arr); // [4, 5, 3]
    
    console.log([...[1, 2, 3], ...[4, 5]]); // [1, 2, 3, 4, 5]
    

10.3 Object.getOwnPropertyDescriptors()

  • Object.getOwnPropertyDescriptors()方法返回一个对象,所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象

  • Object.getOwnPropertyDescriptors()解决了Object.assign()无法正确拷贝get属性和set属性的问题

  • Object.getOwnPropertyDescriptors()方法配合Object.defineProperties()方法,就可以实现正确拷贝

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

10.4 替代__ proto __属性

__proto__属性(前后各两个下划线),用来读取或设置当前对象的原型对象,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的三个方法替代:

1. Object.create()生成操作

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__

const someOtherObj={}
const obj = {
method: function() {}
};
obj.__proto__ = someOtherObj;

// es6 的写法
var obj = Object.create(someOtherObj);
obj.method = function() {}

2. Object.setPrototypeOf()写操作

  • Object.setPrototypeOf方法用来设置一个对象的原型对象(prototype),返回参数对象本身
  • 如果第一个参数不是对象,会自动转为对象。但是由于返回的还是第一个参数,所以这个操作不会产生任何效果
  • 由于undefinednull无法转为对象,所以如果第一个参数是undefinednull,就会报错
const proto = {};
let obj = {};
obj.__proto__ = proto;

// es6 的写法
const proto = {};
const obj = Object.setPrototypeOf({}, proto);

3. Object.getPrototypeOf()读操作

  • Object.setPrototypeOf用于读取一个对象的原型对象
  • 如果参数不是对象,会被自动转为对象
  • 如果参数是undefinednull,它们无法转为对象,所以会报错
function Rectangle() {
  // ...
}

const rec = new Rectangle();

console.log(Object.getPrototypeOf(rec) === Rectangle.prototype);
// true

Object.setPrototypeOf(rec, Object.prototype)
console.log(Object.getPrototypeOf(rec) === Rectangle.prototype);
// false

10.5 for...of...

1. for...of...与for...in...的区别

  • for...of不能直接遍历对象,for...in可以直接遍历对象
  • for...of需要实现iterator接口,对象没有实现iterator接口

2. for...of...的使用

Object.keysObject.valuesObject.entries,作为遍历一个对象的补充手段,供for...of循环使用

let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 };

for (let key of keys(obj)) {
  console.log(key); // 'a', 'b', 'c'
}

for (let value of values(obj)) {
  console.log(value); // 1, 2, 3
}

for (let [key, value] of entries(obj)) {
  console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}

3. Object.keys()

Object.keys方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名

let obj = { a: 1, b: 2, c: 3 };

console.log(Object.keys(obj)); // ['a', 'b', 'c']

4. Object.values()

Object.values方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值

let obj = { a: 1, b: 2, c: 3 };

console.log(Object.values(obj)); // [1, 2, 3]

5. Object.entries()

Object.entries()方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组

let obj = { a: 1, b: 2, c: 3 };

console.log(Object.entries(obj)); // [['a', 1], ['b', 2], ['c', 3]]

6. Object.fromEntries()

Object.fromEntries()方法是Object.entries()的逆操作,用于:

  • 将一个键值对数组转为对象

    let arr = [['a', 1], ['b', 2], ['c', 3]]
    console.log(Object.fromEntries(arr)); // {a: 1, b: 2, c: 3}
    
  • Map 结构转为对象

    const map = new Map().set('foo', true).set('bar', false);
    console.log(map); // {'foo' => true, 'bar' => false}
    console.log(Object.fromEntries(map)); // {foo: true, bar: false}
    
  • 配合URLSearchParams对象,将查询字符串转为对象

    const url = new URLSearchParams('foo=bar&baz=qux');
    console.log(Object.fromEntries(url)); // {foo: 'bar', baz: 'qux'}
    

11.运算符

11.1指数运算符(**)

**是右结合,多个指数运算符连用时,从右往左计算

let x = 2 ** 3 ** 2
// 等同于 2 ** (3 ** 2)
// 512

let a = 1.5;
a **= 2;
// 等同于 a = a ** 2;

11.2可选链操作符(?.)

  • ?.允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效

  • 在引用为空(null或者 undefined) 的情况下不会引起错误,该表达式短路返回值是 undefined

  • 与函数调用一起使用时,如果给定的函数不存在,则返回 undefined

    obj?.prop // 对象属性是否存在
    obj?.[expr] // 同上
    func?.(...args) // 函数或对象方法是否存在
    
  • 以下写法是禁止的,会报错

    // 构造函数
    new a?.()
    new a?.b()
    
    // 链判断运算符的右侧有模板字符串
    a?.`{b}`
    a?.b`{c}`
    
    // 链判断运算符的左侧是 super
    super?.()
    super?.foo
    
    // 链运算符用于赋值运算符左侧
    a?.b = c
    
    // 右侧不得为十进制数值
    foo?.3:0会被解析成foo ? .3 : 0
    

11.3Null 判断运算符 (??)

  • ??运算符左侧的值为nullundefined时,才会返回右侧的值

  • ??||的 区别

    • ||在左侧值为nullundefined空字符串false0,都会继续向右执行
    • ??只在左侧值为nullundefined时,才会继续向右执行
    let a = 0 || 1	// 1
    let b = 0 ?? 1  // 0
    
  • ??一般用于给.?提供默认值

    let res = data.data?.responese ?? 100
    

12.Symbol

12.1定义

Symbol是一种原始数据类型,表示独一无二的值

12.2使用

  • Symbol值通过函数Symbol()产生(不需要new)
// 每一个Symbol实例都是独一无二的
let a = Symbol('foo')
let b = Symbol('foo')
console.log(a===b); //false

// Symbol()函数的参数(可选),表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分
let c = Symbol('bar')
console.log(c.toString()); // "Symbol(bar)"
// 如果参数不是字符串类型会调用toString转字符串
  • Symbol只能通过String()toString()显式的转为字符串,不能使用拼接字符串隐式转换

12.3 Symbol.prototype.description

description属性可以获取Symbol()的参数(描述)

const s = Symbol('foo');
console.log(s.description); // "foo"

12.4 Symbol作为对象的属性名

  • Symbol作为对象的属性名,能保证不会出现同名的属性
let mySymbol = Symbol();

// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
let a = {
  [mySymbol]: 'Hello!'
};

// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
  • Symbol作为对象的属性名,必须使用中括号语法(点语法是键名为字符串时,对中括号语法的简化)

    const mySymbol = Symbol();
    const a = {};
    
    a.mySymbol = 'Hello!'; // 等价于 a['mySymbol'] = 'Hello!'
    a[mySymbol] // undefined
    a['mySymbol'] // "Hello!"
    
    const s = Symbol('foo');
    const t = Symbol('bar');
    // Symbol作为键名必须加上[]
    const obj = {
      [s]: 123,
      [t]() {
        return 1 + 1;
      },
    };
    
  • 对象中的Symbol作为键名的属性,一般通过Object.getOwnPropertySymbols()方法获取,该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值

    const obj = {};
    const foo = Symbol('foo');
    
    obj[foo] = 'bar';
    
    for (let i in obj) {
      console.log(i); // 无输出
    }
    
    console.log(Object.getOwnPropertyNames(obj)); // []
    console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(foo)]
    
  • Reflect.ownKeys()方法可以返回对象中所有类型的键名,包括(非继承)常规键名和 Symbol 键名

    let obj = {
      [Symbol('foo')]: 1,
      bar: 2,
    };
    
    console.log(Reflect.ownKeys(obj));
    //  ["bar", Symbol(foo)]
    

12.5 Symbol.for()和Symbol.keyFor()

  • Symbol.for()可获取以传入的参数作为描述的Symbol,如果有,就返回这个Symbol值,否则就新建一个以该字符串为名称的Symbol值,并将其注册到全局

    let s1 = Symbol.for('foo');
    let s2 = Symbol.for('foo');
    
    s1 === s2 // true
    
  • Symbol.for()Symbol()都会生成新的 Symbol

  • Symbol.for()会被登记在全局环境中供搜索,Symbol()不会

  • Symbol.for()不会每次调用就返回一个新的Symbol类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值

    Symbol.for("bar") === Symbol.for("bar")
    // true
    
    Symbol("bar") === Symbol("bar")
    // false
    
  • Symbol.for()可以保证模块中的全局变量不会被无意间覆盖且保持不变

    // mod.js
    const FOO_KEY = Symbol.for('foo');
    
    function A() {
      this.foo = 'hello';
    }
    
    if (!global[FOO_KEY]) {
      global[FOO_KEY] = new A();
    }
    
    module.exports = global[FOO_KEY];
    
    // 键名使用Symbol方法生成,那么外部将无法引用这个值,当然也就无法改写
    // 同时键名使用Symbol.for方法生成,保证了每次得到的变量都一样
    const a = require('./mod.js');
    

13. set

13.1定义

  • Set本身是一个构造函数,用来生成 Set 数据结构

  • Set实例通过add()方法向 Set 结构加入成员,但成员的值都是唯一的,Set 结构不会添加重复的值

  • Set()函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化

    // 例一
    const s = new Set();
    [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
    for (let i of s) {
      console.log(i);
    }
    // 2 3 5 4
    
    // 例二
    const set = new Set([1, 2, 3, 4, 4]);
    [...set]
    // [1, 2, 3, 4]
    
    // 例三
    const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
    items.size // 5
    
  • Set可以用作数组或字符串去重

    // 去除数组的重复成员
    [...new Set(array)]
    Array.from(new Set(array))
    
    // 去除字符串的重复成员
    [...new Set('ababbc')].join('')
    Array.from(new Set('ababbc')).join('')
    
  • Set加入值的时候,不会发生类型转换

  • Set在判断两个值是否相等时,与===的唯一区别是,向Set加入值时认为NaN等于自身

    let set = new Set();
    let a = NaN;
    let b = NaN;
    set.add(a);
    set.add(b);
    console.log([...set]); // [NaN]
    

13.2 set实例的属性

  • Set.prototype.constructor:构造函数,默认就是Set函数
  • Set.prototype.size:返回Set实例的成员总数

13.3 set实例的方法

  • 操作方法

    Set.prototype.add(value):添加某个值,返回 Set 结构本身

    Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功

    Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员

    Set.prototype.clear():清除所有成员,没有返回值

  • 遍历方法

    Set.prototype.keys():返回键名的遍历器

    Set.prototype.values():返回键值的遍历器

    Set.prototype.entries():返回键值对的遍历器

    Set.prototype.forEach():使用回调函数遍历每个成员

    Set的遍历顺序就是插入顺序

    由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致

    let set = new Set(['red', 'green', 'blue']);
    
    for (let item of set.keys()) {
      console.log(item);
    }
    // red
    // green
    // blue
    
    for (let item of set.values()) {
      console.log(item);
    }
    // red
    // green
    // blue
    
    for (let item of set.entries()) {
      console.log(item);
    }
    // ["red", "red"]
    // ["green", "green"]
    // ["blue", "blue"]
    

    Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法

    Set.prototype[Symbol.iterator] === Set.prototype.values
    // true
    

    这意味着,可以省略values方法,直接用for...of循环遍历 Set

    let set = new Set(['red', 'green', 'blue']);
    
    for (let x of set) {
      console.log(x);
    }
    // red
    // green
    // blue
    

    Set 结构的实例与数组一样,也拥有forEach方法,用于对每个成员执行某种操作,没有返回值

    let set = new Set([1, 4, 9]);
    set.forEach((value, key) => console.log(key + ' : ' + value))
    // 1 : 1
    // 4 : 4
    // 9 : 9
    

    数组的mapfilter方法也可以间接用于 Set

    let set = new Set([1, 2, 3]);
    set = new Set([...set].map(x => x * 2));
    // 返回Set结构:{2, 4, 6}
    
    let set = new Set([1, 2, 3, 4, 5]);
    set = new Set([...set].filter(x => (x % 2) == 0));
    // 返回Set结构:{2, 4}
    

    在遍历操作中,暂时没有方法同步改变原来的 Set 结构,只能整体赋值给原来的 Set 结构

    // 方法一
    let set = new Set([1, 2, 3]);
    set = new Set([...set].map(val => val * 2));
    // set的值是2, 4, 6
    
    // 方法二
    let set = new Set([1, 2, 3]);
    set = new Set(Array.from(set, val => val * 2));
    // set的值是2, 4, 6
    

14. map

14.1定义

MapObject类似,也提供了键值对的集合,但Object只能以字符串作为键,Map解决了这个问题

14.2 map实例的属性

size属性返回 Map 结构的成员总数

const map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2

14.2 map实例的操作方法

  • Map.prototype.set(key, value)

    set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键

    let map = new Map().set(1, 'a').set(2, 'b').set(3, 'c');
    console.log(map); // {1 => 'a', 2 => 'b', 3 => 'c'}
    
  • Map.prototype.get(key)

    get方法读取key对应的键值value,如果找不到key,返回undefined

    const m = new Map();
    const abc = function() {return 1+1};
    m.set(abc, 'function') // 键是函数
    
    const back = m.get(abc)
    console.log(back); // 'function'
    
  • Map.prototype.has(key)

    has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中

    const m = new Map();
    const abc = function() {return 1+1};
    m.set(abc, 'function')
    
    console.log(m.has(abc)); // true
    
  • Map.prototype.delete(key)

    delete方法删除某个键,返回true。如果删除失败,返回false

    const m = new Map();
    const abc = function() {return 1+1};
    const num = 1;
    m.set(abc, 'function').set(num, '1')
    
    console.log(m.delete(abc)); // true
    console.log(m); // {1 => '1'}
    
  • Map.prototype.clear()

    clear方法清除所有成员,没有返回值

    const m = new Map();
    const abc = function () {
      return 1 + 1;
    };
    const num = 1;
    m.set(abc, 'function').set(num, '1');
    
    m.clear();
    console.log(m.size); // 0
    

14.3 map实例的遍历方法

Map的遍历顺序就是插入顺序

  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
  • Map.prototype.forEach():遍历 Map 的所有成员。
const map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries()
for (let [key, value] of map) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

Map默认遍历器接口(Symbol.iterator属性),就是entries方法

map[Symbol.iterator] === map.entries
// true

MapforEach方法,与数组的forEach方法类似,也可以实现遍历和绑定this

// 这里有两个坑
// 第一:对象中简写的方法不能当作构造函数
// 第二:箭头函数的this指向问题
const reporter = {
  report: function (key, value) {
    console.log('Key: %s, Value: %s', key, value);
  },
};

const map = new Map().set(1, 'a').set(2, 'b').set(3, 'c');
map.forEach(function (value, key, map) {
  this.report(key, value);
}, reporter);
// Key: 1, Value: a
// Key: 2, Value: b
// Key: 3, Value: c

15. Proxy

15.1定义

Proxy可以过滤和改写外界对对象的访问

ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。

let proxy = new Proxy(target, handler);
// target参数表示所要拦截的目标对象
// handler参数也是一个对象,用来定制拦截行为

handler对象中支持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)

15.2 Proxy.revocable()

  • Proxy.revocable()方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例

  • 当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误

    let target = {};
    let handler = {};
    
    let {proxy, revoke} = Proxy.revocable(target, handler);
    
    proxy.foo = 123;
    proxy.foo // 123
    
    revoke();
    proxy.foo // TypeError: Revoked
    

15.3 this指向

  • 在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理
  • Proxy 拦截函数内部的this,指向的是handler对象

16. Reflect

16.1定义

Reflect对象的设计目的:

  • Object对象的一些明显属于语言内部的方法,放到Reflect对象上

    // 某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上
    Object.defineProperty()
    Reflect.defineProperty()
    
  • 修改某些Object方法的返回结果,让其变得更合理

    // Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误
    // 而Reflect.defineProperty(obj, name, desc)则会返回false
    
    // 老写法
    try {
      Object.defineProperty(target, property, attributes);
      // success
    } catch (e) {
      // failure
    }
    
    // 新写法
    if (Reflect.defineProperty(target, property, attributes)) {
      // success
    } else {
      // failure
    }
    
  • Object操作都变成函数行为

    // 老写法
    'assign' in Object // true
    
    // 新写法
    Reflect.has(Object, 'assign') // true
    
  • Reflect对象的方法与Proxy对象的方法一一对应,Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础

    Proxy(target, {
      set: function(target, name, value, receiver) {
        var success = Reflect.set(target, name, value, receiver);
        if (success) {
          console.log('property ' + name + ' on ' + target + ' set to ' + value);
        }
        return success;
      }
    })
    

16.2静态方法

Reflect对象一共有 13 个静态方法,大部分与Object对象的同名方法的作用都是相同的,而且它与Proxy对象的方法是一一对应的:

  • 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)

17. Promise

17.1定义

  • Promise是一个容器,里面保存着某个异步操作的结果
  • Promise有三种状态,当前状态只能由异步操作的结果决定:
    • pending(进行中)
    • fulfilled(已成功)
    • rejected(已失败)
  • 一旦状态改变,就不会再变,会一直保持这个结果,这时就称为 resolved(已定型)
  • Promise对象的状态改变,只有两种可能:
    • pending变为fulfilled
    • pending变为rejected
  • Promise一旦新建就会立即执行,无法中途取消
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
// Promise()的结构
ƒ Promise()
|--all: ƒ all()
|--allSettled: ƒ allSettled()
|--any: ƒ any()
|--prototype: Promise
   |--catch: ƒ catch()
   |--constructor: ƒ Promise()
   |--finally: ƒ finally()
   |--then: ƒ then()
   |--Symbol(Symbol.toStringTag): "Promise"
   |--[[Prototype]]: Object
|--race: ƒ race()
|--reject: ƒ reject()
|--resolve: ƒ resolve()

17.2 Promise实例化

  • Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject

    const promise = new Promise(function(resolve, reject) {
      // ... some code
    
      if (/* 异步操作成功 */){
        resolve(value);
      } else {
        reject(error);
      }
    });
    
  • resolve函数的作用是,将Promise对象的状态从 pending 变为 resolved,在异步操作成功时调用,并将异步操作的结果,作为参数传递出去

  • reject函数的作用是,将Promise对象的状态从 pending 变为 rejected,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去

    // 图片加载案例
    const image = new Image();
    
    const preloadImage = function (path) {
      return new Promise(function (resolve, reject) {
        image.onload = resolve => console.log('图片加载成功');
        image.onerror = reject => console.log('图片加载失败');
        image.src = path;
      });
    };
    
    preloadImage( 'https://dss0.bdstatic.com/5aV1bjqh_Q23odCf/static/superman/img/qrcode/qrcode@2x-daf987ad02.png'
    );
    
    const div = document.querySelector('div');
    div.appendChild(image);
    

17.2 Promise.prototype.then()

  • then方法是定义在原型上,供Promise实例使用

  • then方法自带两个可选(但有顺序)的参数,第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数

  • 建议只在then中处理resolved(rejectedcatch中处理)

  • then方法返回的是一个新的Promise实例(可以使用链式写法)

    const promise = new Promise(function (resolve, reject) {
      // ... some code
    
      if (/* 异步操作成功 */ 'something is true') {
        // 状态变为fulfilled
        resolve(value);
      } else {
        // 状态变为rejected
        reject(error);
      }
    });
    
    promise
      .then(
        // fulfilled时执行的回调
        post => asyncFn(post)
      )
      .then(
        // fulfilled时执行的回调
        comments => console.log('resolved: ', comments),
        // rejected时执行的回调
        err => console.log('rejected: ', err)
      );
    

17.3 Promise.prototype.catch()

  • catch方法是定义在原型上,供Promise实例使用

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

  • 状态变为rejected后,才会调用catch方法指定的回调函数,否则会跳过catch

  • Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止,错误总是会被下一个catch语句捕获

  • then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获

    promise
      .then(
        // fulfilled时执行的回调
        post => asyncFn(post)
      )
      .then(
        // fulfilled时执行的回调
        comments => console.log('resolved: ', comments)
      )
      .catch(
        // rejected时执行的回调
        err => console.log('rejected: ', err)
      );
    

17.4 Promise.prototype.finally()

  • finally方法是定义在原型上,供Promise实例使用

  • finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作

  • finally方法的回调函数不接受任何参数

  • finally方法总是会返回原来的值

    promise
      .then(
        // fulfilled时执行的回调
        comments => console.log('resolved: ', comments)
      )
      .catch(
        // rejected时执行的回调
        err => console.log('rejected: ', err)
      )
      .finally(() => {
        // 最后都会执行
        console.log('end');
      });
    

17.5 Promise.resolve()

  • resolvePromise构造函数的静态方法

  • resolve将现有对象转为 Promise 对象

  • 等价写法

    Promise.resolve('foo')
    // 等价于
    new Promise(resolve => resolve('foo'))
    
  • 参数:

    • 参数是一个Promise实例

      Promise.resolve将不做任何修改、原封不动地返回这个实例

    • 参数是一个具有then方法的对象

      Promise.resolve()将这个对象转为Promise对象,然后就立即执行原对象的then方法

    • 其它类型的参数

      Promise.resolve()方法返回一个新的Promise对象,状态为resolved

    • 不带有任何参数

      直接返回一个resolved状态的Promise对象

17.6 Promise.reject()

  • Promise.reject(reason)方法也会返回一个新的Promise实例,该实例的状态为rejected

  • Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数

    Promise.reject('出错了')
    .catch(e => {
      console.log(e === '出错了')
    })
    // true
    

17.7 Promise.all()

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

    const p = Promise.all([p1, p2, p3]);
    
  • Promise.all()方法接受一个数组作为参数,数组的每一项都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理

  • Promise.all()方法的参数可以不是数组,但必须具有Iterator接口,且返回的每个成员都是 Promise 实例

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

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

  • 如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法

    // p2有自己的catch
    const p1 = new Promise((resolve, reject) => {
      resolve('hello');
    })
      .then(result => result)
      .catch(e => e);
    
    const p2 = new Promise((resolve, reject) => {
      throw new Error('报错了');
    })
      .then(result => result)
      .catch(e => e);
    
    Promise.all([p1, p2])
      .then(result => console.log(result))
      .catch(e => console.log(e));
    // ["hello", Error: 报错了]
    
    // p2没有自己的catch
    const p1 = new Promise((resolve, reject) => {
      resolve('hello');
    })
      .then(result => result)
      .catch(e => e);
    
    const p2 = new Promise((resolve, reject) => {
      throw new Error('报错了');
    }).then(result => result);
    
    Promise.all([p1, p2])
      .then(result => console.log(result))
      .catch(e => console.log(e));
    // Error: 报错了
    

17.8 Promise.race()

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

  • Promise.race()方法接受一个数组作为参数,数组的每一项都是 Promise 实例,如果不是,就会先调用Promise.resolve方法,将参数转为 Promise 实例,再进一步处理

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

17.9 Promise.allSettled()

  • Promise.allSettled()方法接受一个数组作为参数,数组的每个成员都是一个 Promise 对象,并返回一个新的 Promise 对象

  • 只有等到参数数组的所有 Promise 对象都发生状态变更(不管是fulfilled还是rejected),返回的 Promise 对象才会发生状态变更

  • 该方法返回的新的 Promise 实例,一旦发生状态变更,状态总是fulfilled

  • 状态变成fulfilled后,它的回调函数会接收到一个数组作为参数,该数组的每个成员对应前面数组的每个 Promise 对象

  • 这些对象的格式是固定的,对应异步操作的结果

    const resolved = Promise.resolve(42);
    const rejected = Promise.reject(-1);
    
    // 返回新的Promise实例
    const allSettledPromise = Promise.allSettled([resolved, rejected]);
    
    // 一旦发生状态变更,状态总是fulfilled
    allSettledPromise.then(function (results) {
      // results是一个数组
      // 该数组的每个成员对应前面数组的每个Promise对象,对象的格式是固定的
      // 异步操作成功时 {status: 'fulfilled', value: value}
      // 异步操作失败时 {status: 'rejected', reason: reason}
      console.log(results);
      // [
      //    { status: 'fulfilled', value: 42 },
      //    { status: 'rejected', reason: -1 }
      // ]
    });
    

17.10 Promise.any()

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

  • 只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态

  • 如果所有参数实例都变成rejected状态,包装实例才会变成rejected状态

  • Promise.any()抛出的错误,不是一个一般的Error错误对象,而是一个AggregateError实例

    const resolved = Promise.resolve(42);
    const rejected = Promise.reject(-1);
    const 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.errors); // [-1, Infinity]
    });
    

18. Iterator遍历器

18.1定义

  • Iterator的作用:

    • 为各种数据结构,提供一个统一的、简便的访问接口
    • 使得数据结构的成员能够按某种次序排列
    • ES6 创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费
  • Iterator的遍历过程:

    • 创建一个指针对象,指向当前数据结构的起始位置
    • 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员
    • 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员
    • 不断调用指针对象的next方法,直到它指向数据结构的结束位置
  • 每一次调用next方法,都会返回一个包含valuedone两个属性的对象,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束

18.2默认Iterator接口

  • 默认的Iterator接口部署在数据结构的Symbol.iterator属性,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”,即不用任何处理,就可以被for...of循环遍历

  • 原生具备Iterator接口的数据结构:

    • Array
    • Map
    • Set
    • String
    • TypedArray
    • 函数的 arguments 对象
    • NodeList 对象
  • 对于类似数组的对象(存在数值键名和length属性)可以引用数组的Iterator接口或者转为真数组

    let iterable = {
      0: 'a',
      1: 'b',
      2: 'c',
      length: 3,
      [Symbol.iterator]: Array.prototype[Symbol.iterator]
    };
    for (let item of iterable) {
      console.log(item); // 'a', 'b', 'c'
    }
    
  • 普通对象直接部署Symbol.iterator方法,并无效果

    let iterable = {
      a: 'a',
      b: 'b',
      c: 'c',
      length: 3,
      [Symbol.iterator]: Array.prototype[Symbol.iterator]
    };
    for (let item of iterable) {
      console.log(item); // undefined, undefined, undefined
    }
    

18.3 for...of的优势

  • for...in没有for...in的缺点:

    • 数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等
    • for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键
    • 某些情况下,for...in循环会以任意顺序遍历键名
  • 不同于forEach方法,它可以与breakcontinuereturn配合使用

    const str = 'abc';
    for (const value of str) {
      if (value === 'c') break;
      console.log(value);
    }
    
  • 提供了遍历所有数据结构的统一操作接口

19. Generator函数

19.1定义

Generator函数是一个状态机,还是一个遍历器对象生成函数,返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态

19.2特征

  • function关键字与函数名之间有一个星号

  • 函数体内部使用yield表达式,定义不同的内部状态

  • 调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象(Iterator遍历器)

  • Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行

    • 必须调用遍历器对象的next方法,使得指针移向下一个状态
    • 每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止
    • next方法返回一个对象,它的value属性就是当前yield表达式的值hellodone属性的值false,表示遍历还没有结束

19.3 yield表达式

  • yield表达式只能用在 Generator 函数里面,用在其他地方都会报错

  • yield表达式如果用在另一个表达式之中,必须放在圆括号里面

  • yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号

  • 遍历器对象的next方法的运行逻辑如下:

    • 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值

    • 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式

    • 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值

    • 如果该函数没有return语句,则返回的对象的value属性值为undefined

      function* gen() {
        yield 123 + 456;
        yield 456 - 123;
        return 'over';
      }
      // 函数gen如果是普通函数,在为变量g赋值时就会执行
      // 但是,函数gen是一个 Generator 函数,就变成只有调用next方法时,函数gen才会执行
      const g = gen();
      
      console.log(g.next()); // {value: 579, done: false}
      console.log(g.next()); // {value: 333, done: false}
      console.log(g.next()); // {value: 'over', done: true}
      console.log(g.next()); // {value: undefined, done: true}
      

19.4 Generator函数与Iterator接口的关系

  • 可以把Generator函数赋值给对象的Symbol.iterator属性做为该对象的遍历器生成函数,从而使得该对象具有Iterator接口

    const obj = {};
    obj[Symbol.iterator] = function* () {
      yield 1;
      yield 2;
      yield 3;
    };
    
    console.log([...obj]); // [1, 2, 3]
    
  • Generator函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身

    function* gen() {
      yield 1;
      yield 2;
      yield 3;
    }
    const g = gen();
    console.log(g[Symbol.iterator]() === g); // true
    

19.5 next方法的参数

  • yield表达式本身没有返回值(总是返回undefined),next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值

  • 由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的

    function* foo(x) {
      let y = 2 * (yield x + 1);
      let z = yield y / 3;
      return x + y + z;
    }
    
    const a = foo(5);
    console.log(a.next()); // {value:6, done:false}
    console.log(a.next()); // {value:NaN, done:false}
    console.log(a.next()); // {value:NaN, done:true}
    
    const b = foo(5);
    console.log(b.next()); // { value:6, done:false }
    console.log(b.next(12)); // { value:8, done:false }
    console.log(b.next(13)); // { value:42, done:true }
    

19.6 for...of

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

    function* foo() {
      yield 1;
      yield 2;
      yield 3;
      return 4;
    }
    
    for (let v of foo()) {
      console.log(v);
    }
    // 1 2 3
    
  • Generator函数加到对象的Symbol.iterator属性上面,为对象提供遍历器接口

    function* objectEntries() {
      let propKeys = Object.keys(this);
    
      for (let propKey of propKeys) {
        yield [propKey, this[propKey]];
      }
    }
    
    const obj = { name: 'zs', age: 18 };
    obj[Symbol.iterator] = objectEntries;
    
    for (let v of obj) {
      console.log(v);
    }
    // ['name', 'zs'] ['age', 18]
    
  • 除了for...of循环以外,扩展运算符(...)、解构赋值和Array.from方法内部调用的,都是遍历器接口

    function* numbers() {
      yield 1;
      yield 2;
      return 3;
      yield 4;
    }
    
    // 扩展运算符
    console.log([...numbers()]); // [1, 2]
    
    // Array.from 方法
    console.log(Array.from(numbers())); // [1, 2]
    
    // 解构赋值
    let [x, y] = numbers();
    console.log(x); // 1
    console.log(y); // 2
    
    // for...of 循环
    for (let n of numbers()) {
      console.log(n);
    }
    // 1 2
    

19.7 Generator.prototype.throw()

  • Generator函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在Generator函数体内捕获

  • throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例

  • 全局的throw命令只能被函数体外的catch语句捕获

  • 如果Generator函数内部没有部署try...catch代码块,那么throw方法抛出的错误,将被外部try...catch代码块捕获

  • 如果Generator函数内部和外部,都没有部署try...catch代码块,那么程序将报错,直接中断执行

  • throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法

// 例一
const gen = function* () {
  try {
    yield;
  } catch (e) {
    console.log('内部捕获', e);
  }
};

var g = gen();
g.next();

try {
  g.throw('a');
  g.throw('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b

// 例二
const gen = function* () {
  while (true) {
    try {
      yield;
    } catch (e) {
      console.log('内部捕获', e);
    }
  }
};

var g = gen();
g.next();

try {
  g.throw('a');
  g.throw('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 内部捕获 a
// 内部捕获 b
// 例三
const gen = function* () {
  try {
    yield;
  } catch (e) {
    console.log('内部捕获', e);
  }
};

var g = gen();
g.next();

try {
  throw new Error('a');
  throw new Error('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 外部捕获 [Error: a]

// 例四
const gen = function* () {
  try {
    yield;
  } catch (e) {
    console.log('内部捕获', e);
  }
};

var g = gen();
g.next();

try {
  throw new Error('a');
  g.throw('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 外部捕获 [Error: a]

// 例五
const gen = function* () {
  try {
    yield;
  } catch (e) {
    console.log('内部捕获', e);
  }
};

var g = gen();
g.next();

try {
  g.throw('a');
  throw new Error('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 [Error: b]
// 例六
const gen = function* () {
  while (true) {
    yield;
    console.log('内部捕获', e);
  }
};

const g = gen();
g.next();

try {
  g.throw('a');
  g.throw('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 外部捕获 a
// 例七
const gen = function* () {
  while (true) {
    yield;
    console.log('内部捕获', e);
  }
};

const g = gen();
g.next();

g.throw('a');
g.throw('b');
console.log('外部捕获', e);
// Uncaught a
// 例八
const gen = function* () {
  try {
    yield 1;
  } catch (e) {
    console.log('内部捕获', e);
  }
};

const g = gen();
try {
  g.throw('a');
} catch (e) {
  console.log('外部捕获', e);
}
// 外部捕获 a

19.8 Generator.prototype.return()

  • return()方法可以返回给定的值,并且终结遍历Generator函数

  • 调用return()方法后,返回值的value属性就是return()方法的参数foo。并且,Generator函数的遍历就终止了,返回值的done属性为true,以后再调用next()方法,done属性总是返回true

    function* gen() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    const g = gen();
    
    console.log(g.next()); // { value: 1, done: false }
    console.log(g.return('foo')); // { value: "foo", done: true }
    console.log(g.next()); // { value: undefined, done: true }
    
  • 如果return()方法调用时,不提供参数,则返回值的value属性为undefined

    function* gen() {
      yield 1;
      yield 2;
      yield 3;
    }
    
    const g = gen();
    
    console.log(g.next()); // { value: 1, done: false }
    console.log(g.return()); // { value: undefined, done: true }
    console.log(g.next()); // { value: undefined, done: true }
    
  • 如果Generator函数内部有try...finally代码块,且正在执行try代码块,那么return()方法会导致立刻进入finally代码块,执行完以后,再返回return()方法指定的返回值

    function* numbers() {
      yield 1;
      try {
        yield 2;
        yield 3;
      } finally {
        yield 4;
        yield 5;
      }
      yield 6;
    }
    const g = numbers();
    console.log(g.next()); // { value: 1, done: false }
    console.log(g.next()); // { value: 2, done: false }
    console.log(g.return(7)); // { value: 4, done: false }
    console.log(g.next()); // { value: 5, done: false }
    console.log(g.next()); // { value: 7, done: true }
    

19.9 yield* 表达式

  • Generator函数内部,调用另一个Generator函数,需要在前者的函数体内部,通过for...of手动完成遍历

  • ES6 提供了yield*表达式,替代此时的for...of,用来在一个Generator函数里面执行另一个Generator函数

  • 在内部的Generator函数有return语句时,则需要用变量const value = yield* iterator接收return语句的值

    function* genFuncWithReturn() {
      yield 'a';
      yield 'b';
      return 'The result';
    }
    function* logReturned(genObj) {
      let result = yield* genObj;
      console.log(result);
    }
    
    console.log([...logReturned(genFuncWithReturn())]);
    // The result
    // [ 'a', 'b' ]
    
  • 任何数据结构只要有Iterator接口,就可以被yield*遍历

    let read = (function* () {
      yield 'hello';
      yield* 'hello';
    })();
    
    console.log(read.next().value); // hello
    console.log(read.next().value); // h
    

19.10 作为对象属性的Generator函数

let obj = {
  * myGeneratorMethod() {
    ···
  }
};
// 等价于
let obj = {
  myGeneratorMethod: function* () {
    // ···
  }
};

19.11 在Generator函数中使用this

function* gen() {
  this.a = 1;
  yield (this.b = 2);
  yield (this.c = 3);
}

function F() {
  return gen.call(gen.prototype);
}

var f = new F();

console.log(f.next()); // {value: 2, done: false}
console.log(f.next()); // {value: 3, done: false}
console.log(f.next()); // {value: undefined, done: true}

console.log(f.a); // 1
console.log(f.b); // 2
console.log(f.c); // 3

20. async函数

20.1定义

async函数是Generator函数的语法糖,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await

20.2 async对Generator的改进

  • Generator函数的执行必须靠执行器,而async函数自带执行器

  • asyncawait,比起*yield,语义更清楚。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果

  • yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面,可以是Promise对象和原始类型的值(但这时会自动转成立即resolved的Promise对象)

  • async函数的返回值是Promise对象,Generator函数的返回值是Iterator对象

20.3 async函数的返回值

  • async函数返回一个Promise对象

  • async函数内部return语句返回的值,会成为then方法回调函数的参数

    async function asyncPrint() {
      const pm = await Promise.resolve(2);
      console.log(pm); // 2
      return pm;
    }
    
    asyncPrint().then(data => console.log(data));
    // 2
    
  • async函数内部抛出错误,会导致返回的Promise对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到

    async function f() {
      throw new Error('出错了');
    }
    
    f().then(
      v => console.log('resolve', v),
      e => console.log('reject', e)
    );
    // reject Error: 出错了
    

20.4 Promise对象的状态变化*

async函数返回的Promise对象,必须等到内部所有await命令后面的Promise对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误

20.5 await命令

  • await命令后面是一个Promise对象,返回该对象的结果

  • await命令后面不是Promise对象,就直接返回对应的值

  • await命令后面是一个thenable对象(即定义了then方法的对象),那么await会将其等同于 Promise 对象

  • 任何一个await语句后面的Promise对象变为reject状态,那么整个async函数都会中断执行(这时可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行)

    // 问题
    async function asyncPrint() {
      const a = await Promise.reject('hello');
      const b = await Promise.resolve('world'); // 不会执行
      return [a,b]
    }
    
    asyncPrint().then(
      data => console.log(data),
      err => console.log(err) // hello
    );
    
    // 解决
    async function asyncPrint() {
      try {
        await Promise.reject('hello');
      } catch (e) {
        console.log(e); // hello
      }
      const b = await Promise.resolve('world');
      return b;
    }
    
    asyncPrint().then(
      data => console.log(data), // world
      err => console.log(err)
    );
    

21. class

21.1 定义

  • 构造函数的语法糖,它的数据类型是function
class MyClass {
  // ...
}
console.log(typeof MyClass); // function
  • 的所有方法都定义在它的prototype属性上面,Object.assign()方法可以很方便地一次添加多个方法到prototype对象上面

    class MyClass {
      constructor() {
        // ...
      }
    }
    
    Object.assign(MyClass.prototype, {
      toString() {},
      toValue() {},
    });
    
  • 的内部所有定义的方法,都是不可枚举的

    class MyClass {
      constructor() {
        // ...
      }
      methodOfClass() {}
    }
    
    // 给类添加的方法
    Object.assign(MyClass.prototype, {
      toString() {},
    });
    
    console.log(Object.keys(MyClass.prototype)); // ['toString']
    console.log(Object.getOwnPropertyNames(MyClass.prototype)); // ['constructor', 'methodOfClass', 'toString']
    

21.2 特点

  • 模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式

  • 不存在变量提升

  • name属性总是返回紧跟在class关键字后面的类名

  • 方法可以是Generator函数(方法之前加上星号*)

  • 方法内部如果含有this,它默认指向实例。但是this的指向很容易被改变

    // 解决方案一
    class Obj {
      constructor() {
        this.getThis = this.getThis.bind(this);
      }
      getThis() {
        console.log(this);
      }
    }
    
    const myObj = new Obj();
    myObj.getThis();
    
    // 解决方案二
    class Obj {
      constructor() {
        // ...
      }
      getThis = () => {
        console.log(this);
      };
    }
    
    const myObj = new Obj();
    myObj.getThis();
    

21.3 construct

  • constructor()方法是类的默认方法,类必须使用new调用来生成对象实例,否则会报错

  • 一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加

  • constructor()方法默认返回实例对象(this)

    // 指定constructor()的返回值,会导致实例指向错误
    class MyClass {
      constructor() {
        return {};
      }
    }
    console.log(new MyClass() instanceof MyClass); // false
    

21.4 实例

  • 实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)

    //定义类
    class MyClass {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
    
      add() {
        return this.x + this.y;
      }
    }
    
    const myInstance = new MyClass(2, 3);
    
    console.log(myInstance.hasOwnProperty('x')); // true
    console.log(myInstance.hasOwnProperty('y')); // true
    console.log(myInstance.hasOwnProperty('add')); // false
    console.log(myInstance.__proto__.hasOwnProperty('add')); // true
    
  • 的所有实例共享一个原型对象

    class MyClass {
      constructor(x, y) {
        // ...
      }
    }
    
    const myFirstInstance = new MyClass(2, 3);
    const mySecondInstance = new MyClass(0, 1);
    
    console.log(myFirstInstance.__proto__ === mySecondInstance.__proto__); //true
    
  • __proto__是浏览器厂商添加的私有属性,不建议显式使用,推荐使用Object.getPrototypeOf方法获取实例对象的原型

    class MyClass {
      constructor(x, y) {
        // ...
      }
    }
    
    const myFirstInstance = new MyClass(2, 3);
    const mySecondInstance = new MyClass(0, 1);
    
    Object.getPrototypeOf(myFirstInstance).addMethod = function () {
      console.log(1);
    };
    mySecondInstance.addMethod(); // 1
    

21.5 取值函数getter和存值函数setter

  • 使用getset关键字,可以对某个属性设置存值函数和取值函数,拦截该属性的存取行为

  • 存值函数和取值函数是设置在属性的Descriptor对象上

class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return '取值';
  }
  set prop(value) {
    console.log('存值: ' + value);
  }
}

const inst = new MyClass();

const descriptor = Object.getOwnPropertyDescriptor(
  MyClass.prototype,
  'prop'
);
console.log('get' in descriptor); // true
console.log('set' in descriptor); // true

inst.prop = 123;
// 存值: 123

console.log(inst.prop);
// 取值

21.6 属性表达式

类的属性名,可以采用表达式

const methodName = 'add';
class MyClass {
  constructor() {
    // ...
  }
  [methodName]() {}
}
console.log(MyClass.prototype.hasOwnProperty('add')); // true

21.7 class表达式

类可以使用表达式的形式定义

// 在内部只能使用Inner做为类名,在外部只能使用Outer做为类名
const Outer = class Inner {
  constructor() {}
};

// 若内部没用用到类名,Inner可以省略
const Outer = class {
  constructor() {}
};

// 快速定义实例
const inst = new (class {
  constructor(age) {
    this.age = age;
  }
  getAge() {
    console.log(this.age);
  }
})(18);
inst.getAge(); // 18

21.8 静态方法

  • 中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法将不会被实例继承,而是直接通过来调用,这就称为静态方法

  • 实例上调用静态方法,会抛出一个错误,表示不存在该方法

  • 静态方法中的this指向本身,而不是实例

  • 静态方法可以与非静态方法重名·

  • 父类的静态方法,可以被子类继承

  • 静态方法也可以从super对象上调用

21.9 实例属性的新写法

实例属性除了定义在constructor()方法里面的this上面,也可以定义在类的最顶层

class Obj {
  num = 0;
  getThis = () => {
    console.log(this);
  };
}

const myObj = new Obj();
console.log(myObj.num); // 0

// 等价于
class Obj {
  constructor() {
    this.num = 0;
  }
  getThis = () => {
    console.log(this);
  };
}

const myObj = new Obj();
console.log(myObj.num); // 0

21.10 静态属性

静态属性class本身的属性,但es6规定,class内部只有静态方法,没有静态属性(不能使用static显式地指定)

// 旧写法
class MyClass {}
MyClass.prop = 1;
console.log(MyClass.prop); // 1

// 新写法(违反es6的规定,但能正常运行)
class MyClass {
  prop = 5;
  static prop = 1;
}
const inst = new MyClass();
console.log(inst.prop); // 5
console.log(MyClass.prop); // 1

21.11 私有属性

  • 私有方法私有属性,是只能在的内部访问的方法和属性,外部不能访问

  • 在传统的面向对象的编程语言中,class的成员一般有3种:

    • public公开类成员

    • private私有类成员, 只能在的内部访问, 不能被继承, 不能被子类/实例访问. 想要让私有类成员能够在外部获取到, 可以通过的内部定义一个公开的(public)函数来返回(return)这个类成员, 这样的话这个类成员可以被用户间接地访问到, 但是它无法被修改

    • protected受保护的类成员, 可以被自身和其子类访问, 可以被子类继承, 不能被实例化对象访问。同样地, 可以在的内部通过一个公开的(public)函数来返回(return)这个类成员,这样的话这个类成员可以被用户间接地访问到, 但是它无法被修改

  • JavaScript私有属性还处于提案阶段,方法是在属性名之前添加#表示

    // 目前Google浏览器支持类的私有属性,在类的外部使用私有属性也会给出报错,但其它代码将不会运行(控制台也没有其它打印信息)
    class MyClass {
      #x;
      constructor(x = 0) {
        this.#x = +x;
      }
      get x() {
        return this.#x;
      }
      set x(value) {
        this.#x = +value;
      }
    }
    console.log(-1); // 无打印信息
    const inst = new MyClass();
    console.log(inst.x); // 无打印信息
    console.log(inst.#x); // Uncaught SyntaxError: Private field '#x' must be declared in an enclosing class
    

21.12 in运算符

  • 如果指定的属性在指定的对象或其原型链中,则in 运算符返回true

  • 判断私有属性时,in只能用在定义该私有属性的的内部

    class A {
      #foo = 0;
      static test(obj) {
        console.log(#foo in obj);
      }
    }
    A.test(new A()); // true
    A.test({}); // false
    
    class B {
      #foo = 0;
    }
    A.test(new B()); // false
    
  • in运算符对于Object.create()Object.setPrototypeOf形成的继承,是无效的,因为这种继承不会传递私有属性

21.13 静态块

  • 每个只能有一个静态块,在静态属性声明后运行,而且只运行一次
  • 静态块的内部不能有return语句
  • 静态块内部可以使用类名this,指代当前
const getNewProp = num => ({ y: num + 3, z: num * 2 });
class C {
  static x = 1;
  static y;
  static z;

  static {
    try {
      const obj = getNewProp(this.x);
      this.y = obj.y;
      this.z = obj.z;
    } catch {
      this.y = 0;
      this.z = 0;
    }
  }
}
console.dir(C); // class C {x: 1 y: 4 z: 2 name: "C"}

21.14 new.target属性

  • new.target返回new命令作用于的那个构造函数

  • 如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined

  • 在函数外部使用new.target会报错

  • class实例化必须使用new,否则会直接报错

  • class内部调用new.target,返回当前class

    function Person(name, age) {
        console.log(new.target === Person);
        this.name = name;
        this.age = age;
    }
    
    const someone = new Person('zs', 18); // true
    const otherone = Person.call(someone,'ls',20) // false
    
  • 子类继承父类时,new.target会返回子类

    // 例一
    class Person {
      constructor(name, age) {
        console.log(new.target.name);
        this.name = name;
        this.age = age;
      }
    }
    
    class Student extends Person {
      constructor(name, age, job) {
        super(name, age); // 必选先调用super才能继承父类的属性
        console.log(new.target.name);
        this.job = job;
      }
    }
    const someone = new Person('zs', 18); // 'Person'
    const otherone = new Student('ls', 20, 'student'); // 'Student' 'Student'
    
    // 例二
    class Person {
      constructor(name, age) {
        this.name = name;
        this.age = age;
        if (new.target === Person) {
          throw new Error('Person不能实例化');
        }
      }
    }
    
    class Student extends Person {
      constructor(name, age, job) {
        super(name, age); // 必选先调用super才能继承父类的属性
        this.job = job;
      }
    }
    const otherone = new Student('ls', 20, 'student');
    console.log(otherone); // {name: 'ls', age: 20, job: 'student'}
    const someone = new Person('zs', 18); // Uncaught Error: Person不能实例化
    console.log(someone); // 无打印信息
    

22. class的继承

22.1 定义

  • ES5 的通过修改原型链实现继承

  • ES6的继承机制与ES5不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this

  • 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错

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

    class Student extends Person {
    }
    
    // 等同于
    class Student extends Person {
      constructor(...args) {
        super(...args);
      }
    }
    
  • 父类的静态方法,也会被子类继承

22.2 Object.getPrototypeOf()

Object.getPrototypeOf()返回一个子类的父类

console.log(Object.getPrototypeOf(Student).name); // 'Person'

22.3 super

  • super作为函数

    • 代表父类的构造函数,返回的是子类的实例,super内部的this指向子类的实例

      class A {
        constructor() {
          console.log(new.target.name);
        }
      }
      class B extends A {
        constructor() {
          super();
        }
      }
      new A(); // A
      new B(); // B
      
    • super()只能用在子类的构造函数之中,用在其他地方就会报错

  • super作为对象

    • 在普通方法中,super指向父类原型对象,方法内部的this指向当前的子类实例

      class A {
        constructor() {
          this.x = 1;
        }
        print() {
          console.log(this.x);
        }
      }
      
      class B extends A {
        constructor() {
          super();
          this.x = 2;
        }
        m() {
          super.print();
        }
      }
      
      const b = new B();
      b.m(); // 2
      
    • 如果通过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指向父类,方法内部的this指向当前的子类

      class A {
        constructor() {
          this.x = 1;
        }
        static print() {
          console.log(this.x);
        }
      }
      
      class B extends A {
        constructor() {
          super();
          this.x = 2;
        }
        static m() {
          super.print();
        }
      }
      
      B.x = 3; // 给B设置的一个静态属性x=3
      B.m(); // 3(获取B的静态属性x)
      
  • 使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错

  • 可以在任意一个对象中,使用super关键字

22.4 类的 prototype 属性和__proto__属性

  • class作为构造函数的语法糖,同时有prototype属性和__proto__属性,同时存在两条继承链:
    • 子类__proto__属性,表示构造函数的继承,总是指向父类
    • 子类prototype属性(子类原型)的__proto__属性,表示方法的继承,总是指向父类的prototype属性(父类原型)

22.5 原生构造函数的继承

  • ES6允许通过继承原生构造函数来定义子类,因为ES6是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承

  • ES6规定,如果Object方法不是通过new Object()这种形式调用,Object构造函数会忽略参数

    class NewObj extends Object{
      constructor(){
        super(...arguments);
      }
    }
    var o = new NewObj({attr: true});
    console.log(o.attr === true); // false
    

23. Module语法

23.1 ES6下模块自动采用严格模式

严格模式的主要限制:

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

23.2 export

  • export提供对外的接口,接口名与模块内部变量之间,建立了一一对应动态绑定的关系,其他脚本可以通过这个接口,取到模块内部实时的值(即使是对象内的属性变化,也能实时获取)

    // test.js
    let obj = {
      a: 1
    };
    setTimeout(() => {
      obj.a = 2;
    }, 3000);
    export { obj };
    
    // App.vue
    <script>
    import MyBing from '@/components/charts/bing/MyBing.vue';
    import { obj } from './utils/test';
    setInterval(() => {
      console.log(obj.a);
      // 1->...->1->2->...
    }, 1000);
    export default {}
    </script>
    
  • exportimport命令可以出现在模块的任何位置,只要处于模块顶层就可以(如果处于块级作用域内,会无法做静态优化,就会报错)

    // 输出变量
    export const a = 1;
    export const b = 2;
    // 等价于(建议使用)
    const a = 1;
    const b = 2;
    export { a, b };
    
    // 输出函数或类
    export function fn() {}
    export class MyClass {}
    // 等价于(建议使用,也可以使用字面量定义函数和类)
    function fn() {}
    class MyClass {}
    export { fn, MyClass };
    
  • 可以使用as重命名接口名

    const n = 1;
    export { n as m };
    

23.3 import

  • import命令接受一对大括号{},里面指定要从其他模块导入的变量名且这些变量名必须与被导入模块对外接口的名称相同

  • 可以使用as重命名导入的变量名

    import { n as m } from './xxx.js';
    
  • import命令输入的变量都是只读的(import本质是输入接口,不允许在加载模块的脚本里面,改写接口)

    import {a} from './xxx.js'
    a = {}; // Syntax Error : 'a' is read-only;
    
  • 如果输入的变量是一个对象,改写它的属性是允许的,但不建议

    import {a} from './xxx.js'
    a.name = ''; // 合法操作,但不建议
    
  • import静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构

    if (m===n) {
      import abc from './abc.js';
    } // 报错
    
  • import语句会执行所加载的模块,但多次重复执行同一句import语句,最终只会执行一次

  • 可以用星号*指定一个静态对象(不允许改变)来实现整体加载,此时所有输出值都加载在这个对象上面

    import * as person from './person';
    // person中的属性不可以更改
    

23.4 export default

  • export default命令本质是输出一个叫做default的变量(所以它后面不能跟变量声明语句),default后跟随它的重命名

    export default add;
    // 等同于
    // export {add as default};
    
    import foo from 'modules';
    // 等同于
    // import { default as foo } from 'modules';
    
  • import可以同时输入默认导出和普通导出

    import x, { a, n as m } from './xxx.js';
    

23.5 export 与 import 的复合写法

  • 一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起

  • 写成一行以后,变量实际上并没有被导入当前模块,只是对外转发了这些变量,当前模块并不能直接使用这些变量

    // 局部输出
    export { foo, bar } from './module.js';
    // 当前模块并不能使用 foo 和 bar
    
    // 接口改名
    export { n as m } from './module.js';
    
    // 整体输出并改名(export * 会忽略default)
    export * as module from './module.js';
    

23.6 import() 动态加载

  • require是运行时加载模块,import命令无法取代require的动态加载功能

  • ES2020引入import()函数,支持动态加载模块

  • import()是异步加载,require()是同步加载

  • import()的参数是所要加载的模块的位置,返回值是一个Promise对象

  • import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数,可以使用对象的解构赋值来获取输出接口,如果模块有default输出接口,可以用参数直接获得

    // 返回值是Promise,回调的参数是代表被导入模块的对象
    import('./abc.js')
    .then(({m, n:x}) => {
      // ...
    });
    
    // 默认导入使用default参数
    import('./abc.js')
    .then(({default}) => {
      console.log(default);
    });
    
    // 同时加载多个模块
    Promise.all([
      import('./module1.js'),
      import('./module2.js'),
      import('./module3.js'),
    ])
    .then(([module1, module2, module3]) => {
       // 所有模块都fullfiled的回调
    });
    
    // 用async函数处理import()
    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();
    

24. Module的加载实现

24.1 脚本默认同步加载

  • 默认情况下,浏览器同步加载脚本(遇到<script>标签会暂停渲染来执行脚本,直到脚本加载完才继续渲染),外部引入的脚本还要先下载脚本
  • 这种同步加载脚本的方式会导致浏览器渲染受阻,因此<script>标签一般都放在html文件的最下面

24.2 脚本的异步加载方式

  • <script>标签打开deferasync属性,脚本就会异步加载

  • defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行("渲染完再执行")

  • async一旦下载完,渲染引擎就会中断渲染,执行完这个脚本后,再继续渲染("下载完就执行")

  • 多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的

    <script src="./abc.js" defer></script>
    <script src="./xyz.js" async></script>
    

24.3 脚本加载模块

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

    <script type="module" src="./foo.js"></script>
    
  • 浏览器对于带有type="module"<script>,都是异步加载,默认带有defer属性

  • <script type="module">也可以使用async

  • 外部的模块脚本的特点:

    • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见

    • 模块脚本自动采用严格模式,不管有没有声明use strict

    • 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口

    • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的

      // 侦测当前代码是否在ES6模块之中
      const isNotModuleScript = this !== undefined;
      
    • 同一个模块如果加载多次,将只执行一次

24.4 ES6模块与CommonJS模块的差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用

    • CommonJS 模块一旦输出一个值,模块内部的变化就影响不到这个值,除非写成一个函数,才能得到内部变动后的值

      // utils.js
      let counter = 3;
      function incCounter() {
        counter++;
      }
      module.exports = {
        counter: counter,
        incCounter: incCounter,
      };
      
      // main.js
      let mod = require('./utils');
      
      console.log(mod.counter);  // 3
      mod.incCounter();
      console.log(mod.counter); // 3
      
      // 解决方式
      let counter = 3;
      function incCounter() {
        counter++;
      }
      module.exports = {
        get counter() {
          return counter
        },
        incCounter: incCounter,
      };
      
      // main.js
      let mod = require('./utils');
      
      console.log(mod.counter);  // 3
      mod.incCounter();
      console.log(mod.counter); // 4
      
    • JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用,等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值(原始值变了,import加载的值也会跟着变)

      // lib.js
      export let counter = 3;
      export function incCounter() {
        counter++;
      }
      
      // main.js
      import { counter, incCounter } from './lib';
      console.log(counter); // 3
      incCounter();
      console.log(counter); // 4
      
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

  • CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段

  • CommonJS 模块加载 ES6 模块需要使用async函数

    (async () => {
      await import('./my-app.mjs');
    })();
    
  • ES6 模块的import命令可以加载 CommonJS 模块,但是只能整体加载,不能只加载单一的输出项

    // 正确
    import packageMain from 'commonjs-package';
    
    // 报错
    import { method } from 'commonjs-package';
    
    // 解决方案一
    import packageMain from 'commonjs-package';
    const { method } = packageMain;
    
    // 解决方案二
    // cjs.cjs
    module.exports = 'cjs';
    
    // esm.mjs
    import { createRequire } from 'module';
    
    const require = createRequire(import.meta.url);
    
    const cjs = require('./cjs.cjs');
    cjs === 'cjs'; // true
    

24.5Node.js的模块加载方式

  • js有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS

  • CommonJS 模块使用require()module.exports,ES6 模块使用importexport

  • Node.js v13.2 版本开始,已经默认打开了 ES6 模块支持

  • .mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置

  • 在项目的package.json文件中,指定type字段为module,项目的.js文件会被当作 ES6 模块加载

    {
       "type": "module"
    }
    
  • require命令不能加载.mjs文件,会报错,只有import命令才可以加载.mjs文件。反过来,.mjs文件里面也不能使用require命令,必须使用import

24.6 package.json 的 main 字段

package.jsonmain字段,指定模块加载的入口文件,

{
  "type": "module",
  "main": "./src/index.js"
}

然后,import命令就可以加载这个模块

// ./my-app.mjs
// 实际加载的是 ./node_modules/es-module-package/src/index.js
import { something } from 'es-module-package';
// 运行该脚本以后,Node.js 就会到./node_modules目录下面,寻找es-module-package模块,然后根据该模块package.json的main字段去执行入口文件

24.7 package.json 的 exports 字段

  • package.json文件的exports字段的优先级高于main字段

  • exports字段可以指定脚本或子目录的别名(如果没有指定别名,就不能用“模块+脚本名”这种形式加载脚本)

    // ./node_modules/es-module-package/package.json
    {
      "exports": {
        "./submodule": "./src/submodule.js"
      }
    }
    
    // xxx.js
    import submodule from 'es-module-package/submodule';
    // 加载 ./node_modules/es-module-package/src/submodule.js
    
  • exports字段的别名如果是.,就代表模块的主入口,优先级高于main字段,并且可以直接简写成exports字段的值

    {
      "exports": {
        ".": "./main.js"
      }
    }
    
    // 等同于(只有一个别名时才可以简化)
    {
      "exports": "./main.js"
    }
    
  • exports字段只有支持 ES6 的 Node.js 才认识,可以用来兼容旧版本的 Node.js

    {
       // 旧版入口
      "main": "./main-legacy.cjs",
       // 新版入口(exports优先级高于main)
      "exports": {
        ".": "./main-modern.cjs"
      }
    }
    
  • 利用.这个别名,可以为 ES6 模块和 CommonJS 指定不同的入口(这个功能需要在 Node.js 运行的时候,打开--experimental-conditional-exports标志)

    {
      "type": "module",
      "exports": {
        ".": {
          "require": "./main.cjs",
          "default": "./main.js"
        }
      }
    }
    

24.8 同时支持两种格式的模块

package.json文件的exports字段,指明两种格式模块各自的加载入口

"exports"{
  "require": "./index.js""import": "./esm/wrapper.js"
}

24.9 内部变量

ES6 模块应该是通用的,同一个模块不用修改,就可以用在浏览器环境和服务器环境。为了达到这个目标,Node.js 规定 ES6 模块之中不能使用 CommonJS 模块的特有的一些内部变量。

首先,就是this关键字。ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块,这是两者的一个重大差异。

其次,以下这些顶层变量在 ES6 模块之中都是不存在的:

  • arguments
  • require
  • module
  • exports
  • __filename
  • __dirname

25. asyncIterator异步遍历器(草案)

25.1 定义

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

    asyncIterator.next()
      .then(
        ({ value, done }) => /* ... */
      );
    
  • 对象的异步遍历器接口,部署在Symbol.asyncIterator属性上

    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 }
    }
    

25.2 for await...of

  • for await...of用于遍历异步的Iterator接口

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

    async function f() {
      for await (const x of createAsyncIterable(['a', 'b'])) {
        console.log(x);
      }
    }
    f()
    // 期待输出
    // a
    // b
    
    // 实际输出(异步遍历器还是一个草案,存在兼容性问题)
    // Uncaught (in promise) ReferenceError: createAsyncIterable is not defined
    
  • 如果next方法返回的 Promise 对象被rejectfor 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
    

25.3 异步Generator函数

  • 异步Generator函数是async函数与Generator函数的结合,返回一个一个异步Iterator

25.4 yield*

yield*语句也可以跟一个异步遍历器,与同步Generator函数一样,for await...of循环会展开yield*

async function* gen1() {
  yield 'a';
  yield 'b';
  return 2;
}

async function* gen2() {
  // result 最终会等于 2
  const result = yield* gen1();
}

(async function () {
  for await (const x of gen2()) {
    console.log(x);
  }
})();
// a
// b