【React系列】ES6学习笔记(一)let与const、解构赋值、函数参数默认值rest参数箭头函数、数组和对象的扩展、Set和Map等
作者:mmseoamin日期:2024-02-05

本文参考自电子书《ECMAScript 6 入门》:https://es6.ruanyifeng.com/

let 和 const 命令

1. let 命令

  • 尽量使用 let 声明变量,而不是 var。
  • let 声明的变量是块级作用域, var 声明的变量是全局作用域。
  • 使用 let 变量必须先声明再使用,否则报错,不存在变量提升。相对的 var 声明的变量如果提前使用,不会报错,只会提示为undefined。
  • let不允许在相同作用域内,重复声明同一个变量。
  • ES6的块级作用域允许使用多层 {} 无限嵌套,只有位于同一层的 let 变量相互影响,跨层之间的 let 变量即便是同名也无影响,只要在同一层内使用自己层的变量即可。
  • ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。(if语句后面最好必须跟上大括号)

    2. const 命令

    • const声明一个只读的常量。一旦声明,常量的值就不能改变。
    • 必须在声明时指定初始值。
    • const的作用域与let命令相同:只在声明所在的块级作用域内有效。同样不存在变量提升,必须先声明后使用,不可重复声明。

      ES6 声明变量的六种方法

      ES5 只有两种声明变量的方法:var命令和function命令。ES6 除了添加let和const命令,后面章节还会提到,另外两种声明变量的方法:import命令和class命令。所以,ES6 一共有 6 种声明变量的方法。

      变量的解构赋值

      1. 数组的解构赋值

      let [a, b, c] = [1, 2, 3];
      

      上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。

      本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。

      let [foo, [[bar], baz]] = [1, [[2], 3]];
      foo // 1
      bar // 2
      baz // 3
      let [ , , third] = ["foo", "bar", "baz"];
      third // "baz"
      let [x, , y] = [1, 2, 3];
      x // 1
      y // 3
      let [head, ...tail] = [1, 2, 3, 4];
      head // 1
      tail // [2, 3, 4]
      let [x, y, ...z] = ['a'];
      x // "a"
      y // undefined
      z // []
      

      如果解构不成功,变量的值就等于undefined。

      let [foo] = [];
      let [bar, foo] = [1];
      

      以上两种情况都属于解构不成功,foo的值都会等于undefined。

      另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。

      let [x, y] = [1, 2, 3];
      x // 1
      y // 2
      let [a, [b], d] = [1, [2, 3], 4];
      a // 1
      b // 2
      d // 4
      

      上面两个例子,都属于不完全解构,但是可以成功。

      如果等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),那么将会报错。

      // 报错
      let [foo] = 1;
      let [foo] = false;
      let [foo] = NaN;
      let [foo] = undefined;
      let [foo] = null;
      let [foo] = {};
      

      上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。

      对于 Set 结构,也可以使用数组的解构赋值。

      let [x, y, z] = new Set(['a', 'b', 'c']);
      x // "a"
      

      默认值

      解构赋值允许指定默认值。

      let [foo = true] = [];
      foo // true
      let [x, y = 'b'] = ['a']; // x='a', y='b'
      let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
      

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

      let [x = 1] = [undefined];
      x // 1
      let [x = 1] = [null];
      x // null
      

      上面代码中,如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined。

      如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

      function f() {
        console.log('aaa');
      }
      let [x = f()] = [1];
      

      上面代码中,因为x能取到值,所以函数f根本不会执行。上面的代码其实等价于下面的代码。

      let x;
      if ([1][0] === undefined) {
        x = f();
      } else {
        x = [1][0];
      }
      

      默认值可以引用解构赋值的其他变量,但该变量必须已经声明。

      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] = [];     // ReferenceError: y is not defined
      

      上面最后一个表达式之所以会报错,是因为x用y做默认值时,y还没有声明。

      2. 对象的解构赋值

      解构不仅可以用于数组,还可以用于对象。

      let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
      foo // "aaa"
      bar // "bbb"
      

      对象的解构与数组有一个重要的不同:

      • 数组的元素是按次序排列的,变量的取值由它的位置决定;
      • 而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
        let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
        foo // "aaa"
        bar // "bbb"
        let { baz } = { foo: 'aaa', bar: 'bbb' };
        baz // undefined
        

        上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined。

        如果解构失败,变量的值等于undefined。

        let {foo} = {bar: 'baz'};
        foo // undefined
        

        上面代码中,等号右边的对象没有foo属性,所以变量foo取不到值,所以等于undefined。

        对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。

        // 例一
        let { log, sin, cos } = Math;
        // 例二
        const { log } = console;
        log('hello') // hello
        

        上面代码的例一将Math对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多。例二将console.log赋值到log变量。

        如果变量名与属性名不一致,必须写成下面这样。

        let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
        baz // "aaa"
        let obj = { first: 'hello', last: 'world' };
        let { first: f, last: l } = obj;
        f // 'hello'
        l // 'world'
        

        这实际上说明,对象的解构赋值是下面形式的简写(参见《对象的扩展》一章)。

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

        也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

        let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
        baz // "aaa"
        foo // error: foo is not defined
        

        上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。

        默认值

        对象的解构也可以指定默认值。

        var {x = 3} = {};
        x // 3
        var {x, y = 5} = {x: 1};
        x // 1
        y // 5
        var {x: y = 3} = {};
        y // 3
        var {x: y = 3} = {x: 5};
        y // 5
        var { message: msg = 'Something went wrong' } = {};
        msg // "Something went wrong"
        

        默认值生效的条件是,对象的属性值严格等于undefined。

        var {x = 3} = {x: undefined};
        x // 3
        var {x = 3} = {x: null};
        x // null
        

        上面代码中,属性x等于null,因为null与undefined不严格相等,所以是个有效的赋值,导致默认值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
        

        4. 数值和布尔值的解构赋值

        解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

        let {toString: s} = 123;
        s === Number.prototype.toString // true
        let {toString: s} = true;
        s === Boolean.prototype.toString // true
        

        上面代码中,数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。

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

        let { prop: x } = undefined; // TypeError
        let { prop: y } = null; // TypeError
        

        5. 函数参数的解构赋值

        函数的参数也可以使用解构赋值。

        function add([x, y]){
          return x + y;
        }
        add([1, 2]); // 3
        

        上面代码中,函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y。对于函数内部的代码来说,它们能感受到的参数就是x和y。

        下面是另一个例子。

        [[1, 2], [3, 4]].map(([a, b]) => a + b);
        // [ 3, 7 ]
        

        函数参数的解构也可以使用默认值。

        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的值。如果解构失败,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]
        move({}); // [undefined, undefined]
        move(); // [0, 0]
        

        上面代码是为函数move的参数指定默认值,而不是为变量x和y指定默认值,所以会得到与前一种写法不同的结果。

        undefined就会触发函数参数的默认值。

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

        6. 圆括号问题

        原则:尽量不要在模式中使用圆括号。

        可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。

        [(b)] = [3]; // 正确
        ({ p: (d) } = {}); // 正确
        [(parseInt.prop)] = [3]; // 正确
        

        上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是p,而不是d;第三行语句与第一行语句的性质一致。

        7. 用途

        变量的解构赋值用途很多。

        (1)交换变量的值

        let x = 1;
        let y = 2;
        [x, y] = [y, x];
        

        上面代码交换变量x和y的值,这样的写法不仅简洁,而且易读,语义非常清晰。

        (2)从函数返回多个值

        函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。

        // 返回一个数组
        function example() {
          return [1, 2, 3];
        }
        let [a, b, c] = example();
        // 返回一个对象
        function example() {
          return {
            foo: 1,
            bar: 2
          };
        }
        let { foo, bar } = example();
        

        (3)函数参数的定义

        解构赋值可以方便地将一组参数与变量名对应起来。

        // 参数是一组有次序的值
        function f([x, y, z]) { ... }
        f([1, 2, 3]);
        // 参数是一组无次序的值
        function f({x, y, z}) { ... }
        f({z: 3, y: 2, x: 1});
        

        (4)提取 JSON 数据

        解构赋值对提取 JSON 对象中的数据,尤其有用。

        let jsonData = {
          id: 42,
          status: "OK",
          data: [867, 5309]
        };
        let { id, status, data: number } = jsonData;
        console.log(id, status, number);
        // 42, "OK", [867, 5309]
        

        上面代码可以快速提取 JSON 数据的值。

        (5)函数参数的默认值

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

        指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || 'default foo';这样的语句。

        (6)遍历 Map 结构

        任何部署了 Iterator 接口的对象,都可以用for...of循环遍历。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) {
          // ...
        }
        

        (7)输入模块的指定方法

        加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。

        const { SourceMapConsumer, SourceNode } = require("source-map");
        

        字符串

        模板字符串

        ES6 引入了模板字符串,使用反引号 `` 括起来的字符串,它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

        // 普通字符串
        `In JavaScript '\n' is a line-feed.`
        // 多行字符串
        `In JavaScript this is
         not legal.`
        console.log(`string text line 1
        string text line 2`);
        // 字符串中嵌入变量
        let name = "Bob", time = "today";
        `Hello ${name}, how are you ${time}?`
        

        上面代码中的模板字符串,都是用反引号表示。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

        let greeting = `\`Yo\` World!`;
        

        如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。

        $('#list').html(`
        
        • first
        • second
        `);

        上面代码中,所有模板字符串的空格和换行,都是被保留的,比如

          标签前面会有一个换行。如果你不想要这个换行,可以使用trim方法消除它。

          $('#list').html(`
          
          • first
          • second
          `.trim());

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

          function authorize(user, action) {
            if (!user.hasPrivilege(action)) {
              throw new Error(
                // 传统写法为
                // 'User '
                // + user.name
                // + ' is not authorized to do '
                // + action
                // + '.'
                `User ${user.name} is not authorized to do ${action}.`);
            }
          }
          

          大括号内部可以放入任意的 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"
          

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

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

          如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的toString方法。

          如果模板字符串中的变量没有声明,将报错。

          // 变量place没有声明
          let msg = `Hello, ${place}`;
          // 报错
          

          由于模板字符串的大括号内部,就是执行 JavaScript 代码,因此如果大括号内部是一个字符串,将会原样输出。

          `Hello ${'World'}`
          // "Hello World"
          

          字符串的遍历

          ES6 为字符串添加了遍历器接口(详见《Iterator》一章),使得字符串可以被for...of循环遍历。

          for (let codePoint of 'foo') {
            console.log(codePoint)
          }
          // "f"
          // "o"
          // "o"
          

          除了遍历字符串,这个遍历器最大的优点是可以识别大于0xFFFF的码点,传统的for循环无法识别这样的码点。

          let text = String.fromCodePoint(0x20BB7);
          for (let i = 0; i < text.length; i++) {
            console.log(text[i]);
          }
          // " "
          // " "
          for (let i of text) {
            console.log(i);
          }
          // "𠮷"
          

          上面代码中,字符串text只有一个字符,但是for循环会认为它包含两个字符(都不可打印),而for...of循环会正确识别出这一个字符。

          标签模板

          模板字符串的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。

          alert`hello`
          // 等同于
          alert(['hello'])
          

          标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。

          但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。

          let a = 5;
          let b = 10;
          tag`Hello ${ a + b } world ${ a * b }`;
          // 等同于
          tag(['Hello ', ' world ', ''], 15, 50);
          

          上面代码中,模板字符串前面有一个标识名tag,它是一个函数。整个表达式的返回值,就是tag函数处理模板字符串后的返回值。

          函数tag依次会接收到多个参数。

          function tag(stringArr, value1, value2){
            // ...
          }
          // 等同于
          function tag(stringArr, ...values){
            // ...
          }
          

          字符串的新增方法

          https://es6.ruanyifeng.com/#docs/string-methods

          • String.fromCodePoint()
          • String.raw()
          • 实例方法:codePointAt()
          • 实例方法:normalize()
          • 实例方法:includes(), startsWith(), endsWith()
          • 实例方法:repeat()
          • 实例方法:padStart(),padEnd()
          • 实例方法:trimStart(),trimEnd()
          • 实例方法:matchAll()
          • 实例方法:replaceAll()
          • 实例方法:at()

            数值的扩展

            • 二进制 0b 和八进制 0o表示法
            • 数值分隔符1_000_000_000_000;
            • Number.isFinite() 检查是否为有限的,即不是Infinity, Number.isNaN()检查一个值是否为NaN。
            • Number.parseInt(), Number.parseFloat() 解析字符串
            • Number.isInteger() 判断一个数值是否为整数。
            • Number.EPSILON 它表示 1 与大于 1 的最小浮点数之间的差。等于 2 的 -52 次方Math.pow(2, -52) 。Number.EPSILON的实质是一个可以用来设置“能够接受的最小误差范围”
            • 安全整数和 Number.isSafeInteger() :JavaScript 能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值。ES6 引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。
            • Math 对象的扩展:ES6 在 Math 对象上新增了 17 个与数学相关的方法。所有这些方法都是静态方法,只能在 Math 对象上调用。
            • BigInt 数据类型:JS 数值的精度只能到 53 个二进制位,大于这个范围的整数,无法精确表示,另外JS也无法表示大于或等于2的1024次方的数值,会返回Infinity。ES2020 引入了一种新的数据类型 BigInt(大整数),来解决这个问题,这是 ECMAScript 的第八种数据类型。BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。表示方法是在数字后面加上后缀n,如15346349309n;

              函数的扩展

              1. 函数参数的默认值

              ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。

              function log(x, y = 'World') {
                console.log(x, y);
              }
              log('Hello') // Hello World
              log('Hello', 'China') // Hello China
              log('Hello', '') // Hello
              

              可以看到,ES6 的写法比 ES5 简洁许多,而且非常自然。下面是另一个例子。

              function Point(x = 0, y = 0) {
                this.x = x;
                this.y = y;
              }
              const p = new Point();
              p // { x: 0, y: 0 }
              

              与解构赋值默认值结合使用

              参数默认值可以与解构赋值的默认值,结合起来使用。

              function foo({x, y = 5}) {
                console.log(x, y);
              }
              foo({}) // undefined 5
              foo({x: 1}) // 1 5
              foo({x: 1, y: 2}) // 1 2
              foo() // TypeError: Cannot read property 'x' of undefined
              

              上面代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数foo()的参数是一个对象时,变量x和y才会通过解构赋值生成。如果函数foo()调用时没提供参数,变量x和y就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况。

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

              上面代码指定,如果没有提供参数,函数foo的参数默认为一个空对象。

              下面是另一个解构赋值默认值的例子。

              function fetch(url, { body = '', method = 'GET', headers = {} }) {
                console.log(method);
              }
              fetch('http://example.com', {})
              // "GET"
              fetch('http://example.com')
              // 报错
              

              上面代码中,如果函数fetch()的第二个参数是一个对象,就可以为它的三个属性设置默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。

              function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
                console.log(method);
              }
              fetch('http://example.com')
              // "GET"
              

              上面代码中,函数fetch没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量method才会取到默认值GET。

              注意,函数参数的默认值生效以后,参数解构赋值依然会进行。

              function f({ a, b = 'world' } = { a: 'hello' }) {
                console.log(b);
              }
              f() // world
              

              上面示例中,函数f()调用时没有参数,所以参数默认值{ a: 'hello' }生效,然后再对这个默认值进行解构赋值,从而触发参数变量b的默认值生效。

              参数默认值的位置

              通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。

              // 例一
              function f(x = 1, y) {
                return [x, y];
              }
              f() // [1, undefined]
              f(2) // [2, undefined]
              f(, 1) // 报错
              f(undefined, 1) // [1, 1]
              // 例二
              function f(x, y = 5, z) {
                return [x, y, z];
              }
              f() // [undefined, 5, undefined]
              f(1) // [1, 5, undefined]
              f(1, ,2) // 报错
              f(1, undefined, 2) // [1, 5, 2]
              

              上面代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined。

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

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

              上面代码中,x参数对应undefined,结果触发了默认值,y参数等于null,就没有触发默认值。

              应用

              利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。

              function throwIfMissing() {
                throw new Error('Missing parameter');
              }
              function foo(mustBeProvided = throwIfMissing()) {
                return mustBeProvided;
              }
              foo()
              // Error: Missing parameter
              

              上面代码的foo函数,如果调用的时候没有参数,就会调用默认值throwIfMissing函数,从而抛出一个错误。

              另外,可以将参数默认值设为undefined,表明这个参数是可以省略的。

              function foo(optional = undefined) { ··· }
              

              2. rest 参数

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

              function add(...values) {
                let sum = 0;
                for (var val of values) {
                  sum += val;
                }
                return sum;
              }
              add(2, 5, 3) // 10
              

              上面代码的add函数是一个求和函数,利用 rest 参数,可以向该函数传入任意数目的参数。

              下面是一个 rest 参数代替arguments变量的例子。

              // arguments变量的写法
              function sortNumbers() {
                return Array.from(arguments).sort();
              }
              // rest参数的写法
              const sortNumbers = (...numbers) => numbers.sort();
              

              上面代码的两种写法,比较后可以发现,rest 参数的写法更自然也更简洁。

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

              // 报错
              function f(a, ...b, c) {
                // ...
              }
              

              函数的length属性,不包括 rest 参数。

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

              3. 严格模式

              从 ES5 开始,函数内部可以设定为严格模式。

              function doSomething(a, b) {
                'use strict';
                // code
              }
              

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

              4. name 属性

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

              function foo() {}
              foo.name // "foo"
              

              这个属性早就被浏览器广泛支持,但是直到 ES6,才将其写入了标准。

              需要注意的是,ES6 对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量,ES5 的name属性,会返回空字符串,而 ES6 的name属性会返回实际的函数名。

              var f = function () {};
              // ES5
              f.name // ""
              // ES6
              f.name // "f"
              

              上面代码中,变量f等于一个匿名函数,ES5 和 ES6 的name属性返回的值不一样。

              如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的name属性都返回这个具名函数原本的名字。

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

              Function构造函数返回的函数实例,name属性的值为anonymous。

              (new Function).name // "anonymous"
              

              bind返回的函数,name属性值会加上bound前缀。

              function foo() {};
              foo.bind({}).name // "bound foo"
              (function(){}).bind({}).name // "bound "
              

              5. 箭头函数

              基本用法

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

              var f = v => v;
              // 等同于
              var f = function (v) {
                return v;
              };
              

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

              var f = () => 5;
              // 等同于
              var f = function () { return 5 };
              var sum = (num1, num2) => num1 + num2;
              // 等同于
              var sum = function(num1, num2) {
                return num1 + num2;
              };
              

              如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

              var sum = (num1, num2) => { return num1 + num2; }
              

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

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

              如果箭头函数只有一行语句,且不需要返回值,就不用写大括号了。箭头函数可以与变量解构结合使用。

              const full = ({ first, last }) => first + ' ' + last;
              // 等同于
              function full(person) {
                return person.first + ' ' + person.last;
              }
              

              箭头函数使得表达更加简洁。

              const isEven = n => n % 2 === 0;
              const square = n => n * n;
              

              箭头函数的一个用处是简化回调函数。

              // 普通函数写法
              [1,2,3].map(function (x) {
                return x * x;
              });
              // 箭头函数写法
              [1,2,3].map(x => x * x);
              

              另一个例子是

              // 普通函数写法
              var result = values.sort(function (a, b) {
                return a - b;
              });
              // 箭头函数写法
              var result = values.sort((a, b) => a - b);
              

              下面是 rest 参数与箭头函数结合的例子。

              const numbers = (...nums) => nums;
              numbers(1, 2, 3, 4, 5)
              // [1,2,3,4,5]
              const headAndTail = (head, ...tail) => [head, tail];
              headAndTail(1, 2, 3, 4, 5)
              // [1,[2,3,4,5]]
              

              使用注意点

              箭头函数有几个使用注意点。

              • (1)箭头函数没有自己的this对象。
              • (2)不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误。
              • (3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
              • (4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

                上面四点中,最重要的是第一点。对于普通函数来说,内部的this指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的this对象,内部的this就是定义时上层作用域中的this。也就是说,箭头函数内部的this指向是固定的,相比之下,普通函数的this指向是可变的。

                function foo() {
                  setTimeout(() => {
                    console.log('id:', this.id);
                  }, 100);
                }
                var id = 21;
                foo.call({ id: 42 });
                // id: 42
                

                上面代码中,setTimeout()的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以打印出来的是42。

                下面例子是回调函数分别为箭头函数和普通函数,对比它们内部的this指向。

                function Timer() {
                  this.s1 = 0;
                  this.s2 = 0;
                  // 箭头函数
                  setInterval(() => this.s1++, 1000); // 这里的this指向setInterval所在的对象,即Timer对象
                  // 普通函数
                  setInterval(function () {
                    this.s2++; // 这里的this指向window对象
                  }, 1000);
                }
                var timer = new Timer(); // 这句执行后,内部的s1就会每隔1秒自增一次,但是s2不会
                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使得它不再可变,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。

                var handler = {
                  id: '123456',
                  init: function() {
                    document.addEventListener('click',
                      event => this.doSomething(event.type), false);
                  },
                  doSomething: function(type) {
                    console.log('Handling ' + type  + ' for ' + this.id);
                  }
                };
                

                上面代码的init()方法中,使用了箭头函数,这导致这个箭头函数里面的this,总是指向handler对象。如果回调函数是普通函数,那么运行this.doSomething()这一行会报错,因为此时this指向document对象。

                总之,箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

                下面是 Babel 转箭头函数产生的 ES5 代码,就能清楚地说明this的指向。

                // ES6
                function foo() {
                  setTimeout(() => {
                    console.log('id:', this.id);
                  }, 100);
                }
                // ES5
                function foo() {
                  var _this = this;
                  setTimeout(function () {
                    console.log('id:', _this.id);
                  }, 100);
                }
                

                上面代码中,转换后的 ES5 版本清楚地说明了,箭头函数里面根本没有自己的this,而是引用外层的this。

                除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。

                不适用场合

                由于箭头函数使得this从“动态”变成“静态”,下面两个场合不应该使用箭头函数。

                第一个场合是定义对象的方法,且该方法内部包括this。

                const cat = {
                  lives: 9,
                  jumps: () => {
                    this.lives--;
                  }
                }
                

                上面代码中,cat.jumps()方法是一个箭头函数,这是错误的。调用cat.jumps()时,如果是普通函数,该方法内部的this指向cat;如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域。

                再看一个例子。

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

                上面例子中,obj.m()使用箭头函数定义。JavaScript 引擎的处理方法是,先在全局空间生成这个箭头函数,然后赋值给obj.m,这导致箭头函数内部的this指向全局对象,所以obj.m()输出的是全局空间的21,而不是对象内部的42。上面的代码实际上等同于下面的代码。

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

                由于上面这个原因,对象的属性建议使用传统的写法定义,不要用箭头函数定义。

                第二个场合是需要动态this的时候,也不应使用箭头函数。

                var button = document.getElementById('press');
                button.addEventListener('click', () => {
                  this.classList.toggle('on');
                });
                

                上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。

                另外,如果函数体很复杂,有许多行,或者函数内部有大量的读写操作,不单纯是为了计算值,这时也不应该使用箭头函数,而是要使用普通函数,这样可以提高代码可读性。

                其他

                • 尾调用优化(尾递归优化,原理是转成循环实现)
                • 函数参数的尾逗号(函数最后一个参数的后面允许写一个逗号)
                • Function.prototype.toString()
                • catch 命令的参数省略(以前要求try...catch结构的catch命令后面必须跟参数,现在不要求了)

                  数组的扩展

                  1. 扩展运算符

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

                  console.log(...[1, 2, 3])
                  // 1 2 3
                  console.log(1, ...[2, 3, 4], 5)
                  // 1 2 3 4 5
                  [...document.querySelectorAll('div')]
                  // [, , ]
                  

                  该运算符主要用于函数调用。

                  function push(array, ...items) {
                    array.push(...items);
                  }
                  function add(x, y) {
                    return x + y;
                  }
                  const numbers = [4, 38];
                  add(...numbers) // 42
                  

                  上面代码中,array.push(...items)和add(...numbers)这两行,都是函数的调用,它们都使用了扩展运算符。该运算符将一个数组,变为参数序列。

                  扩展运算符与正常的函数参数可以结合使用,非常灵活。

                  function f(v, w, x, y, z) { }
                  const args = [0, 1];
                  f(-1, ...args, 2, ...[3]);
                  

                  扩展运算符后面还可以放置表达式。

                  const arr = [
                    ...(x > 0 ? ['a'] : []),
                    'b',
                  ];
                  

                  如果扩展运算符后面是一个空数组,则不产生任何效果。

                  [...[], 1]
                  // [1]
                  

                  下面是扩展运算符取代apply()方法的一个实际的例子,应用Math.max()方法,简化求出一个数组最大元素的写法。

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

                  扩展运算符的应用

                  (1)复制数组

                  数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。

                  const a1 = [1, 2];
                  const a2 = a1;
                  a2[0] = 2;
                  a1 // [2, 2]
                  

                  上面代码中,a2并不是a1的克隆,而是指向同一份数据的另一个指针。修改a2,会直接导致a1的变化。

                  扩展运算符提供了复制数组的简便写法。

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

                  上面的两种写法,a2都是a1的克隆。

                  (2)合并数组

                  扩展运算符提供了数组合并的新写法。

                  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];
                  a3[0] === a1[0] // true
                  a4[0] === a1[0] // true
                  

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

                  (3)与解构赋值结合

                  扩展运算符可以与解构赋值结合起来,用于生成数组。

                  // ES5
                  a = list[0], rest = list.slice(1)
                  // ES6
                  [a, ...rest] = list
                  

                  下面是另外一些例子。

                  const [first, ...rest] = [1, 2, 3, 4, 5];
                  first // 1
                  rest  // [2, 3, 4, 5]
                  const [first, ...rest] = [];
                  first // undefined
                  rest  // []
                  const [first, ...rest] = ["foo"];
                  first  // "foo"
                  rest   // []
                  

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

                  const [...butLast, last] = [1, 2, 3, 4, 5];
                  // 报错
                  const [first, ...middle, last] = [1, 2, 3, 4, 5];
                  // 报错
                  

                  (4)字符串

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

                  [...'hello']
                  // [ "h", "e", "l", "l", "o" ]
                  

                  上面的写法,有一个重要的好处,那就是能够正确识别四个字节的 Unicode 字符。

                  'x\uD83D\uDE80y'.length // 4
                  [...'x\uD83D\uDE80y'].length // 3
                  

                  上面代码的第一种写法,JavaScript 会将四个字节的 Unicode 字符,识别为 2 个字符,采用扩展运算符就没有这个问题。因此,正确返回字符串长度的函数,可以像下面这样写。

                  function length(str) {
                    return [...str].length;
                  }
                  length('x\uD83D\uDE80y') // 3
                  

                  凡是涉及到操作四个字节的 Unicode 字符的函数,都有这个问题。因此,最好都用扩展运算符改写。

                  let str = 'x\uD83D\uDE80y';
                  str.split('').reverse().join('')
                  // 'y\uDE80\uD83Dx'
                  [...str].reverse().join('')
                  // 'y\uD83D\uDE80x'
                  

                  上面代码中,如果不用扩展运算符,字符串的reverse()操作就不正确。

                  (5)实现了 Iterator 接口的对象

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

                  let nodeList = document.querySelectorAll('div');
                  let array = [...nodeList];
                  

                  上面代码中,querySelectorAll()方法返回的是一个NodeList对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于NodeList对象实现了 Iterator。

                  Number.prototype[Symbol.iterator] = function*() {
                    let i = 0;
                    let num = this.valueOf();
                    while (i < num) {
                      yield i++;
                    }
                  }
                  console.log([...5]) // [0, 1, 2, 3, 4]
                  

                  上面代码中,先定义了Number对象的遍历器接口,扩展运算符将5自动转成Number实例以后,就会调用这个接口,就会返回自定义的结果。

                  对于那些没有部署 Iterator 接口的类似数组的对象,扩展运算符就无法将其转为真正的数组。

                  let arrayLike = {
                    '0': 'a',
                    '1': 'b',
                    '2': 'c',
                    length: 3
                  };
                  // TypeError: Cannot spread non-iterable object.
                  let arr = [...arrayLike];
                  

                  上面代码中,arrayLike是一个类似数组的对象,但是没有部署 Iterator 接口,扩展运算符就会报错。这时,可以改为使用Array.from方法将arrayLike转为真正的数组。

                  (6)Map 和 Set 结构,Generator 函数

                  扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 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]
                  

                  上面代码中,变量go是一个 Generator 函数,执行后返回的是一个遍历器对象,对这个遍历器对象执行扩展运算符,就会将内部遍历得到的值,转为一个数组。

                  如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错。

                  const obj = {a: 1, b: 2};
                  let arr = [...obj]; // TypeError: Cannot spread non-iterable object
                  

                  2. Array.from()

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

                  下面是一个类似数组的对象,Array.from()将它转为真正的数组。

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

                  实际应用中,常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的arguments对象。Array.from()都可以将它们转为真正的数组。

                  // NodeList 对象
                  let ps = document.querySelectorAll('p');
                  Array.from(ps).filter(p => {
                    return p.textContent.length > 100;
                  });
                  // arguments 对象
                  function foo() {
                    var args = Array.from(arguments);
                    // ...
                  }
                  

                  上面代码中,querySelectorAll()方法返回的是一个类似数组的对象,可以将这个对象转为真正的数组,再使用filter()方法。

                  只要是部署了 Iterator 接口的数据结构,Array.from()都能将其转为数组。

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

                  上面代码中,字符串和 Set 结构都具有 Iterator 接口,因此可以被Array.from()转为真正的数组。

                  如果参数是一个真正的数组,Array.from()会返回一个一模一样的新数组。(因此可以用于数组的拷贝?)

                  Array.from([1, 2, 3])
                  // [1, 2, 3]
                  

                  值得提醒的是,扩展运算符(...)也可以将某些数据结构转为数组。

                  // arguments对象
                  function foo() {
                    const args = [...arguments];
                  }
                  // NodeList对象
                  [...document.querySelectorAll('div')]
                  

                  扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。Array.from()方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from()方法转为数组,而此时扩展运算符就无法转换。

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

                  上面代码中,Array.from()返回了一个具有三个成员的数组,每个位置的值都是undefined。扩展运算符转换不了这个对象。

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

                  Array.from(arrayLike, x => x * x);
                  // 等同于
                  Array.from(arrayLike).map(x => x * x);
                  Array.from([1, 2, 3], (x) => x * x)
                  // [1, 4, 9]
                  

                  下面的例子是取出一组 DOM 节点的文本内容。

                  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)
                  

                  下面的例子将数组中布尔值为false的成员转为0。

                  Array.from([1, , 2, , 3], (n) => n || 0)
                  // [1, 0, 2, 0, 3]
                  

                  另一个例子是返回各种数据的类型。

                  function typesOf () {
                    return Array.from(arguments, value => typeof value)
                  }
                  typesOf(null, [], NaN)
                  // ['object', 'object', 'number']
                  

                  如果map()函数里面用到了this关键字,还可以传入Array.from()的第三个参数,用来绑定this。

                  3. Array.of()

                  Array.of()方法用于将一组值,转换为数组。

                  Array.of(3, 11, 8) // [3,11,8]
                  Array.of(3) // [3]
                  Array.of(3).length // 1
                  

                  这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。

                  Array() // []
                  Array(3) // [, , ,]
                  Array(3, 11, 8) // [3, 11, 8]
                  

                  上面代码中,Array()方法没有参数、一个参数、三个参数时,返回的结果都不一样。只有当参数个数不少于 2 个时,Array()才会返回由参数组成的新数组。参数只有一个正整数时,实际上是指定数组的长度。

                  Array.of()基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。

                  Array.of() // []
                  Array.of(undefined) // [undefined]
                  Array.of(1) // [1]
                  Array.of(1, 2) // [1, 2]
                  

                  Array.of()总是返回参数值组成的数组。如果没有参数,就返回一个空数组。

                  4. 实例方法: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。

                    5. 实例方法:find(),findIndex(),findLast(),findLastIndex()

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

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

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

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

                    上面代码中,find()方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。

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

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

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

                    find()和findIndex()都是从数组的0号位,依次向后检查。ES2022 新增了两个方法findLast()和findLastIndex(),从数组的最后一个成员开始,依次向前检查,其他都保持不变。

                    6. 实例方法:fill()

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

                    ['a', 'b', 'c'].fill(7)
                    // [7, 7, 7]
                    new Array(3).fill(7)
                    // [7, 7, 7]
                    

                    上面代码表明,fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。

                    fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。

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

                    上面代码表示,fill方法从 1 号位开始,向原数组填充 7,到 2 号位之前结束。

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

                    let arr = new Array(3).fill({name: "Mike"});
                    arr[0].name = "Ben";
                    arr
                    // [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
                    let arr = new Array(3).fill([]);
                    arr[0].push(5);
                    arr
                    // [[5], [5], [5]]
                    

                    7. 实例方法:entries(),keys() 和 values()

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

                    for (let index of ['a', 'b'].keys()) {
                      console.log(index);
                    }
                    // 0
                    // 1
                    for (let elem of ['a', 'b'].values()) {
                      console.log(elem);
                    }
                    // 'a'
                    // 'b'
                    for (let [index, elem] of ['a', 'b'].entries()) {
                      console.log(index, elem);
                    }
                    // 0 "a"
                    // 1 "b"
                    

                    8. 实例方法:includes()

                    Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。ES2016 引入了该方法。

                    [1, 2, 3].includes(2)     // true
                    [1, 2, 3].includes(4)     // false
                    [1, 2, NaN].includes(NaN) // true
                    

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

                    [1, 2, 3].includes(3, 3);  // false
                    [1, 2, 3].includes(3, -1); // true
                    

                    没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。

                    if (arr.indexOf(el) !== -1) {
                      // ...
                    }
                    

                    indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。

                    [NaN].indexOf(NaN)
                    // -1
                    

                    includes使用的是不一样的判断算法,就没有这个问题。

                    [NaN].includes(NaN)
                    // true
                    

                    9. 实例方法:flat(),flatMap()

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

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

                    上面代码中,原数组的成员里面有一个数组,flat()方法将子数组的成员取出来,添加在原来的位置。

                    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]
                    

                    上面代码中,flat()的参数为2,表示要“拉平”两层的嵌套数组。

                    如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。

                    [1, [2, [3]]].flat(Infinity)
                    // [1, 2, 3]
                    

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

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

                    flatMap()只能展开一层数组。

                    10. 实例方法:at()

                    接受一个整数作为参数,返回对应位置的成员,并支持负索引。这个方法不仅可用于数组,也可用于字符串和类型数组(TypedArray)。

                    const arr = [5, 12, 8, 130, 44];
                    arr.at(2) // 8
                    arr.at(-2) // 130
                    

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

                    const sentence = 'This is a sample sentence';
                    sentence.at(0); // 'T'
                    sentence.at(-1); // 'e'
                    sentence.at(-100) // undefined
                    sentence.at(100) // undefined
                    

                    其他

                    • 实例方法:toReversed(),toSorted(),toSpliced(),with()
                    • 实例方法:group(),groupToMap()
                    • 数组的空位:Array(3)返回一个具有 3 个空位的数组。空位不是undefined。ES6 则是明确将空位转为undefined。Array.from()方法、扩展运算符(...)、keys()、values() 等都会将空位转为undefined。for...of循环也会遍历空位。
                    • Array.prototype.sort() 的排序稳定性

                      对象的扩展

                      1. 属性的简洁表示法

                      ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。

                      const foo = 'bar';
                      const baz = {foo};
                      baz // {foo: "bar"}
                      // 等同于
                      const baz = {foo: foo};
                      

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

                      function f(x, y) {
                        return {x, y};
                      }
                      // 等同于
                      function f(x, y) {
                        return {x: x, y: y};
                      }
                      f(1, 2) // Object {x: 1, y: 2}
                      

                      除了属性简写,方法也可以简写。

                      下面是一个实际的例子。

                      let birth = '2000/01/01';
                      const Person = {
                        name: '张三',
                        //等同于birth: birth
                        birth,
                        // 等同于hello: function ()...
                        hello() { console.log('我的名字是', this.name); }
                      };
                      

                      这种写法用于函数的返回值,将会非常方便。

                      function getPoint() {
                        const x = 1;
                        const y = 10;
                        return {x, y};
                      }
                      getPoint()
                      // {x:1, y:10}
                      

                      CommonJS 模块输出一组变量,就非常合适使用简洁写法。

                      let ms = {};
                      function getItem (key) {
                        return key in ms ? ms[key] : null;
                      }
                      function setItem (key, value) {
                        ms[key] = value;
                      }
                      function clear () {
                        ms = {};
                      }
                      module.exports = { getItem, setItem, clear };
                      // 等同于
                      module.exports = {
                        getItem: getItem,
                        setItem: setItem,
                        clear: clear
                      };
                      

                      属性的赋值器(setter)和取值器(getter),事实上也是采用这种写法。

                      简洁写法在打印对象时也很有用。

                      let user = {
                        name: 'test'
                      };
                      let foo = {
                        bar: 'baz'
                      };
                      console.log(user, foo)
                      // {name: "test"} {bar: "baz"}
                      console.log({user, foo})
                      // {user: {name: "test"}, foo: {bar: "baz"}}
                      

                      上面代码中,console.log直接输出user和foo两个对象时,就是两组键值对,可能会混淆。把它们放在大括号里面输出,就变成了对象的简洁表示法,每组键值对前面会打印对象名,这样就比较清晰了。

                      注意,简写的对象方法不能用作构造函数,会报错。

                      const obj = {
                        f() {
                          this.foo = 'bar';
                        }
                      };
                      new obj.f() // 报错
                      

                      上面代码中,f是一个简写的对象方法,所以obj.f不能当作构造函数使用。

                      2. 属性名表达式

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

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

                      上面代码的方法一是直接用标识符作为属性名,方法二是用表达式作为属性名,这时要将表达式放在方括号之内。

                      但是,如果使用字面量方式定义对象(使用大括号),在 ES5 中只能使用方法一(标识符)定义属性。

                      var obj = {
                        foo: true,
                        abc: 123
                      };
                      

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

                      let propKey = 'foo';
                      let obj = {
                        [propKey]: true,
                        ['a' + 'bc']: 123
                      };
                      

                      下面是另一个例子。

                      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
                      

                      注意,属性名表达式与简洁表示法,不能同时使用,会报错。

                      3. 方法的 name 属性

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

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

                      上面代码中,方法的name属性返回函数名(即方法名)。

                      如果对象的方法使用了取值函数(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"
                      

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

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

                      其他

                      • 属性的可枚举性和遍历:for...in、Object.keys(obj)、Object.getOwnPropertyNames(obj) 等方法可以遍历对象的属性
                      • super 关键字:我们知道,this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。
                      • 对象的扩展运算符:《数组的扩展》一章中,已经介绍过扩展运算符(...)。ES2018 将这个运算符引入了对象。
                      • AggregateError 错误对象
                      • Error 对象的 cause 属性

                        对象的新增方法

                        1. Object.is()

                        S5 比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。

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

                        Object.is('foo', 'foo')
                        // true
                        Object.is({}, {})
                        // false
                        

                        不同之处只有两个:一是+0不等于-0,二是NaN等于自身。

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

                        2. Object.assign()

                        基本用法

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

                        const target = { a: 1 };
                        const source1 = { b: 2 };
                        const source2 = { c: 3 };
                        Object.assign(target, source1, source2);
                        target // {a:1, b:2, c:3}
                        

                        Object.assign()方法的第一个参数是目标对象,后面的参数都是源对象。

                        注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

                        const target = { a: 1, b: 1 };
                        const source1 = { b: 2, c: 2 };
                        const source2 = { c: 3 };
                        Object.assign(target, source1, source2);
                        target // {a:1, b:2, c:3}
                        

                        如果只有一个参数,Object.assign()会直接返回该参数。

                        const obj = {a: 1};
                        Object.assign(obj) === obj // true
                        

                        如果该参数不是对象,则会先转成对象,然后返回。

                        typeof Object.assign(2) // "object"
                        

                        由于undefined和null无法转成对象,所以如果它们作为参数,就会报错。

                        Object.assign(undefined) // 报错
                        Object.assign(null) // 报错
                        

                        如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefined和null不在首参数,就不会报错。

                        let obj = {a: 1};
                        Object.assign(obj, undefined) === obj // true
                        Object.assign(obj, null) === obj // true
                        

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

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

                        上面代码中,v1、v2、v3分别是字符串、布尔值和数值,结果只有字符串合入目标对象(以字符数组的形式),数值和布尔值都会被忽略。这是因为只有字符串的包装对象,会产生可枚举属性。

                        Object(true) // {[[PrimitiveValue]]: true}
                        Object(10)  //  {[[PrimitiveValue]]: 10}
                        Object('abc') // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}
                        

                        上面代码中,布尔值、数值、字符串分别转成对应的包装对象,可以看到它们的原始值都在包装对象的内部属性[[PrimitiveValue]]上面,这个属性是不会被Object.assign()拷贝的。只有字符串的包装对象,会产生可枚举的实义属性,那些属性则会被拷贝。

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

                        Object.assign({b: 'c'},
                          Object.defineProperty({}, 'invisible', {
                            enumerable: false,
                            value: 'hello'
                          })
                        )
                        // { b: 'c' }
                        

                        上面代码中,Object.assign()要拷贝的对象只有一个不可枚举属性invisible,这个属性并没有被拷贝进去。

                        属性名为 Symbol 值的属性,也会被Object.assign()拷贝。

                        Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
                        // { a: 'b', Symbol(c): 'd' }
                        

                        注意点

                        (1)浅拷贝

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

                        const obj1 = {a: {b: 1}};
                        const obj2 = Object.assign({}, obj1);
                        obj1.a.b = 2;
                        obj2.a.b // 2
                        

                        上面代码中,源对象obj1的a属性的值是一个对象,Object.assign()拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。

                        (2)同名属性的替换

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

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

                        上面代码中,target对象的a属性被source对象的a属性整个替换掉了,而不会得到{ a: { b: 'hello', d: 'e' } }的结果。这通常不是开发者想要的,需要特别小心。

                        一些函数库提供Object.assign()的定制版本(比如 Lodash 的_.defaultsDeep()方法),可以得到深拷贝的合并。

                        (3)数组的处理

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

                        Object.assign([1, 2, 3], [4, 5])
                        // [4, 5, 3]
                        

                        上面代码中,Object.assign()把数组视为属性名为 0、1、2 的对象,因此源数组的 0 号属性4覆盖了目标数组的 0 号属性1。

                        (4)取值函数的处理

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

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

                        上面代码中,source对象的foo属性是一个取值函数,Object.assign()不会复制这个取值函数,只会拿到值以后,将这个值复制过去。

                        常见用途

                        Object.assign()方法有很多用处。

                        (1)为对象添加属性

                        class Point {
                          constructor(x, y) {
                            Object.assign(this, {x, y});
                          }
                        }
                        

                        上面方法通过Object.assign()方法,将x属性和y属性添加到Point类的对象实例。

                        (2)为对象添加方法

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

                        上面代码使用了对象属性的简洁表示法,直接将两个函数放在大括号中,再使用assign()方法添加到SomeClass.prototype之中。

                        (3)克隆对象

                        function clone(origin) {
                          return Object.assign({}, origin);
                        }
                        

                        上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。

                        不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。

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

                        (4)合并多个对象

                        将多个对象合并到某个对象。

                        const merge =
                          (target, ...sources) => Object.assign(target, ...sources);
                        

                        如果希望合并后返回一个新对象,可以改写上面函数,对一个空对象合并。

                        const merge =
                          (...sources) => Object.assign({}, ...sources);
                        

                        (5)为属性指定默认值

                        const DEFAULTS = {
                          logLevel: 0,
                          outputFormat: 'html'
                        };
                        function processContent(options) {
                          options = Object.assign({}, DEFAULTS, options);
                          console.log(options);
                          // ...
                        }
                        

                        上面代码中,DEFAULTS对象是默认值,options对象是用户提供的参数。Object.assign()方法将DEFAULTS和options合并成一个新对象,如果两者有同名属性,则options的属性值会覆盖DEFAULTS的属性值。

                        注意,由于存在浅拷贝的问题,DEFAULTS对象和options对象的所有属性的值,最好都是简单类型,不要指向另一个对象。否则,DEFAULTS对象的该属性很可能不起作用。

                        const DEFAULTS = {
                          url: {
                            host: 'example.com',
                            port: 7070
                          },
                        };
                        processContent({ url: {port: 8000} })
                        // {
                        //   url: {port: 8000}
                        // }
                        

                        上面代码的原意是将url.port改成 8000,url.host不变。实际结果却是options.url覆盖掉DEFAULTS.url,所以url.host就不存在了。

                        其他

                        • Object.getOwnPropertyDescriptors()
                        • __proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()
                        • Object.keys(),Object.values(),Object.entries()
                        • Object.fromEntries():Object.entries()的逆操作,用于将一个键值对数组转为对象。
                        • Object.hasOwn():判断是否为自身的属性。

                          运算符的扩展

                          • 指数运算符**:如 2 ** 3 表示 2 的 3 次方
                          • 链判断运算符?.:例如const firstName = message?.body?.user?.firstName || 'default';
                          • Null 判断运算符??:它的行为类似||,但是只有运算符左侧的值为null或undefined时,才会返回右侧的值。例如 const headerText = response.settings.headerText ?? 'Hello, world!';
                          • 逻辑赋值运算符:||=、&&=、??=
                          • #!命令

                            Symbol

                            ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。

                            ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它属于 JavaScript 语言的原生数据类型之一,其他数据类型是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、大整数(BigInt)、对象(Object)。

                            Symbol 值通过Symbol()函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

                            let s = Symbol();
                            typeof s
                            // "symbol"
                            

                            上面代码中,变量s就是一个独一无二的值。typeof运算符的结果,表明变量s是 Symbol 数据类型,而不是字符串之类的其他类型。

                            注意,Symbol()函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象,所以不能使用new命令来调用。另外,由于 Symbol 值不是对象,所以也不能添加属性。基本上,它是一种类似于字符串的数据类型。

                            Symbol()函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述。这主要是为了在控制台显示,或者转为字符串时,比较容易区分。

                            let s1 = Symbol('foo');
                            let s2 = Symbol('bar');
                            s1 // Symbol(foo)
                            s2 // Symbol(bar)
                            s1.toString() // "Symbol(foo)"
                            s2.toString() // "Symbol(bar)"
                            

                            上面代码中,s1和s2是两个 Symbol 值。如果不加参数,它们在控制台的输出都是Symbol(),不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。

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

                            const obj = {
                              toString() {
                                return 'abc';
                              }
                            };
                            const sym = Symbol(obj);
                            sym // Symbol(abc)
                            

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

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

                            上面代码中,s1和s2都是Symbol()函数的返回值,而且参数相同,但是它们是不相等的。事实上,如果调用100次Symbol(),会得到100个互不相等的值。

                            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
                            

                            但是,Symbol 值可以显式转为字符串。

                            let sym = Symbol('My symbol');
                            String(sym) // 'Symbol(My symbol)'
                            sym.toString() // 'Symbol(My symbol)'
                            

                            另外,Symbol 值也可以转为布尔值,但是不能转为数值。

                            let sym = Symbol();
                            Boolean(sym) // true
                            !sym  // false
                            if (sym) {
                              // ...
                            }
                            Number(sym) // TypeError
                            sym + 2 // TypeError
                            

                            Set 和 Map 数据结构

                            1. Set

                            基本用法

                            ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

                            Set本身是一个构造函数,用来生成 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
                            

                            上面代码通过add()方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。

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

                            // 例一
                            const set = new Set([1, 2, 3, 4, 4]);
                            [...set]
                            // [1, 2, 3, 4]
                            // 例二
                            const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
                            items.size // 5
                            // 例三
                            const set = new Set(document.querySelectorAll('div'));
                            set.size // 56
                            // 类似于
                            const set = new Set();
                            document
                             .querySelectorAll('div')
                             .forEach(div => set.add(div));
                            set.size // 56
                            

                            上面代码中,例一和例二都是Set函数接受数组作为参数,例三是接受类似数组的对象作为参数。

                            上面代码也展示了一种去除数组重复成员的方法。

                            // 去除数组的重复成员
                            [...new Set(array)]
                            

                            上面的方法也可以用于,去除字符串里面的重复字符。

                            [...new Set('ababbc')].join('')
                            // "abc"
                            

                            向 Set 加入值的时候,不会发生类型转换,所以5和"5"是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是向 Set 加入值时认为NaN等于自身,而精确相等运算符认为NaN不等于自身。

                            let set = new Set();
                            let a = NaN;
                            let b = NaN;
                            set.add(a);
                            set.add(b);
                            set // Set {NaN}
                            

                            上面代码向 Set 实例添加了两次NaN,但是只会加入一个。这表明,在 Set 内部,两个NaN是相等的。

                            另外,两个对象总是不相等的。

                            let set = new Set();
                            set.add({});
                            set.size // 1
                            set.add({});
                            set.size // 2
                            

                            上面代码表示,由于两个空对象不相等,所以它们被视为两个值。

                            Set 实例的属性和方法

                            Set 结构的实例有以下属性。

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

                              Set 实例的方法分为两大类:操作方法(用于操作数据) 和 遍历方法(用于遍历成员)。下面先介绍四个操作方法。

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

                                上面这些属性和方法的实例如下。

                                s.add(1).add(2).add(2);
                                // 注意2被加入了两次
                                s.size // 2
                                s.has(1) // true
                                s.has(2) // true
                                s.has(3) // false
                                s.delete(2) // true
                                s.has(2) // false
                                

                                下面是一个对比,判断是否包括一个键,Object结构和Set结构写法的不同。

                                // 对象的写法
                                const properties = {
                                  'width': 1,
                                  'height': 1
                                };
                                if (properties[someName]) {
                                  // do something
                                }
                                // Set的写法
                                const properties = new Set();
                                properties.add('width');
                                properties.add('height');
                                if (properties.has(someName)) {
                                  // do something
                                }
                                

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

                                const items = new Set([1, 2, 3, 4, 5]);
                                const array = Array.from(items);
                                

                                这就提供了去除数组重复成员的另一种方法。

                                function dedupe(array) {
                                  return Array.from(new Set(array));
                                }
                                dedupe([1, 1, 2, 3]) // [1, 2, 3]
                                

                                遍历操作

                                Set 结构的实例有四个遍历方法,可以用于遍历成员。

                                • Set.prototype.keys():返回键名的遍历器
                                • Set.prototype.values():返回键值的遍历器
                                • Set.prototype.entries():返回键值对的遍历器
                                • Set.prototype.forEach():使用回调函数遍历每个成员

                                  需要特别指出的是,Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。

                                  (1)keys(),values(),entries()

                                  keys方法、values方法、entries方法返回的都是遍历器对象(详见《Iterator 对象》一章)。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。

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

                                  上面代码中,entries方法返回的遍历器,同时包括键名和键值,所以每次输出一个数组,它的两个成员完全相等。

                                  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
                                  

                                  (2)forEach()

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

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

                                  上面代码说明,forEach方法的参数就是一个处理函数。该函数的参数与数组的forEach一致,依次为键值、键名、集合本身(上例省略了该参数)。这里需要注意,Set 结构的键名就是键值(两者是同一个值),因此第一个参数与第二个参数的值永远都是一样的。

                                  另外,forEach方法还可以有第二个参数,表示绑定处理函数内部的this对象。

                                  (3)遍历的应用

                                  扩展运算符(...)内部使用for...of循环,所以也可以用于 Set 结构。

                                  let set = new Set(['red', 'green', 'blue']);
                                  let arr = [...set];
                                  // ['red', 'green', 'blue']
                                  

                                  扩展运算符和 Set 结构相结合,就可以去除数组的重复成员。

                                  let arr = [3, 5, 2, 2, 5, 5];
                                  let unique = [...new Set(arr)];
                                  // [3, 5, 2]
                                  

                                  而且,数组的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}
                                  

                                  因此使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。

                                  let a = new Set([1, 2, 3]);
                                  let b = new Set([4, 3, 2]);
                                  // 并集
                                  let union = new Set([...a, ...b]);
                                  // Set {1, 2, 3, 4}
                                  // 交集
                                  let intersect = new Set([...a].filter(x => b.has(x)));
                                  // set {2, 3}
                                  // (a 相对于 b 的)差集
                                  let difference = new Set([...a].filter(x => !b.has(x)));
                                  // Set {1}
                                  

                                  如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用Array.from方法。

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

                                  上面代码提供了两种方法,直接在遍历操作中改变原来的 Set 结构。

                                  2. Map

                                  含义和基本用法

                                  JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。

                                  const data = {};
                                  const element = document.getElementById('myDiv');
                                  data[element] = 'metadata';
                                  data['[object HTMLDivElement]'] // "metadata"
                                  

                                  上面代码原意是将一个 DOM 节点作为对象data的键,但是由于对象只接受字符串作为键名,所以element被自动转为字符串[object HTMLDivElement]。

                                  为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

                                  const m = new Map();
                                  const o = {p: 'Hello World'};
                                  m.set(o, 'content')
                                  m.get(o) // "content"
                                  m.has(o) // true
                                  m.delete(o) // true
                                  m.has(o) // false
                                  

                                  上面代码使用 Map 结构的set方法,将对象o当作m的一个键,然后又使用get方法读取这个键,接着使用delete方法删除了这个键。

                                  上面的例子展示了如何向 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 实例时,就指定了两个键name和title。

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

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

                                  事实上,不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map构造函数的参数。这就是说,Set和Map都可以用来生成新的 Map。

                                  const set = new Set([
                                    ['foo', 1],
                                    ['bar', 2]
                                  ]);
                                  const m1 = new Map(set);
                                  m1.get('foo') // 1
                                  const m2 = new Map([['baz', 3]]);
                                  const m3 = new Map(m2);
                                  m3.get('baz') // 3
                                  

                                  上面代码中,我们分别使用 Set 对象和 Map 对象,当作Map构造函数的参数,结果都生成了新的 Map 对象。

                                  如果对同一个键多次赋值,后面的值将覆盖前面的值。

                                  const map = new Map();
                                  map
                                  .set(1, 'aaa')
                                  .set(1, 'bbb');
                                  map.get(1) // "bbb"
                                  

                                  上面代码对键1连续赋值两次,后一次的值覆盖前一次的值。

                                  如果读取一个未知的键,则返回undefined。

                                  new Map().get('asfddfsasadf')
                                  // undefined
                                  

                                  注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。

                                  const map = new Map();
                                  map.set(['a'], 555);
                                  map.get(['a']) // undefined
                                  

                                  上面代码的set和get方法,表面是针对同一个键,但实际上这是两个不同的数组实例,内存地址是不一样的,因此get方法无法读取该键,返回undefined。

                                  同理,同样的值的两个实例,在 Map 结构中被视为两个键。

                                  const map = new Map();
                                  const k1 = ['a'];
                                  const k2 = ['a'];
                                  map
                                  .set(k1, 111)
                                  .set(k2, 222);
                                  map.get(k1) // 111
                                  map.get(k2) // 222
                                  

                                  上面代码中,变量k1和k2的值是一样的,但是它们在 Map 结构中被视为两个键。

                                  如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0和-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefined和null也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。

                                  let map = new Map();
                                  map.set(-0, 123);
                                  map.get(+0) // 123
                                  map.set(true, 1);
                                  map.set('true', 2);
                                  map.get(true) // 1
                                  map.set(undefined, 3);
                                  map.set(null, 4);
                                  map.get(undefined) // 3
                                  map.set(NaN, 123);
                                  map.get(NaN) // 123
                                  

                                  实例的属性和操作方法

                                  Map 结构的实例有以下属性和操作方法。

                                  (1)size 属性

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

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

                                  (2)Map.prototype.set(key, value)

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

                                  const m = new Map();
                                  m.set('edition', 6)        // 键是字符串
                                  m.set(262, 'standard')     // 键是数值
                                  m.set(undefined, 'nah')    // 键是 undefined
                                  

                                  set方法返回的是当前的Map对象,因此可以采用链式写法。

                                  let map = new Map()
                                    .set(1, 'a')
                                    .set(2, 'b')
                                    .set(3, 'c');
                                  

                                  (3)Map.prototype.get(key)

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

                                  const m = new Map();
                                  const hello = function() {console.log('hello');};
                                  m.set(hello, 'Hello ES6!') // 键是函数
                                  m.get(hello)  // Hello ES6!
                                  

                                  (4)Map.prototype.has(key)

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

                                  const m = new Map();
                                  m.set('edition', 6);
                                  m.set(262, 'standard');
                                  m.set(undefined, 'nah');
                                  m.has('edition')     // true
                                  m.has('years')       // false
                                  m.has(262)           // true
                                  m.has(undefined)     // true
                                  

                                  (5)Map.prototype.delete(key)

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

                                  const m = new Map();
                                  m.set(undefined, 'nah');
                                  m.has(undefined)     // true
                                  m.delete(undefined)
                                  m.has(undefined)       // false
                                  

                                  (6)Map.prototype.clear()

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

                                  let map = new Map();
                                  map.set('foo', true);
                                  map.set('bar', false);
                                  map.size // 2
                                  map.clear()
                                  map.size // 0
                                  

                                  遍历方法

                                  Map 结构原生提供三个遍历器生成函数和一个遍历方法。

                                  • Map.prototype.keys():返回键名的遍历器。
                                  • Map.prototype.values():返回键值的遍历器。
                                  • Map.prototype.entries():返回所有成员的遍历器。
                                  • Map.prototype.forEach():遍历 Map 的所有成员。

                                    需要特别注意的是,Map 的遍历顺序就是插入顺序。

                                    const map = new Map([
                                      ['F', 'no'],
                                      ['T',  'yes'],
                                    ]);
                                    for (let key of map.keys()) {
                                      console.log(key);
                                    }
                                    // "F"
                                    // "T"
                                    for (let value of map.values()) {
                                      console.log(value);
                                    }
                                    // "no"
                                    // "yes"
                                    for (let item of map.entries()) {
                                      console.log(item[0], item[1]);
                                    }
                                    // "F" "no"
                                    // "T" "yes"
                                    // 或者
                                    for (let [key, value] of map.entries()) {
                                      console.log(key, value);
                                    }
                                    // "F" "no"
                                    // "T" "yes"
                                    // 等同于使用map.entries()
                                    for (let [key, value] of map) {
                                      console.log(key, value);
                                    }
                                    // "F" "no"
                                    // "T" "yes"
                                    

                                    上面代码最后的那个例子,表示 Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。

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

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

                                    此外,Map 还有一个forEach方法,与数组的forEach方法类似,也可以实现遍历。

                                    map.forEach(function(value, key, map) {
                                      console.log("Key: %s, Value: %s", key, value);
                                    });
                                    

                                    forEach方法还可以接受第二个参数,用来绑定this。

                                    const reporter = {
                                      report: function(key, value) {
                                        console.log("Key: %s, Value: %s", key, value);
                                      }
                                    };
                                    map.forEach(function(value, key, map) {
                                      this.report(key, value);
                                    }, reporter);
                                    

                                    上面代码中,forEach方法的回调函数的this,就指向reporter。

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

                                    (1)Map 转为数组

                                    前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(...)。

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

                                    (2)数组 转为 Map

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

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

                                    (3)Map 转为对象

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

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

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

                                    (4)对象转为 Map

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

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

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

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

                                    (5)Map 转为 JSON

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

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

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

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

                                    (6)JSON 转为 Map

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

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

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

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

                                    其他

                                    • WeakSet
                                    • WeakMap
                                    • WeakRef
                                    • FinalizationRegistry

                                      类似Java中的弱引用,这几个主要是为了配合垃圾回收机制提出的。

                                      Iterator 和 for…of 循环

                                      1. Iterator(遍历器)的概念

                                      JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。

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

                                      Iterator 的作用有三个:

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

                                        Iterator 的遍历过程是这样的。

                                        (1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

                                        (2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

                                        (3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

                                        (4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

                                        每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

                                        下面是一个模拟next方法返回值的例子。

                                        var it = makeIterator(['a', 'b']);
                                        it.next() // { value: "a", done: false }
                                        it.next() // { value: "b", done: false }
                                        it.next() // { value: undefined, done: true }
                                        function makeIterator(array) {
                                          var nextIndex = 0;
                                          return {
                                            next: function() {
                                              return nextIndex < array.length ?
                                                {value: array[nextIndex++], done: false} :
                                                {value: undefined, done: true};
                                            }
                                          };
                                        }
                                        

                                        上面代码定义了一个makeIterator函数,它是一个遍历器生成函数,作用就是返回一个遍历器对象。对数组['a', 'b']执行这个函数,就会返回该数组的遍历器对象(即指针对象)it。

                                        指针对象的next方法,用来移动指针。开始时,指针指向数组的开始位置。然后,每次调用next方法,指针就会指向数组的下一个成员。第一次调用,指向a;第二次调用,指向b。

                                        next方法返回一个对象,表示当前数据成员的信息。这个对象具有value和done两个属性,value属性返回当前位置的成员,done属性是一个布尔值,表示遍历是否结束,即是否还有必要再一次调用next方法。

                                        总之,调用指针对象的next方法,就可以遍历事先给定的数据结构。

                                        对于遍历器对象来说,done: false和value: undefined属性都是可以省略的,因此上面的makeIterator函数可以简写成下面的形式。

                                        function makeIterator(array) {
                                          var nextIndex = 0;
                                          return {
                                            next: function() {
                                              return nextIndex < array.length ?
                                                {value: array[nextIndex++]} :
                                                {done: true};
                                            }
                                          };
                                        }
                                        

                                        由于 Iterator 只是把接口规格加到数据结构之上,所以,遍历器与它所遍历的那个数据结构,实际上是分开的,完全可以写出没有对应数据结构的遍历器对象,或者说用遍历器对象模拟出数据结构。下面是一个无限运行的遍历器对象的例子。

                                        var it = idMaker();
                                        it.next().value // 0
                                        it.next().value // 1
                                        it.next().value // 2
                                        // ...
                                        function idMaker() {
                                          var index = 0;
                                          return {
                                            next: function() {
                                              return {value: index++, done: false};
                                            }
                                          };
                                        }
                                        

                                        上面的例子中,遍历器生成函数idMaker,返回一个遍历器对象(即指针对象)。但是并没有对应的数据结构,或者说,遍历器对象自己描述了一个数据结构出来。

                                        如果使用 TypeScript 的写法,遍历器接口(Iterable)、指针对象(Iterator)和next方法返回值的规格可以描述如下。

                                        interface Iterable {
                                          [Symbol.iterator]() : Iterator,
                                        }
                                        interface Iterator {
                                          next(value?: any) : IterationResult,
                                        }
                                        interface IterationResult {
                                          value: any,
                                          done: boolean,
                                        }
                                        

                                        2. 默认 Iterator 接口

                                        Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of循环(详见下文)。当使用for...of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。

                                        一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。

                                        ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内(参见《Symbol》一章)。

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

                                        上面代码中,对象obj是可遍历的(iterable),因为具有Symbol.iterator属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有next方法。每次调用next方法,都会返回一个代表当前成员的信息对象,具有value和done两个属性。

                                        ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被for...of循环遍历。原因在于,这些数据结构原生部署了Symbol.iterator属性(详见下文),另外一些数据结构没有(比如对象)。凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。

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

                                        • Array
                                        • Map
                                        • Set
                                        • String
                                        • TypedArray
                                        • 函数的 arguments 对象
                                        • NodeList 对象

                                          下面的例子是数组的Symbol.iterator属性。

                                          let arr = ['a', 'b', 'c'];
                                          let iter = arr[Symbol.iterator]();
                                          iter.next() // { value: 'a', done: false }
                                          iter.next() // { value: 'b', done: false }
                                          iter.next() // { value: 'c', done: false }
                                          iter.next() // { value: undefined, done: true }
                                          

                                          上面代码中,变量arr是一个数组,原生就具有遍历器接口,部署在arr的Symbol.iterator属性上面。所以,调用这个属性,就得到遍历器对象。

                                          对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,for...of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。

                                          对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。

                                          一个对象如果要具备可被for...of循环调用的 Iterator 接口,就必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。

                                          class RangeIterator {
                                            constructor(start, stop) {
                                              this.value = start;
                                              this.stop = stop;
                                            }
                                            [Symbol.iterator]() { return this; }
                                            next() {
                                              var value = this.value;
                                              if (value < this.stop) {
                                                this.value++;
                                                return {done: false, value: value};
                                              }
                                              return {done: true, value: undefined};
                                            }
                                          }
                                          function range(start, stop) {
                                            return new RangeIterator(start, stop);
                                          }
                                          for (var value of range(0, 3)) {
                                            console.log(value); // 0, 1, 2
                                          }
                                          

                                          上面代码是一个类部署 Iterator 接口的写法。Symbol.iterator属性对应一个函数,执行后返回当前对象的遍历器对象。

                                          下面是通过遍历器实现“链表”结构的例子。

                                          function Obj(value) {
                                            this.value = value;
                                            this.next = null;
                                          }
                                          Obj.prototype[Symbol.iterator] = function() {
                                            var iterator = { next: next };
                                            var current = this;
                                            function next() {
                                              if (current) {
                                                var value = current.value;
                                                current = current.next;
                                                return { done: false, value: value };
                                              }
                                              return { done: true };
                                            }
                                            return iterator;
                                          }
                                          var one = new Obj(1);
                                          var two = new Obj(2);
                                          var three = new Obj(3);
                                          one.next = two;
                                          two.next = three;
                                          for (var i of one){
                                            console.log(i); // 1, 2, 3
                                          }
                                          

                                          上面代码首先在构造函数的原型链上部署Symbol.iterator方法,调用该方法会返回遍历器对象iterator,调用该对象的next方法,在返回一个值的同时,自动将内部指针移到下一个实例。

                                          下面是另一个为对象添加 Iterator 接口的例子。

                                          let obj = {
                                            data: [ 'hello', 'world' ],
                                            [Symbol.iterator]() {
                                              const self = this;
                                              let index = 0;
                                              return {
                                                next() {
                                                  if (index < self.data.length) {
                                                    return {
                                                      value: self.data[index++],
                                                      done: false
                                                    };
                                                  }
                                                  return { value: undefined, done: true };
                                                }
                                              };
                                            }
                                          };
                                          

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

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

                                          NodeList 对象是类似数组的对象,本来就具有遍历接口,可以直接遍历。上面代码中,我们将它的遍历接口改成数组的Symbol.iterator属性,可以看到没有任何影响。

                                          下面是另一个类似数组的对象调用数组的Symbol.iterator方法的例子。

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

                                          注意,普通对象部署数组的Symbol.iterator方法,并无效果。

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

                                          如果Symbol.iterator方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错。

                                          var obj = {};
                                          obj[Symbol.iterator] = () => 1;
                                          [...obj] // TypeError: [] is not a function
                                          

                                          上面代码中,变量obj的Symbol.iterator方法对应的不是遍历器生成函数,因此报错。

                                          有了遍历器接口,数据结构就可以用for...of循环遍历(详见下文),也可以使用while循环遍历。

                                          var $iterator = ITERABLE[Symbol.iterator]();
                                          var $result = $iterator.next();
                                          while (!$result.done) {
                                            var x = $result.value;
                                            // ...
                                            $result = $iterator.next();
                                          }
                                          

                                          上面代码中,ITERABLE代表某种可遍历的数据结构,$iterator是它的遍历器对象。遍历器对象每次移动指针(next方法),都检查一下返回值的done属性,如果遍历还没结束,就移动遍历器对象的指针到下一步(next方法),不断循环。

                                          3. 调用 Iterator 接口的场合

                                          有一些场合会默认调用 Iterator 接口(即Symbol.iterator方法),除了下文会介绍的for...of循环,还有几个别的场合。

                                          (1)解构赋值

                                          对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。

                                          let set = new Set().add('a').add('b').add('c');
                                          let [x,y] = set;
                                          // x='a'; y='b'
                                          let [first, ...rest] = set;
                                          // first='a'; rest=['b','c'];
                                          

                                          (2)扩展运算符

                                          扩展运算符(...)也会调用默认的 Iterator 接口。

                                          // 例一
                                          var str = 'hello';
                                          [...str] //  ['h','e','l','l','o']
                                          // 例二
                                          let arr = ['b', 'c'];
                                          ['a', ...arr, 'd']
                                          // ['a', 'b', 'c', 'd']
                                          

                                          上面代码的扩展运算符内部就调用 Iterator 接口。

                                          实际上,这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。

                                          let arr = [...iterable];
                                          

                                          (3)yield*

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

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

                                          (4)其他场合

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

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

                                            4. 字符串的 Iterator 接口

                                            字符串是一个类似数组的对象,也原生具有 Iterator 接口。

                                            var someString = "hi";
                                            typeof someString[Symbol.iterator]
                                            // "function"
                                            var iterator = someString[Symbol.iterator]();
                                            iterator.next()  // { value: "h", done: false }
                                            iterator.next()  // { value: "i", done: false }
                                            iterator.next()  // { value: undefined, done: true }
                                            

                                            上面代码中,调用Symbol.iterator方法返回一个遍历器对象,在这个遍历器上可以调用 next 方法,实现对于字符串的遍历。

                                            可以覆盖原生的Symbol.iterator方法,达到修改遍历器行为的目的。

                                            var str = new String("hi");
                                            [...str] // ["h", "i"]
                                            str[Symbol.iterator] = function() {
                                              return {
                                                next: function() {
                                                  if (this._first) {
                                                    this._first = false;
                                                    return { value: "bye", done: false };
                                                  } else {
                                                    return { done: true };
                                                  }
                                                },
                                                _first: true
                                              };
                                            };
                                            [...str] // ["bye"]
                                            str // "hi"
                                            

                                            上面代码中,字符串 str 的Symbol.iterator方法被修改了,所以扩展运算符(...)返回的值变成了bye,而字符串本身还是hi。

                                            5. Iterator 接口与 Generator 函数

                                            Symbol.iterator()方法的最简单实现,还是使用 Generator 函数。

                                            let myIterable = {
                                              [Symbol.iterator]: function* () {
                                                yield 1;
                                                yield 2;
                                                yield 3;
                                              }
                                            };
                                            [...myIterable] // [1, 2, 3]
                                            // 或者采用下面的简洁写法
                                            let obj = {
                                              * [Symbol.iterator]() {
                                                yield 'hello';
                                                yield 'world';
                                              }
                                            };
                                            for (let x of obj) {
                                              console.log(x);
                                            }
                                            // "hello"
                                            // "world"
                                            

                                            上面代码中,Symbol.iterator()方法几乎不用部署任何代码,只要用 yield 命令给出每一步的返回值即可。

                                            6. 遍历器对象的 return(),throw()

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

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

                                            function readLinesSync(file) {
                                              return {
                                                [Symbol.iterator]() {
                                                  return {
                                                    next() {
                                                      return { done: false };
                                                    },
                                                    return() {
                                                      file.close();
                                                      return { done: true };
                                                    }
                                                  };
                                                },
                                              };
                                            }
                                            

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

                                            // 情况一
                                            for (let line of readLinesSync(fileName)) {
                                              console.log(line);
                                              break;
                                            }
                                            // 情况二
                                            for (let line of readLinesSync(fileName)) {
                                              console.log(line);
                                              throw new Error();
                                            }
                                            

                                            上面代码中,情况一输出文件的第一行以后,就会执行return()方法,关闭这个文件;情况二会在执行return()方法关闭文件之后,再抛出错误。

                                            注意,return()方法必须返回一个对象,这是 Generator 语法决定的。

                                            throw()方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。

                                            7. for…of 循环

                                            ES6 借鉴 C++、Java、C# 和 Python 语言,引入了for...of循环,作为遍历所有数据结构的统一的方法。

                                            一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。

                                            for...of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。

                                            数组

                                            数组原生具备iterator接口(即默认部署了Symbol.iterator属性),for...of循环本质上就是调用这个接口产生的遍历器,可以用下面的代码证明。

                                            const arr = ['red', 'green', 'blue'];
                                            for(let v of arr) {
                                              console.log(v); // red green blue
                                            }
                                            const obj = {};
                                            obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);
                                            for(let v of obj) {
                                              console.log(v); // red green blue
                                            }
                                            

                                            上面代码中,空对象obj部署了数组arr的Symbol.iterator属性,结果obj的for...of循环,产生了与arr完全一样的结果。

                                            for...of循环可以代替数组实例的forEach方法。

                                            const arr = ['red', 'green', 'blue'];
                                            arr.forEach(function (element, index) {
                                              console.log(element); // red green blue
                                              console.log(index);   // 0 1 2
                                            });
                                            

                                            JavaScript 原有的for...in循环,只能获得对象的键名,不能直接获取键值。ES6 提供for...of循环,允许遍历获得键值。

                                            var arr = ['a', 'b', 'c', 'd'];
                                            for (let a in arr) {
                                              console.log(a); // 0 1 2 3
                                            }
                                            for (let a of arr) {
                                              console.log(a); // a b c d
                                            }
                                            

                                            上面代码表明,for...in循环读取键名,for...of循环读取键值。如果要通过for...of循环,获取数组的索引,可以借助数组实例的entries方法和keys方法(参见《数组的扩展》一章)。

                                            for...of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟for...in循环也不一样。

                                            let arr = [3, 5, 7];
                                            arr.foo = 'hello';
                                            for (let i in arr) {
                                              console.log(i); // "0", "1", "2", "foo"
                                            }
                                            for (let i of arr) {
                                              console.log(i); //  "3", "5", "7"
                                            }
                                            

                                            上面代码中,for...of循环不会返回数组arr的foo属性。

                                            Set 和 Map 结构

                                            Set 和 Map 结构也原生具有 Iterator 接口,可以直接使用for...of循环。

                                            var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
                                            for (var e of engines) {
                                              console.log(e);
                                            }
                                            // Gecko
                                            // Trident
                                            // Webkit
                                            var es6 = new Map();
                                            es6.set("edition", 6);
                                            es6.set("committee", "TC39");
                                            es6.set("standard", "ECMA-262");
                                            for (var [name, value] of es6) {
                                              console.log(name + ": " + value);
                                            }
                                            // edition: 6
                                            // committee: TC39
                                            // standard: ECMA-262
                                            

                                            上面代码演示了如何遍历 Set 结构和 Map 结构。值得注意的地方有两个,首先,遍历的顺序是按照各个成员被添加进数据结构的顺序。其次,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
                                            

                                            计算生成的数据结构

                                            有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6 的数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。

                                            • entries() 返回一个遍历器对象,用来遍历[键名, 键值]组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 Iterator 接口,默认就是调用entries方法。
                                            • keys() 返回一个遍历器对象,用来遍历所有的键名。
                                            • values() 返回一个遍历器对象,用来遍历所有的键值。

                                              这三个方法调用后生成的遍历器对象,所遍历的都是计算生成的数据结构。

                                              let arr = ['a', 'b', 'c'];
                                              for (let pair of arr.entries()) {
                                                console.log(pair);
                                              }
                                              // [0, 'a']
                                              // [1, 'b']
                                              // [2, 'c']
                                              

                                              类似数组的对象

                                              类似数组的对象包括好几类。下面是for...of循环用于 字符串、DOM NodeList 对象、arguments对象的例子。

                                              // 字符串
                                              let str = "hello";
                                              for (let s of str) {
                                                console.log(s); // h e l l o
                                              }
                                              // DOM NodeList对象
                                              let paras = document.querySelectorAll("p");
                                              for (let p of paras) {
                                                p.classList.add("test");
                                              }
                                              // arguments对象
                                              function printArgs() {
                                                for (let x of arguments) {
                                                  console.log(x);
                                                }
                                              }
                                              printArgs('a', 'b');
                                              // 'a'
                                              // 'b'
                                              

                                              对于字符串来说,for...of循环还有一个特点,就是会正确识别 32 位 UTF-16 字符。

                                              for (let x of 'a\uD83D\uDC0A') {
                                                console.log(x);
                                              }
                                              // 'a'
                                              // '\uD83D\uDC0A'
                                              

                                              并不是所有类似数组的对象都具有 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...of结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。但是,这样情况下,for...in循环依然可以用来遍历键名。

                                              let es6 = {
                                                edition: 6,
                                                committee: "TC39",
                                                standard: "ECMA-262"
                                              };
                                              for (let e in es6) {
                                                console.log(e);
                                              }
                                              // edition
                                              // committee
                                              // standard
                                              for (let e of es6) {
                                                console.log(e);
                                              }
                                              // TypeError: es6[Symbol.iterator] is not a function
                                              

                                              上面代码表示,对于普通的对象,for...in循环可以遍历键名,for...of循环会报错。

                                              一种解决方法是,使用Object.keys方法将对象的键名生成一个数组,然后遍历这个数组。

                                              for (var key of Object.keys(someObject)) {
                                                console.log(key + ': ' + someObject[key]);
                                              }
                                              

                                              另一个方法是使用 Generator 函数将对象重新包装一下。

                                              const obj = { a: 1, b: 2, c: 3 }
                                              function* entries(obj) {
                                                for (let key of Object.keys(obj)) {
                                                  yield [key, obj[key]];
                                                }
                                              }
                                              for (let [key, value] of entries(obj)) {
                                                console.log(key, '->', value);
                                              }
                                              // a -> 1
                                              // b -> 2
                                              // c -> 3
                                              

                                              与其他遍历语法的比较

                                              以数组为例,JavaScript 提供多种遍历语法。最原始的写法就是for循环。

                                              for (var index = 0; index < myArray.length; index++) {
                                                console.log(myArray[index]);
                                              }
                                              

                                              这种写法比较麻烦,因此数组提供内置的forEach方法。

                                              myArray.forEach(function (value) {
                                                console.log(value);
                                              });
                                              

                                              这种写法的问题在于,无法中途跳出forEach循环,break命令或return命令都不能奏效。

                                              for...in循环可以遍历数组的键名。

                                              for (var index in myArray) {
                                                console.log(myArray[index]);
                                              }
                                              

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

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

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

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

                                                for (let value of myArray) {
                                                  console.log(value);
                                                }
                                                

                                                有着同for...in一样的简洁语法,但是没有for...in那些缺点。