ES6小记

  • 变量的解构赋值


  • 默认值生效的条件是,对象的属性值严格等于(===)undefined
var {x = 3} = {x: undefined};
x // 3

var {x = 3} = {x: null};
x // null
//-----
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]
//-----
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]
复制代码

  • 如果要将一个已经声明的变量用于解构赋值,必须非常小心。下面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。

  • 下面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。关于圆括号与解构赋值的关系,参见下文。

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

// 正确的写法
let x;
({x} = {x: 1});
复制代码

  • 解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefinednull无法转为对象,所以对它们进行解构赋值,都会报错。
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
复制代码

  • 圆括号问题

    解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。

    由此带来的问题是,如果模式中出现圆括号怎么处理。ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。

    但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号。

    • 不能使用圆括号的情况

      • 变量声明语句(因为它们都是变量声明语句,模式不能使用圆括号。)

        // 全部报错
        let [(a)] = [1];
        
        let {x: (c)} = {};
        let ({x: c}) = {};
        let {(x: c)} = {};
        let {(x): c} = {};
        
        let { o: ({ p: p }) } = { o: { p: 2 } };
        复制代码
      • 函数参数(函数参数也属于变量声明,因此不能带有圆括号。)

        // 报错
        function f([(z)]) { return z; }
        // 报错
        function f([z,(x)]) { return x; }
        复制代码
      • 赋值语句的模式(代码将整个模式放在圆括号之中,导致报错。)

        // 全部报错
        ({ p: a }) = { p: 42 };
        ([a]) = [5];
        复制代码
        // 报错 代码将一部分模式放在圆括号之中,导致报错。
        [({ p: a }), { x: c }] = [{}, {}];
        复制代码
    • 可以使用圆括号的情况

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

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

    • 正则的扩展


    • 实际上,y修饰符号隐含了头部匹配的标志^

    • ES6 还为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。


    • 具名组匹配:

      const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
      
      const matchObj = RE_DATE.exec('1999-12-31');
      const year = matchObj.groups.year; // "1999"
      const month = matchObj.groups.month; // "12"
      const day = matchObj.groups.day; // "31"
      复制代码
    • 解构赋值和替换:字符串替换时,使用$<组名>引用具名组。

      let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
      
      '2015-01-02'.replace(re, '$<day>/$<month>/$<year>')
      // '02/01/2015'
      复制代码
    • 上面代码中,replace方法的第二个参数是一个字符串,而不是正则表达式。

      replace方法的第二个参数也可以是函数,该函数的参数序列如下。

      '2015-01-02'.replace(re, (
         matched, // 整个匹配结果 2015-01-02
         capture1, // 第一个组匹配 2015
         capture2, // 第二个组匹配 01
         capture3, // 第三个组匹配 02
         position, // 匹配开始的位置 0
         S, // 原字符串 2015-01-02
         groups // 具名组构成的一个对象 {year, month, day}
       ) => {
       let {day, month, year} = groups;
       return `${day}/${month}/${year}`;
      });
      复制代码

      具名组匹配在原来的基础上,新增了最后一个函数参数:具名组构成的一个对象。函数内部可以直接对这个对象进行解构赋值。


    • 引用:

      如果要在正则表达式内部引用某个“具名组匹配”,可以使用\k<组名>的写法。

      const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;
      RE_TWICE.test('abc!abc') // true
      RE_TWICE.test('abc!ab') // false
      复制代码

      数字引用(\1)依然有效。

      const RE_TWICE = /^(?<word>[a-z]+)!\1$/;
      RE_TWICE.test('abc!abc') // true
      RE_TWICE.test('abc!ab') // false
      复制代码

      这两种引用语法还可以同时使用。

      const RE_TWICE = /^(?<word>[a-z]+)!\k<word>!\1$/;
      RE_TWICE.test('abc!abc!abc') // true
      RE_TWICE.test('abc!abc!ab') // false
      复制代码

    • 格式化金额数字

      function formatNumberToMoney(money) {
        const reg = /(?<=\d)(\d{3})/g;
      	// 为了便于将大数也格式化BigInit,先将末位的n去掉  
        return (`${money}` || "").replace("n", "").replaceAll(reg, ",$1");
      }
      
      function formatNumberToMoney(money) {
        return Number(money).toLocalString();
      }
      复制代码

  • 函数

    • 使用参数默认值时,函数不能有同名参数。
    // 不报错
    function foo(x, x, y) {
      // ...
    }
    
    // 报错
    function foo(x, x, y = 1) {
      // ...
    }
    // SyntaxError: Duplicate parameter name not allowed in this context
    复制代码

    另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。

    let x = 99;
    function foo(p = x + 1) {
      console.log(p);
    }
    
    foo() // 100
    
    x = 100;
    foo() // 101
    复制代码

    上面代码中,参数p的默认值是x + 1。这时,每次调用函数foo,都会重新计算x + 1,而不是默认p等于 100。


    • 函数的length属性

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

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

      上面代码中,length属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。比如,上面最后一个函数,定义了 3 个参数,其中有一个参数c指定了默认值,因此length属性等于3减去1,最后得到2

      这是因为length属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest 参数也不会计入length属性。

      (function(...args) {}).length // 0
      复制代码

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

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

    • 作用域

      一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。

      var x = 1;
      
      function f(x, y = x) {
        console.log(y);
      }
      
      f(2) // 2
      复制代码

      上面代码中,参数y的默认值等于变量x。调用函数f时,参数形成一个单独的作用域。在这个作用域里面,默认值变量x指向第一个参数x,而不是全局变量x,所以输出是2

      再看下面的例子。

      let x = 1;
      
      function f(y = x) {
        let x = 2;
        console.log(y);
      }
      
      f() // 1
      复制代码

      上面代码中,函数f调用时,参数y = x形成一个单独的作用域。这个作用域里面,变量x本身没有定义,所以指向外层的全局变量x。函数调用时,函数体内部的局部变量x影响不到默认值变量x

      如果此时,全局变量x不存在,就会报错。

      function f(y = x) {
        let x = 2;
        console.log(y);
      }
      
      f() // ReferenceError: x is not defined
      复制代码

      下面这样写,也会报错。

      var x = 1;
      
      function foo(x = x) {
        // ...
      }
      
      foo() // ReferenceError: x is not defined
      复制代码

      上面代码中,参数x = x形成一个单独作用域。实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错”x 未定义“。

      如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。请看下面的例子。

      let foo = 'outer';
      
      function bar(func = () => foo) {
        let foo = 'inner';
        console.log(func());
      }
      
      bar(); // outer
      复制代码

      上面代码中,函数bar的参数func的默认值是一个匿名函数,返回值为变量foo。函数参数形成的单独作用域里面,并没有定义变量foo,所以foo指向外层的全局变量foo,因此输出outer

      如果写成下面这样,就会报错。

      function bar(func = () => foo) {
        let foo = 'inner';
        console.log(func());
      }
      
      bar() // ReferenceError: foo is not defined
      复制代码

      上面代码中,匿名函数里面的foo指向函数外层,但是函数外层并没有声明变量foo,所以就报错了。

      下面是一个更复杂的例子。

      var x = 1;
      function foo(x, y = function() { x = 2; }) {
        var x = 3;
        y();
        console.log(x);
      }
      
      foo() // 3
      x // 1
      复制代码

      上面代码中,函数foo的参数形成一个单独作用域。这个作用域里面,首先声明了变量x,然后声明了变量yy的默认值是一个匿名函数。这个匿名函数内部的变量x,指向同一个作用域的第一个参数x。函数foo内部又声明了一个内部变量x,该变量与第一个参数x由于不是同一个作用域,所以不是同一个变量,因此执行y后,内部变量x和外部全局变量x的值都没变。

      如果将var x = 3var去除,函数foo的内部变量x就指向第一个参数x,与匿名函数内部的x是一致的,所以最后输出的就是2,而外层的全局变量x依然不受影响。

      var x = 1;
      function foo(x, y = function() { x = 2; }) {
        x = 3;
        y();
        console.log(x);
      }
      
      foo() // 2
      x // 1
      复制代码

  • 函数的扩展

    • 严格模式:

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

      function doSomething(a, b) {
        'use strict';
        // code
      }
      复制代码

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

      // 报错
      function doSomething(a, b = a) {
        'use strict';
        // code
      }
      
      // 报错
      const doSomething = function ({a, b}) {
        'use strict';
        // code
      };
      
      // 报错
      const doSomething = (...a) => {
        'use strict';
        // code
      };
      
      const obj = {
        // 报错
        doSomething({a, b}) {
          'use strict';
          // code
        }
      };
      复制代码

      这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。

      // 报错
      function doSomething(value = 070) {
        'use strict';
        return value;
      }
      复制代码

      上面代码中,参数value的默认值是八进制数070,但是严格模式下不能用前缀0表示八进制,所以应该报错。但是实际上,JavaScript 引擎会先成功执行value = 070,然后进入函数体内部,发现需要用严格模式执行,这时才会报错。

      虽然可以先解析函数体代码,再执行参数代码,但是这样无疑就增加了复杂性。因此,标准索性禁止了这种用法,只要参数使用了默认值、解构赋值、或者扩展运算符,就不能显式指定严格模式。

      两种方法可以规避这种限制。第一种是设定全局性的严格模式,这是合法的。

      'use strict';
      
      function doSomething(a, b = a) {
        // code
      }
      复制代码

      第二种是把函数包在一个无参数的立即执行函数里面。

      const doSomething = (function () {
        'use strict';
        return function(value = 42) {
          return value;
        };
      }());
      复制代码

    • 箭头函数

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

      (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);
        // 普通函数
        setInterval(function () {
          this.s2++;
        }, 1000);
      }
      
      var timer = new Timer();
      
      setTimeout(() => console.log('s1: ', timer.s1), 3100);
      setTimeout(() => console.log('s2: ', timer.s2), 3100);
      // s1: 3
      // s2: 0
      复制代码

      上面代码中,Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数),后者的this指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1被更新了 3 次,而timer.s2一次都没更新。

      箭头函数实际上可以让this指向固定化,绑定this使得它不再可变,这种特性很有利于封装回调函数。下面是一个例子,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的指向有几个?

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

      答案是this的指向只有一个,就是函数foothis,这是因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。所以不管怎么嵌套,t1t2t3都输出同样的结果。如果这个例子的所有内层函数都写成普通函数,那么每个函数的this都指向运行时所在的不同对象。

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

      function foo() {
        setTimeout(() => {
          console.log('args:', arguments);
        }, 100);
      }
      
      foo(2, 4, 6, 8)
      // args: [2, 4, 6, 8]
      复制代码

      上面代码中,箭头函数内部的变量arguments,其实是函数fooarguments变量。

      另外,由于箭头函数没有自己的this,所以当然也就不能用call()apply()bind()这些方法去改变this的指向。

      (function() {
        return [
          (() => this.x).bind({ x: 'inner' })()
        ];
      }).call({ x: 'outer' });
      // ['outer']
      复制代码

      上面代码中,箭头函数没有自己的this,所以bind方法无效,内部的this指向外部的this

      长期以来,JavaScript 语言的this对象一直是一个令人头痛的问题,在对象方法中使用this,必须非常小心。箭头函数”绑定”this,很大程度上解决了这个困扰。


    • 不适合使用箭头函数的场合

      由于箭头函数使得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就会动态指向被点击的按钮对象。

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


    • 尾调用优化

      尾调用之所以与其他调用不同,就在于它的特殊的调用位置。

      我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到AB的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。

      尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。

      function f() {
        let m = 1;
        let n = 2;
        return g(m + n);
      }
      f();
      
      // 等同于
      function f() {
        return g(3);
      }
      f();
      
      // 等同于
      g(3);
      复制代码

      上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量mn的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除f(x)的调用帧,只保留g(3)的调用帧。

      这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。

      注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。

      function addOne(a){
        var one = 1;
        function inner(b){
          return b + one;
        }
        return inner(a);
      }
      复制代码

      上面的函数不会进行尾调用优化,因为内层函数inner用到了外层函数addOne的内部变量one

      注意,目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持。


    • 尾递归

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

      递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

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

      上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。

      如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

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

      还有一个比较著名的例子,就是计算 Fibonacci 数列,也能充分说明尾递归优化的重要性。

      非尾递归的 Fibonacci 数列实现如下。

      function Fibonacci (n) {
        if ( n <= 1 ) {return 1};
      
        return Fibonacci(n - 1) + Fibonacci(n - 2);
      }
      
      Fibonacci(10) // 89
      Fibonacci(100) // 超时
      Fibonacci(500) // 超时
      复制代码

      尾递归优化过的 Fibonacci 数列实现如下。

      function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
        if( n <= 1 ) {return ac2};
      
        return Fibonacci2 (n - 1, ac2, ac1 + ac2);
      }
      
      Fibonacci2(100) // 573147844013817200000
      Fibonacci2(1000) // 7.0330367711422765e+208
      Fibonacci2(10000) // Infinity
      复制代码

      由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 亦是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出(或者层层递归造成的超时),相对节省内存。


  • 数组的扩展

  • 数组的空位

    数组的空位指,数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位。

    Array(3) // [, , ,]
    复制代码

    上面代码中,Array(3)返回一个具有 3 个空位的数组。

    注意,空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。

    0 in [undefined, undefined, undefined] // true
    0 in [, , ,] // false
    复制代码

    上面代码说明,第一个数组的 0 号位置是有值的,第二个数组的 0 号位置没有值。

    ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。

    • forEach(), filter(), reduce(), every()some()都会跳过空位。
    • map()会跳过空位,但会保留这个值
    • join()toString()会将空位视为undefined,而undefinednull会被处理成空字符串。
    // forEach方法
    [,'a'].forEach((x,i) => console.log(i)); // 1
    
    // filter方法
    ['a',,'b'].filter(x => true) // ['a','b']
    
    // every方法
    [,'a'].every(x => x==='a') // true
    
    // reduce方法
    [1,,2].reduce((x,y) => x+y) // 3
    
    // some方法
    [,'a'].some(x => x !== 'a') // false
    
    // map方法
    [,'a'].map(x => 1) // [,1]
    
    // join方法
    [,'a',undefined,null].join('#') // "#a##"
    
    // toString方法
    [,'a',undefined,null].toString() // ",a,,"
    复制代码

    ES6 则是明确将空位转为undefined

    Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。

    Array.from(['a',,'b'])
    // [ "a", undefined, "b" ]
    复制代码

    扩展运算符(...)也会将空位转为undefined

    [...['a',,'b']]
    // [ "a", undefined, "b" ]
    复制代码

    copyWithin()会连空位一起拷贝。

    [,'a','b',,].copyWithin(2,0) // [,"a",,"a"]
    复制代码

    fill()会将空位视为正常的数组位置。

    new Array(3).fill('a') // ["a","a","a"]
    复制代码

    for...of循环也会遍历空位。

    let arr = [, ,];
    for (let i of arr) {
      console.log(1);
    }
    // 1
    // 1
    复制代码

    上面代码中,数组arr有两个空位,for...of并没有忽略它们。如果改成map方法遍历,空位是会跳过的。

    entries()keys()values()find()findIndex()会将空位处理成undefined

    // entries()
    [...[,'a'].entries()] // [[0,undefined], [1,"a"]]
    
    // keys()
    [...[,'a'].keys()] // [0,1]
    
    // values()
    [...[,'a'].values()] // [undefined,"a"]
    
    // find()
    [,'a'].find(x => true) // undefined
    
    // findIndex()
    [,'a'].findIndex(x => true) // 0
    复制代码

    由于空位的处理规则非常不统一,所以建议避免出现空位。


  • 待更新

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享