上周在公司组织了 ES6
新特性的分享会,主要讲了工程化简介、ES6
的新特性与前端经常使用的几种构建工具的配合使用。ES6
这块主要讲了一些咱们平时开发中常常会用到的新特性。在这里整理一下关于 ES6
的部分。
<!--more-->
一共讲解了 8 个经常使用的 ES6
新特性,讲解过程也是由浅入深。废话很少说,下面进入正文。前端
// Before function decimal(num, fix) { fix = fix === void(0) ? 2 : fix; return +num.toFixed(fix); }
// After function decimal(num, fix = 2) { return +num.toFixed(fix); }
首先,咱们看一下以前咱们是怎么写函数默认值的:咱们一般会使用三元运算符来判断入参是否有值,而后决定是否使用默认值运行函数(如示例中 fix = fix === void(0) ? 2 : fix
)面试
而在 ES6
中,咱们能够直接在函数的显示入参中指定函数默认值(function decimal(num, fix = 2){}
),很明显,这种写法更天然易懂,也更加方便,不过有一点须要注意:ajax
设定了默认值的入参,应该放在没有设置默认值的参数以后,也就是咱们不该该这样写:function decimal(fix = 2, num){}
,虽然经过变通手段也能够正常运行,但不符合规范。算法
// Before // Before.1 var type = 'simple'; 'This is a ' + type + ' string join.' // Before.2 var type = 'multiline'; 'This \nis \na \n' + type + '\nstring.' // Before.3 var type = 'pretty singleline'; 'This \ is \ a \ ' + type + '\ string.' // OR // Before.4 'This ' + 'is' + 'a' + type + 'string.'
// After var type = 'singleline'; `This is a ${type} string.` var type = 'multiline'; `This is a ${type} string.` var type = 'pretty singleline'; `This \ is \ a \ ${type} \ string.`
咱们以前在对字符串和变量进行拼接的时候,一般都是反复一段一段使用引号包裹的字符串,再反复使用加号进行拼接(Before.1)。多行字符串的时候咱们还要写上蹩脚的 \n
来换行以获得一个多行的字符串(Before.2)。数组
在字符串过长的时候可能会使用 \
在编辑器中书写多行字符串来表示单行字符串,用来方便较长的字符串在编辑器中的阅读(Before.3),或者简单粗暴的反复引号加号这样多行拼接(Before.4)。闭包
ES6
中咱们可使用反引号(`,位于 TAB
上方)来输入一段简单明了的多行字符串,还能够在字符串中经过 ${变量名}
的形式方便地插入一个变量,是否是方便多了!app
var [a, ,b] = [1, 2, 3, 4, 5]; console.log(a); // => 1 console.log(b); // => 3
数组解构,使用变量声明关键字声明一个形参数组([a, , b]
),等号后跟一个待解构目标数组([1, 2, 3]
),解构时能够经过留空的方式跳过数组中间的个别元素,可是在形参数组中必须留有相应空位才能够继续解构以后的元素,若是要跳过的元素处于数组末端,则在形参数组中能够不予留空。async
var {b, c} = {a: 1, b: 2, c: 3}; console.log(b); // => 2 console.log(c); // => 3
对象解构与数组解构大致相同,不过须要注意一点编辑器
形参对象({b, c}
)的属性或方法名必须与待解构的目标对象中的属性或方法名彻底相同才能解构到对应的属性或方法模块化
var example = function() { return {a: 1, b: 2, c: 3}; } var {a: d, b: e, c: f} = example(); console.log(d, e, f); // => 1, 2, 3
对象匹配解构是对象解构的一种延伸用法,咱们能够在形参对象中使用:
来更改解构后的变量名。
function example({param: value}) { return value; } console.log(example({param: 5})); // => 5
函数的入参解构也是对象解构的一种延伸用法,咱们能够经过改写入参对象目标值为变量名的方式,在函数内部直接获取到入参对象中某个属性或方法的值。
function example({x, y, z = 0}) { return x + y + z; } console.log(example({x: 1, y: 2})); // => 3 console.log(example({x: 1, y: 2, z: 3})); // => 6
这是入参解构的另外一种用法,咱们能够在入参对象的形参属性或方法中使用等号的方式给入参对象的某些属性或方法设定默认值。
无变量提高
// Before console.log(num); // => undefined var num = 1;
// After console.log(num); // => ReferenceError let num = 1;
使用 var
声明的变量会自动提高到当前做用域的顶部,若是声明位置与做用域顶部之间有另外一个同名变量,很容易引发难以预知的错误。使用 let
声明的变量则不会进行变成提高,规避了这个隐患。
注意:
var
声明的变量提高后虽然在声明语句以前输出为undefined
,但这并不表明num
变量尚未被声明,此时num
变量已经完成声明并分配了相应内存,只不过该变量目前的值为undefined
,并非咱们声明语句中赋的初始值1
。
有块级做用域
// Before { var num = 1; console.log(num); // => 1 } console.log(num); // => 1
// After { let num = 1; console.log(num); // => 1 } console.log(num); // => ReferenceError
let
声明的变量只能在当前块级做用域中使用,最多见的应用大概就是 for(let i = 0, i < 10; i++) {}
,相信许多小伙伴在面试题中见过,哈哈。
禁止重复声明
// Before var dev = true; var dev = false; console.log(dev); // => false
// After let dev = true; let dev = false; // => SyntaxError
var
声明的变量能够重复声明,并且不会有任何警告或者提示,就这样悄悄的覆盖了一个值,隐患如变量提高同样让人担心。( ̄┰ ̄*)
而 let
声明的变量若是进行重复声明,则会直接抛出一个语法错误(是的,就是直接明确地告诉你:你犯了一个至关低级的语法错误哦)
无变量提高
有块级做用域
禁止重复声明
前 3 点跟
let
一个套路,就很少说了
禁止重复赋值
const DEV = true; DEV = false; // => TypeError
基于静态常量的定义咱们能够很明显知道,const
声明的常量一经声明便不能再更改其值,无需多说。
必须附初始值
const DEV; // => SyntaxError
也是基于定义,const
声明的常量既然一经声明便不能再更改其值,那声明的时候没有附初始值显然是不合理的,一个没有任何值的常量是没有意义的,浪费内存。
ES6
新增了许多(至关多)的库函数,这里只介绍一些比较经常使用的。
题外话:多了解一下内建函数与方法有时候能够很方便高效地解决问题。有时候绞尽脑汁写好的一个算法,没准已经有内建函数实现了!并且内建函数通过四海八荒众神的考验,性能必定不错,哈哈。
Number.EPSILON Number.isInteger(Infinity); // => false Number.isNaN('NaN'); // => false
首先是 ᶓ 这个常量属性,表示小数的极小值,主要用来判断浮点数计算是否精确,若是计算偏差小于该阈值,则能够认为计算结果是正确的。
而后是 isInteger()
这个方法用来判断一个数是否为整数,返回布尔值。
最后是 isNaN()
用来判断入参是否为 NaN
。是的,咱们不再用经过 NaN
不等于 NaN
才能肯定一个 NaN
就是 NaN
这种反人类的逻辑来判断一个 NaN
值了!
if(NaN !== NaN) { console.log("Yes! This is actually the NaN!"); }
另外还有两个小改动:两个全局函数 parseInt()
与 parseFloat()
被移植到 Number
中,入参反参保持不变。这样全部数字处理相关的都在 Number
对象上嘞!规范多了。
'abcde'.includes('cd'); // => true 'abc'.repeat(3); // => 'abcabcabc' 'abc'.startsWith('a'); // => true 'abc'.endsWith('c'); // => true
inclueds()
方法用来判断一个字符串中是否存在指定字符串
repeat()
方法用来重复一个字符串生成一个新的字符串
startsWith()
方法用来判断一个字符串是否以指定字符串开头,能够传入一个整数做为第二个参数,用来设置查找的起点,默认为 0
,即从字符串第一位开始查找
endsWith()
与 startsWith()
方法相反
Array.from(document.querySelectorAll('*')); // => returns a real array. [0, 0, 0].fill(7, 1); // => [0, 7, 7] [1, 2, 3].findIndex(function(x) { return x === 2; }); // => 1 ['a', 'b', 'c'].entries(); // => Iterator [0: 'a'], [1: 'b'], [2: 'c'] ['a', 'b', 'c'].keys(); // => Iterator 0, 1, 2 ['a', 'b', 'c'].values(); // => Iterator 'a', 'b', 'c' // Before new Array(); // => [] new Array(4); // => [,,,] new Array(4, 5, 6); // => [4, 5, 6] // After Array.of(); // => [] Array.of(4); // => [4] Array.of(4, 5, 6); // => [4, 5, 6]
首先是 from()
方法,该方法能够将一个类数组对象转换成一个真正的数组。还记得咱们以前常写的 Array.prototype.slice.call(arguments)
吗?如今能够跟他说拜拜了~
以后的 fill()
方法,用来填充一个数组,第一个参数为将要被填充到数组中的值,可选第二个参数为填充起始索引(默认为 0),可选第三参数为填充终止索引(默认填充到数组末端)。
findIndex()
用来查找指定元素的索引值,入参为函数,函数形参跟 map()
方法一致,很少说。最终输出符合该条件的元素的索引值。
entries()
、keys()
、values()
三个方法各自返回对应键值对、键、值的遍历器,可供循环结构使用。
最后一个新增的 of()
方法主要是为了弥补 Array
当作构造函数使用时产生的怪异结果。
let target = { a: 1, b: 3 }; let source = { b: 2, c: 3 }; Object.assign(target, source); // => { a: 1, b: 2, c: 3}
assign()
方法用于合并两个对象,不过须要注意的是这种合并是浅拷贝。可能看到这个方法咱们还比较陌生,不过了解过 jQuery
源码的应该知道 $.extend()
这个方法,例如在下面这个粗糙的 $.ajax()
模型中的应用:
$.ajax = function(opts) { var defaultOpts = { method: 'GET', async: true, //... }; opts = $.extend(defaultOpts, opts); }
从这咱们能够看到 TC39
也是在慢慢吸取百家所长,努力让 JavaScript
变得更好,更方便开发者的使用。
Object
新增的特性固然不止这一个assign()
方法,一共增长了十多个新特性,特别是对属性或方法名字面量定义的加强方面,很值得一看,感兴趣的自行查找资料进行了解哈,印象会更深入!
Math
对象上一样增长了许多新特性,大部分都是数学计算方法,这里只介绍两个经常使用的
Math.sign(5); // => +1 Math.sign(0); // => 0 Math.sign(-5); // => -1 Math.trunc(4.1); // => 4 Math.trunc(-4.1); // => -4
sign()
方法用来判断一个函数的正负,使用与对应返回值如上。
trunc()
用来取数值的整数部分,咱们以前可能常用 floor()
方法进行取整操做,不过这个方法有一个问题就是:它自己是向下取整,当被取整值为正数的时候计算结果彻底 OK,可是当被取整值为负数的时候:
Math.floor(-4.1); // => -5
插播一个小 Tip:使用位操做符也能够很方便的进行取整操做,例如:
~~3.14
or3.14 | 0
,也许这更加方便 : )
箭头函数无疑是 ES6
中一个至关重要的新特性。
共享父级 this
对象
共享父级 arguments
不能当作构造函数
var arr = [1, 2, 3, 4, 5, 6]; // Before arr.filter(function(v) { return v > 3; }); // After arr.filter(v => v > 3); // => [4, 5, 6]
先后对比很容易理解,能够明显看出箭头函数极大地减小了代码量。
var arr = [1, 2, 3, 4, 5, 6]; arr.map((v, k, thisArr) => { return thisArr.reverse()[k] * v; }) // => [6, 10, 12, 12, 10, 6]
一个简单的首尾相乘的算法,对比最简表达式咱们能够发现,函数的前边都省略了 function
关键字,可是多个入参时需用括号包裹入参,单个入参是时可省略括号,入参写法保持一致。后面使用胖箭头 =>
链接函数名与函数体,函数体的写法保持不变。
// Before var obj = { arr: [1, 2, 3, 4, 5, 6], getMaxPow2: function() { var that = this, getMax = function() { return Math.max.apply({}, that.arr); }; return Math.pow(getMax(), 2); } } // After var obj = { arr: [1, 2, 3, 4, 5, 6], getMaxPow2: function() { var getMax = () => { return Math.max.apply({}, this.arr); } return Math.pow(getMax(), 2); } }
注意看中第 5 行 var that = this
这里声明的一个临时变量 that
。在对象或者原型链中,咱们之前常常会写这样一个临时变量,或 that
或 _this
,诸如此类,以达到在一个函数内部访问到父级或者祖先级 this
对象的目的。
现在在箭头函数中,函数体内部没有本身的 this
,默认在其内部调用 this
的时候,会自动查找其父级上下文的 this
对象(若是父级一样是箭头函数,则会按照做用域链继续向上查找),这无疑方便了许多,咱们无需在多余地声明一个临时变量来作这件事了。
注意:
某些状况下咱们可能须要函数有本身的 this
,例如 DOM
事件绑定时事件回调函数中,咱们每每须要使用 this
来操做当前的 DOM
,这时候就须要使用传统匿名函数而非箭头函数。
在严格模式下,若是箭头函数的上层函数均为箭头函数,那么 this
对象将不可用。
另,因为箭头函数没有本身的
this
对象,因此箭头函数不能当作构造函数。
咱们知道在函数体中有 arguments
这样一个伪数组对象,该对象中包含该函数全部的入参(显示入参 + 隐式入参),当函数体中有另一个函数,而且该函数为箭头函数时,该箭头函数的函数体中能够直接访问父级函数的 arguments
对象。
function getSum() { var example = () => { return Array .prototype .reduce .call(arguments, (pre, cur) => pre + cur); } return example(); } getSum(1, 2, 3); // => 6
因为箭头函数自己没有
arguments
对象,因此若是他的上层函数都是箭头函数的话,那么arguments
对象将不可用。
最后再巩固一下箭头函数的语法:
当箭头函数入参只有一个时能够省略入参括号;
当入参多余一个或没有入参时必须写括号;
当函数体只有一个 return
语句时能够省略函数体的花括号与 return
关键字。
类也是 ES6
一个不可忽视的新特性,虽然只是句法上的语法糖,可是相对于 ES5
,学习 ES6
的类以后对原型链会有更加清晰的认识。
本质为对原型链的二次包装
类没有提高
不能使用字面量定义属性
动态继承类的构造方法中 super
优先 this
/* 类不会被提高 */ let puppy = new Animal('puppy'); // => ReferenceError class Animal { constructor(name) { this.name = name; } sleep() { console.log(`The ${this.name} is sleeping...`); } static type() { console.log('This is an Animal class.'); } } let puppy = new Animal('puppy'); puppy.sleep(); // => The puppy is sleeping... /* 实例化后没法访问静态方法 */ puppy.type(); // => TypeError Animal.type(); // => This is an Animal class. /* 实例化前没法访问动态方法 */ Animal.sleep(); // => TypeError /* 类不能重复定义 */ class Animal() {} // => SyntaxError
以上咱们使用 class
关键字声明了一个名为 Animal
的类。
虽然类的定义中并未要求类名的大小写,但鉴于代码规范,推荐类名的首字母大写。
两点注意事项:
在类的定义中有一个特殊方法 constructor()
,该方法名固定,表示该类的构造函数(方法),在类的实例化过程当中会被调用(new Animal('puppy')
);
类中没法像对象同样使用 prop: value
或者 prop = value
的形式定义一个类的属性,咱们只能在类的构造方法或其余方法中使用 this.prop = value
的形式为类添加属性。
最后对比一下咱们以前是怎样写类的:
function Animal(name) { this.name = name; } Animal.prototype = { sleep: function(){ console.log('The ' + this.name + 'is sleeping...'); } }; Animal.type = function() { console.log('This is an Animal class.'); }
class
关键字真真让这一切变得清晰易懂了~
class Programmer extends Animal { constructor(name) { /* 在 super 方法以前 this 不可用 */ console.log(this); // => ReferenceError super(name); console.log(this); // Right! } program() { console.log("I'm coding..."); } sleep() { console.log('Save all files.'); console.log('Get into bed.'); super.sleep(); } } let coder = new Programmer('coder'); coder.program(); // => I'm coding... coder.sleep(); // => Save all files. => Get into bed. => The coder is sleeping.
这里咱们使用 class
定义了一个类 Programmer
,使用 extends
关键字让该类继承于另外一个类 Animal
。
若是子类有构造方法,那么在子类构造方法中使用 this
对象以前必须使用 super()
方法运行父类的构造方法以对父类进行初始化。
在子类方法中咱们也可使用 super
对象来调用父类上的方法。如示例代码中子类的 sleep()
方法:在这里咱们重写了父类中的 sleep()
方法,添加了两条语句,并在方法末尾使用 super
对象调用了父类上的 sleep()
方法。
俗话讲:没有对比就没有伤害 (*゜ー゜*),咱们最后来看一下之前咱们是怎么来写继承的:
function Programmer(name) { Animal.call(this, name); } Programmer.prototype = Object.create(Animal.prototype, { program: { value: function() { console.log("I'm coding..."); } }, sleep: { value: function() { console.log('Save all files.'); console.log('Get into bed.'); Animal.prototype.sleep.apply(this, arguments); } } }); Programmer.prototype.constructor = Programmer;
若是前文类的定义中的先后对比不足为奇,那么这个。。。
给你一个眼神,本身去体会 (⊙ˍ⊙),一脸懵逼.jpg
啊哈,终于写到最后一部分了。
模块系统是一切模块化的前提,在未推出 ES6 Module
标准以前,相信大伙儿已经被满世界飞的 AMD
、CMD
、UMD
、CommonJS
等等百花齐放的模块化标准搞的晕头转向了吧。可是,如今 TC39
在 ECMAScript2015(ES6)
版本里终于推出了正式的模块化规范,前端模块系统的大一统时代已经到来了!
OMG,这段话写的好燃 orz
废话有点多。。。
下面我们来了解一个这个模块系统的基本规范。
为方便描述,下文中导出对象指一切可导出的内容(变量、函数、对象、类等),勿与对象(
Object
)混淆。
导入对象同理。
封闭的代码块
每一个模块都有本身彻底独立的代码块,跟做用域相似,可是更加封闭。
无限制导出导出
一个模块理论上能够导出无数个变量、函数、对象属性、对象方法,甚至一个完整的类。可是咱们应该时刻牢记单一职责这一程序设计的基本原则,不要试图去开发一个臃肿的巨大的面面俱到的模块,合理控制代码的颗粒度也是开发可维护系统必不可少的一部分。
严格模式下运行
模块默认状况下在严格模式下运行('use strict;'
),这时候要注意一些取巧甚至有风险的写法应该避免,这也是保证代码健壮性的前提。
export const DEV = true; export function example() { //... } export class expClass { //... } export let obj = { DEV, example, expClass, //... }
使用 export
关键字,后面紧跟声明关键字(let
、function
等)声明一个导出对象,这种声明并同时导出的导出方式称做内联导出。
未被导出的内容(变量、函数、类等)因为独立代码块的缘由,将仅供模块内部使用(可类比成一种闭包)。
// module example.js const DEV = true; function example() { //... } class expClass { //... } let obj = { DEV, example, expClass, //... } // module example.js export {DEV, example, expClass, obj}; export {DEV, example as exp, expClass, obj};
相对于内联导出,上边的这种方式为对象导出。咱们能够像写普通 JS
文件同样写主要的功能逻辑,最后经过 export
集中导出。
在导出时咱们可使用 as
关键字改变导出对象的名称。
export default {DEV, example as exp, expClass, obj}; // OR export default obj; // OR export default const DEV = true;
咱们能够在 export
关键字后接 default
来设置模块的默认导出对象,须要注意的是:一个模块只能有一个默认导出。
先很少说,后面讲导入的时候再细讲相互之间的关联。
前文咱们定义了一个名为 example
的模块,写在文件 example.js
中,下面咱们来导入并使用这个模块。
import example from './example.js'; // OR import default as example from './example.js';
使用 import
关键字导入一个模块,上边这两种写法是等效的。默认导入对象既是模块默认导出对象,即对应模块定义中的 export default
所导出的内容。
此外咱们还能够这样导入一个模块:
import {DEV, example} from './example.js'; import * as exp from './example.js'; import {default as expMod, * as expAll, DEV, example as exp} from './example.js';
这种导入方式对应模块定义中的 export {DEV, example, expClass, obj}
或 export const DEV = true
。下面咱们逐行分析:
第一行,咱们使用对象导入的方式导入一个模块内容,可能有些人已经发现,这跟解构赋值很类似,但也有不一样,下面会讲到。须要注意的是形参对象({DEV, example}
)与模块定义中导出的名称必须保持一致。
第二行,导入时可使用通配符 *
配合 as
关键字一次性导出模块中全部内容,最终导入的内容放在 exp
对象中。
第三行,在使用对象导入来导入一个模块的指定内容时,也可使用 as
关键字更改最终导入对象的名称,这里表现出与解构赋值的一个不一样之处,忘记解构赋值的小伙伴能够翻翻前文对比一下哈~
最后,在导入一个模块后咱们就能够直接使用模块的函数、变量、类等了,完整的代码示例:
import {DEV, example, expClass as EC} from './example.js'; if(DEV) { let exp = new EC(); // anything you want... example(); }
好嘞!到这里,ES6
经常使用的 8 个新特性就讲完了,恭喜你耐心地看完了。固然,还有许多地方没有讲到,有时间的话会考虑继续写一些。
好嘞,就这样吧,但愿对你有所帮助,拜拜~<(* ̄▽ ̄*)/。
文中部分专业名词因为未找到合适译文,最后自行翻译,若有不妥,欢迎指正。