ES2015新特性簡介

風靈使發表於2018-10-25

ES6(ECMAScript 6)終於在2015年6月正式釋出了。距離上一次正式公開的ES5(於2009年釋出的)已經相距了不短的時間了。其實在ES6正式釋出之前的一段時間內,ECMA組織已經不再向ES6中新增新特性了,所以其實在ES6正式釋出之前,業界已經有許多對ES6的相關實踐了。比如阮一峰的ECMAScript 6入門其實就是早於ES6的正式釋出時間的。

本文將主要基於lukehoban/es6features,參考眾多的部落格文章資料,對新出的ES6中新增的特性做一個簡介。後續本系列將會產出針對ES6不同特性的文章。

ES6新特性列表

下面的表格給出了ES6包含的所有特性,

新增特性 關鍵詞 用法 描述
箭頭操作符 Arrows v => console.log(v) 類似於部分強型別語言中的lambda表示式
類的支援 Classes - 原生支援類,讓javascript的OOP編碼更加地道
增強的物件字面量 enhanced object literals - 增強物件字面量
字串模板 template strings ${num} 原生支援字串模板,不再需要第三方庫的支援
解構賦值 destructuring [x, y] = [‘hello’, ‘world’] 使用過python的話,你應該很熟悉這個語法
函式引數擴充套件 default, rest, spread - 函式引數可以使用預設值、不定引數以及擴充引數了
let、const let、const - javascript中可以使用塊級作用域和宣告常量了
for…of遍歷 for…of for (v of someArray) { … } 又多了一種折騰陣列、Map等資料結構的方法了
迭代器和生成器 iterators, generator, iterables - ES6較為難以理解的新東西,後面會有相關文章
Unicode unicode - 原生的unicode更加完美的支援
模組和模組載入 modules, modules loader - ES6中開始支援原生模組化啦
map, set, weakmap, weakset - - 新的資料結構
監控代理 proxies - 我們可以監聽物件發生了哪些事,並可以自定義對應的操作
Symbols - - 我們可以使用symbol來建立一個不同尋常的key
Promises - - 這傢伙經常在討論非同步處理流程時被提到
新的API math, number, string, array, object - 原生的功能性API就是方便些
內建物件可以被繼承 subclassable built-ins - 可以基於內建物件,比如Array,來生成一個類
二進位制、八進位制字面量 - - 可以直接在es6中使用二進位制或者八進位制字面量了
Reflect API - - 反射API?
尾呼叫 tail calls - ES6中會自動幫你做一些尾遞迴方面的優化

ok,上面就是es6中涉及到的所有的新增特性。下面我們針對每一個topic,舉一個sample example加以說明,不過並不會深入闡述。

箭頭操作符

何為箭頭操作符?其實箭頭操作符其實就是使用=>語法來代替函式。如果你熟悉C#或者Java之類的強型別語音,你應該知道lambda語法,其實箭頭操作符做的事情跟lambda表示式有異曲同工之妙。具體的用法看下面的例子,

var arr = [1, 2, 3];
// 傳統寫法
arr.forEach(function (v) {
    console.log(v);
});
// 使用箭頭操作符
arr.forEach( v => console.log(v));

在傳統寫法中,我們需要寫一個匿名的函式,然後給這個函式傳入引數,在函式體中對此引數做相關處理。而使用箭頭操作符後,它簡化了函式的編寫(其實是不需要再寫匿名函式了)。

箭頭操作符的左側可以接收一個簡單的引數,也可以使用一個引數列表。右側進行具體的操作或者是返回值。下面的示例展示了箭頭操作符更一般的用法,

// 在表示式中使用
var odds = evens.map(v => v + 1);
var nums = evens.map((v, i) => v + i);
var pairs = evens.map(v => ({even: v, odd: v + 1}))
// 在申明中使用
nums.forEach(v => {
    if (v % 5 === 0) fives.push(v);
});
// 使用this指標
var bob = {
    _name: "Bob",
    _friends: [],
    printFriends() {
        this._friends.forEach(f => consol.log(this._name + " knows " + f));
    }
}

類的原生支援

ES6中提供了類的原生支援,引入了class關鍵字,其實它是一種實現OOP程式設計思想的語法糖。它是基於原型鏈的。

其實javascript本身就是物件導向的語音,只不過沒有Java之類的語言那麼學院派,它更加靈活。ES6中提供的類其實就是對原型模型的包裝。

ES6提供原生class支援後,類方面的操作,比如類的建立、繼承等等更加直觀了。並且父類方法的呼叫、例項化、靜態方法和建構函式等OOP概念都更加形象化了。

我們來看一些示例,看在ES6中到底如何使用學院化的使用類,

// 類的定義
class Animal {
    // ES6中的構造器,相當於建構函式
    constructor(name) {
        this.name = name;
    }
    // 例項方法
    sayName() {
        console.log('My Name is ' + this.name);
    }
}
// 類的繼承
class Programmer extends Animal {
    constructor(name) {
        // 直接呼叫父類構造器進行初始化
        super(name);
    }
    // 子類自己的例項方法
    program() {
        console.log('I\'am coding...');
    }
    // 靜態方法
    static LEVEL() {
        console.log('LEVEL BAD!');
    }
}
// 一些測試
var doggy=new Animal('doggy'),
larry=new Programmer('larry');
doggy.sayName(); // ‘My name is doggy’
larry.sayName(); // ‘My name is larry’
larry.program(); // ‘I'm coding...’
Programmer.LEVEL(); // ‘LEVEL BAD!’

可以看到,ES6中我們可以完全用Java中的思想來寫建立類、繼承類、例項方法、初始化等等。

物件字面量的增強

在ES6中,物件字面量被增強了,寫法更賤geek,同時可以在定義物件的時候做的事情更多了。比如,

  • 可以在物件字面量裡面定義原型
  • 定義方法可以不用function關鍵字了
  • 更加方便的定義方法
  • 直接呼叫原型鏈上層(父層)的方法

具體的我們看下面的示例程式碼,

var human = {
    breathe() {
        console.log('breathing...');
    }
};
function sleep() {
    console.log('sleeping...');
}
var worker = {
    __proto__: human, // 設定原型,相當於繼承human
    company: 'code',
    sleep, // 相當於 `sleep: sleep`
    work() {
        console.log('working...');
    }
};
human.breathe(); // `breathing...`
worker.sleep(); // `sleeping...`
worker.work(); // `working...`

字串模板

ES6中提供原生的字串模板支援。其語法很簡單使用使用反引號

`

來建立字串模板,字串模板中可以使用${placeholder}來生成佔位符。

如果你有使用後端模板的經驗,比如smarty之類的,那麼你將對此不會太陌生。讓我們來看段示例,

var num = Math.random();
console.log(`your random num is ${num}`);

字串模板其實並不是很難的東西,相對比較容易理解。個人覺得其目前主要的使用場景就是改變了動態生成字串的方式,不再像以前那樣使用字串拼接的方式。

解構賦值

如果你熟悉python等語言,你對解構賦值的概念應該很瞭解。解構的含義簡單點就是自動解析陣列或者物件中值,解析出來之後一次賦值給一系列的變數。

利用這個特性,我們可以讓一個函式返回一個陣列,然後利用解構賦值得到陣列中的每一個元素。讓我們來看一些例子。

function getValue() {
    return [1, 2];
}
var [x, y] = getValue();
var [name, age] = ['larry', 26];
console.log('x: ' + x + ', y: ' + y); // x: 1, y: 2
console.log('name: ' + name + ', age: ' + age); // name: larry, age: 26
// 交換兩個變數的值
var [a, b] = [1, 2];
[a, b] = [b, a];
console.log('a: ' + a + ', b: ' + b);

函式引數的擴充套件

ES6中對函式引數作了很大的擴充套件,包括

  • 預設引數
  • 不定引數
  • 擴充套件引數

預設引數,即我們可以為函式的引數設定預設值了。可能你在某些js庫的原始碼經常會看到類似下面這樣的操作,

function foo(name) {
    name = name || 'larry';
    console.log(name);
}

上面的或操作其實就是為了給引數name設定預設值。

而在ES6中,我們可以採用更加方便的方式,如下,

function foo(name = 'larry') {
    console.log(name);
}
foo(); // 'larry'
foo('gejiawen'); // 'gejiawen'

不定引數的意思是,我們的函式接受的引數是不確定的,由具體的呼叫時決定到底會有幾個引數。其實在ES6之前的規範中我們大可以使用arguments變數來達到不定引數的判斷,但是無疑這一方案過於複雜。

不定引數的用法是這樣的,我們在函式引數中使用...x這樣的方式來代表所有的引數。其中x的作用就是指代所有的不定引數集合(其實它就是一個陣列)。讓我們來看個例子,

function add(...x) {
    return x.reduce((m, n) => m + n);
}
console.log(add(1, 2, 3)); // 6
console.log(add(1, 2, 3, 4, 5)); // 15

關於擴充引數,其實擴充引數其實一種語法糖,它允許傳遞一個陣列(或者類陣列)作為函式的引數,而不需要apply操作,函式內部可以自動將其當前不定引數解析,然後可以獲得每一個陣列(類陣列)元素作為實際引數。看下面的示例,

function foo(x, y, z) {
    return x + y + z;
}
console.log(foo(...[1, 2, 3])); // 6

新增letconst關鍵字

ES6中新增了兩個關鍵字,分別為letconst

let用於宣告物件,作用類似var,但是let宣告的變數擁有更狹隘的作用域,脫離特定的作用域後變數的生命週期就終結了。這將有效的避免javascript中由於隱式變數作用域提升而導致的各種bug,甚至是記憶體洩露等等問題。

const用於宣告常量。

讓我們來看一些例子。

for (let i = 0; i < 2; i++) {
    console.log(i); // 0, 1
}
console.log(i); // undefined, 嚴格模式下會報錯

for...of遍歷

ES6中新增了一種遍歷陣列和物件的方法,叫做for...of。他與for...in用法相似。看下面的例子,

var arr = [1, 2, 3];
for (let v of arr) {
    console.log(v); // 1,2,3
}
for (let i in arr) {
    console.log(arr[i]); // 1,2,3
}

從上面的示例程式碼中,我們可以看出,for…of遍歷時提供的是value,而for…in遍歷提供的是key。

迭代器和生成器

ES6中新增的迭代器和生成器的相關概念相對來說有點複雜,後面將會產出專門的文章針對這個topic進行闡述。

Promise

Promises是處理非同步操作的一種模式,之前在很多三方庫中有實現,比如jQuery的deferred物件。當你發起一個非同步請求,並繫結了.when().done()等事件處理程式時,其實就是在應用promise模式。下面是一段示例程式碼,

//建立promise
var promise = new Promise(function(resolve, reject) {
    // 進行一些非同步或耗時操作
    if ( /*如果成功 */ ) {
        resolve("Stuff worked!");
    } else {
        reject(Error("It broke"));
    }
});
//繫結處理程式
promise.then(function(result) {
    //promise成功的話會執行這裡
    console.log(result); // "Stuff worked!"
}, function(err) {
    //promise失敗會執行這裡
    console.log(err); // Error: "It broke"
});

關於promise更多的詳細內容,後面將會產出專門的文章針對這個topic進行闡述。

Unicode的支援

ES6中對Unicode的支援更加完善了,可以使用使用正規表示式的u標記對unicode字串進行匹配。

// same as ES5.1
"????".length == 2
// new RegExp behaviour, opt-in ‘u’
"????".match(/./u)[0].length == 2
// new form
"\u{20BB7}"=="????"=="\uD842\uDFB7"
// new String ops
"????".codePointAt(0) == 0x20BB7
// for-of iterates code points
for(var c of "????") {
  console.log(c);
}

模組化的原生支援

在ES6中,javascript從語言層面開始支援module機制了。在ES6之前,各種模組化JS程式碼的機制和規範早就已經大行其道,比如CommonJS、AMC、CMD等。不過這些都是一些第三方的規範並不是javascript官方的標準規範。

ES6中定義的module機制的用法如下,

// point.js檔案
module "point" {
    export class Point {
        constructor (x, y) {
            public x = x;
            public y = y;
        }
    }
}
// app.js檔案
module point from "/point.js";
import Point from "point"
var origin = new Point(0, 0);
console.log(origin.x, origin.y); // 0, 0

其中app.js中的前兩句的作用是宣告匯入模組,並且指定需要匯入的介面。其實這兩句可以合併成一句,import Point from "/point.js"或者是這樣import * as Point from "/point.js"。不過,如果合併成一句,在使用匯入模組中的暴露介面時,需要這麼來使用,

var origin = new Point.Point(0, 0);

新增四種資料結構

Map,Set, WeakMap, WeakSet這四個資料結構是ES6中新增的集合型別。同時提供了更加方便的獲取屬性值的方法,不用像以前一樣用hasOwnProperty來檢查某個屬性是屬於原型鏈上的還是當前物件的。同時,在進行屬性值新增與獲取時有專門的get,set魔術方法。

看下面的示例程式碼,

// Sets
var s = new Set();
s.add("hello").add("goodbye").add("hello");
s.size === 2;
s.has("hello") === true;
// Maps
var m = new Map();
m.set("hello", 42);
m.set(s, 34);
m.get(s) == 34;

有時候我們會把物件作為另一個物件的鍵用來存放屬性值,普通集合型別,比如一個簡單的物件會阻止垃圾回收器對這些作為屬性鍵存在的物件的回收,有造成記憶體洩漏的危險。而WeakMap,WeakSet則更加安全些,這些作為屬性鍵的物件如果沒有別的變數在引用它們,則會被回收釋放掉,具體還看下面的例子。

// Weak Maps
var wm = new WeakMap();
wm.set(s, { extra: 42 });
wm.size === undefined
// Weak Sets
var ws = new WeakSet();
ws.add({ data: 42 });//因為新增到ws的這個臨時物件沒有其他變數引用它,所以ws不會儲存它的值,也就是說這次新增其實沒有意思

新增內建物件的APIs

Math,Number,String還有Object等新增了許多新的API。下面的示例程式碼對這些新的API進行了簡單展示。

Number.EPSILON;
Number.isInteger(Infinity); // false
Number.isNaN("NaN"); // false
Math.acosh(3); // 1.762747174039086
Math.hypot(3, 4); // 5
Math.imul(Math.pow(2, 32) - 1, Math.pow(2, 32) - 2); // 2
"abcde".includes("cd"); // true
"abc".repeat(3); // "abcabcabc"
Array.from(document.querySelectorAll('*')); // Returns a real Array
Array.of(1, 2, 3); // Similar to new Array(...), but without special one-arg behavior
[0, 0, 0].fill(7, 1); // [0,7,7]
[1, 2, 3].find(x => x == 3); // 3
[1, 2, 3].findIndex(x => x == 2); // 1
[1, 2, 3, 4, 5].copyWithin(3, 0); // [1, 2, 3, 1, 2]
["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"
Object.assign(Point, { origin: new Point(0,0) });

Symbols

我們知道物件其實是鍵值對的集合,而鍵通常來說是字串。在ES6中,除了字串外,我們還可以用symbol這種值來做為物件的鍵。Symbol是一種基本型別,像數字,字串還有布林一樣,它不是一個物件。

Symbol通過呼叫symbol函式產生,它接收一個可選的名字引數,該函式返回的symbol是唯一的。之後就可以用這個返回值做為物件的鍵了。Symbol還可以用來建立私有屬性,外部無法直接訪問由symbol做為鍵的屬性值。

看下面的演示程式碼,

(function() {
    // 建立symbol
    var key = Symbol("key");
    function MyClass(privateData) {
        this[key] = privateData;
    }
    MyClass.prototype = {
        doStuff: function() {
            ... this[key] ...
        }
    };
})();
var c = new MyClass("hello")
c["key"] === undefined // 無法訪問該屬性,因為是私有的

相關文章