前言 ECMAScript2015(es6)新特性
正文 let
和const
在还没有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 ();
而如果使用了let
和const
来定义变量的话。
上面的代码就会报错,因为这两个关键字定义的变量是基于块级作用域 的,变量不会提升。
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 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),在同一个作用域中,let
和const
定义的变量之前都不能使用这个变量。
对于var
和let
,也有另一个有趣的现象,这个现象出现在循环中。
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 ]();
上面的代码从我们人的角度看应该输出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 ]();
使用了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 ]();
通过创建一个立即执行函数来创建一个新的作用域。
从而保证函数内部输出的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 ( ) { console .log (i); }; }
上面的例子无论哪个按钮,点击获取的i
都会是buttons
的长度(因为在i === buttons.length
的时候跳出了循环)。
当然现在基本上都用let
和const
来定义变量进行使用了。
只要遵循先定义,再使用的话,就基本不会出现类似的情况了。
let
和const
的区别就是let
定义的变量可以被重新赋值,而const
不行。
如果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 ();
我们可以使用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 = (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
。
而instanceof
在ie5
以上就支持了。
感觉微软都要放弃ie
了,即使是ie11
,感觉用的人也越来越少了,新版edge
的内核还是用的谷歌的内核,以后兼容应该会越来越容易吧。
回到Student
构造函数,通过上面的分析可以总结出来,也就是不能通过直接调用Student
函数,不然会报错。
1 2 new Student (); Student ();
因为此时的函数内部的this
指向的不是一个Student
的实例对象。
既然不是Student
的实例对象,那么就this instanceof Student
一定返回false
,而false
情况就会触发函数_classCallCheck
抛出错误。
接下来就是通过_createClass
来挂载方法了。
挂载的方法分为原型上挂载和静态挂载,也就是对象方法和类方法。
可以简单地写个静态方法测试下。
_createClass
传入构造器,对象方法描述符数组,类方法描述符数组,然后通过_defineProperties
来完成挂载。
extend
和super
es6
的class
也支持extend
和super
关键字。
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 = (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
这个函数。
如果this
为 void 0
(void 0
为undefined
),抛出错误"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 = (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
由于没有第二个参数,从而判断了self
,self
此时明显就是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 ); }, 1000 ); }, };
或者可以通过bind
来绑定函数的上下文。
1 2 3 4 5 6 7 8 9 10 11 let o = { val : 1 , say ( ) { setTimeout ( function ( ) { console .log (this .val ); }.bind (this ), 1000 ); }, };
如果使用箭头函数那么问题就会消失。
1 2 3 4 5 6 7 8 let o = { val : 1 , say ( ) { setTimeout (() => { console .log (this .val ); }, 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 2 3 4 5 6 7 function fn (a = 1 , b = true ) { console .log (a); console .log (b); } fn (0 , false ); fn ();
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 6 function fn (firstArg, ...restArgs ) { console .log (firstArg); console .log (restArgs); } fn (1 , 2 , 3 , 4 , 5 );
destructuring(解构) 1 2 const array = [1 , 2 , 3 ];const [first, second] = array;
1 2 const array = [1 , 2 , 3 ];const [first, , second] = array;
配合展开符号使用:
1 2 const array = [1 , 2 , 3 ];const [first, ...rest] = array;
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, age, };
Set 和 Map es6
内置了两个经典的数据结构。Set
可以看成没有重复元素的数组。Map
可以看成元素是键值对的数组。
之前可以用字面对象来模拟Map
,但是键只能是数字或者字符串,而es6
的Map
没有这个限制了。
1 var map = { a : 1 , 2 : 3 , true : 1 };
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));
其他 es6
还要很多新特性,比如
Promise
Proxy
Reflect
Symbol
模板字符串
模块化import
和export
...
这些都可以在 阮一峰写的ECMAScript 6 入门 上查看