你可能已经据说过 ECMAScript 6
(简称 ES6)了。ES6 是 Javascript 的下一个版本,它有不少很棒的新特性。这些特性复杂程度各不相同,但对于简单的脚本和复杂的应用都颇有用。在本文中,咱们将讨论一些精心挑选的 ES6 特性
,这些特性能够用于你平常的 Javascript 编码中。html
请注意,当前浏览器已经全面展开对这些 ES6 新特性的支持,尽管目前的支持程度还有所差别。若是你须要支持一些缺乏不少 ES6 特性的旧版浏览器,我将介绍一些当前能够帮助你开始使用 ES6 的解决方案。node
文中大多数代码示例都带有“运行代码”连接,你能够查看代码并运行它。git
你习惯于用 var
声明变量。如今你也能够用 let
了。二者微妙的差异在于做用域。var
声明的变量做用域为包围它的函数,而 let
声明的变量做用域仅在它所在的块中。 es6
if(true) { let x = 1; } console.log(x); // undefined
这样使得代码更加干净,减小滞留的变量。看看如下经典的数组遍历:github
for(let i = 0, l = list.length; i < l; i++) { // do something with list[i] } console.log(i); // undefined
举个例子,一般状况下,咱们在同一做用域里使用变量 j
来完成另外一个遍历。可是,如今有了 let
,能够安全地再一次声明 i
变量。由于它只在被声明的块中有效。web
还有另外一个用于声明块做用域变量的方法。使用 const
,你能够声明一个值的只读引用。必须直接给一个变量赋值。若是尝试修改变量或者没有当即给变量赋值,都将报错:typescript
const MY_CONSTANT = 1; MY_CONSTANT = 2 // Error const SOME_CONST; // Error
注意,对象的属性或数组成员仍是能够改变的。npm
const MY_OBJECT = {some: 1}; MY_OBJECT.some = 'body'; // Cool
箭头函数为 Javascript 语言增色很多。它使得代码更简洁。咱们早早地在本文中介绍箭头函数,这样就能够在后面的示例中加以利用了。如下代码片断是箭头函数和咱们熟悉的 ES5 版本的写法:gulp
let books = [{title: 'X', price: 10}, {title: 'Y', price: 15}]; let titles = books.map( item => item.title ); // ES5 equivalent: var titles = books.map(function(item) { return item.title; });
运行代码数组
若是咱们观察箭头函数的语法,会发现其中并无出现 function
关键词。只保留零或多个参数,“胖箭头”(=>
)和函数表达式。return
声明被隐式加入。
带有零或多个参数时,必须使用括号:
// No arguments books.map( () => 1 ); // [1, 1] // Multiple arguments [1,2].map( (n, index) => n * index ); // [0, 2]
若是须要写更多的逻辑或更多的空格,能够把函数表达式放在({...}
)块中。
let result = [1, 2, 3, 4, 5].map( n => { n = n % 3; return n; });
箭头函数不单只是为了输入更少的字符,它们的表现也和通常函数不一样。它继承了当前上下文的 this
和 arguments
。这就意味着,你能够避免写 var that = this
这样丑陋的代码,也不须要把函数绑定到正确的上下文了。举例以下(注意对比 this.title
和 ES5 版本的 that.title
的不一样):
let book = { title: 'X', sellers: ['A', 'B'], printSellers() { this.sellers.forEach(seller =&gt;console.log(seller + ' sells ' + this.title)); } } // ES5 equivalent: var book = { title: 'X', sellers: ['A', 'B'], printSellers: function() { var that = this; this.sellers.forEach(function(seller) { console.log(seller + ' sells ' + that.title) }) } }
几个方便的方法被添加到 String
的原型中。其中大多数用于简化须要用 indexOf()
方法来解决的问题的复杂度,并达到一样的效果:
'my string'.startsWith('my'); //true 'my string'.endsWith('my'); // false 'my string'.includes('str'); // true
很简单可是颇有效。添加了另一个方便的用于建立重复字符串的方法:
'my '.repeat(3); // 'my my my '
模板字符串提供一个简洁的方式来实现字符串插值。你可能已经对这种语法很熟悉了;它基于美圆符号和花括号 ${..}
。模板字符串置于引号之中。如下是快速示例:
let name = 'John', apples = 5, pears = 7, bananas = function() { return 3; } console.log(`This is ${name}.`); console.log(`He carries ${apples} apples, ${pears} pears, and ${bananas()} bananas.`); // ES5 equivalent: console.log('He carries ' + apples + ' apples, ' + pears + ' pears, and ' + bananas() +' bananas.');
以上形式对比 ES5 仅仅是便于字符串拼接。事实上,模板字符串还能够用于多行字符串。记住空格也是字符串的一部分。
let x = `1... 2... 3 lines long!`; // Yay // ES5 equivalents: var x = "1...n" + "2...n" + "3 lines long!"; var x = "1...n2...n3 lines long!";
Array
对象增长了一些新的静态方法,Array
原型上也增长了一些新方法。
首先, Array.from
从类数组和可遍历对象中建立 Array
的实例。类数组对象示例包括:
arguments
;document.getElementByTagName()
返回的 nodeList
;Map
和 Set
数据结构。
let itemElements = document.querySelectorAll('.items'); let items = Array.from(itemElements); items.forEach(function(element) { console.log(element.nodeType) }); // A workaround often used in ES5: let items = Array.prototype.slice.call(itemElements);
在上面的例子中,能够看到 items
数组拥有 forEach
方法,该方法是 itemElements
集合所不具有的。
Array.from
的一个有趣的特性是它的第二个可选参数mapFunction
。该参数容许你经过一次单独调用建立一个新的映射数组。
let navElements = document.querySelectorAll('nav li');
let navTitles = Array.from(navElements, el => el.textContent);
而后,咱们可使用 Array.of
方法,该方法的表现很像 Array
构造函数。它适合只传递一个参数的状况。所以 Array.of
是 new Array()
的更优选择。然而,更多的状况下,你会想使用数组字面量。
let x = new Array(3); // [undefined, undefined, undefined] let y = Array.of(8); // [8] let z = [1, 2, 3]; // Array literal
最后但一样重要的,有几个方法被添加到 Array
的原型上。我想 find
方法将会很受 Javascript 开发者欢迎。
find
返回回调返回 true
的第一个元素。findIndex
返回回调函数返回 true
的第一个元素的下标。fill
用所给参数“覆盖”数组的元素。[5, 1, 10, 8].find(n => n === 10) // 10 [5, 1, 10, 8].findIndex(n => n === 10) // 2 [0, 0, 0].fill(7) // [7, 7, 7] [0, 0, 0, 0, 0].fill(7, 1, 3) // [0, 7, 7, 7, 0]
Math
对象新增了几个方法。
Math.sign
返回数字的符号,结果为 1
、-1
或 0
。Math.trunc
返回无小数位的数字。Math.cbrt
返回数字的立方根。Math.sign(5); // 1 Math.sign(-9); // -1 Math.trunc(5.9); // 5 Math.trunc(5.123); // 5 Math.cbrt(64); // 4
若是你想学习更多 ES6 中的 number 和 math 新特性, Dr. Axel Rauschmayer将为你解答。
扩展操做符(...
)这个语法用于特定地方扩展元素很是方便,例如函数调用中的参数。让你了解它们用途的最好方法就是举例子了。
首先,咱们看看如何在一个另数组中扩展一个数组的元素。
let values = [1, 2, 4]; let some = [...values, 8]; // [1, 2, 4, 8] let more = [...values, 8, ...values]; // [1, 2, 4, 8, 1, 2, 4] // ES5 equivalent: let values = [1, 2, 4]; // Iterate, push, sweat, repeat... // Iterate, push, sweat, repeat...
扩展语法在传参数调用函数时也很是有用:
let values = [1, 2, 4]; doSomething(...values); function doSomething(x, y, z) { // x = 1, y = 2, z = 4 } // ES5 equivalent: doSomething.apply(null, values);
正如你所看到的,该语法让咱们免去一般使用 fn.apply()
的麻烦。它很是灵活,由于扩展操做符能够用在参数列表中的任意位置。这意味着如下调用方式会产生同样的结果:
let values = [2, 4];
doSomething(1, ...values);
咱们已经把扩展操做符应用在数组和参数中。事实上,它能够用在全部的可遍历对象中,例如一个 NodeList
:
let form = document.querySelector('#my-form'), inputs = form.querySelectorAll('input'), selects = form.querySelectorAll('select'); let allTheThings = [form, ...inputs, ...selects];
如今, allTheThings
是一个包含 <form>
节点、 <input>
子节点和 <select>
子节点的二维数组。
解构提供了一个方便地从对象或数组中提取数据的方法。对于初学者,请看如下数组示例:
let [x, y] = [1, 2]; // x = 1, y = 2 // ES5 equivalent: var arr = [1, 2]; var x = arr[0]; var y = arr[1];
使用这个语法,能够一次性给多个变量赋值。一个很好的附加用处是能够很简单地交换变量值:
let x = 1, y = 2; [x, y] = [y, x]; // x = 2, y = 1
解构也能够用于对象。注意对象中必须存在对应的键:
JavaScript
let obj = {x: 1, y: 2}; let {x, y} = obj; // x = 1, y = 2
你也可使用该机制来修改变量名:
JavaScript
let obj = {x: 1, y: 2}; let {x: a, y: b} = obj; // a = 1, b = 2
另外一个有趣的模式是模拟多个返回值:
function doSomething() { return [1, 2] } let [x, y] = doSomething(); // x = 1, y = 2
解构能够用来为参数对象赋默认值。经过对象字面量,能够模拟命名参数:
function doSomething({y = 1, z = 0}) { console.log(y, z); } doSomething({y: 2});
参数
在 ES6 中,能够定义函数的参数默认值。语法以下:
function doSomething(x, y = 2) { return x * y; } doSomething(5); // 10 doSomething(5, undefined); // 10 doSomething(5, 3); // 15
看起来很简洁,对吧? 我确定你以前在 ES5 中曾经须要给某些参数赋默认值:
function doSomething(x, y) { y = y === undefined ? 2 : y; return x * y; }
传递 undefined
或不传参数时都会触发参数使用默认值。
咱们已经学习了省略号操做符。剩余参数和它很相似。它一样是使用 ...
语法,容许你把末尾的参数保存在数组中:
function doSomething(x, ...remaining) { return x * remaining.length; } doSomething(5, 0, 0, 0); // 15
模块固然是一个受欢迎的 Javascript 语言新功能。我想仅仅是这个主要特性就值得咱们投入到 ES6 中来。
当前任何重要的 Javascript 项目都使用某种模块系统 —— 多是“展现模块模式”或其余 AMD 或 CommonJS 扩展形式的东西。然而,浏览器并无任何模块系统特性。为了实现 AMD 或 CommonJS,你一般须要一个构建步骤或加载器。解决这个问题的工具包括 RequireJS、Browserify 和 WebPack。
ES6 规范包含模块化的新语法和加载器。若是你将来想使用模块,应该使用这个语法。现代构建工具支持这种形式(可能经过插件),因此你能够放心使用。(不用担忧 —— 咱们将在后面的“转译”章节中讨论)
在 ES6 的模块语法中。模块设计围绕 export
和 import
关键词。如今让咱们看一个包含两个模块的例子:
// lib/math.js export function sum(x, y) { return x + y; } export var pi = 3.141593; // app.js import { sum, pi } from "lib/math"; console.log('2π = ' + sum(pi, pi));
正如你所见,能够存在多个 export
声明。每个都要明确地指明输出值的类型(本例中的function
和 var
)。
本例中的 import
声明使用一种语法(相似解构)来明肯定义被导入的内容。可使用 *
通配符,结合 as
关键词给模块提供一个本地名称,把模块当成一个总体导入。
// app.js import * as math from "lib/math"; console.log('2π = ' + math.sum(math.pi, math.pi));
模块系统有一个 default
输出。它能够是一个函数。只须要提供一个本地名称就能够导入这个默认值(即无解构):
// lib/my-fn.js export default function() { console.log('echo echo'); } // app.js import doSomething from 'lib/my-fn'; doSomething();
请注意 import
声明是同步的,可是模块代码需在全部依赖加载完后才会运行。
类是 ES6 中备受热议的一个特性。一部分人认为它不符合 Javascript 的原型特性,另外一部分人认为类能够下降从其余语言转过来的入门门槛,并帮助人们构建大规模应用。无论怎样,它是 ES6 的一部分。这里咱们快速介绍一下。
类的建立围绕 class
和 constructor
关键词。如下是个简短的示例:
class Vehicle { constructor(name) { this.name = name; this.kind = 'vehicle'; } getName() { return this.name; } } // Create an instance let myVehicle = new Vehicle('rocky');
注意类的定义不是通常的对象,所以,类的成员间没有逗号。
创造一个类的对象时,须要使用 new
关键词。继承一个基类时,使用 extends
:
class Car extends Vehicle { constructor(name) { super(name); this.kind = 'car' } } let myCar = new Car('bumpy'); myCar.getName(); // 'bumpy' myCar instanceof Car; // true myCar instanceof Vehicle; //true
从衍生类中,你可使用从任何构造函数或方法中使用 super
来获取它的基类:
super()
调用父类构造函数。super.getName()
。还有更多关于类的内容。若是你想深刻了解,我推荐 Dr.Axel Rauschmayer 的 《Classes in ECAMScript 6》
记号是一个新的原生数据类型,像 Number
和 String
同样。你可使用记号为对象属性建立惟一标识或建立惟一的常量。
const MY_CONSTANT = Symbol(); let obj = {}; obj[MY_CONSTANT] = 1;
注意经过记号产生的键值对不能经过 Object.getOwnPropertyNames()
得到,在 for...in
遍历、 Object.keys()
、JSON.stringify()
中均不可见。这是与基于字符串的键相反的。你能够经过 Object.getOwnPropertySymbols()
获取一个对象的记号数组。
记号与 const
配合很合适,由于它们都有不可改变的特性。
const CHINESE = Symbol(); const ENGLISH = Symbol(); const SPANISH = Symbol(); switch(language) { case CHINESE: // break; case ENGLISH: // break; case SPANISH: // break; default: // break; }
你能够为 symbol 添加描述。虽然不能够经过描述获取 symbol,可是可用于代码调试。
const CONST_1 = Symbol('my symbol'); const CONST_2 = Symbol('my symbol'); typeof CONST_1 === 'symbol'; // true CONST_1 === CONST_2; // false
想学习更多关于 symbols 的内容吗?Mozilla 开发者网络有一个关于该新的 symbol primitive的文章。
咱们如今能够用 ES6 来写代码了。正如介绍中提到的,浏览器对 ES6 特性的支持尚不普遍,且各浏览器也各不相同。颇有可能你写的的代码在用户的浏览器中不能彻底解析。这就是咱们为何须要把代码转换成能在当前的任何浏览器中良好运行的旧版本 Javascript(ES5) 。这种转换一般称为“转译”。咱们必须在应用中这么作,直到全部咱们想兼容的浏览器都能运行 ES6 为止。
转译代码并不难。你能够经过命令行直接转译代码,也能够把它做为 Grunt 或 Gulp 之类的任务管理器的插件包含进来。有不少转译解决方案,包括 Babel,Traceur 和 TypeScript。例如, 经过 Babel(以前的 “6to5”) 开始使用 ES6 的多种方式 。大多数 ES6 特性供你自由使用。
既然你对 ES6 充满热情和期待,为何不开始使用它呢。根据你想使用的特性和须要兼容的浏览器或环境(好比 Node.js),你可能须要在工做流中引入转译器。若是你肯定要使用它们,文件监听器和浏览器动态刷新器可使你的编码体验更加流畅。
若是你是从零开始,你可能只想经过命令行转译代码(能够从 Babel CLI documentation 查看示例)。若是你已经使用任务运行器,如 Grunt 或 Gulp,你能够添加相似 gulp-babel 或Webpack babel-loader 的插件。对于 Grunt,可以使用 grunt-babel 和不少其余 ES6 相关 的插件。Browserify 的用户可能会想看看 babelify 。
大多数特性能够被转换成兼容 ES5 的代码且开销很小。其余的特性则须要额外处理(可由转译器提供),可能有性能损失。若是想把玩一下 ES6 并查看转译后的代码的样子,可使用各类交互环境(也就是 REPL):
注意 TypeScript 不彻底是一个转译器。它是一个类型化的 Javascript 超集,编译成 Javascript 代码。在其它特性中,它支持不少 ES6 特性,很像其余编译器。
总的来讲,部分 ES6 特性几乎是能够“免费”使用的,好比模块,箭头函数,剩余参数和类。这些特性只需很小的开销就能够被转译成 ES5 。Array
、String
和 Math
对象和原型的附加方法(如 Array.from()
和 "it".startsWith("you")
)须要所谓的“polyfills”。Polyfills 是浏览器未原生支持的功能的临时补充。你能够先加载 profill,而后你的代码就能够在浏览器中运行,仿佛浏览器有这个功能同样。Babel 和 Traceur 都提供这种 polyfills。
可在Kangax 的 ES6 兼容性表格 中查看转译器和浏览器支持的 ES6 特性的完整概述。在写本文时,最新的浏览器已经支持 55% 到 70% 以上 ES6 特性了,看到这个真是鼓舞人心啊。Microsoft Edge、Google Chrome 和 Mozilla 的 Firefox 已经在这方面相互竞争了,这对 web 技术整体来讲是很是好的。
就我的而言,能够轻松地使用模块、箭头函数和剩余参数之类的 ES6 新特性对于个人代码是一个极大的提升,是生产力的解放。既然我已经习惯了写 ES6 代码并转译成 ES5,随着时间的推移,更多的 ES6 的好处将会天然显现。
一旦你安装了转译器,你可能新从 let
和箭头函数之类的“小”特性开始使用。记住本来就是用 ES5 写的代码将不会被转译器转译。当你使用 ES6 来提升你的代码,而且喜欢它时,你能够逐渐往你的代码中添加更多的 ES6 特性。或者把部分代码转换成新模块或类语法。我保证这样会很爽!
ES6 的内容比本文中所涉及的多得多。未涉及的特性包括 Map
、Set
、标签模板字符串、生成器、Proxy
和 Promise
。让我知道你是否但愿下篇文章涉及这些特性。无论怎样,我很高兴推荐 Axel Rauschmayer 博士写的覆盖全部 ES6 特性的《Exploring ES6》供深刻研究。
当浏览器不断添加新特性时, 经过使用转译器,你的全部代码被有效地“锁定”到 ES5 。因此,就算浏览器彻底支持了某一个 ES6 特性,兼容 ES5 的版本将被使用,可能会性能更差。你能够期望在某个点(在当时你须要兼容的浏览器和环境)上全部的 ES6 特性最终被支持。到那时,咱们须要管理它们,选择性地防止 ES6 特性转译成 ES5,以减小性能开销。考虑这个因素,判断当前是不是开始使用(部分)ES6 的时候。部分公司认为是这样。