你可能已經聽說過 ECMAScript 6 (簡稱 ES6)了。ES6 是 Javascript 的下一個版本,它有很多很棒的新特性。這些特性復雜程度各不相同,但對於簡單的腳本和復雜的應用都很有用。在本文中,我們將討論一些精心挑選的 ES6 特性,這些特性可以用於你日常的 Javascript 編碼中。
請注意,當前瀏覽器已經全面展開對這些 ES6 新特性的支持,盡管目前的支持程度還有所差異。如果你需要支持一些缺少很多 ES6 特性的舊版瀏覽器,我將介紹一些當前可以幫助你開始使用 ES6 的解決方案。
文中大多數代碼示例都帶有“運行代碼”鏈接,你可以查看代碼並運行它。
變量
LET
你習慣於用 var 聲明變量。現在你也可以用 let 了。兩者微妙的差別在於作用域。var 聲明的變量作用域為包圍它的函數,而 let 聲明的變量作用域僅在它所在的塊中。
if(true) { let x = 1; } console.log(x); // undefined
這樣使得代碼更加干凈,減少滯留的變量。看看以下經典的數組遍歷:
for(let i = 0, l = list.length; i < l; i++) { // do something with list[i] } console.log(i); // undefined
舉個例子,通常情況下,我們在同一作用域里使用變量 j 來完成另一個遍歷。但是,現在有了 let,可以安全地再一次聲明 i 變量。因為它只在被聲明的塊中有效。
CONST
還有另一個用於聲明塊作用域變量的方法。使用 const,你可以聲明一個值的只讀引用。必須直接給一個變量賦值。如果嘗試修改變量或者沒有立即給變量賦值,都將報錯:
const MY_CONSTANT = 1; MY_CONSTANT = 2 // Error const SOME_CONST; // Error
注意,對象的屬性或數組成員還是可以改變的。
const MY_OBJECT = {some: 1};
MY_OBJECT.some = 'body'; // Cool
箭頭函數
箭頭函數為 Javascript 語言增色不少。它使得代碼更簡潔。我們早早地在本文中介紹箭頭函數,這樣就可以在后面的示例中加以利用了。以下代碼片段是箭頭函數和我們熟悉的 ES5 版本的寫法:
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 對象新增了幾個方法。
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 或不傳參數時都會觸發參數使用默認值。
REST參數
我們已經學習了省略號操作符。剩余參數和它很類似。它同樣是使用 ... 語法,允許你把末尾的參數保存在數組中:
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 的時候。部分公司認為是這樣。
