ES6常用特性

Let、Const、${表达式}、箭头函数、解构等你已经常用的特性

这里先大概说一下这些特性,后面详细剖析

  • Let
    let 声明的变量作用域仅在它所在的代码块内有效。
    1
    2
    3
    4
    if(true) {
    let x = 1;
    }
    console.log(x); // undefined

了解更多,可以查看[ES6深入剖析之Let和块作用域]

  • Const
    const用来定义常量(静态变量): 一旦被定义就不能再被修改。
    1
    2
    3
    Const REG =  /^\d{1,3}$/;
    REG = /^\d{1,6}$/;
    //报错 Unexpected identifier
  1. 常量一旦被定义后就不能再被修改。
  2. 定义在块作用域内部的常量,在外部不能访问到
  3. 在定义常量的前面,是不能访问到常量的,因此我们通常要将常量定义在文件的最前面
  4. 常量是不能被重复的定义的,这是为了保证常量定义使用时候的安全型。

    • 模板字符串

      • ‘这里写的字符串可以换行’,写模板再也不用去拼接字符串了
      • ‘${这里可以写表达式}’,代替字符串拼接
        1
        2
        3
        const book = "钢铁是怎样炼成的";
        const intro = `<h1>我最喜欢的一本书是${book}</h1>
        <p>赶紧介绍一下它</p>`;
    • 箭头函数

      • 基本用法

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

      1
      2
      var f = v => v;
      f(1); //1

      上面的箭头函数等同于:

      1
      2
      3
      var f = function(v) {
      return v;
      };

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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      var f = () => 5;
      // 等同于
      var f = function () { return 5 };

      var sum = (num1, num2) => num1 + num2;
      // 等同于
      var sum = function(num1, num2) {
      return num1 + num2;
      };
      ```
      如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
      ```javascript
      var sum = (num1, num2) => { return num1 + num2; }

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

      1
      2
      3
      4
      5
      // 报错
      let getTempItem = id => { id: id, name: "Temp" };

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

      如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。

      1
      let fn = () => void doesNotReturn();

      箭头函数可以与变量解构结合使用。

      1
      2
      3
      4
      5
      6
      const full = ({ first, last }) => first + ' ' + last;

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

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

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

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

      1
      2
      3
      4
      5
      6
      7
      // 正常函数写法
      [1,2,3].map(function (x) {
      return x * x;
      });

      // 箭头函数写法
      [1,2,3].map(x => x * x);

      回调函数中有多个参数时:

      1
      2
      3
      4
      5
      6
      7
      // 正常函数写法
      var result = values.sort(function (a,b) {
      return a - b;
      });

      // 箭头函数写法
      var result = values.sort((a,b) => a - b);

      下面是 rest 参数(在函数的扩展中讲)与箭头函数结合的例子。

      1
      2
      3
      4
      5
      6
      7
      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对象的指向是可变的,但是在箭头函数中,它是固定的。
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        //普通函数
        function foo() {
        setTimeout(function() {
        console.log('id:', this.id);
        }, 100);
        }

        var id = 21;

        foo.call({ id: 42 }); // id: 21

        //箭头函数
        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。

      箭头函数可以让setTimeout里面的this,绑定定义时所在的作用域,而不是指向运行时所在的作用域。下面是另一个例子。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      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.s2从undefined变为NaN。

      箭头函数可以让this指向固定化,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      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);
      }
      };

      this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

      所以,箭头函数转成 ES5 的代码就是我们常用的this备份,如下。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      // ES6
      function foo() {
      setTimeout(() => {
      console.log('id:', this.id);
      }, 100);
      }

      // ES5
      function foo() {
      var _this = this;

      setTimeout(function () {
      console.log('id:', _this.id);
      }, 100);
      }

      请问下面的代码之中有几个this?

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      function foo() {
      return () => {
      return () => {
      return () => {
      console.log('id:', this.id);
      };
      };
      };
      }

      var f = foo.call({id: 1});

      var t1 = f.call({id: 2})()(); // id: 1
      var t2 = f().call({id: 3})(); // id: 1
      var t3 = f()().call({id: 4}); // id: 1

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

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

      1
      2
      3
      4
      5
      6
      7
      8
      function foo() {
      setTimeout(() => {
      console.log('args:', arguments);
      }, 100);
      }

      foo(2, 4, 6, 8)
      // args: [2, 4, 6, 8]

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

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

      1
      2
      3
      4
      5
      6
      (function() {
      return [
      (() => this.x).bind({ x: 'inner' })()
      ];
      }).call({ x: 'outer' });
      // ['outer']

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

  • 解构
    解构提供了一个方便地从对象或数组中提取数据的方法。

    1
    2
    3
    4
    5
    6
    let [x, y] = [1, 2]; // x = 1, y = 2

    // ES5 equivalent:
    var arr = [1, 2];
    var x = arr[0];
    var y = arr[1];

    利用解构,可以一次性给多个变量赋值。一个很好的附加用处是可以很简单地交换变量值:

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

    这让我想到一个有趣的问题:

    如何在不定义第三个变量的情况下,交换x,y的值

    1
    2
    3
    4
    let x = 1,y = 2;
    x = x + y; // x = x + y = 1 + 2 = 3
    y = x - y; // y = (x + y) - y = x = 1
    x = x - y; // x = (x + y) - x = y

    不过这里有解构我们可以更容易做到了

    解构也可以用于对象。注意对象中必须存在对应的键,不然会获取到undefined

    1
    2
    let obj = {x: 1, y: 2};
    let {x, y} = obj; // x = 1, y = 2

    也可以使用该机制来修改变量名:

    1
    2
    let obj = {x: 1, y: 2};
    let {x: a, y: b} = obj; // a = 1, b = 2

    模拟多个返回值:

    1
    2
    3
    4
    5
    function doSomething() {
    return [1, 2]
    }

    let [x, y] = doSomething(); // x = 1, y = 2

    为参数对象赋默认值。通过对象字面量,可以模拟命名参数:

    1
    2
    3
    4
    function doSomething({y = 1, z = 0}) {
    console.log(y, z);
    }
    doSomething({y: 2});

字符串方法扩展

  • startsWith(): 判断字符串是以参数字符开头的
    • 第一个参数就是字符串
    • 第二个参数表示判断的位置(可不传)
  • endsWith(): 判断元字符串是以参数字符串结尾的
    • 第一个参数是判断的字符串
    • 第二个参数表示判断的位置(可不传)
  • includes():判断字符串是否包含参数字符串
    • 第一个参数表示被包含的字符串
    • 第二个参数判断的位置(可不传)
      1
      2
      3
      4
      5
      6
      7
      8
      var str = "我们是搜狐社交产品中心前端团队",
      res1 = str.startsWith('我们', 0),
      res2 = str.endsWith('团', 14),
      res3 = str.includes('个', 2);
      console.log(res1,res2,res3) //true true false
      str.startsWith('我');str.startsWith('我们'); // true
      str.endsWith("队");str.endsWith("团队"); // true
      str.includes('是') // true
  • repeat(): 方法返回一个新字符串,表示将原字符串重复n次。

    1
    "a123".repeat(2); // "a123a123"

    Number对象的扩展

    • Number.isFanite(): 用于检查其参数是否是无穷大(不存在或者是NaN返回false,对于数字返回值true)
    • Number.isNaN(): 当参数是NaN时候,返回true,其他情况都返回false

      • 需要注意的是es5中判断一个变量是否为NaN采用isNaN()函数与这里ES6的Number.isNaN()的区别()
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      //预期应该只有NaN才会返回true

      isNaN(undefined) //true
      isNaN(NaN) //true
      isNaN('qwer') //true
      isNaN(123) //false
      //不符合预期

      Number.isNaN(undefined) //false
      Number.isNaN(NaN) //true
      Number.isNaN('qwer') //false
      Number.isNaN(123) //false
      //符合预期
    • Number.isInteger(): 用来判断一个值是否为整数

      1
      2
      3
      4
      5
      Number.isInteger(2) // true
      Number.isInteger(2.0) // true
      Number.isInteger(2.1) // false
      Number.isInteger("15") // false
      Number.isInteger(true) // false

Array对象的扩展

  • Array.from() 用来将其他对象转换成数组

    • 能转换的其他对象的要求:
      1.部署了Iterator接口的对象,比如:SetMapArray
      2.类数组对象,什么叫类数组对象,就是一个对象必须有length属性,没有length,转出来的就是空数组。

      1
      2
      3
      4
      5
      6
      7
      //转换map
      const map1 = new Map();
      map1.set('k1', 1);
      map1.set('k2', 2);
      map1.set('k3', 3);
      console.log('%s', Array.from(map1))
      //k1,1,k2,2,k3,3
      1
      2
      3
      4
      5
      //转换set
      const set1 = new Set();
      set1.add(1).add(2).add(3)
      console.log('%s', Array.from(set1))
      //1,2,3
      1
      2
      3
      4
      5
      //转换字符串
      console.log('%s', Array.from('hello world'))
      console.log('%s', Array.from('\u767d\u8272\u7684\u6d77'))
      //h,e,l,l,o, ,w,o,r,l,d
      //白,色,的,海
      1
      2
      3
      4
      5
      6
      7
      8
      //类数组对象
      console.log('%s', Array.from({
      0: '0',
      1: '1',
      3: '3',
      length:4
      }))
      //0,1,,3
    • Array.from可以接受三个参数

      1
      Array.from(arrayLike[, mapFn[, thisArg]])

    arrayLike:被转换的的对象;
    mapFn:map函数;
    thisArg:map函数中this指向的对象;

  • Array.of(): 将一组值转化成一个数组,为了解决源生Array构造函数创建数组的一个问题

    • Array当传递参数不同,传递的参会表示不同的含义,如果一个参数,这个参数表示的数组的长度,当传递两个或者多个参数时候,这些表示数组的成员,of为了解决Array构造的参数不同得到结果行为不一致的问题,参数表示数组的成员,不论参数多少个
      1
      2
      console.log(Array.of(5)) //[5]
      console.log(Array.of(5, 6)) //[5,6]
数组实例的扩展
  • copyWithin():在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。
    1
    Array.prototype.copyWithin(target, start = 0, end = this.length)

它接受三个参数,这三个参数都应该是数值,如果不是,会自动转为数值:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]
// 将3号位复制到0号位

[1, 2, 3, 4, 5].copyWithin(0, 3, 4)
// [4, 2, 3, 4, 5]

// -2相当于3号位,-1相当于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)
// [4, 2, 3, 4, 5]

// 将3号位复制到0号位
[].copyWithin.call({length: 5, 3: 1}, 0, 3)
// {0: 1, 3: 1, length: 5}

// 将2号位到数组结束,复制到0号位
let i32a = new Int32Array([1, 2, 3, 4, 5]); //32 位整数值的类型化数组
i32a.copyWithin(0, 2);
// Int32Array [3, 4, 5, 4, 5]

// 对于没有部署 TypedArray 的 copyWithin 方法的平台
// 需要采用下面的写法
[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
// Int32Array [4, 2, 3, 4, 5]
  • find() 和 findIndex():
  • find(): 数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined
    1
    2
    3
    [1, 3, 5, 7, 9, 11].find(function(value, index, arr) {
    return value > 9;
    }) // 11

可以看出,find方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。

  • findIndex():数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
    1
    2
    3
    [1, 3, 5, 7, 9, 11].findIndex(function(value, index, arr) {
    return value > 9;
    }) // 5

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

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

1
2
3
4
5
[NaN].indexOf(NaN)
// -1

[NaN].findIndex(y => Object.is(NaN, y))
// 0

上面代码中,indexOf方法无法识别数组的NaN成员,但是findIndex方法可以借助Object.is方法做到。

  • fill():fill方法使用给定值,填充一个数组。
    1
    2
    3
    4
    5
    ['a', 'b', 'c'].fill(7)
    // [7, 7, 7]

    new Array(3).fill(7)
    // [7, 7, 7]

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

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

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

  • entries(),keys() 和 values():

    entries(),keys()和values()——用于遍历数组。它们都返回一个遍历器(Iterator)对象,可以用for…of循环进行遍历。

keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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"

  • includes():回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。
    1
    2
    3
    [1, 2, 3].includes(2)     // true
    [1, 2, 3].includes(4) // false
    [1, 2, NaN].includes(NaN) // true

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

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

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

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

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

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

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

1
2
[NaN].includes(NaN)
// true

另外,Map 和 Set 数据结构有一个has方法,需要注意与includes区分。

  • Map 结构的has方法,是用来查找键名的,比如Map.prototype.has(key)、WeakMap.prototype.has(key)、Reflect.has(target, propertyKey)。
  • Set 结构的has方法,是用来查找值的,比如Set.prototype.has(value)、WeakSet.prototype.has(value)。