ES6常用知识点总结

  这片文章主要是基于阮一峰老师的ECMAScript 6 入门。在看了阮一峰老师的这ES6入门之后,自己做了一下总结,将一些觉得对自己目前有用的东西整理出来方便日后再来巩固复习。总觉得看别人的东西当时懂了过了一段时间就忘记了,所以我总是会将别人的东西验证一遍,这样对知识的理解是能提升一个层次的。

  • 1、ECMAScript和JavaScript的关系以及ES6的含义?
  • 2、let和const
  • 3、变量的解构赋值
  • 4、字符串的扩展
  • 5、数值的扩展
  • 6、函数的扩展
  • 7、数组的扩展
  • 8、对象的扩展
  • 9、对象的新增方法
  • 10、Symbol
  • 11、Set
  • 12、Map
  • 13、Proxy
  • 14、Reflect
  • 15、Promise
  • 16、Iterator和for…of循环

1、ECMAScript和JavaScript的关系

  前者是后者的规格,后者是前者的一种实现。ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。

2、let和const

 2.1 let

let用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效
let和const有几个特点:
1、 不存在变量声明提升;
2、 暂时性死区(只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。);
3、 不允许重复声明。不允许在相同作用域内,重复声明同一个变量。
4、 块级作用域

    块级作用域的例子
    {
        let a=12;
        var b=23;
    }
    console.log(b);//23
    console.log(a);// a is not defined
    for(let i=0;i<10;i++){				
      
    }
    console.log(i);// is not defined
-----------------------------------------------------------------
    var a = [];
    for (let i = 0; i < 10; i++) {
      a[i] = function () {
        console.log(i);
      };
    }
    a[6](); // 6
复制代码

  分析:变量i是let声明的,所以i只在let声明的代码块内有效。for循环一共循环了10次,每一次都是一个独立的代码块——{},所以每次循环中的i都是独立的,当前的i只在当前循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。
  for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

    for (let i = 0; i < 3; i++) {
      let i = 'abc';
      console.log(i);
    }
    // abc
    // abc
    // abc
复制代码

2.2 不存在变量声明提升

    // var 的情况
    console.log(foo); // 输出undefined,变量声明提升,相当于在输出之前就var  foo;
    var foo = 2;
    // let 的情况
    console.log(bar); // 报错ReferenceError,没有变量声明提升
    let bar = 2;
    ————————————————————————————————————————————————————————————
复制代码

2.3 暂时性死区(temporal dead zone,简称 TDZ)
  在区块中使用let和const命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错(声明之前都是死区)。本质:只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

2.4 不允许重复声明

  let不允许在相同作用域内,重复声明同一个变量。

    function func() {
      let a = 10;
      var a = 1;
    }
    funb()// // 报错 Identifier 'a' has already been declared
    
    function func() {
      let a = 10;
      let a = 1;
    }
    func()// 报错 Identifier 'a' has already been declared
    ————————————————————————————————————————————————————————
    function bar(x = y, y = 2) {
      return [x, y];
    }
    bar(); // 报错   参数x默认值等于另一个参数y,而此时y还没有声明,属于"死区"(参数读取从左至右)。
复制代码

  不能在函数内部重新声明参数。

    function funb(arg) {
      let arg;
    }
    func() // 报错Identifier 'arg' has already been declared     形参arg跟局部变量arg在同一个{}内,所以报错
    function func(arg) {
      {
        let arg;
        console.log(arg);//undefined
      }
      console.log(arg);//34
     }
     func(34)
复制代码

2.5 块级作用域

优点:

1、没有块级作用域,内层变量可能会覆盖外层变量(变量声明提升)。
2、用来计数的循环变量会泄露为全局变量。

特点:

1、 允许任意嵌套。
2、 外层作用域无法读取内层作用域的变量。
3、 使得立即执行函数不再必要了。
4、 允许在块级作用域中声明函数,函数声明类似于var,函数声明会提升到所在的块级作用域的头部。

2.6 const

  const一旦声明变量,就必须立即初始化,不能留到以后赋值,且变量的值也不能改变。本质:并不是变量的值不得改动,而是变量指向的那个内存地址所不得改动。
  对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。

1、变量指向的是对象时,可以改变该对象的属性。但是不可将该变量指向另一个对象。
    const obj = {}
    // 为 foo 添加一个属性,可以成功
    obj.prop = 123;
    // 将 obj 指向另一个对象,就会报错,此时已经改变了obj所指向的内存地址了
    obj = {}; // TypeError: "foo" is read-only
1、变量指向的是数组时,可以改变该数组中的元素及数组的属性。但是不可将该变量指向另一个数组。
    const a = [];
    a.push('Hello'); // 可执行
    a.length = 0;    // 可执行
    a = ['Dave'];    // 报错,指向了另一个数组
复制代码

2.7 ES6声明变量的6种方式

  var、function、let、const、class、import。es5只有var和function两种。
  顶层对象的差异: 在浏览器环境指的是window对象,在 Node中 指的是global对象,在Web Worker 里面,self也指向顶层对象。
  ES5 之中,顶层对象的属性与全局变量是等价的。ES6中的var命令和function命令声明的全局变量,依旧是顶层对象的属性;let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。

    var a = 1;
    window.a // 1
    
    let b = 1;
    window.b // undefined
复制代码

3、变量的解构赋值

  ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构。本质上,这种写法属于模式匹配,只要等号两边的模式相同,左边的变量就会被赋予对应的值。 如果解构不成功,变量的值就等于undefined。

 3、1 数组的结构赋值

事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。

 

    let [foo = true] = [];foo // true
    let [x, y = 'b'] = ['a']; // x='a', y='b'
    let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
    let [x = 1] = [null]; x //null null不严格等于undefined,但是null==undefined
    let [x = 1, y = x] = [];     // x=1; y=1
    let [x = 1, y = x] = [2];    // x=2; y=2
    let [x = 1, y = x] = [1, 2]; // x=1; y=2
    let [x = y, y = 1] = [];     //  y is not undefined
    从左到右的读取。
     let [x , y] = [];//[undefined,undefined]
复制代码

   注意:ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined。

 3、2 对象的解构赋值

   对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

    let { bar, foo } = { foo: "aaa", bar: "bbb" };
    foo // "aaa"
    bar // "bbb"
    let { baz } = { foo: "aaa", bar: "bbb" };baz // undefined
    let { foo: baz } = { foo: "aaa", bar: "bbb" };
    baz//"aaa",foo是匹配的模式,baz才是变量名
    
    let { foo, bar } = { foo: "aaa", bar: "bbb" };
    是下面表示的简写。
    let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
复制代码

   由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。数组arr的0键对应的值是1,[arr.length - 1]就是2键,对应的值是3。方括号这种写法,属于属性名表达式

    let arr = [1, 2, 3];
    let {0 : first, [arr.length - 1] : last} = arr;
    first // 1
    last // 3
复制代码

 3、3 字符串的解构赋值

   字符串被转换成了一个类似数组的对象。

    const [a, b, c, d, e] = 'hello';
    a // "h"
    b // "e"
    c // "l"
    d // "l"
    e // "o"
复制代码

   类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。

    let {length : len} = 'hello';
    len // 5
复制代码

 3、4 数值和布尔值的解构赋值

    let {toString: s} = 123;
    s === Number.prototype.toString // true
    let {toString: s} = true;
    s === Boolean.prototype.toString // true
复制代码

   解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。 由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。

    let { prop: x } = undefined; // TypeError
    let { prop: y } = null; // TypeError
复制代码

 3、5 函数参数的解构赋值

    function move({x = 0, y = 0} = {}) {
        return [x, y];
    }
    move({x: 3, y: 8}); // [3, 8]
    move({x: 3}); // [3, 0]
    move({}); // [0, 0]
    move(); // [0, 0]
复制代码

   上面代码中,函数move为变量x和y指定默认值,函数move的参数是一个对象,通过对这个对象进行解构,得到变量x和y的值。如果解构失败,x和y等于默认值。用实参将{}覆盖。

    function move({x, y} = { x: 0, y: 0 }) {
      return [x, y];
    }
    move({x: 3, y: 8}); // [3, 8]
    move({x: 3}); // [3, undefined],相当于{x,y}={x,3}
    move({}); // [undefined, undefined],相当于{x,y}={};
    move(); // [0, 0]
复制代码

   上面代码是为函数move的参数(形参)指定默认值,而不是为变量x和y指定默认值,所以会得到与前一种写法不同的结果。这种写法直接是将所传参数将默认参数进行覆盖。用实参将{x:0,y:0}覆盖。
   上面两种写法本质上都是用所传参数将默认参数进行覆盖。

 3、6 解构赋值的用处

  (1) 交换变量的值。

    let{x,y}={y,x};    
复制代码

  (2) 从函数返回多个值;

    function example() {
      return {
        foo: 1,
        bar: 2
      };
    }
    let { foo, bar } = example();
复制代码

  (3) 函数参数的定义;

    function f([x, y, z]) { ... }
    f([1, 2, 3]);
复制代码

  (4) 提取json数据

    let jsonData = {
        id: 42,
        status: "OK",
        data: [867, 5309]
    };
    let { id, status, data: number } = jsonData;
复制代码

  (5) 输入模块的指定方法;

    const { SourceMapConsumer, SourceNode } = require("source-map");
复制代码

  (6) 函数参数的默认值(这样避免了在函数内部再设置默认值)

    jQuery.ajax = function (url, {
          async = true,
          beforeSend = function () {},
          cache = true,
          complete = function () {},
          crossDomain = false,
          global = true,
          // ... more config
    } = {}) {
      // ... do stuff
    };
复制代码

  (7) 遍历map结构(map原生支持iterator接口)

    const map = new Map();
    map.set('first', 'hello');
    map.set('second', 'world');
    for (let [key, value] of map) {
      console.log(key + " is " + value);
    }
    // first is hello
    // second is world
    // 获取键名
    for (let [key] of map) {
      // ...
    }
    // 获取键值
    for (let [,value] of map) {
      // ...
    }
复制代码

4、字符串的扩展

 4、1 includes()、startsWith()、endsWith()

includes():返回布尔值,表示是否找到了参数字符串。
startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。 endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。

  这三个方法都支持第二个参数,表示开始搜索的位置。endsWith的行为与其他两个方法有所不同,n表示的是结束搜索的位置,而其他两个方法针n个表示的是开始搜索的位置。

 4、2 repeat()

  repeat方法返回一个新字符串,表示将原字符串重复n次。 参数如果是小数,会被取整。相当于调用了parseInt()。

    ”a”.repeat(1.9)==>”a”.
复制代码

  如果repeat的参数是字符串,则会先转换成数字。

    'na'.repeat('na') // ""  Number("na")等于NAN
    'na'.repeat('3') // "nanana"
复制代码

 4、3 padStart(),padEnd()

  padStart()用于头部补全,padEnd()用于尾部补全。padStart()和padEnd()一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。   (1) 如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串。

    'xxx'.padStart(2, 'ab') // 'xxx'
    'xxx'.padEnd(2, 'ab') // 'xxx'
    'xxx'.padStart(5, 'ab') // 'abxxx'
    'xxx'.padEnd(5, 'ab') // 'xxxab'
复制代码

  (2) 如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串。

    'abc'.padStart(10, '0123456789') // '0123456abc'
复制代码

  (3) 如果省略第二个参数,默认使用空格补全长度。

    'x'.padStart(4) // '   x'
    'x'.padEnd(4) // 'x   '
复制代码

 4、4 模板字符串

  是增强版的字符串,用反引号(`)标识。

  (1) 如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

    let greeting = `\`Yo\` World!`;  
复制代码

  (2) 所有模板字符串的空格和换行,都是被保留的,比如ul标签前面会有一个换行。如果你不想要这个换行,可以使用trim方法消除它。

    $('#list').html(`
    
  • first
  • second
`.trim()); 复制代码

  (3)模板字符串中嵌入变量,需要将变量名写在${}之中。

  (4)大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。如果大括号中的值不是字符串,将按照一般的规则转为字符串。本质:模板字符串的大括号内部,就是执行 JavaScript 代码。

    let x = 1;
    let y = 2;
    `${x} + ${y} = ${x + y}`
    // "1 + 2 = 3"
    `${x} + ${y * 2} = ${x + y * 2}`
    // "1 + 4 = 5"
    let obj = {x: 1, y: 2};
    `${obj.x + obj.y}`
    // "3"
复制代码

  (5)模板字符串之中还能调用函数。

    function fn() {
      return "Hello World";
    }
    `foo ${fn()} bar`
    // foo Hello World bar
复制代码

5、数值的扩展

 5、1 Number.isFinite()、Number.isNaN()

  这两个新方法只对数值有效,不会先调用Number()方法。

Number.isFinite(): 用来检查一个数值是否为有限的(finite)。如果参数类型不是数值,Number.isFinite一律返回false。
Number.isNaN(): 用来检查一个值是否为NaN。Number.isNaN()只有对于NaN才返回true,非NaN一律返回false。

 5、2 Number.isSafeInteger

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

 5、3 Math对象的扩展

  扩展方法在使用时都会参数使用Number()转为数值来来处理。

  Math.trunc(): 用于去除一个数的小数部分,返回整数部分。对于非数值,Math.trunc内部使用Number方法将其先转为数值(本质上就是parseInt()方法)。

    Math.trunc(4.1) // 4
    Math.trunc(4.9) // 4
    Math.trunc(-4.1) // -4
    Math.trunc(-4.9) // -4
    Math.trunc(-0.1234) // -0
    Math.trunc('123.456') // 123
    Math.trunc(true) //1
    Math.trunc(false) // 0
    Math.trunc(null) // 0
    Math.trunc('123.456') // 123
    Math.trunc(true) //1
    Math.trunc(false) // 0
    Math.trunc(null) // 0
复制代码

  Math.sign(): 用来判断一个数到底是正数、负数、还是零。

• 参数为正数,返回+1;
• 参数为负数,返回-1;
• 参数为 0,返回0;
• 参数为-0,返回-0;
• 其他值,返回NaN。

    Math.sign(-5) // -1
    Math.sign(5) // +1
    Math.sign(0) // +0
    Math.sign(-0) // -0
    Math.sign(NaN) // NaN
复制代码

  Math.cbrt(): 用于计算一个数的立方根。对于非数值,Math.cbrt方法内部也是先使用Number方法将其转为数值。

    Math.cbrt(-1) // -1
    Math.cbrt(0)  // 0
    Math.cbrt(1)  // 1
    Math.cbrt(2)  // 1.2599210498948734
    Math.cbrt("8")//2
复制代码

  Math.imul(): 方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。

    Math.imul(2, 4)   // 8
    Math.imul(-1, 8)  // -8
    Math.imul(-2, -2) // 4
复制代码

  Math.hypot(): 方法返回所有参数的平方和的平方根。

    Math.hypot(3, 4);        // 5
    Math.hypot(3, 4, 5);     // 7.0710678118654755
    Math.hypot();            // 0
    Math.hypot(NaN);         // NaN
    Math.hypot(3, 4, 'foo'); // NaN
    Math.hypot(3, 4, '5');   // 7.0710678118654755
    Math.hypot(-3);          // 3
复制代码

6、函数的扩展

 6、1 函数参数的默认值

  参数变量是默认声明的,所以不能用let或const再次声明。

    function foo(x = 5) {
      let x = 1; // error
      const x = 2; // error
    }
    foo()//Identifier 'x' has already been declared
复制代码

 6、2 函数的length属性

  指定了默认值以后,函数的length属性,将返回没有指定默认值之前的的参数的个数。 也就是说,指定了默认值后,length属性将失真。默认值后面的参数将不参加计算。函数的length属性,不包括 rest 参数。

    (function (a) {}).length // 1
    (function (a = 5) {}).length // 0
    (function (a, b, c = 5) {}).length // 2
    (function(...args) {}).length // 0
复制代码

 6、3 作用域

  一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。(本质上是暂时性死区和不能重复声明)

    var x = 1;
    function f(x, y = x) {
      //let x=3;Identifier 'x' has already been declared
      //let y=7;// Identifier 'y' has already been declared
      console.log(y);//2
    }
    f(2) 
复制代码

 6、4 rest参数

  形式为(...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。注意:rest参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

    function push(array, ...items) {
      items.forEach(function(item) {
        array.push(item);
        console.log(item);
      });
    }
    var a = [];
    push(a, 1, 2, 3)
复制代码

 6、5 严格模式

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

 6、6 name属性

  如果将一个匿名函数赋值给一个变量,ES5的name属性,会返回空字符串,而 ES6 的name属性会返回实际的函数名。 如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的name属性都返回这个具名函数原本的名字。

    const bar = function baz() {};
    // ES5
    bar.name // "baz"
    // ES6
    bar.name // "baz"
复制代码

 6、7 箭头函数

  ES6 允许使用“箭头”(=>)定义函数。如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。 如果箭头函数直接返回一个对象,必须在对象外面加上括号 ,否则会报错。

    // 报错
    let getTempItem = id => { id: id, name: "Temp" };
    // 不报错
    let getTempItem = id => ({ id: id, name: "Temp" });
复制代码

箭头函数需要注意的地方有以下几点
  1. 函数体内的this对象,就是定义时所在的对象(固定不变),而不是使用时所在的对象。
  2. 不可以当作构造函数, 也就是说,不可以使用new命令,否则会抛出一个错误。   3. 不可以使用arguments对象, 该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  4. 不可以使用yield命令, 因此箭头函数不能用作 Generator 函数。

    function Timer() {
      this.s1 = 0;
      this.s2 = 0;
      // 箭头函数
      setInterval(() => this.s1++, 1000);
      // 普通函数
      setInterval(function () {
        this.s2++;//this表示window,setInterval是window的属性
      }, 1000);
    }
    var timer = new Timer();
    setTimeout(() => console.log('s1: ', timer.s1), 3100);
    setTimeout(() => console.log('s2: ', timer.s2), 3100);
    // s1: 3
    // s2: 0
复制代码

  上面代码中,Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数),后者的this指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1被更新了 3 次,而timer.s2一次都没更新。 this固定化的本质:并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。 正常情况下,this引用的是函数据以执行的环境对象,或者说是调用该函数的对象。

    function foo() {
      return () => {
        return () => {
          return () => {
            console.log('id:', this.id);
          };
        };
      };
    }
    var f = foo.call({id: 1});
    var t1 = f.call({id: 2})()(); // id: 1
    var t2 = f().call({id: 3})(); // id: 1
    var t3 = f()().call({id: 4}); // id: 1
复制代码

  上面代码之中,只有一个this,就是函数foo的this,所以t1、t2、t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。

箭头函数不适用场合:

  1、定义函数的方法(此时应该用普通函数的方式)

    var lives=18;
    const cat = {
      lives: 9,
      a:this.lives,//this指向的是window,
      say:function(){
      	console.log(this.lives);//this指的是cat
      },
      jumps: () => {
        this.lives--;//this指的是window,定义时的this指的就是window
      }
    }
    cat.say();
    cat.jumps();
    console.log(cat.a);
复制代码

  2、需要动态this的时候

    var button = document.getElementById('press');
    button.addEventListener('click', () => {
      this.classList.toggle('on');//this指的是window
    })
复制代码

适用场合:回调

    var handler = {
      id: '123456',
      init: function() {
        document.addEventListener('click',
          event => this.doSomething(event.type), false);//this指的是handler
      },
      doSomething: function(type) {
        console.log('Handling ' + type  + ' for ' + this.id);//this指的是hander
      }
    };
复制代码

 6、8 尾调用

  是指某个函数的最后一步是调用另一个函数。 尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。(就是说外层函数的作用域链会被销毁,但它的活动对象任然会留在内存中)

    function f(x){
      return g(x);
    }
复制代码

 6、9 尾递归

  尾调用自身,就称为尾递归。缺点:把所有用到的内部变量改写成函数的参数。优点:不会发生栈溢出,相对节省内存。

    function factorial(n, total) {
      if (n === 1) return total;
      return factorial(n - 1, n * total);
    }
    factorial(5, 1) // 120
复制代码

采用es6语法(参数的默认值)可以解决这个缺点

    function factorial(n, total=1) {
      if (n === 1) return total;
      return factorial(n - 1, n * total);
    }
    factorial(5) // 120
复制代码

7、数组的扩展

 7、1 扩展运算符

  扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。 扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。

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

    console.log(…[1,2,3])// 1 2 3
复制代码

  扩展运算符的应用:
  1、 替代函数的apply用法

    // ES5 的写法
    function f(x, y, z) {
      // ...
    }
    var args = [0, 1, 2];
    f.apply(null, args);
    // ES6的写法
    function f(x, y, z) {
      // ...
    }
    let args = [0, 1, 2];
    f(...args);
复制代码

  2、求取数组中的最大值:

    // ES5 的写法
    Math.max.apply(null, [14, 3, 77])
    // ES6 的写法
    Math.max(...[14, 3, 77])
    // 等同于
    Math.max(14, 3, 77);
复制代码

  3、 简化push函数的用法

    // ES5的 写法
    var arr1 = [0, 1, 2];
    var arr2 = [3, 4, 5];
    Array.prototype.push.apply(arr1, arr2);
    // ES6 的写法
    let arr1 = [0, 1, 2];
    let arr2 = [3, 4, 5];
    arr1.push(...arr2);
复制代码

  4、 复制数组

    const a1 = [1, 2];
    // 写法一
    const a2 = [...a1];
    // 写法二
    const [...a2] = a1;
    写法一二相当于把数组中a1的元素复制到a2中
    const a1 = [1, 2];
    const a2 = a1.concat();
    a2[0] = 2;
    a1 // [1, 2]
   上面两个方法修改a2都不会对a1产生影响。   
复制代码

  5、 合并数组

    const arr1 = ['a', 'b'];
    const arr2 = ['c'];
    const arr3 = ['d', 'e'];
    // ES5 的合并数组
    arr1.concat(arr2, arr3);
    // [ 'a', 'b', 'c', 'd', 'e' ]
    // ES6 的合并数组
    [...arr1, ...arr2, ...arr3]
    // [ 'a', 'b', 'c', 'd', 'e' ]
    这两种方法都是浅拷贝,使用的时候需要注意。
    
    const a1 = [{ foo: 1 }];
    const a2 = [{ bar: 2 }];
    const a3 = a1.concat(a2);
    const a4 = [...a1, ...a2];
    console.log(a3[0] === a1[0]) // true 指向相同的内存地址
    console.log(a4[0] === a1[0]) // true 指向相同的内存地址
    a3[0].foo=2;
    console.log(a1)//{foo: 2}
复制代码

  a3和a4是用两种不同方法合并而成的新数组,但是它们的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了原数组的成员,会同步反映到新数组。

  6、 与解构赋值结合

    const [first, ...rest] = [1, 2, 3, 4, 5];
    console.log(first); // 1
    console.log(rest)  // [2, 3, 4, 5]
    const [first, ...rest] = [];
    console.log(first) // undefined
    console.log(rest)  // []
    const [first, ...rest] = ["foo"];
    console.log(first)  // "foo"
    console.log(rest)   // []
复制代码

  将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。跟rest参数一样。

    const [...butLast, last] = [1, 2, 3, 4, 5];// 报错
    const [first, ...middle, last] = [1, 2, 3, 4, 5];//报错,Rest element must be last element
复制代码

  7、 字符串

  扩展运算符还可以将字符串转为真正的数组。

    [...'hello']; // [ "h", "e", "l", "l", "o" ]
    […'hello'].length;//5
复制代码

  8、 实现了 Iterator 接口的对象

  任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。

    let nodeList = document.querySelectorAll('div');
    let array = [...nodeList];//实现了Iterator接口
    let arrayLike = {
      '0': 'a',
      '1': 'b',
      '2': 'c',
      length: 3
    };
    let arr = [...arrayLike];// // TypeError: Cannot spread non-iterable object.
    //可以改成下面这样
    let arr=Array.form(arrayLike)//把对象变成数组,把类似数组的变成数组
复制代码

  map结构:

    let map = new Map([
      [1, 'one'],
      [2, 'two'],
      [3, 'three'],
    ]);
    let arr = [...map.keys()]; // [1, 2, 3]
    Generator 函数:
    const go = function*(){
      yield 1;
      yield 2;
      yield 3;
    };
    [...go()] // [1, 2, 3]
复制代码

 7、2 Array.from()

  Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
  任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。

    Array.from({ length: 3 });
    // [ undefined, undefined, undefined ]
复制代码

  1、类似于数组的对象

    let arrayLike = {
        '0': 'a',
        '1': 'b',
        '2': 'c',
        length: 3
    };
    // ES5的写法
    var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
    // ES6的写法
    let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
复制代码

  2、可遍历的对象:

    Array.from('hello')
    // ['h', 'e', 'l', 'l', 'o']
    let namesSet = new Set(['a', 'b'])
    Array.from(namesSet) // ['a', 'b']
复制代码

  3、不支持该方法的浏览器,可以用下面这种方法来进行兼容:

    const toArray = (() =>
      Array.from ? Array.from : obj => [].slice.call(obj)
    )();
复制代码

  4、Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

    Array.from(arrayLike, x => x * x);
    // 等同于
    Array.from(arrayLike).map(x => x * x);
    Array.from([1, 2, 3], (x) => x * x)// [1, 4, 9]
    let spans = document.querySelectorAll('span.name');
    // map()
    let names1 = Array.prototype.map.call(spans, s => s.textContent);
    // Array.from()
    let names2 = Array.from(spans, s => s.textContent)
复制代码

  5、Array.from()可以将各种值转为真正的数组。

    Array.from({ length: 2 }, () => 'jack')// ['jack', 'jack']
复制代码

  6、将字符串转为数组,然后返回字符串的长度

    function countSymbols(string) {
      return Array.from(string).length;
    }
复制代码

 7、3 Array.of()

  用于将一组值,转换为数组。这个方法的主要目的,是弥补数组构造函数Array()的不足。Array.of基本上可以用来替代Array()或new Array()。

    Array() // []
    Array(3) // [, , ,]
    Array(3, 11, 8) // [3, 11, 8]
    Array.of(3, 11, 8) // [3,11,8]
    Array.of(3) // [3]
    Array.of(3).length // 1
复制代码

 7、4 copywithin()

  数组实例的copyWithin方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。

Array.prototype.copyWithin(target, start = 0, end = this.length)

target(必需):从该位置开始替换数据。如果为负值,表示倒数。
start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。
end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。

    [1, 2, 3, 4, 5].copyWithin(0, 3)// [4, 5, 3, 4, 5]
    上面代码表示将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2。
    [1, 2, 3, 4, 5].copyWithin(0, 2)// [3, 4, 5, 4, 5]
    [1, 2, 3, 4, 5].copyWithin(0, 2,3)// [3, 2, 3, 4, 5]
复制代码

 7、5 find()和findIndex()

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

    [1, 4, -5, 10].find((n) => n < 0)// -5
    [1, 5, 10, 15].find(function(value, index, arr) {
      return value > 9;
    }) // 10
复制代码

  数组实例的findIndex(),返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。

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

  这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。

function f(v){
  return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person);    // 26
复制代码

  这两个方法都可以发现NaN,弥补了数组的indexOf方法的不足。

    [NaN].indexOf(NaN)
    // -1
    [NaN].findIndex(y => Object.is(NaN, y))
    // 0 
复制代码

  Object.is()用来比较两个值是否严格相等,与严格相等运算符(===)一样。

 7、6 fill()

    //使用给定值,填充一个数组,并返回填充后的数组。
    ['a', 'b', 'c'].fill(7)
    // [7, 7, 7]
    new Array(3).fill(7)
    // [7, 7, 7]
复制代码

  数组中已有的元素,会被全部抹去。 fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。这根splice()很像,只是splice方法没有返回值。

    ['a', 'b', 'c'].fill(7, 1, 2)// ['a', 7, 'c']
复制代码

  注意:如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。

    let arr = new Array(3).fill({name: "Mike"});
    arr[0].name = "Ben";
    console.log(arr)// [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
复制代码

 7、7 entries()、keys()、values()

  它们都返回一个遍历器对象(Iterator),可以用for...of循环进行遍历。

    let letter = ['a', 'b', 'c'];
    let keys=letter.keys();
    let values=letter.values()
    let entries = letter.entries();
    console.log(keys,values,entries)
    // Array Iterator {} Array Iterator {} Array Iterator {}
    for (let index of keys) {
      console.log(index);
    }   //0, //1,//2
复制代码

 7、8 includes()

  返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。

    [1, 2, 3].includes(3, 3);  // false
    [1, 2, 3].includes(3, -1); // true
复制代码

  该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。

  indexOf方法有两个缺点。

1、不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1。
2、它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。

    [NaN].includes(NaN) //true
复制代码

  可以用如下方法来判断当前环境是否支持该方法。

    const contains = (() =>
    	Array.prototype.includes ?
    	(arr, value) => arr.includes(value) :
    	(arr, value) => arr.some(el => el === value)
    )();
    console.log(contains(['foo', 'bar'], 'baz')); // => false 
复制代码

 7、9 flat()、flatMap()

  用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1。

    [1, 2, [3, [4, 5]]].flat()     // [1, 2, 3, [4, 5]]
    [1, 2, [3, [4, 5]]].flat(2)    // [1, 2, 3, 4, 5]
    [1, [2, [3]]].flat(Infinity)   // [1, 2, 3],这种方式不管嵌套多少层,都会被拉平。
复制代码

  如果原数组有空位,flat()方法会跳过空位。

    [1, 2, , 4, 5].flat()    // [1, 2, 4, 5]
复制代码

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

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

 7、10 数组的空位

  注意,空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值。

ES5:

1、forEach(), filter(), reduce(), every() 和some()都会跳过空位。
2、map()会跳过空位,但会保留这个值。
3、join()和toString()会将空位视为undefined,而undefined和null会被处理成空字符串。

ES6:明确将空位转为undefined。

8、对象的扩展

 8、1 属性的简洁表示法

    const foo = 'bar';
    const baz = {foo};
    console.log(baz)   // {foo: "bar"}
    // 等同于
    const baz = {foo: foo};
复制代码

  ES6 允许在对象之中,直接写变量。这时,属性名为变量名, 属性值为变量的值。

    function f(x, y) {
      return {x, y};
    }
    // 等同于
    function f(x, y) {
      return {x: x, y: y};
    }
    f(1, 2)    // {x: 1, y: 2}
复制代码

 8、2 属性名表达式

  JavaScript 定义对象的属性,有两种方法。

    // 方法一
    obj.foo = true;
    // 方法二
    obj['a' + 'bc'] = 123;
复制代码

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

    let lastWord = 'last word';
    const a = {
      'first word': 'hello',
      [lastWord]: 'world'
    };
    a['first word'] // "hello"
    a[lastWord] // "world"
    a['last word'] // "world"
复制代码

  表达式还可以用于定义方法名。

    let obj = {
      ['h' + 'ello']() {
        return 'hi';
      }
    };
    obj.hello() // hi
复制代码

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

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

 8、3 方法的name属性

    const person = {
        sayName() {
            console.log('hello!');
        },
    };
    person.sayName.name   // "sayName"
复制代码

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

    const obj = {
      get foo() {},
      set foo(x) {}
    };
    obj.foo.name
    // TypeError: Cannot read property 'name' of undefined
    const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
    descriptor.get.name // "get foo"
    descriptor.set.name // "set foo"
复制代码

  如果对象的方法是一个 Symbol 值,那么name属性返回的是这个 Symbol 值的描述。

    const key1 = Symbol('description');
    const key2 = Symbol();
    let obj = {
      [key1]() {},
      [key2]() {},
    };
    obj[key1].name // "[description]"
    obj[key2].name // ""
复制代码

  有两种特殊情况:bind方法创造的函数,name属性返回bound加上原函数的名字;Function构造函数创造的函数,name属性返回anonymous(匿名)。

    (new Function()).name // "anonymous"
    var doSomething = function() {
      // ...
    };
    doSomething.bind().name // "bound doSomething"
复制代码

 8、4 属性的可枚举性和遍历

  1、可枚举性: 对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

    let obj = { foo: 123 };
    Object.getOwnPropertyDescriptor(obj, 'foo')
    //  {
    //    value: 123,
    //    writable: true,
    //    enumerable: true,//可枚举性
    //    configurable: true
    //  }
复制代码

  目前,有四个操作会忽略enumerable为false的属性。

1、 for...in循环:只遍历对象自身的和继承的可枚举的属性。
2、 Object.keys():返回对象自身的可枚举的属性的属性。
3、 JSON.stringify():只串行化对象自身的可枚举的属性。
4、 Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。

总结:尽量不要用for...in循环,而用Object.keys()代替。

  2、属性的遍历

  1、 for…in:for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
  2、 Object.keys(obj) 返回一个数组,包括对象自身的所有可枚举属性(不含 Symbol 属性)的键名。
  3、 Object.getOwnPropertyNames(obj) 返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
  4、 Object.getOwnPropertySymbols(obj) 返回一个数组,包含对象自身的所有 Symbol 属性的键名。
  5、 Reflect.ownKeys(obj) 返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

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

1、首先遍历所有数值键,按照数值升序排列。
2、其次遍历所有字符串键,按照加入时间升序排列。
3、最后遍历所有 Symbol 键,按照加入时间升序排列。

    Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })    // ['2', '10', 'b', 'a', Symbol()]
复制代码

 8、5 对象的扩展运算符

  对象的解构赋值 (在=赋值左边) 用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。属于浅拷贝。

    let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
    x // 1    y // 2      z // { a: 3, b: 4 }
复制代码

  对象的解构赋值的注意事项

  1、由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefined或null,就会报错,因为它们无法转为对象。

    let { x, y, ...z } = null; // 运行时错误
    let { x, y, ...z } = undefined; // 运行时错误
复制代码

  2、解构赋值必须是最后一个参数,否则会报错。

    let { ...x, y, z } = someObject; // 句法错误
    let { x, ...y, ...z } = someObject; // 句法错误
复制代码

  3、扩展运算符的解构赋值,不能复制继承自原型对象的属性。

    let o1 = { a: 1 };
    let o2 = { b: 2 };
    o2.__proto__ = o1;
    let { ...o3 } = o2;
    o3 // { b: 2 }
    o3.a // undefined
    Object.create({ x: 1, y: 2 });创建的是原型对象
    const o = Object.create({ x: 1, y: 2 });
    o.z = 3;
    let { x, ...newObj } = o;//newObj只能获取z的值
    let { y, z } = newObj;
    x // 1
    y // undefined
    z // 3
复制代码

  4、 变量声明语句之中,如果使用解构赋值,扩展运算符后面必须是一个变量名,而不能是一个解构赋值表达式。

    let { x, ...{ y, z } } = o;
    // SyntaxError: ... must be followed by an identifier in declaration contexts
复制代码

  扩展运算符: (在等号=右边) 对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。

  对象的扩展运算符的注意事项

  1、由于数组是特殊的对象,所以对象的扩展运算符也可以用于数组。

    let foo = { ...['a', 'b', 'c'] };
    console.log(foo);// {0: "a", 1: "b", 2: "c"}
复制代码

  2、 如果扩展运算符后面不是对象,则会自动将其转为对象。

    // 等同于 {...Object(1)}
    {...1} // {}    由于该对象没有自身属性,所以返回一个空对象。
    // 等同于 {...Object(true)}
    {...true} // {}
    // 等同于 {...Object(undefined)}
    {...undefined} // {}
    // 等同于 {...Object(null)}
    {...null} // {}
复制代码

  3、 如果扩展运算符后面是字符串,它会自动转成一个类似数组的对象,因此返回的不是空对象。

    {...'hello'}    // {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
复制代码

  4、 对象的扩展运算符等同于使用Object.assign()方法。

    let aClone = { ...a };
    // 等同于
    let aClone = Object.assign({}, a);
复制代码

  5、 如果想完整克隆一个对象,还拷贝对象原型的属性,可以采用下面的写法。

    // 写法1
    const clone2 = Object.assign(
      Object.create(Object.getPrototypeOf(obj)),
      obj
    );
    // 写法2
    const clone3 = Object.create(
      Object.getPrototypeOf(obj),
      Object.getOwnPropertyDescriptors(obj)
    )
复制代码

  6、 扩展运算符可以用于合并两个对象。

    let ab = { ...a, ...b };
    // 等同于
    let ab = Object.assign({}, a, b);
复制代码

  7、 如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。

    let a={x:2,y:3;z:4} 
    let aWithOverrides = { ...a, x: 1, y: 2 };
    console.log(aWithOverrides)// {x: 1, y: 2, z: 4}
    
    let arr={a:1,b:2}
    let arr1={b:3,c:4}
    let arr2={...arr,...arr1}
    console.log(arr2)    // {a: 1, b: 3, c: 4}
复制代码

9、对象的新增方法

 9、1 Object.is()

  用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。与ES5的不同之处只有两个:一是+0不等于-0,二是NaN等于自身。

    +0 === -0 //true
    NaN === NaN // false
    Object.is(+0, -0) // false
    Object.is(NaN, NaN) // true
复制代码

  ES5 可以通过下面的代码,部署Object.is。

    Object.defineProperty(Object, 'is', {
      value: function(x, y) {
        if (x === y) {
          // 针对+0 不等于 -0的情况
          return x !== 0 || 1 / x === 1 / y;
        }
        // 针对NaN的情况
        return x !== x && y !== y;
      },
      configurable: true,
      enumerable: false,
      writable: true
    });
复制代码

 9、2 Object.assign()

  用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。 如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

    const target = { a: 1, b: 1 };
    const source1 = { b: 2, c: 2 };
    const source2 = { c: 3 };
    const target={};
    Object.assign(target, source1, source2);    
    Console.log(target); // {a:1, b:2, c:3}
复制代码

  注意1:由于undefined和null无法转成对象,所以如果它们作为参数,就会报错。如果undefined和null不在首参数,就不会报错。

    Object.assign(undefined) // 报错
    Object.assign(null) // 报错
    let obj = {a: 1};
    Object.assign(obj, undefined) === obj // true
    Object.assign(obj, null) === obj // true
复制代码

  注意2:其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。

    const v1 = 'abc';
    const v2 = true;
    const v3 = 10;
    const obj = Object.assign({}, v1, v2, v3);
    console.log(obj); // { "0": "a", "1": "b", "2": "c" }
复制代码

  注意3:Object.assign拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。

  Object.assign的特点

  1、 Object.assign()是浅拷贝。
  2、 同名属性的替换;(后者替换前者)
  3、 数组的处理。Object.assign把数组视为属性名为 0、1、2 的对象,因此源数组的 0 号属性4覆盖了目标数组的 0 号属性1。

    Object.assign([1, 2, 3], [4, 5])
    // [4, 5, 3]
复制代码

  4、 取值函数的处理;(求值后再复制)

    const source = {
      get foo() { return 1 }
    };
    const target = {};
    Object.assign(target, source)
    // { foo: 1 }
复制代码

  Object.assign的常见用途:

  1、 为对象添加属性

    class Point {
      constructor(x, y) {
        Object.assign(this, {x, y});
      }
    }
复制代码

  2、 为对象添加方法

    Object.assign(SomeClass.prototype, {
      someMethod(arg1, arg2) {
      },
      anotherMethod() {
      }
    });
    // 等同于下面的写法
    SomeClass.prototype.someMethod = function (arg1, arg2) {
    };
    SomeClass.prototype.anotherMethod = function () {
    };
复制代码

  3、 克隆对象(克隆自身与其继承的值)

    function clone(origin) {
      let originProto = Object.getPrototypeOf(origin);
      return Object.assign(Object.create(originProto), origin);
    }
复制代码

  4、 合并多个对象

    const merge = (target, ...sources) => Object.assign(target, ...sources);
复制代码

  5、 为属性指定默认值

    const DEFAULTS = {
      logLevel: 0,
      outputFormat: 'html'
    };
    function processContent(options) {
      options = Object.assign({}, DEFAULTS, options);
    }
复制代码

 9、3 Object.getOwnPropertyDescriptors()

  返回指定对象所有自身属性(非继承属性)的描述对象。主要是为了解决Object.assign()无法正确拷贝get属性和set属性的问题。

 9、4. __proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()

   __proto__属性:

  用来读取或设置当前对象的prototype对象。目前,所有浏览器(包括 IE11)都部署了这个属性。建议不要使用此属性。使用下面的Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。

  Object.setPrototypeOf(): 用来设置一个对象的prototype对象。如果第一个参数不是对象,会自动转为对象。但是由于返回的还是第一个参数,所以这个操作不会产生任何效果。 由于undefined和null无法转为对象,所以如果第一个参数是undefined或null,就会报错。

    Object.setPrototypeOf(1, {}) === 1 // true
    Object.setPrototypeOf('foo', {}) === 'foo' // true
    Object.setPrototypeOf(true, {}) === true // true
    Object.setPrototypeOf(undefined, {})
    // TypeError: Object.setPrototypeOf called on null or undefined
    Object.setPrototypeOf(null, {})
    // TypeError: Object.setPrototypeOf called on null or undefined
复制代码

  Object.getPrototypeOf() 用于读取一个对象的原型对象。如果参数不是对象,会被自动转为对象。如果参数是undefined或null,它们无法转为对象,所以会报错。

    Object.getPrototypeOf(1) === Number.prototype // true
    Object.getPrototypeOf('foo') === String.prototype // true
    Object.getPrototypeOf(true) === Boolean.prototype // true
    Object.getPrototypeOf(null)
    // TypeError: Cannot convert undefined or null to object
    Object.getPrototypeOf(undefined)
    // TypeError: Cannot convert undefined or null to object
复制代码

 9、5 Object.keys(),Object.values(),Object.entries()

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

  Object.values(): 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。属性名为数值的属性,是按照数值大小,从小到大遍历的,因此返回的顺序是b、c、a。Object.values会过滤属性名为 Symbol 值的属性。

    const obj = { 100: 'a', 2: 'b', 7: 'c' };
    Object.values(obj)
    // ["b", "c", "a"]
    Object.values({ [Symbol()]: 123, foo: 'abc' });
    // ['abc']   会过滤属性名为 Symbol 值的属性。
          如果参数不是对象,Object.values会先将其转为对象。如果Object.values方法的参数是一个字符串,会返回各个字符组成的一个数组。
    Object.values('foo')   // ['f', 'o', 'o']
    Object.values(42) // []
    Object.values(true) // []
复制代码

  Object.entries(): 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。除了返回值不一样,该方法的行为与Object.values基本一致。

    const obj = { foo: 'bar', baz: 42 };
    Object.entries(obj)
    // [ ["foo", "bar"], ["baz", 42] ]
复制代码

  主要用途: 1、 遍历对象的属性。 2、 将对象转为真正的Map结构。

    const obj = { foo: 'bar', baz: 42 };
    const map = new Map(Object.entries(obj));
    map // Map { foo: "bar", baz: 42 }
复制代码

  Object.fromEntries(): 是Object.entries()的逆操作,用于将一个键值对数组转为对象。

 9、6 Obj.hasOwnProperty(obj.prop)

  返回一个布尔值,指示对象自身属性中是否具有指定的属性。和 in 运算符不同,该方法会忽略掉那些从原型链上继承到的属性。

    o = new Object();
    o.prop = 'exists';
    o.hasOwnProperty('prop');             // 返回 true
    o.hasOwnProperty('toString');         // 返回 false
    o.hasOwnProperty('hasOwnProperty');   // 返回 false
复制代码

10、Symbol

  新的原始数据类型Symbol,表示独一无二的值,是javascript的第七种数据类型。前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

  注意事项:

  1、Symbol函数前不能使用new命令,否则会报错。由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。 Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述。

    let s1 = Symbol('foo');
    let s2 = Symbol('bar');
    console.log(s1) // Symbol(foo)
    console.log(s1.toString()) //  Symbol(foo)
复制代码

  2、如果 Symbol 的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个 Symbol 值。

    const obj = {
      toString() {
        return 'abc';
      }
    };
    const sym = Symbol(obj);
    console.log(sym)   // Symbol(abc)
复制代码

  3、Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。

    // 没有参数的情况
    let s1 = Symbol();
    let s2 = Symbol();
    s1 === s2 // false
    // 有参数的情况
    let s1 = Symbol('foo');
    let s2 = Symbol('foo');
    s1 === s2 // false
复制代码

  4、Symbol 值不能与其他类型的值进行运算,会报错。

    let sym = Symbol('My symbol');
    "your symbol is " + sym
    // TypeError: can't convert symbol to string
    `your symbol is ${sym}`
    // TypeError: can't convert symbol to string
复制代码

  5、Symbol 值可以显式转为字符串。也可以转为布尔值,但是不能转为数值。

    let sym = Symbol('My symbol');
    String(sym) // 'Symbol(My symbol)'
    sym.toString() // 'Symbol(My symbol)'
    let sym = Symbol();
    Boolean(sym) // true
    !sym  // false
    if (sym) {
    }
    Number(sym) // TypeError
    Console.log(sym + 2) // TypeError
复制代码

 10.1、 作为属性名

  Symbol 值作为对象属性名时,不能用点运算符。一般使用方括号。

    const mySymbol = Symbol();
    const a = {};
    a.mySymbol = 'Hello!';//此处的mySymbol只能看成是字符串,而不能看成是一个Symbol值
    console.log(a[mySymbol])  // undefined
    consoe.log(a['mySymbol']) // "Hello!"
复制代码

  在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。

    let s = Symbol();
    let obj = {
      [s]: function (arg) { ... }
    };
    obj[s](123);
复制代码

  Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的。 Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。 魔术字符串:在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。利用Symbol来消除。

    const shapeType = {
      triangle: Symbol('Triangle')
    };
    
    function getArea(shape, options) {
      let area = 0;
      switch (shape) {
        case shapeType.triangle:
          area = 5 * options.width * options.height;
          break;
      }
      return area;
    }
    getArea(shapeType.triangle, { width: 100, height: 100 });
复制代码

 10、2 属性名的遍历

  Symbol 作为属性名,该属性不会出现在for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有 Symbol 属性名。 可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。

 10、3 Symbol.for()、Symbol.keyFor()

  Symbol.for(“foo”): 接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。

    let s1 = Symbol.for('foo');
    let s2 = Symbol.for('foo');
    console.log(s1 === s2)   // true
复制代码

  Symbol.keyFor: 返回一个已登记的 Symbol 类型值的key。返回一个使用Symbol.for()方法定义的key。

    let s1 = Symbol.for("foo");
    Symbol.keyFor(s1) // "foo"
    let s2 = Symbol("foo");
    Symbol.keyFor(s2) // undefined
复制代码

  注意:Symbol.for为 Symbol 值登记的名字,是全局环境的,可以在不同的 iframe 或 service worker 中取到同一个值。

 10、4 内置的Symbol值

  Symbol.hasInstance: 对象的Symbol.hasInstance属性,指向一个内部方法。当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)。

    class MyClass {
      [Symbol.hasInstance](foo) {
        return foo instanceof Array;
      }
    }
    Console.log([1, 2, 3] instanceof new MyClass()) // trueSymbol.isConcatSpreadable
复制代码

  Symbol.isConcatSpreadable: 等于一个布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开。

    let arr1 = ['c', 'd'];
    ['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
    arr1[Symbol.isConcatSpreadable] // undefined
    let arr2 = ['c', 'd'];
    arr2[Symbol.isConcatSpreadable] = false;
    ['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']
复制代码

  类似数组的对象正好相反,默认不展开。它的Symbol.isConcatSpreadable属性设为true,才可以展开。

    let obj = {length: 2, 0: 'c', 1: 'd'};
    ['a', 'b'].concat(obj, 'e') // ['a', 'b', obj, 'e']
    obj[Symbol.isConcatSpreadable] = true;
    ['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']
复制代码

  Symbol.species: 指向一个构造函数。创建衍生对象时,会使用该属性。下面例子中b、c是a的衍生对象。

    class MyArray extends Array {
    	static get [Symbol.species]() { return Array; }
    }
    const a = new MyArray(1, 2, 3);
    const b = a.map(x => x);
    const c = a.filter(x => x > 1);
    b instanceof MyArray // false
    b instanceof Array //true
    c instanceof MyArray // false
复制代码

  主要的用途: 有些类库是在基类的基础上修改的,那么子类使用继承的方法时,作者可能希望返回基类的实例,而不是子类的实例。
  还有其他属性就不一一例举了:Symbol.match、Symbol.replace、Symbol.search、Symbol.split、Symbol.iterator、Symbol.toPrimitive、Symbol.toStringTag(toString())、Symbol.unscopables(with)

11、Set

 11.1、 Set

  一种数据结构,类似于数组,但是成员的值都是唯一的,没有重复的值。

    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
复制代码

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

    // 例一
    const set = new Set([1, 2, 3, 4, 4]);
    console.log([...set])
    // [1, 2, 3, 4]
    let arr = [3, 5, 2, 2, 5, 5];
    let unique = [...new Set(arr)];
    // [3, 5, 2]
    // 例二
    const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
    items.size // 5
    // 例三
    const set = new Set(document.querySelectorAll('div'));
    set.size // 56
复制代码

  将set转为数组的方式

    [...new Set('ababbc')]
复制代码

  可以用于去重

    [...new Set('ababbc')].join('')  //abc 
复制代码

  向 Set 加入值的时候,不会发生类型转换,所以5和"5"是两个不同的值。在 Set 内部,两个NaN是相等。

 11.2、 Set实例的属性和方法

  实例属性:

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

  操作方法:

1、add(value):添加某个值,返回 Set 结构本身。
2、delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
3、has(value):返回一个布尔值,表示该值是否为Set的成员。
4、clear():清除所有成员,没有返回值。

  Array.from方法可以将 Set 结构转为数组。

    const items = new Set([1, 2, 3, 4, 5]);
    //方法1
    const array = Array.from(items);
    //方法2
    const arr=[…items]
复制代码

  遍历方法:

1、keys():返回键名的遍历器。
2、values():返回键值的遍历器。
3、entries():返回键值对的遍历器。
4、forEach():使用回调函数遍历每个成员。

  由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。   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
复制代码

  数组的map和filter方法也可以间接用于 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}
复制代码

 11.3、 WeakSet

  WeakSet 结构与 Set 类似,但是,它与 Set 有两个区别。

首先:WeakSet 的成员只能是对象,而不能是其他类型的值。

    const ws = new WeakSet();
    ws.add(1)
    // TypeError: Invalid value used in weak set
    ws.add(Symbol())
    // TypeError: invalid value used in weak set
复制代码

其次:WeakSet 中的对象都是弱引用,垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。 因此,WeakSet 适合临时存放一组对象。由于上面这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失。WeakSet 不可遍历。
作为构造函数,WeakSet 可以接受一个数组或类似数组的对象作为参数注意,是a数组的成员成为 WeakSet 的成员,而不是a数组本身。这意味着,数组的成员只能是对象。

    const a = [[1, 2], [3, 4]];
    const ws = new WeakSet(a);
    // WeakSet {[1, 2], [3, 4]}
    const b = [3, 4];
    const ws = new WeakSet(b);
    // Uncaught TypeError: Invalid value used in weak set(…)
复制代码

  有以下几个实例方法:add()、delete()、has()。

12、Map

 12.1、Map

  JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。

  Map: 一种数据结构,类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

  作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个表示键值对的数组。任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map构造函数的参数。这就是说,Set和Map都可以用来生成新的 Map。

    const map = new Map([
      ['name', '张三'],
      ['title', 'Author']
    ]);
    map.size // 2
    map.has('name') // true
    map.get('name') // "张三"
    map.has('title') // true
    map.get('title') // "Author"
复制代码

  Map构造函数接受数组作为参数,实际上执行的是下面的算法。

    const items = [
      ['name', '张三'],
      ['title', 'Author']
    ];
    const map = new Map();
    items.forEach(
      ([key, value]) => map.set(key, value)
    );
复制代码

  如果对同一个键多次赋值,后面的值将覆盖前面的值。只有对同一个对象的引用,Map 结构才将其视为同一个键

    const map = new Map();
    map.set(1, 'aaa').set(1, 'bbb');
    map.get(1) // "bbb"
    
    const map = new Map();
    map.set(['a'], 555);
    map.get(['a']) // undefined //这两个[“a”]的内存地址不一样
复制代码

  注意: 虽然NaN不严格相等于自身,但 Map 将其视为同一个键。

 12.2、实例的属性和操作方法:

  size、set()、get()、has()、delete()、clear()

 12.3、遍历方法

1、 keys():返回键名的遍历器。
2、values():返回键值的遍历器。
3、entries():返回所有成员的遍历器。
4、forEach():遍历 Map 的所有成员。

  Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...)。

    const map = new Map([
      [1, 'one'],
      [2, 'two'],
      [3, 'three'],
    ]);
    [...map.keys()]   // [1, 2, 3]
    [...map.values()]   // ['one', 'two', 'three']
    [...map.entries()]   // [[1,'one'], [2, 'two'], [3, 'three']]
    [...map]   // [[1,'one'], [2, 'two'], [3, 'three']]
复制代码

  结合数组的map方法、filter方法,可以实现 Map 的遍历和过滤(Map 本身没有map和filter方法)。

    const map0 = new Map().set(1, 'a').set(2, 'b').set(3, 'c')
    const map1 = new Map(
      [...map0].filter(([k, v]) => k < 3)
    );
    // 产生 Map 结构 {1 => 'a', 2 => 'b'}
    const map2 = new Map(
      [...map0].map(([k, v]) => [k * 2, '_' + v])
        );
    // 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'}
复制代码

 12.4、与其他数据结构的互相转换:

  1、 Map与数组的转换:

    const myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
    [...myMap] //Map转数组
    // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
复制代码

  数组转Map

    new Map([
      [true, 7],
      [{foo: 3}, ['abc']]
    ])
    // Map {
    //   true => 7,
    //   Object {foo: 3} => ['abc']
    // }
复制代码

  2、 Map与对象的转换

  Map转对象

    function strMapToObj(strMap) {
      let obj = Object.create(null);
      for (let [k,v] of strMap) {
        obj[k] = v;
      }
      return obj;
    }
    const myMap = new Map() .set('yes', true).set('no', false);
    strMapToObj(myMap)   // { yes: true, no: false }
复制代码

  对象转Map

    function objToStrMap(obj) {
      let strMap = new Map();
      for (let k of Object.keys(obj)) {
        strMap.set(k, obj[k]);
      }
      return strMap;
    }
    objToStrMap({yes: true, no: false})   // Map {"yes" => true, "no" => false}
复制代码

  3、 Map与Json的转换 Map 的键名都是字符串,这时可以选择转为对象 JSON。

    function strMapToJson(strMap) {
      return JSON.stringify(strMapToObj(strMap));
    }
    let myMap = new Map().set('yes', true).set('no', false);
    strMapToJson(myMap)
    // '{"yes":true,"no":false}'
复制代码

  Map 的键名有非字符串,这时可以选择转为数组 JSON。

    function mapToArrayJson(map) {
      return JSON.stringify([...map]);
    }
    let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
    mapToArrayJson(myMap)
    // '[[true,7],[{"foo":3},["abc"]]]'
复制代码

  Json转Map,所有键名都是字符串。

    function jsonToStrMap(jsonStr) {
      return objToStrMap(JSON.parse(jsonStr));
    }
    jsonToStrMap('{"yes": true, "no": false}')
    // Map {'yes' => true, 'no' => false}
复制代码

  Json转Map,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。

    function jsonToMap(jsonStr) {
      return new Map(JSON.parse(jsonStr));
    }
    jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
    // Map {true => 7, Object {foo: 3} => ['abc']}
复制代码

 12.5、WeakMap:

  WeakMap与Map的区别有两点。

首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。 WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。

  注意 ,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

    const wm = new WeakMap();
    let key = {};
    let obj = {foo: 1};
    wm.set(key, obj);
    obj = null;//解除引用,相当于切断联系,但是{foo:1}所占内存空间依然存在。
    console.log(wm.get(key))
    // Object {foo: 1}
复制代码

  WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操作(即没有keys()、values()和entries()方法),也没有size属性。WeakMap只有四个方法可用:get()、set()、has()、delete()。

  weakMap的用途:

  1、 就是 DOM 节点作为键名

    let myElement = document.getElementById('logo');
    let myWeakmap = new WeakMap();
    myWeakmap.set(myElement, {timesClicked: 0});
    myElement.addEventListener('click', function() {
      let logoData = myWeakmap.get(myElement);
      logoData.timesClicked++;
    }, false);
复制代码

  2、 部署私有属性

    const _counter = new WeakMap();
    const _action = new WeakMap();
    class Countdown {
      constructor(counter, action) {
        _counter.set(this, counter);
        _action.set(this, action);
      }
      dec() {
        let counter = _counter.get(this);
        if (counter < 1) {
            return;
        }
        _counter.set(this, --counter);
        if (counter === 0) {
          _action.get(this)();
        }
      }
    }
    const c = new Countdown(2, () => console.log('DONE'));
    c.dec()
    c.dec()
    // DONE
复制代码

 13、Proxy

  Proxy 用于修改某些操作的默认行为。 Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

    var proxy = new Proxy(target, handler);
复制代码

  target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。
  注意:要使得Proxy起作用,必须针对Proxy实例进行操作,而不是针对目标对象进行操作。如果handler没有设置任何拦截,那就等同于直接通向原对象。

    var target = {};
    var handler = {};
    var proxy = new Proxy(target, handler);
    proxy.a = 'b';
    target.a // "b"
复制代码

  Proxy 实例也可以作为其他对象的原型对象。

    var proxy = new Proxy({}, {
      get: function(target, property) {
        return 35;
      }
    });
    let obj = Object.create(proxy);
    obj.time // 35
复制代码

 13.1、 Proxy 支持的拦截操作一览,一共 13 种。

  1、 get(target, propKey,receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。

  2、 set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。

  3、has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。但是has拦截对for...in循环不生效。

  4、deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。

  5、ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。

  6、getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。

  7、defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。

  8、preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。

  9、getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。

  10、isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。否则返回值会被自动转为布尔值。

  11、setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。

  12、apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。

  13、construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。回的必须是一个对象,否则会报错。

 13.2、 Proxy.revocable()

  方法返回一个可取消的 Proxy 实例。

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

 13.3、 this问题

  即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。

    const _name = new WeakMap();
    class Person {
      constructor(name) {
        _name.set(this, name);
      }
      get name() {
        return _name.get(this);
      }
    }
    const jane = new Person('Jane');
    console.log(jane.name) // 'Jane'
    const proxy = new Proxy(jane, {});
    console.log(proxy.name) // undefined 
复制代码

  如果handler没有设置任何拦截,那就等同于直接通向原对象。因为通过调用构造函数时this指的是Person,而proxy.name获取name时的this指的是proxy,所以此时取不到值。

  实例:通过拦截使得this绑定原始对象。

    const target = new Date('2015-01-01');
    const handler = {
      get(target, prop) {
        if (prop === 'getDate') {
          return target.getDate.bind(target);
        }
        return Reflect.get(target, prop);
      }
    };
    const proxy = new Proxy(target, handler);
    proxy.getDate() // 1
复制代码

14、Reflect

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

  1. 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。

  2. 修改某些Object方法的返回结果,让其变得更合理。

  3. 让Object操作都变成函数行为。

    // 老写法
    'assign' in Object // true
    // 新写法
    Reflect.has(Object, 'assign') // true
复制代码

  4. Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。

  5. Reflect对象的静态方法有13个,跟Proxy的静态方法时一一对应的。

  1)Reflect.get(target, name, receiver):查找并返回target对象的name属性,如果没有该属性,则返回undefined。

    var myObject = {
      foo: 1,
      bar: 2,
      get baz() {
        return this.foo + this.bar;
      },
    }
    console.log(Reflect.get(myObject, 'foo') )// 1
    console.log(Reflect.get(myObject, 'baz') )// 3
    var myReceiverObject = {
      foo: 4,
      bar: 4,
    };
    console.log((Reflect.get(myObject, 'baz', myReceiverObject)) // 8
复制代码

  如果name属性部署了读取函数(getter),则读取函数的this绑定receiver。

  2)Reflect.set(target, name, value, receiver): Reflect.set方法设置target对象的name属性等于value。如果name属性设置了赋值函数,则赋值函数的this绑定receiver。如果第一个参数不是对象,Reflect.set会报错。

    var myObject = {
      foo: 1,
      set bar(value) {
        return this.foo = value;
      },
    }
    
    myObject.foo // 1
    
    Reflect.set(myObject, 'foo', 2);
    myObject.foo // 2
    
    Reflect.set(myObject, 'bar', 3)
    myObject.foo // 3
复制代码

  3)Reflect.has(obj,name):对应name in obj里面的in运算符。如果第一个参数不是对象,Reflect.has和in运算符都会报错。

    var myObject = {
      foo: 1,
    };
    // 旧写法
    'foo' in myObject // true
    // 新写法
    Reflect.has(myObject, 'foo') // true
复制代码

….

  实例:利用proxy实现观察者模式

    const queuedObservers = new Set();//观察者队列
    const observe = fn => queuedObservers.add(fn);//添加观察者
    const observable = obj => new Proxy(obj, {set});//添加代理
    function set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      queuedObservers.forEach(observer => observer());
      return result;
    }
    //观察目标
    const person = observable({
      name: '张三',
      age: 20
    });
    //观察者
    function print() {
      console.log(`${person.name}, ${person.age}`)
    }
    observe(print);
    person.name = '李四';
复制代码

 15、Promise

  是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

  Promise对象有以下两个特点。

  1) 对象的状态不受外界影响。 Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。

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

  优点: 将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

  不足:

  1、无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  2、如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  3、当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

 15.1、 基本用法

    const promise = new Promise(function(resolve, reject) {
      // ... some code
      if (/* 异步操作成功 */){
        resolve(value);
      } else {
        reject(error);
      }
    });
复制代码

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

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

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

    promise.then(function(value) {
      // success
    }, function(error) {
      // failure
    });
复制代码

  实例:

    function timeout(ms) {
      return new Promise((resolve, reject) => {
        setTimeout(resolve, ms, 'done');
      });
    }
    timeout(100).then((value) => {
      console.log(value);   //done
    });
复制代码

  Promise新建后就会立即执行:then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行。

    let promise = new Promise(function(resolve, reject) {
      console.log('Promise');
      resolve();
    });
    promise.then(function() {
      console.log('resolved.');
    });
    console.log('Hi!');
    顺序:// Promise   // Hi!  // resolved
复制代码

 15.2、 Promise.prototype.then()

  它的作用是为 Promise 实例添加状态改变时的回调函数。 前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。

  then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

    getJSON("/posts.json").then(function(json) {
      return json.post;
    }).then(function(post) {//参数post是上一个then返回的数据-json.post
      // ...
    });
复制代码

  如果then()返回的是一个Promise,那么接下来的代码可以这么写:

    getJSON("/post/1.json").then(
      post => getJSON(post.commentURL)
    ).then(
      comments => console.log("resolved: ", comments),//成功
      err => console.log("rejected: ", err)//失败
    );
复制代码

 15.3、 Promise.prototype.catch

  方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。用于捕获Promise对象和then()方法指定的回调函数中的错误。

  Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。 如果 Promise 状态已经变成resolved,再抛出错误是无效的。

    const promise = new Promise(function(resolve, reject) {
      resolve('ok');
      throw new Error('test');
    });
    promise.then(function(value) { console.log(value) })
      .catch(function(error) { console.log(error) });
    // ok
复制代码

  Promise 在resolve语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。

 15.4、 Promise.prototype.finally()

  用于指定不管 Promise 对象最后状态如何,都会执行的操作。不管前面的 Promise 是fulfilled还是rejected,都会执行回调函数callback。

    Promise.resolve(2).then(() => {}, () => {})
    // resolve 的值是 undefined
    Promise.resolve(2).finally(() => {})
    // resolve 的值是 2
    Promise.reject(3).then(() => {}, () => {})
    // reject 的值是 undefined
    Promise.reject(3).finally(() => {})
    // reject 的值是 3
复制代码

 15.5、 Promise.all()

    const p = Promise.all([p1, p2, p3]);
复制代码

  参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

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

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

 15.6、 Promise.race()

    const p = Promise.race([p1, p2, p3]);
复制代码

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

 15.7、 Promise.resolve()

  将现有对象转为 Promise 对象。

    Promise.resolve('foo')
    // 等价于
    new Promise(resolve => resolve('foo'))
复制代码

  Promise.resolve方法的参数分成四种情况。

  (1)参数是一个 Promise 实例。那么Promise.resolve将不做任何修改、原封不动地返回这个实例。

  (2)参数是一个thenable对象。thenable对象指的是具有then方法的对象,Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。

    let thenable = {
      then: function(resolve, reject) {
        resolve(42);
      }
    };
    let p1 = Promise.resolve(thenable);
    p1.then(function(value) {
      console.log(value);  // 42
    });
复制代码

  (3)参数不是具有then方法的对象,或根本就不是对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。

    const p = Promise.resolve('Hello');
    p.then(function (s){
      console.log(s)
    });
    // Hello
复制代码

  (4)不带有任何参数。直接返回一个resolved状态的 Promise 对象。注意: 立即resolve的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。setTimeout(fn, 0)在下一轮“事件循环”开始时执行,

    setTimeout(function () {
      console.log('three');
    }, 0);
    Promise.resolve().then(function () {
      console.log('two');
    });
    console.log('one');
    // one
    // two
    // three
复制代码

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

 15.8、 Promise.reject()

  返回一个新的 Promise 实例,该实例的状态为rejected。

    const p = Promise.reject('出错了');
    // 等同于
    const p = new Promise((resolve, reject) => reject('出错了'))
    p.then(null, function (s) {
      console.log(s)
    });
    // 出错了
复制代码

  注意: Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。

    const thenable = {
      then(resolve, reject) {
        reject('出错了');
      }
    };
    Promise.reject(thenable)
    .catch(e => {
      console.log(e === thenable) // true
    })
复制代码

 15.9、 Promise.try()

  Promise.try为所有操作提供了统一的处理机制。事实上,Promise.try就是模拟try代码块,就像promise.catch模拟的是catch代码块。

    Promise.try(() => database.users.get({id: userId})).then(...).catch(...)
复制代码

 15.10 实例:加载图片

    const preloadImage = function (path) {
      return new Promise(function (resolve, reject) {
        const image = new Image();
        image.onload  = resolve(src,path);
        image.onerror = reject(src);
        image.src = path;
      });
    };
    preloadImage(“http://www.tj.com/123.jpg”).then(function(img,path){
    //此时的参数path为undefined,因为此时的path是preloadImage(path)中的形参,是局部变量,在调用结束后他的内存就被释放了。
    },function(img){})
复制代码

 16、Iterator和for…of循环

  Iterator是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。 Iterator 的作用有三个:

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

  ES6 规定,默认的 Iterator接口部署在数据结构的Symbol.iterator属性上,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历”的。

    const obj = {
      [Symbol.iterator] : function () {
        return {
          next: function () {
            return {
              value: 1,
              done: true
            };
          }
        };
      }
    };
复制代码

  原生具备 Iterator 接口的数据结构如下。

  1、Array
  2、Map
  3、Set
  4、String
  5、TypedArray
  6、函数的 arguments 对象
  7、NodeList 对象

  下面是两个通过调用Symbol.iterator方法来Iterator接口的例子。

    //数组的遍历 部署 Iterator 接口 let iter = arr[Symbol.iterator]();
    let arr = ['a', 'b', 'c'];
    let iter = arr[Symbol.iterator]();
    console.log(iter.next()) // { value: 'a', done: false }
    console.log (iter.next()) // { value: 'b', done: false }
    console.log (iter.next()) // { value: 'c', done: false }
    console.log (iter.next()) // { value: undefined, done: true }
    //字符串的遍历 部署 Iterator 接口 someString[Symbol.iterator]();
    var someString = "hi";
    typeof someString[Symbol.iterator]
    // "function"
    var iterator = someString[Symbol.iterator]();
    iterator.next()  // { value: "h", done: false }
    iterator.next()  // { value: "i", done: false }
    iterator.next()
复制代码

  对于类似数组的对象(存在数值键名和length属性),部署 Iterator 接口,有一个简便方法,就是Symbol.iterator方法直接引用数组的 Iterator 接口。

    NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
    // 或者
    NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
    [...document.querySelectorAll('div')] // 可以执行了
复制代码

 16.1、 调用iterator接口的场合

(1) 解构赋值

    var set=[1,2,3]
    let [first, ...rest] = set;
复制代码

(2) 扩展运算符

    var str = 'hello';
    console.log([...str]) //  ['h','e','l','l','o']
复制代码

(3) yield*

    let generator = function* () {
      yield 1;
      yield* [2,3,4];
      yield 5;
    };
    
    var iterator = generator();
    
    iterator.next() // { value: 1, done: false }
    iterator.next() // { value: 2, done: false }
    iterator.next() // { value: 3, done: false }
    iterator.next() // { value: 4, done: false }
    iterator.next() // { value: 5, done: false }
    iterator.next() // { value: undefined, done: true }
复制代码

(4) 其他一些场合(for…of、Array.form())

  JavaScript 原有的for...in循环,只能获得对象的键名,不能直接获取键值。ES6 提供for...of循环,允许遍历获得键值。数组的遍历器接口只返回具有数字索引的属性。

    var arr = ['a', 'b', 'c', 'd'];
    arr.foo = 'hello';
    //获取的是键名
    for (let a in arr) {
      console.log(a); // 0 1 2 3
    }
    //获取的是键值
    for (let a of arr) {
      console.log(a); // a b c d
    }
复制代码

  Set 结构遍历时,返回的是一个值,而 Map 结构遍历时,返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。

    let map = new Map().set('a', 1).set('b', 2);
    for (let pair of map) {
      console.log(pair); // ['a', 1]   // ['b', 2]
    }
    for (let [key, value] of map) {
      console.log(key + ' : ' + value); // a : 1   // b : 2
    }
复制代码

  并不是所有类似数组的对象都具有 Iterator 接口,一个简便的解决方法,就是使用Array.from方法将其转为数组。

    let arrayLike = { length: 2, 0: 'a', 1: 'b' };
    // 报错
    for (let x of arrayLike) {
      console.log(x);
    }
    // 正确
    for (let x of Array.from(arrayLike)) {
      console.log(x);
    }
复制代码

  for...in循环有几个缺点。

  1、数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
  2、for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
  3、某些情况下,for...in循环会以任意顺序遍历键名。

  总之,for...in循环主要是为遍历对象而设计的,不适用于遍历数组。

  for...of循环相比上面几种做法,有一些显著的优点:

  1、有着同for...in一样的简洁语法,但是没有for...in那些缺点。
  2、不同于forEach方法,它可以与break、continue和return配合使用。
  3、提供了遍历所有数据结构的统一操作接口。

目录导航

  • 17、Generator
  • 18、async函数
  • 20、Class
  • 21、Module
  • 22、编程风格(性能优化)

 17、Generator

  是 ES6 提供的一种异步编程解决方案。 语法上是一个状态机,封装了多个内部状态 。执行 Generator 函数会返回一个遍历器对象。这一点跟promise很像,promise是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
  Generator 函数是一个普通函数,但是有两个特征。

1、function关键字与函数名之间有一个星号(位置不固定);

2、函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

    function* helloWorldGenerator() {
      yield 'hello';
      yield 'world';
      return 'ending';
    }
    var hw = helloWorldGenerator();
    hw.next()  // { value: 'hello', done: false }
    hw.next()// { value: 'world', done: false }
    hw.next()// { value: 'ending', done: true }
    hw.next() // { value: undefined, done: true }
复制代码

  该函数有三个状态:hello,world 和 return 语句(结束执行)。调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态(执行yield后面的语句,直到遇到yield或者return语句)。

 17.1、 yield表达式

  yield表达式就是暂停标志。并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行。   yield表达式与return语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。

  注意:

1、 yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。
2、 yield表达式如果用在另一个表达式之中,必须放在圆括号里面。

    function* demo() {
      console.log('Hello' + yield); // SyntaxError
      console.log('Hello' + yield 123); // SyntaxError
      console.log('Hello' + (yield)); // OK
      console.log('Hello' + (yield 123)); // OK
    }
复制代码

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

    function* demo() {
      foo(yield 'a', yield 'b'); // OK
      let input = yield; // OK
    }
复制代码

  任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。
  Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

    var myIterable = {};
    myIterable[Symbol.iterator] = function* () {
      yield 1;
      yield 2;
      yield 3;
    };
    [...myIterable] // [1, 2, 3]
复制代码

  Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。

    function* gen(){
      // some code
    }
    var g = gen();
    g[Symbol.iterator]() === g   // true
复制代码

 17.2、 next方法的参数

  yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。 从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。

    function* f() {
      for(var i = 0; true; i++) {
        var reset = yield i;
        if(reset) { i = -1; }
      }
    }
    var g = f();
    console.log(g.next()) // { value: 0, done: false }
    console.log (g.next()) // { value: 1, done: false }
    console.log (.next(true) )// { value: 0, done: false } 执行i=-1,然后i++变成了0
复制代码

  再看下面的一个例子

    function* foo(x) {
      var y = 2 * (yield (x + 1));
      var z = yield (y / 3);
      return (x + y + z);
    }
    var a = foo(5);
    console.log(a.next()) // Object{value:6, done:false}
    console.log(a.next()) // Object{value:NaN, done:false},此时的y等于undefined
    console.log(a.next()) // Object{value:NaN, done:true}
    var b = foo(5);
    console.log(b.next()) // { value:6, done:false }
    console.log(b.next(12)) // { value:8, done:false } 此时的y=2*12 
    console.log(b.next(13)) // { value:42, done:true } 5+24+13
复制代码

  通过next方法的参数,向 Generator 函数内部输入值的例子。

    //例子1
    function* dataConsumer() {
      console.log('Started');
      console.log(`1. ${yield}`);
      console.log(`2. ${yield}`);
      return 'result';
    }
    let genObj = dataConsumer();
    genObj.next();// Started。执行了 console.log('Started');和`1. ${yield}`这两句
    genObj.next('a') // 1. a。执行了 console.log(`1. ${yield}`);和`2. ${yield}`这两句
    console.log(genObj.next('b') )   //2.b    {value: "result", done: true}。执行了console.log(`2. ${yield}`);和return 'result';这两句
复制代码

  上面的console.log(1. ${yield});分两步执行,首先执行yield,等到执行next()时再执行console.log();

    //例子2
    function* dataConsumer() {
      console.log('Started');
      yield 1;
      yield;
      var a=yield;
      console.log("1. "+a);
      var b=yield;
      console.log("2. "+b);
      return 'result';
    }
    let genObj = dataConsumer();
    console.log( genObj.next())
    console.log(genObj.next());
    console.log(genObj.next('a'))
    console.log( genObj.next('b'));
复制代码

  输出结果如下:四次输出结果如红线框中所示

 

 

  结果分析:第一次调用next(),执行到yield 1结束;第二次调用next()执行到yield结束;第三次调用next("a")执行 var a=yield中的yield;第四次调用next("b")方法调用var a=yield语句和var b=yield中的yield;

 

 17.3、 for…of

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

    function* foo() {
      yield 1;
      yield 2;
      yield 3;
      yield 4;
      yield 5;
      return 6;
    }
    for (let v of foo()) {
      console.log(v);
    }
    // 1 2 3 4 5
复制代码

  一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。
  除了for...of循环以外,扩展运算符(...)、解构赋值和Array.from方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数,并且遇到Generator 函数中的return语句结束。

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

 17.4、 Generator.prototype.throw()

  在函数体外抛出错误,然后在 Generator 函数体内捕获。如果是全局throw()命令,只能被函数体外的catch语句捕获。

    var g = function* () {
      try {
        yield;
      } catch (e) {
        console.log('内部捕获', e);
      }
    };
    var i = g();
    i.next();
    try {
      i.throw('a');//被内部捕获,所以下面的代码还能正常运行
      i.throw('b');//被外部捕获
    } catch (e) {
      console.log('外部捕获', e);
    }
    // 内部捕获 a
    // 外部捕获 b
复制代码

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

    var g = function* () {
      while (true) {
        yield;
        console.log('内部捕获', e);
      }
    };
    var i = g();
    i.next();
    try {
      i.throw('a');//被外部捕获,所以下面的代码不运行了
      i.throw('b');
    } catch (e) {
      console.log('外部捕获', e);
    }
    // 外部捕获 a
复制代码

  如果 Generator 函数内部和外部,都没有部署try...catch代码块,那么程序将报错,直接中断执行。 throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。

    function* gen() {
      try {
        yield 1;
      } catch (e) {
        console.log('内部捕获');
      }
    }
    
    var g = gen();
    g.throw(1);
    // Uncaught 1
复制代码

  throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。

    var gen = function* gen(){
      try {
        yield console.log('a');
      } catch (e) {
        // ...
      }
      yield console.log('b');
      yield console.log('c');
    }
    var g = gen();
    g.next() // a
    g.throw() // b
    g.next() // c
复制代码

  另外,throw命令与g.throw方法是无关的,两者互不影响。

    var gen = function* gen(){
      yield console.log('hello');
      yield console.log('world');
    }
    
    var g = gen();
    g.next();
    
    try {
      throw new Error();
    } catch (e) {
      g.next();
    }
    // hello
    // world
复制代码

  一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next方法,将返回一个value属性等于undefined、done属性等于true的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。

    function* g() {
      yield 1;
      console.log('throwing an exception');
      throw new Error('generator broke!');//中断函数的运行
      yield 2;
      yield 3;
    }
    
    function log(generator) {
      var v;
      console.log('starting generator');
      try {
        v = generator.next();
        console.log('第一次运行next方法', v);
      } catch (err) {
        console.log('捕捉错误', v);
      }
      try {
        v = generator.next();
        console.log('第二次运行next方法', v);//因为上面代码调用时报错了,所以不会执行该语句
      } catch (err) {
        console.log('捕捉错误', v);
      }
      try {
        v = generator.next();
        console.log('第三次运行next方法', v);
      } catch (err) {
        console.log('捕捉错误', v);
      }
      console.log('caller done');
    }
    log(g());
    // starting generator
    // 第一次运行next方法 { value: 1, done: false }
    // throwing an exception
    // 捕捉错误 { value: 1, done: false }
    // 第三次运行next方法 { value: undefined, done: true }
    // caller done
复制代码

17.5、 Generator.prototype.return()

  返回给定的值,并且终结遍历 Generator 函数。

    function* gen() {
      yield 1;
      yield 2;
      yield 3;
    }
    var g = gen();
    g.next()        // { value: 1, done: false }
    g.return('foo') // { value: "foo", done: true } //
    g.next()        // { value: undefined, done: true }
复制代码

  如果 Generator 函数内部有try...finally代码块,且正在执行try代码块,那么return方法会推迟到finally代码块执行完再执行。

    function* numbers () {
      yield 1;
      try {
        yield 2;
        yield 3;
      } finally {
        yield 4;
        yield 5;
      }
      yield 6;
    }
    var g = numbers();
    g.next() // { value: 1, done: false }
    g.next() // { value: 2, done: false }
    g.return(7) // { value: 4, done: false }
    g.next() // { value: 5, done: false }
    g.next() // { value: 7, done: true }
    g.next() // { value: undefined, done: true }
复制代码

17.6、 next()、throw()、return()的共同点及区别

  它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。

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

17.7、 yield* 表达式

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

    function* foo() {
        yield 'a';
        yield 'b';
    }
    function* bar() {
      yield 'x';
      yield* foo(); //
      yield 'y';
    }
    // 等同于
    function* bar() {
      yield 'x';
      yield 'a';
      yield 'b';
      yield 'y';
    }
    // 等同于
    function* bar() {
      yield 'x';
      for (let v of foo()) {
        yield v;
      }
      yield 'y';
    }
    for (let v of bar()){
      console.log(v);
    }
    // "x"   // "a"   // "b"   // "y"
    function* inner() {
        yield 'hello!';
    	return "test"
    }
    function* outer1() {
      yield 'open';
      yield inner();
      yield 'close';
    }
    var gen = outer1()
    console.log(gen.next().value) // "open"
    var test=gen.next().value // 返回一个遍历器对象
    console.log(test.next().value) //"hello"
    console.log(test.next().value)// "test"
    console.log(gen.next().value) // "close"
复制代码

  yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for...of循环。

    function* concat(iter1, iter2) {
      yield* iter1;
      yield* iter2;
    }
    // 等同于
    function* concat(iter1, iter2) {
      for (var value of iter1) {
        yield value;
      }
      for (var value of iter2) {
        yield value;
      }
    }
复制代码

  如果yield*后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。

    function* gen(){
      yield* ["a", "b", "c"];
    }
    console.log(gen().next()) // { value:"a", done:false }
复制代码

  实际上,任何数据结构只要有 Iterator 接口,就可以被yield*遍历。 如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据。

    function* foo() {
      yield 2;
      yield 3;
      return "foo";
    }
    
    function* bar() {
      yield 1;
      var v = yield* foo();
      console.log("v: " + v);
      yield 4;
    }
    var it = bar();
    it.next()
    // {value: 1, done: false}
    it.next()
    // {value: 2, done: false}
    it.next()
    // {value: 3, done: false}
    it.next();
    // "v: foo"
    // {value: 4, done: false}
    it.next()
    // {value: undefined, done: true}
    
    function* iterTree(tree) {
      if (Array.isArray(tree)) {
        for(let i=0; i < tree.length; i++) {
          yield* iterTree(tree[i]);
        }
      } else {
        yield tree;
      }
    }
    const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
    for(let x of iterTree(tree)) {
      console.log(x);
    }
    // a  // b   // c   // d   // e
复制代码

 17.8 作为对象的属性的Generator函数

    let obj = {
      * myGeneratorMethod() {
        •••
      }
    };
复制代码

 17.9、 Generator函数的this

  Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype对象上的方法

    function* g() {}
    g.prototype.hello = function () {
      return 'hi!';
    };
    let obj = g();
    obj instanceof g // true
    obj.hello() // 'hi!'
复制代码

  通过生成一个空对象,使用call方法绑定 Generator 函数内部的this。

    function* F() {
      this.a = 1;
      yield this.b = 2;
      yield this.c = 3;
    }
    var obj = {};
    var f = F.call(obj);//调动F()并且把obj作为this传进去,这样给obj添加a、b、c属性
    console.log(f.next());  // Object {value: 2, done: false}
    console.log(f.next());  // Object {value: 3, done: false}
    console.log(f.next());  // Object {value: undefined, done: true}
    console.log(obj.a) // 1
    console.log(obj.b) // 2
    console.log(obj.c) // 3
复制代码

  将obj换成F.prototype。将这两个对象统一起来。再将F改成构造函数,就可以对它执行new命令了。

    function* gen() {
      this.a = 1;
      yield this.b = 2;
      yield this.c = 3;
    }
    function F() {
      return gen.call(gen.prototype);
    }
    var f = new F();
    f.next();  // Object {value: 2, done: false}
    f.next();  // Object {value: 3, done: false}
    f.next();  // Object {value: undefined, done: true}
    f.a // 1
    f.b // 2
    f.c // 3
复制代码

  多个线程(单线程情况下,即多个函数)可以并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态(suspended),线程(或函数)之间可以交换执行权。并行执行、交换执行权的线程(或函数),就称为协程。

 17.10、 应用

  1、 异步操作的同步表达。 通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。

    function makeAjaxCall(url,callBack){
        var xhr;
        if (window.XMLHttpRequest)
        {
            //IE7+, Firefox, Chrome, Opera, Safari 浏览器执行代码
            xhr=new XMLHttpRequest();
        }else{
            // IE6, IE5 浏览器执行代码
            xhr=new ActiveXObject("Microsoft.XMLHTTP");
        }
        xhr.open("GET",makeAjaxCall,true);//确保浏览器兼容性。
        xhr.onreadystatechange=function(){
            if (xhr.readyState==4 && xhr.status==200)
            {   
               if(xhr.status>=200&&xhr.status<300||xhr.status==304){
                  callBack(xhr.responseText;); 
               } 
            }
        }
        xmlhttp.send();
    }

    function* main() {
        var result = yield request("https://juejin.im/editor/posts/5cb209e36fb9a068b52fb360");
        var resp = JSON.parse(result);
        console.log(resp.value);
    }
    function request(url) {
          makeAjaxCall(url, function(response){
            it.next(response);//将response作为上一次yield的返回值
          });
    }
    var it = main();
    it.next();
复制代码

  使用yield表达式可以手动逐行读取文件。

    function* numbers() {
      let file = new FileReader("numbers.txt");
      try {
        while(!file.eof) {
          yield parseInt(file.readLine(), 10);
        }
      } finally {
        file.close();
      }
    }
复制代码

  2、 控制流管理

    step1(function (value1) {
      step2(value1, function(value2) {
        step3(value2, function(value3) {
          step4(value3, function(value4) {
            // Do something with value4
          });
        });
      });
    });
复制代码

  使用Promise

    Promise.resolve(step1)
      .then(step2)
      .then(step3)
      .then(step4)
      .then(function (value4) {
        // Do something with value4
      }, function (error) {
        // Handle any error from step1 through step4
      })
      .done();
复制代码

  使用Generator

    function* longRunningTask(value1) {
      try {
        var value2 = yield step1(value1);
        var value3 = yield step2(value2);
        var value4 = yield step3(value3);
        var value5 = yield step4(value4);
        // Do something with value4
      } catch (e) {
        // Handle any error from step1 through step4
      }
    }
    scheduler(longRunningTask(initialValue));
    function scheduler(task) {
      var taskObj = task.next(task.value);
      // 如果Generator函数未结束,就继续调用
      if (!taskObj.done) {
        task.value = taskObj.value
        scheduler(task);
      }
    }
    function step1(value){
    	return value*2;
    }
    function step2(value){
    	return value*2;
    }
    function step3(value){
    	return value*2;
    }
    function step4(value){
    	return value*2;
    }
复制代码

  注意,上面这种做法,只适合同步操作,即所有的task都必须是同步的,不能有异步操作。   3、 部署iterator接口

    function* iterEntries(obj) {
      let keys = Object.keys(obj);
      for (let i=0; i < keys.length; i++) {
        let key = keys[i];
        yield [key, obj[key]];
      }
    }
    let myObj = { foo: 3, bar: 7 };
    for (let [key, value] of iterEntries(myObj)) {
      console.log(key, value);
    }
    // foo 3
    // bar 7
复制代码

  4、 作为数据结构

    function* doStuff() {
      yield fs.readFile.bind(null, 'hello.txt');
      yield fs.readFile.bind(null, 'world.txt');
      yield fs.readFile.bind(null, 'and-such.txt');
    }
    for (task of doStuff()) {}
      // task是一个函数,可以像回调函数那样使用它
复制代码

17.11、 Generator函数的异步调用(**需要好好理解弄懂**)

  异步编程的方法主要有这几种:

1、回调函数(耦合性太强)
2、事件监听
3、发布/订阅
4、Promise 对象
5、generator
  1. 使用Generator来封装异步函数

    var fetch = require('node-fetch');
    function* gen(){
      var url = 'https://api.github.com/users/github';
      var result = yield fetch(url);
      console.log(result.bio);
    }
    var g = gen();
    var result = g.next();
    result.value.then(function(data){
      return data.json();
    }).then(function(data){
      g.next(data);
    });
复制代码

  首先执行 Generator 函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个 Promise 对象,因此要用then方法调用下一个next方法。   2. Thunk函数
  编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。

    function f(m) {
      return m * 2;
    }
    f(x + 5);
    // 等同于
    var thunk = function () {
      return x + 5;
    };
    function f(thunk) {
      return thunk() * 2;
    }
    f(thunk)
    // 正常版本的readFile(多参数版本)
    fs.readFile(fileName, callback);
    // Thunk版本的readFile(单参数版本)
    var Thunk = function (fileName) {
      return function (callback) {
        return fs.readFile(fileName, callback);
      };
    };
    var readFileThunk = Thunk(fileName);
    readFileThunk(callback);
复制代码

  3. 基于 Promise 对象的自动执行

    var fs = require('fs');
    var readFile = function (fileName){
      return new Promise(function (resolve, reject){
        fs.readFile(fileName, function(error, data){
          if (error) return reject(error);
          resolve(data);
        });
      });
    };
    var gen = function* (){
      var f1 = yield readFile('/etc/fstab');
      var f2 = yield readFile('/etc/shells');
      console.log(f1.toString());
      console.log(f2.toString());
    };
复制代码

  然后,手动执行上面的 Generator 函数。

    var g = gen();
    g.next().value.then(function(data){
      g.next(data).value.then(function(data){
        g.next(data);
      });
    });
复制代码

  自动执行器写法:

    function run(gen){
      var g = gen();
      function next(data){
        var result = g.next(data);
        if (result.done) return result.value;
        result.value.then(function(data){
          next(data);
        });
      }
      next();
    }
    run(gen);
复制代码

18、async函数

  async函数是Generator 函数的语法糖。async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。 async函数对 Generator 函数的改进,体现在以下四点。

  1. 内置执行器 调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。也就是说,async函数的执行,与普通函数一模一样,只要一行。
  2. 更好的语义 async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
  3. 更广的适用性。 await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
  4. 返回值是 Promise。 async函数的返回值是 Promise 对象,进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

 18.1、 Async的语法

1、async函数返回一个 Promise 对象。 async函数内部return语句返回的值,会成为then方法回调函数的参数。async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。

    async function f() {
      return 'hello world';
    }
    f().then(v => console.log(v))
    // "hello world"
    async function f() {
      throw new Error('出错了');
    }
    f().then(
      v => console.log(v),
      e => console.log(e)
    )
复制代码

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

 18.2、 Await命令

  正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

    async function f() {
      // 等同于
      // return 123;
      return await 123;
    }
    f().then(v => console.log(v))
    // 123
复制代码

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

 18.3、 错误处理

  如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject。

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

 18.4、 使用注意点

  1) await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。

    async function myFunction() {
      try {
        await somethingThatReturnsAPromise();
      } catch (err) {
        console.log(err);
      }
    }
    // 另一种写法
    async function myFunction() {
      await somethingThatReturnsAPromise()
      .catch(function (err) {
        console.log(err);
      });
    }
复制代码

  2) 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

    // 写法一
    let [foo, bar] = await Promise.all([getFoo(), getBar()]);
    // 写法二
    let fooPromise = getFoo();
    let barPromise = getBar();
    let foo = await fooPromise;//直接返回
    let bar = await barPromise;
复制代码

  3) await命令只能用在async函数之中,如果用在普通函数,就会报错。

    async function dbFuc(db) {
      let docs = [{}, {}, {}];
      // 报错
      docs.forEach(function (doc) {
        await db.post(doc);
      });
    }
复制代码

  如果确实希望多个请求并发执行,可以使用Promise.all方法。

    async function dbFuc(db) {
      let docs = [{}, {}, {}];
      let promises = docs.map((doc) => db.post(doc));
      let results = await Promise.all(promises);
      console.log(results);
    }
复制代码

  4) async 函数可以保留运行堆栈。

    const a = () => {
      b().then(() => c());
    };
复制代码

  当b()运行的时候,函数a()不会中断,而是继续执行。等到b()运行结束,可能a()早就运行结束了,b()所在的上下文环境已经消失了。如果b()或c()报错,错误堆栈将不包括a()。

    const a = async () => {
      await b();
      c();
    };
复制代码

  b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()或c()报错,错误堆栈将包括a()。

 18.5、 实例:按顺序完成异步操作

    async function logInOrder(urls) {
      for (const url of urls) {
        const response = await fetch(url);
        console.log(await response.text());
      }
    }
复制代码

  上面代码的问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。

    async function logInOrder(urls) {
      // 并发读取远程URL
      const textPromises = urls.map(async url => {
        const response = await fetch(url);
        return response.text();
      });
      // 按次序输出
      for (const textPromise of textPromises) {
        console.log(await textPromise);
      }
    }
复制代码

  虽然map方法的参数是async函数,但它是并发执行的,因为只有async函数内部是继发执行,外部不受影响。

 18.6、 异步遍历器

  异步遍历器的最大的语法特点,就是调用遍历器的next方法,返回的是一个 Promise 对象。

    asyncIterator
      .next()
      .then(
        ({ value, done }) => /* ... */
      );
复制代码

 18.7、 异步 Generator 函数

  语法上,异步 Generator 函数就是async函数与 Generator 函数的结合。

    async function* gen() {
      yield 'hello';
    }
    const genObj = gen();
    genObj.next().then(x => console.log(x));
    // { value: 'hello', done: false }
复制代码

 异步 Generator 函数内部,能够同时使用await和yield命令。可以这样理解,await命令用于将外部操作产生的值输入函数内部,yield命令用于将函数内部的值输出。

19、Class

 19.1、class的基本语法

 新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法 而已。ES6 的类,完全可以看作构造函数的另一种写法。 事实上,类的所有方法都定义在类的prototype属性上面。

1、ES6 的类,完全可以看作构造函数的另一种写法。类本身就指向构造函数。

    Point === Point.prototype.constructor // true
复制代码

2、类的所有方法都定义在类的prototype属性上面。

3、在类的实例上面调用方法,其实就是调用原型上的方法。

    p1.constructor === Point.prototype.constructor // true

    function Point(x, y) {
      this.x = x;
      this.y = y;
    }
    Point.prototype.toString = function () {
      return '(' + this.x + ', ' + this.y + ')';
    };
    var p = new Point(1, 2);
    //改成类的写法
    class Point {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
      toString() {
        return '(' + this.x + ', ' + this.y + ')';
      }
    }
    typeof Point // "function"
    Point === Point.prototype.constructor // true 类本身就指向构造函数。
    var p1=new Point(2,4);
    p1.constructor === Point.prototype.constructor // true
    
    Point.prototype.constructor === Point // true
    Object.keys(Point.prototype)// []
复制代码

  上面代码中,toString方法是Point类内部定义的方法,它是不可枚举的。这一点与 ES5 的行为不一致。

19.1、 constructor方法

  constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。类必须使用new调用,否则会报错。

    class Foo {
      constructor() {
        return Object.create(null);
      }
    }
    new Foo() instanceof Foo
    // false
复制代码

 19.2、 类的实例

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

    //定义类
    class Point {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
      toString() {
        return '(' + this.x + ', ' + this.y + ')';
      }
    }
    var point = new Point(2, 3);
    point.toString() // (2, 3)
    point.hasOwnProperty('x') // true
    point.hasOwnProperty('y') // true
    point.hasOwnProperty('toString') // false
    point.__proto__.hasOwnProperty('toString') // true
    //toString是原型上的方法,构造方法中的才是实例属性
复制代码

  与 ES5 一样,类的所有实例共享一个原型对象。

    var p1 = new Point(2,3);
    var p2 = new Point(3,2);
    p1.__proto__ === p2.__proto__
    //true
复制代码

 19.3、取值函数(getter)和存值函数(setter)

  在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

 19.4、 属性表达式

    let methodName = 'getArea';
    class Square {
      constructor(length) {
        // ...
      }
      [methodName]() {
        // ...
      }
    }
复制代码

 19.5、 Class表达式

    const MyClass = class Me {
      getClassName() {
        return Me.name;
      }
    };
复制代码

  这个类的名字是Me,但是Me只在 Class 的内部可用,指代当前类。在 Class 外部,这个类只能用MyClass引用。

    let inst = new MyClass();
    inst.getClassName() // Me
    Me.name // ReferenceError: Me is not defined
复制代码

  如果类的内部没用到的话,可以省略Me。

    const MyClass = class { /* ... */ };
复制代码

  采用 Class 表达式,可以写出立即执行的 Class。

    let person = new class {
      constructor(name) {
        this.name = name;
      }
      sayName() {
        console.log(this.name);
      }
    }('张三');
    person.sayName(); // "张三" 
复制代码

  class的注意事项:
  1、严格模式。类和模块的内部,默认就是严格模式。
  2、不存在提升。类不存在变量提升。
  3、name属性总是返回紧跟在class关键字后面的类名。
  4、Generator 方法。Symbol.iterator方法返回一个Foo类的默认遍历器,for...of循环会自动调用这个遍历器。

    class Foo {
      constructor(...args) {
        this.args = args;
      }
      * [Symbol.iterator]() {
        for (let arg of this.args) {
          yield arg;
        }
      }
    }
    for (let x of new Foo('hello', 'world')) {
      console.log(x); // hello,world
    }
复制代码

  5、 This的指向。 类的方法内部如果含有this,它默认指向类的实例。 但是,必须非常小心,一旦单独使用该方法,很可能报错。this会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是undefined)

    class Logger {
      printName(name = 'there') {
        this.print(`Hello ${name}`);
      }
      print(text) {
        console.log(text);
      }
    }
    const logger = new Logger();
    const { printName } = logger;
    printName(); // TypeError: Cannot read property 'print' of undefined 本来是实例的方法,但是此时printName()不是实例调用的,所以this指向不明,默认为undefined
复制代码

  一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。

    class Logger {
      constructor() {
        this.printName = this.printName.bind(this);
      }
      // ...
    }
复制代码

 19.6、 静态方法

  如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。 如果静态方法包含this关键字,这个this指的是类,而不是实例。静态方法可以与非静态方法重名。

    class Foo {
      static bar() {
        this.baz();
      }
      static baz() {
        console.log('hello');
      }
      baz() {
        console.log('world');
      }
    }
    Foo.bar() // hello
复制代码

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

    class Foo {
      static classMethod() {
        return 'hello';
      }
    }
    class Bar extends Foo {
    }
    Bar.classMethod() // 'hello'
复制代码

  静态方法也是可以从super对象上调用的。

    class Foo {
      static classMethod() {
        return 'hello';
      }
    }
    class Bar extends Foo {
      static classMethod() {
        return super.classMethod() + ', too';
      }
    }
    Bar.classMethod() // "hello, too"
复制代码

 19.7、 实力属性的新写法

  这个属性也可以定义在类的最顶层,其他都不变。这种新写法的好处是,所有实例对象自身的属性都定义在类的头部,看上去比较整齐,一眼就能看出这个类有哪些实例属性。

    class IncreasingCounter {
      _count = 0;
      get value() {
        console.log('Getting the current value!');
        return this._count;
      }
      increment() {
        this._count++;
      }
    }
复制代码

 19.8、 静态属性

    class MyClass {
      static myStaticProp = 42;
      constructor() {
        console.log(MyClass.myStaticProp); // 42
      }
    }
复制代码

 19.9、 私有方法和私有属性

  1、 将私有方法移出模块,因为模块内部的所有方法都是对外可见的。

    class Widget {
      foo (baz) {
        bar.call(this, baz);
      }
      // ...
    }
    function bar(baz) {
      return this.snaf = baz;
    }
复制代码

  2、利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。一般情况下无法获取到它们,因此达到了私有方法和私有属性的效果。但是也不是绝对不行,Reflect.ownKeys()依然可以拿到它们。

    const bar = Symbol('bar');
    const snaf = Symbol('snaf');
    export default class myClass{
      // 公有方法
      foo(baz) {
        this[bar](baz);
      }
      // 私有方法
      [bar](baz) {
        return this[snaf] = baz;
      }
      // ...
    };
复制代码

 19.10、new.target()

  ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数 。如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。 Class 内部调用new.target,返回当前Class。在函数外部,使用new.target会报错。

    function Person(name) {
        if (new.target !== undefined) {
        this.name = name;
    } else {
            throw new Error('必须使用 new 命令生成实例');
        }
    }
    // 另一种写法
    function Person(name) {
        if (new.target === Person) {
            this.name = name;
        } else {
            throw new Error('必须使用 new 命令生成实例');
        }
    }
    var person = new Person('张三'); // 正确
    var notAPerson = Person.call(person, '张三');  // 报错 
复制代码

  子类继承父类时,new.target会返回子类。主要是看new后面的类是哪个

    class Rectangle {
    constructor(length, width) {
        console.log(new.target === Rectangle);
        // ...
      }
    }
    class Square extends Rectangle {
      constructor(length,width) {
        super(length, width);
      }
    }
    var c=new Rectangle(1,2);
    var obj = new Square(3); // 输出 false
复制代码

 19.11、 类的继承

  Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

    class ColorPoint extends Point {
      constructor(x, y, color) {
        super(x, y); // 调用父类的constructor(x, y)
        this.color = color;
      }
      toString() {
        return this.color + ' ' + super.toString(); // 调用父类的toString()
      }
    }
复制代码

1、 super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。
2、 子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。 或者是不写constructor(){},写了必须写super()。

    class Point { /* ... */ }
    class ColorPoint extends Point {
          constructor() {
          }
    }
    let cp = new ColorPoint(); // ReferenceError
    ————————————————————————————————————————————————————————————
    class ColorPoint extends Point {
    }
    // 等同于
    class ColorPoint extends Point {
      constructor(...args) {
        super(...args);
      }
    }
复制代码

3、 ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
4、 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。 5 子类实例对象cp同时是ColorPoint和Point(父类)两个类的实例,这与 ES5 的行为完全一致。
6 父类的静态方法,也会被子类继承。

 19.12、 Object.getPrototypeOf()

  Object.getPrototypeOf方法可以用来从子类上获取父类。可以使用这个方法判断,一个类是否继承了另一个类。

    Object.getPrototypeOf(ColorPoint) === Point// true
复制代码

 19.13、 Super关键字

1、 super作为函数调用时,代表父类的构造函数 。ES6 要求,子类的构造函数必须执行一次super函数。 super虽然代表了父类A的构造函数,但是返回的是子类B的实例。 作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。

    class A {
      constructor() {
        console.log(new.target.name);//new.targe构造函数
      }
    }
    class B extends A {
      constructor() {
        super();
      }
    }
    new A() // A
    new B() // B
复制代码

2、 super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。所以定义在父类实例上的方法或属性,是无法通过super调用的。

    lass A {
      p() {
        return 2;
      }
    }
    class B extends A {
      constructor() {
        super();
        console.log(super.p()); // 2
      }
    }
    let b = new B();
复制代码

  在子类普通方法中通过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();
      }
    }
    let b = new B();
    b.m() // 2
复制代码

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

    class A {
      constructor() {
        this.x = 1;
      }
    }
    class B extends A {
      constructor() {
        super();
        this.x = 2;
        super.x = 3;//此时的super相当于this
        console.log(super.x); // undefined
        console.log(this.x); // 3
      }
    }
    let b = new B();
复制代码

  而当读取super.x的时候,读的是A.prototype.x,所以返回undefined。

    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.m() // 3
复制代码

  静态方法B.m里面,super.print指向父类的静态方法。这个方法里面的this指向的是B,而不是B的实例。

 19.14、 类的 prototype 属性和__proto__属性

   ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。

instance.__proto__===A.prototype//instance是A的实例
复制代码

   Class作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

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

    class A {
    }
    class B extends A {
    }
    console.log(B.__proto__ === A) // true,
    console.log(B.prototype.__proto__ === A.prototype )// true,
    // 等同于
    Object.create(A.prototype);
复制代码

   作为一个对象,子类(B)的原型(__proto__属性)是父类(A);作为一个构造函数,子类(B)的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例。

 19.15、实例的 __proto__ 属性

  子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。(p2是子类,p1是父类)

    p2.__proto__.__proto__ === p1.__proto__ // true
    解析:
    p2.__proto__===p2的类.prototype;
    p2的类.prototype.__proto__===p2的类的父类的.prototype
    p1.__proto__===p2的类的父类的.prototype。
复制代码

  因此,通过子类实例的__proto__.__proto__属性,可以修改父类实例的行为。

    p2.__proto__.__proto__.printName = function () {
      console.log('Ha');
    };
    p1.printName() // "Ha"
复制代码

20、Module

 20、1 严格模式

  ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";。 严格模式主要有以下限制。

  1. 变量必须声明后再使用。
  2. 函数的参数不能有同名属性,否则报错。
  3. 不能使用with语句。
  4. 不能对只读属性赋值,否则报错。
  5. 不能使用前缀 0 表示八进制数,否则报错。
  6. 不能删除不可删除的属性,否则报错。
  7. 不能删除变量delete prop,会报错,只能删除属性delete global[prop]。
  8. eval不会在它的外层作用域引入变量(没懂)。
  9. eval和arguments不能被重新赋值。
  10. arguments不会自动反映函数参数的变化。
  11. 不能使用arguments.callee。(指向用于arguments对象的函数)
  12. 不能使用arguments.caller,值为undefined。(caller属性保存着调动当前函数的函数的引用)
  13. 禁止this指向全局对象。
  14. 不能使用fn.caller和fn.arguments获取函数调用的堆栈。
  15. 增加了保留字(比如protected、static和interface)。

 20、2 export的用法

  export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。   export写法种类:

1、使用大括号指定所要输出的一组变量。export {firstName, lastName, year}; 2、直接使用export关键字输出该变量。export var year = 1958;

    export var firstName = 'Michael';
    export var lastName = 'Jackson';
    export var year = 1958;
    等同于下面这中写法
    var firstName = 'Michael';
    var lastName = 'Jackson';
    var year = 1958;
    export {firstName, lastName, year};
复制代码

  通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。

    function v1() { ... }
    function v2() { ... }
    export {
      v1 as streamV1,
      v2 as streamV2,
      v2 as streamLatestVersion
    };
复制代码

  注意1:export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

    // 报错
    export 1;
    // 报错
    var m = 1;
    export m;
    // 报错
    function f() {}
    export f;
复制代码

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

    export var foo = 'bar';
    setTimeout(() => foo = 'baz', 500);
复制代码

  注意3:export命令可以出现在模块的任何位置,只要处于模块顶层就可以。

    function foo() {
      export default 'bar' // SyntaxError
    }
    foo()
复制代码

 20、3 import的用法

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

    import {a} from './xxx.js'
    a = {}; // Syntax Error : 'a' is read-only;
    但是,如果a是一个对象,改写a的属性是允许的。
    import {a} from './xxx.js'
    a.foo = 'hello'; // 合法操作
复制代码

  import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。

    import {myMethod} from 'util';
    //util是模块文件名,由于不带有路径,必须通过配置,告诉引擎怎么取到这个模块。
复制代码

  注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。import是静态执行,所以不能使用表达式和变量 ,这些只有在运行时才能得到结果的语法结构。

    // 报错
    import { 'f' + 'oo' } from 'my_module';
    // 报错
    let module = 'my_module';
    import { foo } from module;
    // 报错
    if (x === 1) {
      import { foo } from 'module1';
    } else {
      import { foo } from 'module2';
    }
复制代码

  逐一指定要加载的方法:

    import { area, circumference } from './circle';
    console.log('圆面积:' + area(4));
    console.log('圆周长:' + circumference(14));
复制代码

 20、4 模块的整体加载 import *

  整体加载的写法: import * from "module"

    import * as circle from './circle';
    console.log('圆面积:' + circle.area(4));
    console.log('圆周长:' + circle.circumference(14));
复制代码

 20、5 export default

  用到export default命令,为模块指定默认输出。

    // export-default.js
    export default function () {
      console.log('foo');
    }
    // import-default.js
    import customName from './export-default'; 
    //因为是默认输出的,所以这时import命令后面,不使用大括号。并且可以随意取名。
    customName(); // 'foo'
复制代码

  1、下面代码中,foo函数的函数名foo,在模块外部是无效的。加载的时候,视同匿名函数加载。

    function foo() {
      console.log('foo');
    }
    export default foo;
复制代码

  2、一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。 本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。但是建议import时还是用default后面的名字。

    // modules.js
    function add(x, y) {
      return x * y;
    }
    export {add as default};
    // 等同于
    // export default add;
    // app.js
    import { default as foo } from 'modules';
    // 等同于
    // import foo from 'modules';
复制代码

  3、因为export default命令的本质是将后面的值,赋给default变量,所以可以直接将一个值写在export default之后。

    // 正确
    export default 42;
    // 报错
    export 42;
复制代码

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

    import _, { each, forEach } from 'lodash';
复制代码

  5、 export default也可以用来输出类。

    // MyClass.js
    export default class { ... }
    // main.js
    import MyClass from 'MyClass';
    let o = new MyClass();
复制代码

 20、5 export和import的复合写法

    export { foo, bar } from 'my_module';
    // 可以简单理解为
    import { foo, bar } from 'my_module';
    export { foo, bar };
复制代码

  写成一行以后,foo和bar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo和bar。 默认接口的写法如下。

    export { default } from 'foo';
复制代码

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

    export { es6 as default } from './someModule';
    // 等同于
    import { es6 } from './someModule';
    export default es6;
复制代码

  同样地,默认接口也可以改名为具名接口。

    export { default as es6 } from './someModule';
复制代码

 20、6 模块的继承

    // circleplus.js
    export * from 'circle';
    export var e = 2.71828182846;
    export default function(x) {
      return Math.exp(x);
    }
复制代码

  上面代码中的export*,表示再输出circle模块的所有属性和方法。*注意,export 命令会忽略circle模块的default方法。

    // main.js
    import * as math from 'circleplus';//整体加载的写法
    import exp from 'circleplus';
    console.log(exp(math.e));
    import exp表示,将circleplus模块的默认方法加载为exp方法。
复制代码

 20、7 Import()

  可以实现动态加载。运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。import()返回一个 Promise 对象。

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

    import('./myModule.js')
    .then(({export1, export2}) => {
      // ...•
    });
复制代码

  上面代码中,export1和export2都是myModule.js的输出接口,可以解构获得。 如果模块有default输出接口,可以用参数直接获得。

    import('./myModule.js')
    .then(myModule => {
      console.log(myModule.default);
    });
复制代码

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

    import('./myModule.js')
    .then(({default: theDefault}) => {
      console.log(theDefault);
    });
复制代码

 20、8 module的加载实现

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

    
    
    
复制代码

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

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

    const isNotModuleScript = this !== undefined;
复制代码

 20、9 ES6 模块与 CommonJS 模块

   ES6 模块与 CommonJS 模块完全不同。 它们有两个重大差异。

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

  第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
  第一个差异是因为CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

    // lib.js
    var counter = 3;
    function incCounter() {
      counter++;
    }
    module.exports = {
      counter: counter,
      incCounter: incCounter,
    };
    // main.js
    var mod = require('./lib');
    console.log(mod.counter);  // 3
    mod.incCounter();
    console.log(mod.counter); // 3
复制代码

  这是因为mod.counter是一个原始类型的值 ,会被缓存。除非写成一个函数,才能得到内部变动后的值。

    // lib.js
    var counter = 3;
    function incCounter() {
      counter++;
    }
    module.exports = {
      get counter() {
        return counter
      },  
      incCounter: incCounter,
    };
    // main.js
    var mod = require('./lib');
    console.log(mod.counter);  // 3
    mod.incCounter();
    console.log(mod.counter); // 4
复制代码

  可以对obj添加属性,但是重新赋值就会报错。 因为变量obj指向的地址是只读的,不能重新赋值,这就好比main.js创造了一个名为obj的const变量。

    // lib.js
    export let obj = {};
    // main.js
    import { obj } from './lib';
    obj.prop = 123; // OK
    obj = {}; // TypeError
复制代码

  commonJS和ES6内部变量的区别:

  1、ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块。
  2、以下这些顶层变量在 ES6 模块之中都是不存在的。

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

 20.10、 ES6加载CommonJS模块(整体输入)

  Node 会自动将module.exports属性,当作模块的默认输出,即等同于export default xxx。

    // a.js
    module.exports = {
      foo: 'hello',
      bar: 'world'
    };
    // 等同于
    export default {
      foo: 'hello',
      bar: 'world'
    };
复制代码

  由于 ES6 模块是编译时确定输出接口,CommonJS 模块是运行时确定输出接口,所以采用import命令加载 CommonJS 模块时,不允许采用下面的写法。

    // 不正确
    import { readFile } from 'fs';
复制代码

  因为fs是 CommonJS格式,只有在运行时才能确定readFile接口,而import命令要求编译时就确定这个接口。解决方法就是改为整体输入。

    // 正确的写法一
    import * as express from 'express';
    const app = express.default();
    // 正确的写法二
    import express from 'express';
    const app = express();
复制代码

 20.11、 CommonJS加载ES6模块(import()函数)

  CommonJS 模块加载 ES6 模块,不能使用require命令,而要使用import()函数。ES6 模块的所有输出接口,会成为输入对象的属性。

 20.12、 CommonJS 模块的加载原理。

  require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。

    {
      id: '...',
      exports: { ... },
      loaded: true,
      ...
    }
复制代码

  该对象的id属性是模块名,exports属性是模块输出的各个接口,loaded属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。以后需要用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。

 20.13、 CommonJS的循环加载

  一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

    //a.js
    exports.done = false;
    var b = require('./b.js');
    console.log('在 a.js 之中,b.done = %j', b.done);
    exports.done = true;
    console.log('a.js 执行完毕');
    //b.js
    exports.done = false;
    var a = require('./a.js');
    console.log('在 b.js 之中,a.done = %j', a.done);
    exports.done = true;
    console.log('b.js 执行完毕');
    //main.js
    var a = require('./a.js');
    var b = require('./b.js');
    console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
    $ node main.js
复制代码

执行结果如下:

 

ES6常用知识点总结_第1张图片

 

 

  在main.js中的详细执行过程如下:

  a.js脚本先输出一个done变量,然后加载另一个脚本文件b.js。注意,此时a.js代码就停在这里,等待b.js执行完毕,再往下执行。 b.js执行到第二行,就会去加载a.js,这时,就发生了“循环加载”。系统会去a.js模块对应对象的exports属性取值,可是因为a.js还没有执行完,从exports属性只能取回已经执行的部分,而不是最后的值。(a.js已经执行的部分,只有一行。)然后,b.js接着往下执行,等到全部执行完毕,再把执行权交还给a.js。于是,a.js接着往下执行,直到执行完毕。

 20.14、 ES6模块的循环加载

  ES6 模块是动态引用,如果使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用

    // a.mjs
    import {bar} from './b';
    console.log('a.mjs');
    console.log(bar);
    export let foo = 'foo';
    //function foo() { return 'foo' }
    //export {foo};
    // b.mjs
    import {foo} from './a';
    console.log('b.mjs');
    console.log(foo);
    export let bar = 'bar';
    //function bar() { return 'bar' }
    //export {bar};

    $ node --experimental-modules a.mjs
    b.mjs
    ReferenceError: foo is not defined
复制代码

  上述代码的详细执行过程如下:

  首先,执行a.mjs以后,引擎发现它加载了b.mjs,因此会优先执行b.mjs,然后再执行a.mjs。接着,执行b.mjs的时候,已知它从a.mjs输入了foo接口,这时不会去执行a.mjs,而是认为这个接口已经存在了,继续往下执行。执行到第三行console.log(foo)的时候,才发现这个接口根本没定义,因此报错。这可以通过将foo写成函数来解决这个问题。 这是因为函数具有提升作用(提升到顶部),在执行import {bar} from './b'时,函数foo就已经有定义了,所以b.mjs加载的时候不会报错。这也意味着,如果把函数foo改写成函数表达式,也会报错。

21、编程风格(性能优化)

  1. 建议不再使用var命令,而是使用let命令取代。
  2. 在let和const之间,建议优先使用const,尤其是在全局环境,不应该设置变量,只应设置常量。 原因:一个是const可以提醒阅读程序的人,这个变量不应该改变;另一个是const比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算;最后一个原因是 JavaScript 编译器会对const进行优化,所以多使用const,有利于提高程序的运行效率,也就是说let和const的本质区别,其实是编译器内部的处理不同。
  3. 静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。
    // bad
    const a = "foobar";
    const b = 'foo' + a + 'bar';
    // good
    const a = 'foobar';
    const b = `foo${a}bar`;
复制代码
  1. 解构赋值 使用数组成员对变量赋值时,优先使用解构赋值。
    const arr = [1, 2, 3, 4];
    // bad
    const first = arr[0];
    const second = arr[1];
    // good
    const [first, second] = arr;
复制代码

  函数的参数如果是对象的成员,优先使用解构赋值。

    // bad
    function getFullName(user) {
      const firstName = user.firstName;
      const lastName = user.lastName;
    }
    // good
    function getFullName(obj) {
      const { firstName, lastName } = obj;
    }
    // best
    function getFullName({ firstName, lastName }) {
    }
复制代码
  1. 对象

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

    // bad
    const a = { k1: v1, k2: v2, };
    const b = {
      k1: v1,
      k2: v2
    };
    // good
    const a = { k1: v1, k2: v2 };
    const b = {
      k1: v1,
      k2: v2,
    };
复制代码

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

    // bad
    const a = {};
    a.x = 3;
    // if reshape unavoidable
    const a = {};
    Object.assign(a, { x: 3 });
    // good
    const a = { x: null };
    a.x = 3;
复制代码
  1. 使用扩展运算符(...)拷贝数组。使用 Array.from 方法,将类似数组的对象转为数组。
    const itemsCopy = [...items];
    const foo = document.querySelectorAll('.foo');
    const nodes = Array.from(foo);
复制代码
  1. 简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。
  2. 不要在函数体内使用 arguments 变量,使用 rest 运算符(...)代替。
    // bad
    function concatenateAll() {
      const args = Array.prototype.slice.call(arguments);
      return args.join('');
    }
    // good
    function concatenateAll(...args) {
      return args.join('');
    }
复制代码
  1. 使用默认值语法设置函数参数的默认值。
    // bad
    function handleThings(opts) {
      opts = opts || {};
    }
    // good
    function handleThings(opts = {}) {
      // ...
    }
复制代码
  1. 注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要key: value的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。
  2. 总是用 Class,取代需要 prototype 的操作。因为 Class 的写法更简洁,更易于理解。
    // bad
    function Queue(contents = []) {
      this._queue = [...contents];
    }
    Queue.prototype.pop = function() {
      const value = this._queue[0];
      this._queue.splice(0, 1);
      return value;
    }
    // good
    class Queue {
      constructor(contents = []) {
        this._queue = [...contents];
      }
      pop() {
        const value = this._queue[0];
        this._queue.splice(0, 1);
        return value;
      }
    }
复制代码
  1. 使用extends实现继承,因为这样更简单,不会有破坏instanceof运算的危险。
  2. 如果模块只有一个输出值,就使用export default,如果模块有多个输出值,就不使用export default。export default与普通的export不要同时使用。
  3. 不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default)。
    // bad
    import * as myObject from './importModule';
    // good
    import myObject from './importModule';
复制代码
  1. 如果模块默认输出一个函数,函数名的首字母应该小写。如果模块默认输出一个对象,对象名的首字母应该大写。
    function makeStyleGuide() {
    }
    export default makeStyleGuide;//函数
    const StyleGuide = {
      es6: {
      }
    };
    export default StyleGuide;//对象


作者:慕斯不想说话
链接:https://juejin.im/post/5cb209e36fb9a068b52fb360
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

你可能感兴趣的:(【ES678点滴知识,】)