ECMAScript2015(es6)新特性

前言

ECMAScript2015(es6)新特性

正文

letconst

在还没有es6的时候,定义变量都是用关键字var来进行定义。

var定义的变量是基于函数作用域的,变量会进行提升。

1
2
3
4
5
6
7
function fn() {
{
var a = 0;
}
console.log(a);
}
fn();

运行之后输出了0,上面这段代码和下面这段等价。

1
2
3
4
5
6
7
8
function fn() {
var a;
{
a = 0;
}
console.log(a);
}
fn();

而如果使用了letconst来定义变量的话。

上面的代码就会报错,因为这两个关键字定义的变量是基于块级作用域的,变量不会提升。

1
2
3
4
5
6
7
function fn() {
{
let a = 0;
}
console.log(a);
}
fn(); // 报错

上面变量a的作用域就只有在离他最近的一对大括号里面,而外层其实是没有定义的,所以报错。

1
2
3
4
5
6
7
8
function fn() {
let a = 1;
{
let a = 0;
}
console.log(a);
}
fn(); // 输出1

内层的定义不会影响到外层,他们的作用域是不同的。

1
2
3
4
5
6
7
8
function fn() {
let a = 1;
{
var a = 0;
}
console.log(a);
}
fn(); // 报错

上面这段代码报错的原因是变量大括号内由var定义的a提升到函数的最顶端了。

但是外层已经有let进行定义了,导致了变量的重复定义。

上面的代码可以等效为下面这段:

1
2
3
4
5
6
7
8
9
function fn() {
var a;
let a = 1; // 重复定义
{
a = 0;
}
console.log(a);
}
fn(); // 报错

这也叫做暂时性死区(temporal dead zone,简称 TDZ),在同一个作用域中,letconst定义的变量之前都不能使用这个变量。

对于varlet,也有另一个有趣的现象,这个现象出现在循环中。

1
2
3
4
5
6
7
8
9
var fn = [];

for (var i = 0; i < 3; i++) {
fn[i] = function () {
console.log(i);
};
}

fn[2](); //输出3

上面的代码从我们人的角度看应该输出2的。

输出3的原因就是变量i的作用域不是在for循环内的,而是在for循环外。

这样函数内部输出的就是最外层同一个变量i了。

如果使用了let,那么这个奇怪的现象就会消失。

1
2
3
4
5
6
7
8
9
const fn = [];

for (let i = 0; i < 3; i++) {
fn[i] = function () {
console.log(i);
};
}

fn[2](); //输出2

使用了let,是的每次循环内的i都是不同的作用域,也就是每次循环都会生成一个新的i

当然如果不使用let,也可以通过闭包来解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
var fn = [];

for (var i = 0; i < 3; i++) {
fn[i] = (function (i) {
return function () {
console.log(i);
};
})(i);
}

fn[2](); //输出2

通过创建一个立即执行函数来创建一个新的作用域。

从而保证函数内部输出的i不会是for外部的i

之前刚开始接触js的时候,被这种情况给坑过。

场景就是对一组按钮进行点击事件的绑定,绑定的内部逻辑需要用到当前按钮的索引。

1
2
3
4
5
6
7
8
9
let buttons = document.getElementsByClassName("...");

for (var i = 0; i < buttons.length; i++) {
buttons[i].onClick = function () {
// 使用i变量
// 比如:
console.log(i);
};
}

上面的例子无论哪个按钮,点击获取的i都会是buttons的长度(因为在i === buttons.length的时候跳出了循环)。

当然现在基本上都用letconst来定义变量进行使用了。

只要遵循先定义,再使用的话,就基本不会出现类似的情况了。

letconst的区别就是let定义的变量可以被重新赋值,而const不行。

1
2
const a = 1;
a = 2; // 报错

如果const定义的是一个对象的话,那么是可以改变对象内部的属性值的。

但是不能重新赋予一个新的对象。

简单点理解就是变量指向对象的地址是不能改变的。

1
2
3
4
5
6
7
8
const a = {
val: 1,
};

a.val = 2; // 可以
a = {
val: 2,
}; // 报错

class

之前如果想要new一个对象的话,一般是通过编写一个函数,再在函数的原型式挂载方法来实现。

es6 引入了class,类这个概念。

可以以传统的面向对象的方式来编写代码,但是本质上还是一种语法糖,js的继承实现还是基于原型链的。

1
2
3
4
5
6
7
8
9
10
11
12
class Student {
constructor(id, name) {
this.id = id;
this.name = name;
}
say() {
console.log(`I am a student named '${this.name}'whose id is ${this.id}.`);
}
}

const student = new Student("10086", "Dedicatus545");
student.say(); // 输出 I am a student named 'Dedicatus545' whose id is 10086.

我们可以使用babel来看看对于低版本的浏览器,是如何进行编译的。

这里以Student这个类来进行进行babel编译。

在线的地址 Babel 中文网 · Babel - 下一代 JavaScript 语法的编译器

编译之后发现,对于不同的类都有几段相同的代码,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function _instanceof(left, right) {
if (
right != null &&
typeof Symbol !== "undefined" &&
right[Symbol.hasInstance]
) {
return !!right[Symbol.hasInstance](left);
} else {
return left instanceof right;
}
}

function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}

function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}

function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}

然后主体的代码为下面这段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var Student = /*#__PURE__*/ (function () {
function Student(id, name) {
_classCallCheck(this, Student);

this.id = id;
this.name = name;
}

_createClass(Student, [
{
key: "say",
value: function say() {
console.log(
"I am a student named "
.concat(this.name, " whose id is ")
.concat(this.id, ".")
);
},
},
]);

return Student;
})();

主体代码通过一个立即执行函数来进行构造函数的初始化。

首先在Student的构造函数中调用了_classCallCheck函数,也就是每次new一个新的对象都会执行这个函数。

_classCallCheck通过_instanceof来判断是否抛出错误。

_instanceof可以简单理解为关键字instanceof,只不过以函数的形式进行表示,并且内部也尝试使用Symbol.hasInstance这个静态的函数来判断。

当然使用instanceof内部也是通过执行Symbol.hasInstance来判断的,但是instanceof可以兼容更低的版本。

因为Symbol也是 es6 才提出来的。

MDN 上显示,ie 现在完全不支持Symbol

instanceofie5以上就支持了。

感觉微软都要放弃ie了,即使是ie11,感觉用的人也越来越少了,新版edge的内核还是用的谷歌的内核,以后兼容应该会越来越容易吧。

回到Student构造函数,通过上面的分析可以总结出来,也就是不能通过直接调用Student函数,不然会报错。

1
2
new Student(); // 可以
Student(); // 报错 Cannot call a class as a function

因为此时的函数内部的this指向的不是一个Student的实例对象。

既然不是Student的实例对象,那么就this instanceof Student一定返回false,而false情况就会触发函数_classCallCheck抛出错误。

接下来就是通过_createClass来挂载方法了。

挂载的方法分为原型上挂载和静态挂载,也就是对象方法和类方法。

可以简单地写个静态方法测试下。

_createClass传入构造器,对象方法描述符数组,类方法描述符数组,然后通过_defineProperties来完成挂载。

extendsuper

es6class也支持extendsuper关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
class People {
constructor(id, name) {
this.id = id;
this.name = name;
}
}

class Student extends People {
constructor(id, name, age) {
super(id, name);
this.age = age;
}
}

同样,我们可以看看这段代码,经过babel编译成兼容es5的代码是怎么样的。

公共函数部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
function _typeof(obj) {
"@babel/helpers - typeof";
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof = function _typeof(obj) {
return typeof obj;
};
} else {
_typeof = function _typeof(obj) {
return obj &&
typeof Symbol === "function" &&
obj.constructor === Symbol &&
obj !== Symbol.prototype
? "symbol"
: typeof obj;
};
}
return _typeof(obj);
}

function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true },
});
if (superClass) _setPrototypeOf(subClass, superClass);
}

function _setPrototypeOf(o, p) {
_setPrototypeOf =
Object.setPrototypeOf ||
function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}

function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived),
result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
}

function _possibleConstructorReturn(self, call) {
if (call && (_typeof(call) === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}

function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError(
"this hasn't been initialised - super() hasn't been called"
);
}
return self;
}

function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Date.prototype.toString.call(Reflect.construct(Date, [], function () {}));
return true;
} catch (e) {
return false;
}
}

function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf
? Object.getPrototypeOf
: function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}

function _instanceof(left, right) {
if (
right != null &&
typeof Symbol !== "undefined" &&
right[Symbol.hasInstance]
) {
return !!right[Symbol.hasInstance](left);
} else {
return left instanceof right;
}
}

function _classCallCheck(instance, Constructor) {
if (!_instanceof(instance, Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}

其中_classCallCheck_instanceof之前讲过了(这里由于没有编写方法,所以没有_createClass_defineProperties这两个函数)。

_getPrototypeOf_setPrototypeOf_typeof都是简单的进行兼容处理而已。

剩下的有_inherits_createSuper_possibleConstructorReturn_assertThisInitialized以及_isNativeReflectConstruct这几个函数。

可以先看两个类经过编译后是什么样子的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var People = function People(id, name) {
_classCallCheck(this, People);

this.id = id;
this.name = name;
};

var Student = /*#__PURE__*/ (function (_People) {
_inherits(Student, _People);

var _super = _createSuper(Student);

function Student(id, name, age) {
var _this;

_classCallCheck(this, Student);

_this = _super.call(this, id, name);
_this.age = age;
return _this;
}

return Student;
})(People);

先调用了_inherits,传入了两个构造函数。

然后在_inherits函数中有下面这段代码。

1
2
3
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: { value: subClass, writable: true, configurable: true },
});

没错,这就是之前在继承那一篇帖子说过的原型式继承,不必通过new来创建父类的对象,而是只需要一个指向父类原型的对象即可。

然后紧接着下面这句:

1
if (superClass) _setPrototypeOf(subClass, superClass);

这句使得父类的静态方法也能够通过子类进行调用了,因为函数本质上也是一个对象。

接下来执行了_createSuper

先通过_isNativeReflectConstruct判断当前环境下能否使用Reflect

然后返回了一个_createSuperInternal函数。

内部先获取了父类构造器,然后以当前的上下文来执行这个构造器,这对应我们之前在继承中说到的借用构造方法,把父类上绑定的属性绑定在子类上。

1
2
result = Reflect.construct(Super, arguments, NewTarget);
result = Super.apply(this, arguments);

最后返回了_possibleConstructorReturn,这个函数目的是检查构造函数是否返回了某些东西。

如果父类的构造器已经返回了一个函数或者对象的时候,那么直接返回这个函数或者对象,如果不是,那就要检查this

也就是执行_assertThisInitialized这个函数。

如果thisvoid 0void 0undefined),抛出错误"this hasn't been initialised - super() hasn't been called"

这个报错的意思就是子类没有调用super初始化父类。

需要指出,子类继承父类,必须在子类构造函数的开头调用super,不然报错。

那么,babel是如何做到这一点的呢,把super去掉再编译之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Student = /*#__PURE__*/ (function (_People) {
_inherits(Student, _People);

var _super = _createSuper(Student);

function Student(id, name, age) {
var _this;

_classCallCheck(this, Student);

_this.age = age;
return _possibleConstructorReturn(_this);
}

return Student;
})(People);

发现少了一句:

1
_this = _super.call(this, id, name);

并且结尾return _this变成return _possibleConstructorReturn(_this)

此时就很明朗了,没有调用super,此时的_this都不是一个对象,传入_possibleConstructorReturn

由于没有第二个参数,从而判断了selfself此时明显就是undefined,所以报错了。

arrow function (箭头函数)

箭头函数解决了this变量的绑定问题。

之前,使用到this的地方要格外的小心,比如下面setTimeout的回调函数。

1
2
3
4
5
6
7
8
let o = {
val: 1,
say() {
setTimeout(function () {
console.log(this.val);
}, 1000);
},
};

此时的打印的输出为undefined,因为传进setTimeout参数的函数中的this已经不指向o了。

在没出现箭头函数的时候,可以使用变量self指向this来保持函数内this的指向。

1
2
3
4
5
6
7
8
9
let o = {
val: 1,
say() {
let self = this;
setTimeout(function () {
console.log(self.val); //输出1
}, 1000);
},
};

或者可以通过bind来绑定函数的上下文。

1
2
3
4
5
6
7
8
9
10
11
let o = {
val: 1,
say() {
setTimeout(
function () {
console.log(this.val); //输出1
}.bind(this),
1000
);
},
};

如果使用箭头函数那么问题就会消失。

1
2
3
4
5
6
7
8
let o = {
val: 1,
say() {
setTimeout(() => {
console.log(this.val); // 输出1
}, 1000);
},
};

箭头函数可以让代码更加简介易读。

1
2
const array = [1, 2, 3];
array.map((v) => v + 1); // 如果只需简单的处理,只要写成一行,默认就是返回值了。

default(函数参数默认值)

之前如果想要设置函数的默认参数,一般是使用||

1
2
3
4
5
6
function fn(a, b) {
a = a || 1;
b = b || true;
console.log(a);
console.log(b);
}

这么做的问题就是如果传入了假值就会使用到默认值,但是不符合实际的逻辑。

1
fn(0, false); // a 为 1, b 为 true

现在只需要在参数列表顺便指定默认值即可,并且符合实际逻辑。

1
2
3
4
5
6
7
function fn(a = 1, b = true) {
console.log(a);
console.log(b);
}

fn(0, false); // a = 0 , b = false
fn(); // a = 1 , b = true

rest arguments(剩余参数)

之前如果想对函数参数进行分割,一般使用函数内部变量arguments配合数组slice方法。

1
2
3
4
5
6
7
8
9
function fn() {
var sliceFn = Array.prototype.slice;
var firstArg = sliceFn.call(arguments, 0, 1)[0];
var restArgs = sliceFn.call(arguments, 1);
console.log(firstArg);
console.log(restArgs);
}

fn(1, 2, 3, 4, 5); // 输出 1 [2, 3, 4, 5]

现在只需要使用...展开符号即可分割参数数组。

1
2
3
4
5
6
function fn(firstArg, ...restArgs) {
console.log(firstArg);
console.log(restArgs);
}

fn(1, 2, 3, 4, 5); // 输出 1 [2, 3, 4, 5]

destructuring(解构)

1
2
const array = [1, 2, 3];
const [first, second] = array; // first 为 1, second 为 2
1
2
const array = [1, 2, 3];
const [first, , second] = array; // 中间隔开了一个元素,first 为 1, second 为 3

配合展开符号使用:

1
2
const array = [1, 2, 3];
const [first, ...rest] = array; // first 为 1, rest 为 [2, 3]

Enhanced object literals(增强的对象字面量)

之前如果想往对象的属性赋值的话,一个是以 属性名:属性名对应值的变量

在属性名和变量名一样的情况下写起来非常的繁琐。

1
2
3
4
5
6
7
8
9
const a = { val: 1 };
const b = { age: 22 };

const val = a.val;
const age = b.age;
const c = {
val: val, // 重复的变量名
age: age, // 重复的变量名
};

而现在就可以省略相同名字的字段了。

1
2
3
4
5
6
7
8
9
const a = { val: 1 };
const b = { age: 22 };

const val = a.val;
const age = b.age;
const c = {
val, // 省略了,表示属性名是val且值为变量名为val的值
age,
};

Set 和 Map

es6内置了两个经典的数据结构。
Set可以看成没有重复元素的数组。
Map可以看成元素是键值对的数组。

之前可以用字面对象来模拟Map,但是键只能是数字或者字符串,而es6Map没有这个限制了。

1
var map = { a: 1, 2: 3, true: 1 }; // 这里的true也是字符串,不要被迷惑了
1
2
3
4
5
var map = new Map();
var key = { a: 1 };
var val = { b: 2 };
map.set(key, val); // 对象作为键
console.log(map.get(key)); // 输出 {b: 2}

其他

es6还要很多新特性,比如

  • Promise
  • Proxy
  • Reflect
  • Symbol
  • 模板字符串
  • 模块化importexport
  • ...

这些都可以在 阮一峰写的ECMAScript 6 入门 上查看