es6 教程

数组的扩展

扩展运算符

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

应用

  • 复制数组

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

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

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

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

ES5 只能用变通方法来复制数组。

const a1 = [1, 2];
const a2 = a1.concat();

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

上面代码中,a1 会返回原数组的克隆,再修改 a2 就不会对 a1 产生影响。

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

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

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

  • 合并数组

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

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

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

  • 与解构赋值结合

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

// 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];
// 报错
  • 字符串

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

[...'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 操作就不正确。

  • 实现了 Iterator 接口的对象

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

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

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

对于那些没有部署 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 转为真正的数组。

  • 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
  • 改变函数的调用

将数组作为函数的参数传递进去的时候,通常使用的方法是 Function.prototype.apply,例如下面的例子:

var students = ['Abby','Andy'];
var otherStudents = ['Jane','Tom'];
Array.prototype.push.apply(students,otherStudents);
console.log(students); // Abby Andy Jane Tom

这样写起来就不太方便,现在有了延展操作符,它可以将数组,更确切的说是(可遍历对象)进行展开然后作为函数入参进行调用。现在只需要像下面简单地直接调用 push 方法就行了。

var students = ['Abby','Andy'];
var otherStudents = ['Jane','Tom'];
students.push(...otherStudents);
console.log(students); // Abby Andy Jane Tom

参考:ECMAScript 6 入门 by 阮一峰 数组的扩展 - 数组的扩展

函数的扩展

箭头函数注意点

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

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

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

class的继承

new.target 属性

new 是从构造函数生成实例对象的命令。ES6new 命令引入了一个 new.target 属性,该属性一般用在构造函数之中,返回 new 命令作用于的那个构造函数。如果构造函数不是通过 new 命令调用的,new.target 会返回 undefined,因此这个属性可以用来确定构造函数是怎么调用的。

function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

// 另一种写法
function Person(name) {
  if (new.target === Person) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三');  // 报错

上面代码确保构造函数只能通过 new 命令调用。

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

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    this.length = length;
    this.width = width;
  }
}

var obj = new Rectangle(3, 4); // 输出 true

需要注意的是,子类继承父类时,new.target 会返回子类。

class Rectangle {
  constructor(length, width) {
    console.log(new.target === Rectangle);
    // ...
  }
}

class Square extends Rectangle {
  constructor(length) {
    super(length, length);
  }
}

var obj = new Square(3); // 输出 false

上面代码中,new.target 会返回子类。

利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。

class Shape {
  constructor() {
    if (new.target === Shape) {
      throw new Error('本类不能实例化');
    }
  }
}

class Rectangle extends Shape {
  constructor(length, width) {
    super();
    // ...
  }
}

var x = new Shape();  // 报错
var y = new Rectangle(3, 4);  // 正确

上面代码中,Shape 类不能被实例化,只能用于继承。

注意,在函数外部,使用 new.target 会报错。

ES6 class extends null 的语法设计是不是毫无意义?

问题描述

class X extends null { }

ES6 的类语法中,一个继承的特例是继承自 null。我本以为这可以生成一个不继承自 Object.prototype 的纯净原型链,而事实是这个类将无法直接 new,而需要:

class X extends null {
  constructor() { return Object.create(null); }
}

可问题是,这种情况下,类中的方法等将都被无视、继承关系也将被无视,例如:

class X extends null {
    constructor(){ return Object.create(null); }
    methodX(){ }
}
'methodX' in new X; // false
class Y extends X {
    methodY(){ }
}
'methodY' in new Y; // false

那还要这个语法有何用?就算不 extends null,也可以在构造函数中 return 任何自定义的对象。

同时,我说的需求却被晾在一边,只能这样实现:

function Null(){}
Null.prototype = Object.create(null);
class X extends Null { }

答案出自知乎紫云飞(阿里巴巴集团阿里妈妈事业部前端工程师)

ES6 里的 class 根据有没有 extends 分句可以分为两种,基类和派生类。基类的 constructor 方法必须不能包含 super(),否则在 parse 阶段就会报一个静态错误, 而相反,派生类必须要调用 super(),否则会在类实例化时报一个运行时错误。

有一个比较特殊的派生类写法就是题干里说的 extends null,在当年制定 ES6 规范的时候,对于这种写法有另外两个选择,要么在类声明时让它报错,也就是 extends 后面的表达式不允许为 null ;要么让它等同于没有写 extends,也就是相当于写一个基类。无论选择哪种,其实都是禁用掉这种语法了。

ES6 规范最终采用的是现在的处理方法,extends null 会正常声明出一个派生类,而且这个类的 prototype 属性的原型是 null,也就是说这个类的实例不会像不写 extends 那样继承自 Object.prototype,而是会继承自 null,就像之前用 Object.create(null) 创建的对象一样(当然,多了一层原型链)。

出发点是这样的,让它物有所用,但是就像你看到的,直到 2015 年 6 月 ES6 规范发布,也没来得及把它弄完美,其实也很正常,ES6 的改动实在是太大了,有很多更重要的事去讨论,去完善,这个不太常用的东西就被忽略了。就像你看到的,在带有 extends null 的派生类中执行 super() 会转而去实例化它的父类也就是 Function.prototype,而它却不是个构造函数,所以就报错了。

在 2016 年,有人提 issueextends null 这个语法是个完全没法用的垃圾,需要修复,然后当时 ES8 的编辑就想到个很简单的修复办法,就是把它规定成是一个基类,基类就不再需要调用 super() 了,也就不会报错了。修复之后 class extends null {}class {} 的区别就仅仅只有实例的原型链不同了,前者指向 null,后者指向 Object.prototype,看起来很完美。

直到 2017 年年初的时候,浏览器们开始实现这个改动,Chakra 的人啥都没说就按规范实现了,但 V8 的人在实现的时候觉得这个改动虽然让 extends null 变的有用了,可带来了其它的小问题:

1.普通的基类里写了 super() 会是个静态错误,而 extends null 这种基类里可以写 super(),只要执行不到就不会报错,不统一了。为什么不能也是个静态错误?因为 extends 后面可能是个表达式,而那个表达式在运行时才能知道是不是 null

class Base1  {
 constructor() {
   // early error
   super();
 }
}

class Base2 extends null {
 constructor() {
   // allowed, but will throw if run
   if (false) super();
 }
}
  1. extends 后面是个表达式的的时候需要分情况判断该不该调用 super() ,可能是个坑。
function MaybeDerived(base) {
 return class extends base {
   constructor() {
     // if derived, must call super; if base, must not
     if (base !== null) super();
   }
 };
}

然后 V8 的这个工程师建议规范再次做个改动,只针对 null 字面量做特殊处理,只在 extends null 的时候才会产生基类(就可以静态检测有没有写 super() 了),extends someNullExpression 还是按照 ES6 的方式产生派生类。然后 TC39 的其它人不同意,认为 extends nullextends someNullExpression 有不同的表现的话,就更糟糕了,然后大家一致认为,如果没有好的解决办法,就把原先的改动先回滚到 ES6 的表现吧,未来想到更完美的解决办法再改。

然后到 2017 年年底还是没有解决,然后 ES6 的编辑发了 issue 继续讨论这个问题,因为这毕竟算是他的 bug,不过至今依然没有讨论出结果。

如果现在你想写一个实例原型的原型为 null 的类,可以用:

class C extends null {
  constructor() {
    return Object.create(new.target.prototype)
  }

  methodX() {}
}

或者:

class C {
  methodX() {}
}

Object.setPrototypeOf(C.prototype, null)

原文:ES6 class extends null 的语法设计是不是毫无意义?

Generator 函数

概念

  1. Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
  2. 语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
  3. 执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

为什么es6的generator函数被设计为不支持箭头函数写法,而async函数却可以?

为什么es6的generator函数被设计为不支持箭头函数写法,而async函数却可以?

答案待定

参考:

  1. 为什么es6的generator函数被设计为不支持剪头函数写法,而async函数却可以?

Module

严格模式

ES6的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

严格模式主要有以下限制:

.....

禁止this指向全局对象

不能使用fn.caller和fn.arguments获取函数调用的堆栈

增加了保留字(比如protected、static和interface)

上面这些限制,模块都必须遵守。由于严格模式是ES5引入的,不属于ES6,所以请参阅相关ES5书籍,本书不再详细介绍了。

其中,尤其需要注意this的限制。ES6模块之中,顶层的this指向undefined,即不应该在顶层代码使用this

上次更新: 2018-7-17 18:00:22