ES6:下一版本的JavaScript的新特性

2016-01-20    分類:WEB開發、程式設計開發、首頁精華2人評論發表於2016-01-20

本文由碼農網 – 小駱原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃

你可能已經聽說過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
翻譯作者:碼農網 – 小駱
轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]

相關文章