ES6:下一版本的JavaScript的新特性
你可能已經聽說過EMCAScript6(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聲明變量,你可以很安全地再聲明一次,因為它只在自己塊級作用域內定義和有效。
CONST
聲明塊級作用域內的變量的另一種方法是使用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關鍵字,剩下的就是0個或多個參數、(=>)箭頭和函數表達式。注意:return語句將隱式地被添加進來。
如果是0個或多個參數,必須添加括號:
// 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 => 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的prototype中添加了幾個方便的方法,大部分是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對象現在新增了一些靜態方法以及prototype上的一些方法。
第一、Array.from方法從類數組或可迭代對象上創建Array的實例。類數組對象的例子包括:
1、函數中的arguments對象
2、document.getElementsByTagName放回的一個nodeList對象
3、新的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的prototype中添加了幾個方法,其中的find方法我覺得Javascript開發者將會非常喜歡。
1、find方法:獲取回調函數return true的第一個元素。
2、findIndex方法:獲取回調函數return true的第一個元素的索引
3、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對象也添加了幾個方法。
1、Math.sign 返回一個數字的符號,有1,-1或0三個值分別表示正值,負值或0
2、Math.trunc 返回一個數字去掉小數位數后的數
3、Math.cbrt 返回一個數字的立方根
Math.sign(5); // 1 Math.sign(-9); // -1 Math.trunc(5.9); // 5 Math.trunc(5.123); // 5 Math.cbrt(64); // 4
如果你想要學習更多的新的Math內容,點擊new number and math features in ES6。
擴展操作符
擴展操作符(…)是一個非常方便的語法,它用于在數組的特殊的地方擴展元素,比如函數調用中的參數。下面展示一些例子來說明它的用處。
首先,我們來看看如何通過另一個數組來擴展數組的元素:
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);
我們已經將擴展操作符應用到Array和arguents中了。實際上,所有的可迭代的對象都可以應用擴展操作符,比如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
解構也能用于對象上,要保證key值匹配。
let obj = {x: 1, y: 2}; let {x, y} = obj; // x = 1, y = 2
也可以通過這個機制來修改變量的名稱
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
解構同樣也可以指定argument對象的默認值,通過字面量對象,可以模擬命名參數。
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中非常值得挖掘的一個特性。
現如今,任何重要的JS項目都會使用某種模塊系統-可能是“暴露型模塊模式”或者更廣泛的AMD和Common.js。但是,瀏覽器是沒有任何模塊系統的特性的,總是需要為AMD或CommonJS模塊構建加載模塊,處理這些的工具包括RequireJS,Browserify和Webpack。
ES6規范中同時包含了模塊中的語法和加載機制。如果你想要在以后使用模塊,應該使用下面的語法。現代的構建工具可以通過插件支持這種格式,所以我們可以盡管去使用它。(不用擔心,我們在后面的“Transpilation”章節中會討論這個問題)。
現在,在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));
模塊系統有一個默認模塊,它也可以是函數。要導出模塊內的默認值,需要提供一個本地名稱:
// lib/my-fn.js export default function() { console.log('echo echo'); } // app.js import doSomething from 'lib/my-fn'; doSomething();
注意:import語句是同步的,但是它會等到所有依賴的加載完畢才會執行。
Classes
類是ES6中討論得很多的一個特性。一些人類違反了JS的原型性質,而其他人覺得這降低了初學者和來自其他開發語言的開發者的入門門檻,并且能夠幫助他們編寫大規模應用。無論如何,它都是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關鍵字來訪問基類的構造函數或方法:
1、要訪問基類構造函數,使用super()
2、要訪問基類中的方法,是用super.getName()
類還有更多的用法,如果想要深入地學習這方面,可以看看Classes in ECMAScript6
Symbol
Symbol是一種新的原始數據類型,和Number、String一樣。我們可以使用symbol來給對象創建唯一的ID或唯一的常量。
const MY_CONSTANT = Symbol(); let obj = {}; obj[MY_CONSTANT] = 1;
注意:Object.getOwnPropertyNames方法不會返回Symbol生成鍵值,在for..in循環中,Object.keys()和JSON.stringify()也是不可見的,這是與普通的字符串key值的區別所在。我們可以通過Object.getOwnPropertySymbols()獲取對象中的symbol數組。
因為不可變的特點,Symbols常常與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
想要學習更多的symbol內容可以查看symbol primitive
Transpilation
現在我們可以使用ES6來寫代碼了。前面介紹中提到的瀏覽器還沒有廣泛地支持ES6的特性,而且支持性各不相同。你的用戶使用的瀏覽器很有可能不完全懂得解析ES6代碼。所以我們要將這些代碼轉換成上一個版本的Javascript(ES5),它們可以很好地運行在現代瀏覽器上,這種轉換通常被稱為Transpilation。在瀏覽器支持ES6之前,都需要在我們的應用程序中做這一項轉換。
開始
轉換代碼并不困難,可以直接通過命令行轉換代碼,或者在Grunt和Gulp中作為一個Task包含在插件里面。有很多轉換代碼的方案,比如Babel,Traceur和TypeScript。可以看看這個使用Babel的例子many ways to start using ES6,很多ES6的特性都會進行處理。
那我們如何使用ES6呢?首先,根據你想要使用的ES6特性和你需要支持的瀏覽器或運行環境(比如Node.js),在你的工作流中結合一個編譯轉換器。如果你希望的話,還有一些監視文件變化和瀏覽器實時刷新的插件來讓你體驗無縫的編碼。
如果是從頭開始,你可能只是想要使用命令行來轉換代碼(查看例子Babel CLI documentation)。如果你已經使用過grunt或gulp之類的工具,你可以添加一個比如gulp-babel的插件,或者Webpack中的babel-loader插件。對于Grunt,有一個grunt-babel,還有很多其他的ES6-related plugins。對于使用Browserify的開發者,可以看看babelify。
很多特性被轉換成ES5的兼容性代碼后,并不會有很大的開銷,通過編譯器提供的臨時性方案會有一點點的性能損耗。你可以通過各種交互環境(也稱作RELPs)來看看使用ES6代碼和編譯后的代碼是什么樣的:
1、Traceur:website,REPL
2、Babel:website,REPL
3、TypeScript:website,REPL
4、ScratchJS(chrome插件)
注意,TypeScript并不完全是一個轉換器,它是強類型的Javascript的超集,可以編譯成Javascript,它和其他轉換器一樣,支持很多ES6特性。
究竟如何使用?
通常來說,一些ES6的特性可以自由地使用,比如模塊、箭頭函數,不定參數和類。這些特性不會用太多開銷,就可以轉換成ES5代碼。而Array、String和Math對象上和原型上的方法(比如Array.from等等)需要所謂的“polyfills”。Polyfills是對那些瀏覽器還沒有原生支持的特性的一個臨時方案。你可以首先加載polyfills,如果瀏覽器有此函數,代碼就會正常運行,Babel和Traceur都會提供類似的polyfills。
可以查看ES6兼容性表來看看轉換器和瀏覽器對ES6新特性的支持情況。令人激動的是,在寫這篇文章的時候,最新的瀏覽器已經支持了所有ES6特性的55%到70%。Microsoft Edge,Google Chrome和Mozilla Firefox相互競爭,這對整個Web的發展有很大的意義。
就我個人而言,我發現能夠很簡單地使用ES6中的新特性,比如模塊,箭頭函數和不定參數等等是一種解脫,也是對自己編碼的一個顯著的提升。現在我很享受使用ES6寫代碼,然后將其轉換成ES5代碼。ES6的優點隨著時間的增長會越來越明顯。
下一步呢?
只要安裝了一個轉換器,就可以開始使用一些小的特性,比如let和箭頭函數。記住,已經編寫好的ES5代碼,轉換器會原封不動地保留下來。當你使用ES6去優化你的代碼,慢慢地喜歡用它,你就可以逐步將越來越多的ES6特性應用到代碼中。也許有一些代碼會有新的模塊或類語法,但是我保證一切都會越來越好的!
除了文章中提到的特性,還有更多的ES6的東西沒有被提到,比如Map,Set,標簽模板字符串,生成器,Proxy和Promise,如果你想知道請關注后續的文章。另外,如果想要深入學習,我推薦Exploring ES6這本書,書里面提到了所有的ES6特性。
最后的思考
通過使用轉換器,所有的代碼實際上是轉換成了ES5。而瀏覽器一直在添加新特性,所以,即便瀏覽器完全支持某個特定的ES6特性,最后還是運行ES5兼容版本的代碼,這樣可能表現會更糟糕。你可以期待,在你需要兼容的瀏覽器和運行環境里,所有的ES6特性最終都會被支持。但是在那之前,我們需要管理好這些ES6特性的支持情況,選擇性地禁用某些ES6特性來減少轉換成ES5代碼后帶來的不必要的開銷。知道了這些,你就可以決定是否要使用ES6中的特性。
譯者信息
小駱,90后碼農一個,潛水于互聯網中,專注web開發,喜愛寫代碼,個人博客狼狼的藍胖子
譯文鏈接:http://www.codeceo.com/article/es6-next-javascript.html
英文原文:ECMAScript 6 (ES6): What’s New In The Next Version Of JavaScript
翻譯作者:碼農網 – 小駱