1. 程式人生 > >ECMAScript 6規範總結(長文慎入)

ECMAScript 6規範總結(長文慎入)

閒話

學習ES6的動機起於對其promise標準的好奇,它與jQuery原始碼中Deferred不同,而且在非同步程式設計中加入了Generator,在後續ES7中更有Async。這勾起我強烈的興趣瞭解ES6更多的內容,於是完整的學習了阮一峰老師的《ECMAScript 6入門》

本文不對規範細節做詳細說明。希望通過這篇部落格,記錄自己所理解的es6的語言風格和程式設計思想。

注:以《ECMAScript 6入門》為藍本,大量用例出自其中。

ES6介紹

ECMAScript 6(簡稱ES6)是JavaScript語言的下一代標準,於2015年6月正式釋出,也稱ECMAScript 2015。

摘自《ECMAScript 6入門》

ECMAScript 3.0(1999年12月)成為通行標準,奠定了JavaScript通行標準,直到今天,我們一開始學,都是在學3.0版本語法。
ECMAScript 4.0草案(2007年10月),對ES3做了徹底升級,各方代表對是否通過產生嚴重分歧。2008年7月,EMCA開會決定終止開發,將其中涉及現有功能改善的一小部分,釋出為ECMAScript 3.1(會後不久改名為ECMAScript 5),將其他激進的設想放在以後的版本,由於會議的氣氛,該專案代號起名為Harmony(和諧)。

2009年12月,ECMAScript 5.0正式釋出。Harmony專案一分為二,一些較為可行的設想定為JavaScript.next繼續開發,後來演變成ES6,一些不是很成熟的設想,被視為JavaScript.next.next,在更遠的將來再考慮推出。

2011年6月,ECMAscript 5.1版釋出,並且成為ISO國際標準(ISO/IEC 16262:2011)
2015年6月,ECMAScript 6正式通過,成為國際標準。

ES6總覽

tips:

  • ES6規範的原則是儘可能完整的向下相容,除了塊級作用域支援外,原有程式碼幾乎不受影響。通過新增API及語法擴充套件支援。隨著規範的普及,完全參照嚴格模式'use strict'將成為程式設計最佳實踐

  • 不同類別的工具方法掛載在對應的建構函式上,而不是作為全域性方法(如isNaN() -> Number.isNaN()),對原有全域性方法進行了遷移(原有的還在)。

下面,分 5 點對 ES6 進行全面解讀。ES6總覽後,為每點的分條詳述。

1、語法升級

對基本語法進行了增強,並調整為塊級作用域支援。

用更直觀的“宣告式”思想(解構賦值、...擴充套件運算子、無 this 上下文困擾的箭頭函式、for…of 遍歷),對取值、賦值、物件表示、建構函式及繼承等的過程進行了大幅簡化。

2、模組化

靜態化的模組系統支援(預設嚴格模式程式設計)。完美的迴圈依賴處理(commonjs只算半支援),動態的輸出值引用。

3、型別升級

Number 新的二/八進位制寫法、浮點誤差、安全數;String RegExp:全面支援32位utf16字元,定義了超簡易的模板字串拼接(並可便捷的自定義模板處理規則);引入基本資料型別Symbol,代表獨一無二值,有效防止屬性命名衝突;Array陣列空位處理方法的修正,提供 for…of 遍歷及對名值遍歷的API支援;新增資料結構SetMap及弱引用的WeakSetWeakMap,可去重儲存value、key-value。

資料結構的增加,使得ES6 for…of遍歷不僅僅需要對陣列、字串等帶有length屬性的類陣列生效,還需要能夠個性化定製。抽象出Symbol.iterator介面,凡是帶有該介面的物件均可被遍歷(僅有length屬性的類陣列不可以),呼叫該介面。比如陣列會呼叫Array.prototypeSymbol.iterator

Symbol屬性的新增也使物件列舉相關的API增加了幾個(是否列舉Symbol、原型鏈、不可列舉屬性)

tips:遍歷與列舉的不同在於,遍歷是對值(value)的,列舉是對鍵(key)的。遍歷的順序是Symbol.iterator介面定義的(陣列是0~n數字順序);列舉是底層內部定義的(順序:先數字排序、屬性按時間排、Symbol按時間排),未開放許可權

4、語言層面

分層的許可權

為了便於理解,我把底層行為分為 規則層(基於物件,被遍歷、被列舉、被正則匹配、被new、被轉型別等)、屬性配置層(基於屬性,propertyDescriptor)。

ES6的一大特點是,開放許可權。姑且把我所理解的許可權分為 5 類:原型鏈、呼叫棧、作用域鏈、物件規則層、屬性配置層。

ES6函式嚴格模式執行時不再對呼叫棧引用,此時支援尾遞迴優化。作用域鏈引用不可開放,這是詞法作用域安全性、隔離性的根本。開放了規則層自定義,使得開發者能夠自定義一些對細部規則的反應。開放了原型鏈的訪問,使得已有物件也能直接改變原型鏈引用,使更強大的繼承容易做到(通常儘可能不用)。屬性配置層到了ES5就比較完善了。

ES6把規則層的部分行為抽象為一系列介面,涉及被正則匹配、被判斷instanceof、被for…of遍歷、陣列是否可展開、構造器的返回物件和stringTag等。出於防止命名衝突的考慮,都使用Symbol值(獨一無二),儲存在內建的Symbol建構函式的屬性上,共11個(很多並不是語言層面的重要規則操作,定位:偏個性化的需求 + 部分重要規則)

Object例項是js裡的基礎物件,包括函式都是由object衍生而來。它是一種基本的key-value式的資料結構。

  • 1、物件下通過內建Symbol規則屬性個性化定義特殊行為時如何反應。
  • 2、每個屬性的value,只是屬性描述的一部分。Object.getOwnPropertyDescriptor(obj, pro)可獲取,設定是否可列舉、可定義、只讀、是否為訪問器(get、set)。

Proxy和Reflect

ES6新增Proxy資料型別,可以通過new Proxy(obj, handler)生成物件操作的代理,本質是一個攔截層,涉及增刪查改屬性值、設定原型鏈、屬性配置、遍歷列舉、環境繫結、new等等操作(部分內建Symbol不是對物件的主要操作,只是小的個性化補充,就不包含在內了)。

新增Reflect,提供了所有與Proxy對應的語言預設操作方法,一一對應,目前有 14 個。

Reflect有著幾乎所有對物件的重要操作,ES6以前跟語言相關的配置操作都在Object上,都遷移了過去,並且對設定型的API都以返回false表示設定失敗,而不是丟擲錯誤。以後語言內部相關的方法都將擴充到Reflect,Object上不一定會新增。

5、非同步程式設計

傳統的非同步使用回撥函式,函式以引數形式傳入以待呼叫。複雜情況時,回撥函式裡可能也有非同步邏輯,導致層層巢狀。而且還需要手動catch錯誤。

ES6推出了promise標準。既能把每層的邏輯解耦分開,又有自動的機制catch錯誤。通過then串聯起來要執行的邏輯。

ES6支援Generator函式。它是語言層面的支援,用同步的方式來順序書寫非同步程式碼,以yield暫停。相較promise有著更直觀的控制流管理,“半協程”的實現,使得在yield程序的切換中仍然保留著呼叫棧,使得內部定義的 try…catch 總能捕捉到內部的錯誤,是完全意義上的同步式寫法。雖然在promise的原始碼中利用詞法作用域的特點也能解決。

但Generator只相當於一個狀態機,宣告式的定義了流程,還需要封裝一個co模組函式才能實現支援非同步邏輯的自動流程處理。

ES7提供了Async函式,是Generator的語法糖,呼叫時等同於被co函式載入執行的Generator函式。到此,非同步程式設計算是得到了最佳實踐。

語法升級

核心:用一目瞭然的方式,簡化表達。定義ES6推薦的最佳程式設計實踐。

1、作用域

ES6支援了塊級作用域,{} 部分包裹的程式碼塊具有獨立的作用域,如if、for。新增let(變數) const(常量)定義變數,必須先定義後使用,不會變數提升,更不容易出錯,填var的坑。

{
    let a = 5;
    const b = 4;  // 不能重新賦值或改變引用,但能改變引用物件內的屬性
    a = 3;  // 3
    b = 3;  // error
}
console.log(a)  // error

// 自執行函式 作用有 2 點:1.防止全域性汙染; 2.構造閉包儲存變數狀態
// 在只需 第1點 時,可以 { 程式碼 } 替代

函式宣告可以在塊級內宣告 { }不再報錯,只在塊級作用域中變數提升。

if (true)
    function a() {}  // error

// 正確版本,不能省略{}
if (true) {
    function a() {}
}
console.log(a);  // error

2、取值、賦值、物件表達

物件簡寫

let b ='check';
let obj = {
    a: 1,
    b,  // 等同 b: b ,即 b: 'check'

    c(x, y) { return x+y },  // 等同 c: function(){}

    get d() { return 2; }, // 設定 d 的 get 取值器函式
    [Symbol('foo')]() {return true},  // 設定 [Symbol('foo')] 的函式值
    * e(x) { yield x; }  // 設定 e 的Generator函式值

function test(x, y) {
    return {x, y};  // {x: x, y: y}
}

一步到位 的賦值方法 —— 解構賦值 + 預設值,直觀、高效。

let [a, [b, c]] = [3, 'str'];
// a=3, b='s', c='t'   陣列型賦值:要求右側值有Symbol.iterator介面,如陣列、字串

let {a, b=4, c=4, d: _d=5} = {a: 2, b: 3};  // 等同 let {a: a, b: b=4, c: c=4, d: _d=5} = {a: 2, b: 3};
// a=2, b=3, c=4(預設值), _d=5(預設值)

[x, y] = [y, x]  // 交換賦值


/* ---- 優化示例 ---- */

// ES3,遇上一個 fun(args) 的API,args有7個可選屬性介面,看得出麼,一臉懵逼(゚Д゚≡゚Д゚)
function sb(args) {
    return args.a + args.b * args.c;
}

// ES6
function sb({a, b, c}) {
    return a + b * c;  // 無引數時出錯
}
sb({a:1, b:2, c:3});  // 7

// 不傳引數時預設為 {a:0, b:0, c:0}
function sb({a, b, c} = {a:0, b:0, c:0}) {
    return a + b * c;
}
sb();  // 7
sb({a:2});  // error, 不使用預設值,但b、c為undefined

// 屬性預設值
// 無值引數取預設值{},無a、b、c引數,預設取0
function sb({a=0, b=0, c=0} = {}) { // 等同 {a: a=0, b: b=0, c: c=0} = {}
    return a + b * c;
}
sb();  // 7

引入“…”擴充套件運算子陣列環境(函式引數算陣列環境),用於賦值(左側=)為rest引數(只可用於尾引數),用於取值則為擴充套件值(=右側,需Symbol.iterator介面支援)

/* 賦值,rest 引數 */
let [a ,b, ...c] = [1, 2, 3, 4, 5];
// a=1, b=2, c=[3, 4, 5]
let [a ,b, ...[c, d]] = [1, 2, 3];
// a=1, b=2, c=3, d=undefined

function t(a, ...arr) {}
t(1,2,3) -> a=1, arr[1, 2]

/* 取值 */
let a = [...[1, 2], 3, ...'str'];
// [1, 2, 3, 's', 't', 'r']

/* 兩者結合 —— 解決平常厭惡的只能apply傳入相同引數的問題 */
function test(...args) {  // 賦值
    return function _test() {
        return fun(...args);  // 取值,等同 fun.apply(this, args)
    }
}

ES7提案 引入“…”擴充套件運算子物件環境。用於賦值(左側=)為rest引數(只可用於尾引數),用於取值則為擴充套件值(=右側,只擴充套件自身的可列舉屬性,等同Object.key(obj))

/* 賦值,rest 引數 */
let {a ,b, ...re} = {a:1, b:2, c:3, d:4, e:5};
// a=1, b=2, re={c:3, d:4, e:5]
let {...{x, y}} = {x:1, y:2};
// x=1, y=2

/* 取值 */
let a = {...[1, 2], gg:3, ...{x:4, y:5}};
// {'0':1, '1':2, gg:3, x:4, y:5}

ES7提案 ‘::’簡化bindapplycall

foo::bar;
// 等同於
bar.bind(foo);

foo::bar(...arguments);
// 等同於
bar.apply(foo, arguments);

::console.log  // 等同於 console::console.log

3、箭頭函式

只相當於一個簡單的 { } 塊級程式碼段(選擇性使用)。沒有普通函式的能力:獨立的 this 上下文(這有時候是坑的來源。箭頭函式 bind 也無效)、arguments 引數、對呼叫棧的訪問。不能用作Generator狀態機。

let a = (x) => x+2;
// '=>' 左側的引數若是一個,可簡寫為 let a = x => x+2;
// '=>' 右側 x+2 是 {return x+2;} 的簡寫,{}中包含函式中所有程式碼

// 若返回物件,可({})返回。(x) => {return {id: x};} 可簡寫為 x => ({id: x})

var obj = {
    a: 1,
    b: function() {
        setTimeout( () => {
            this.a++;  // this 為 b 函式內 this,可以更簡單的繫結環境
        }, 0);
    }
};

// 便捷易懂的管道
let f = (x=0) => (y=0) => ({
    before: (z=0, w=0) => x + y * z - w,
    after: (z=0, w=0) => x * y - z - w
});
f(1)(1).before(1, 1);  // 1

4、class

替代傳統建構函式。不會變數提升。static 代表靜態,其他為原型方法。在內部定義靜態屬性無效,結尾無分號

class A {
    constructor(x, y) {  // 若省略,則預設 constructor() {}
        this.x = x;
        this.y = y;
    }
    add() {  // 例項方法
        return x + y;
    }
    get z() {  // 取值函式,obj.z = true;
        return true;
    }
    static classMethod(obj) {  // 靜態方法,A.static
        obj.x = 0;
        obj.y = 0;
    }
}
A.staticProp = 1;

ES7提案 新增例項屬性、靜態屬性值預設值定義。prop = 1; static staticProp = 2;帶分號

class A {
    constructor() {  // 若省略,則預設 constructor() {}
        this.x = 1;
    }
}
A.staticProp = 2;

// 等同, ES6 暫不支援
class A {
    x = 1;
    static classMethod = 2;
}

extends繼承,能夠繼承 static 屬性、原型屬性。對例項屬性的繼承不再通過轉移環境 A.apply(this, arguments),這樣無法完整的繼承內部建構函式(如 Array 例項繼承後沒有動態變化的 length)。直接通過建立原函式例項,在該例項上修改,並設定原型的方式實現完整繼承。

class A {
    constructor(x, y) {
        this.x = x;
        this.y = y;
        this.bool = new.target === A;  // new.target 指向建構函式
    }
    prop() { return 1; }
    static staticProp() { return 2; } 
}

// extends 後可跟函式、返回函式的表示式
class B extends A {
    constructor(x, y, z) {  // 省略,則  constructor(...args) { super(...args); }
        super(x, y);  // 呼叫 A(x, y), 之後才可以 this.xxx 進行賦值
        this.z = z;
    }
    prop() {  // super 此處指父類例項
        return 10 + super.prop();
    }
    static staticProp() {  // super 此處指父類
        return 10 + super.staticProp();
    }
}

new A(1,2).bool // true
new B(1,2).bool  // false , new.target 指向了B

模組化Moudule

ES6 新增模組(module)體系,有效解決命名衝突、複雜依賴的歷史問題。模組內預設嚴格模式’use strict’

commonjs規範一樣可以支援迴圈依賴(嚴格說 commonjs 只能算一半支援,ES6規範完全支援),但卻是更底層的,靜態化的處理,使得編譯時就能確定模組的依賴關係,以及輸入和輸出的變數(引入的變數為動態引用,會一直隨著源模組變數的值的變化而變化)。

對commonjs(這裡用A->B表示A模組引用B模組):A->B且B->A。當從A開始執行時,B第一次引用的A只是執行一部分的,A第一次引用的B是全部的,但建立在B獲取到不完全的A的基礎上。然後等A執行完,A、B模組才能被無bug的引用(詳情見《es6入門》裡module章節的解釋)

export 輸出、import 引出、export default 預設輸出項,as 定義別名,* 用於 import 代表除預設項外所有

// 輸出通常不這樣寫,除了 export default
export let a = 'str';
export function b() {};

// a.js
let a = 1, b = 2, c = 3;
export {a, b as aliasB};  // 以別名輸出
export default c;  // default 是特殊別名,一個模組只能一個
// 等同 export {a, b as aliasB, c as default};

// b.js
import def, {a, aliasB} from './a';
// 等同 import {a, aliasB, default as def} from './a';

// c.js
import * as mou from './a';
// mou.a = 1, mou.aliasB =2 。 * 內不包括 export default 的值,需單獨引入

export * from './a';  // 繼承,直接引入並全部傳出

型別升級

[Number]

1、處理數值的方法遷移到Number函式上,使語言結構更清晰。如全域性方法isFinite()、isNaN()、parseInt()、parseFloat(),其中判斷數值的Number.isFinite()、Number.isNaN()若引數不為數值、布林值,直接返回 false

2、在全語言的支援上,認為 +0 -0 不同,NaN 與自身相同(ES6向後相容,但在新增API中判斷值的相等性時支援)

3個老問題:
1、二進位制、八進位制的表示存在歧義
2、由於以浮點數儲存,因意外的誤差導致不相等
3、不在-2^532^53之間的數無法正確表示、運算

1、優化了二進位制、八進位制的表示,分別為0b/0B0o/0O,十六進位制不變0x/0X

3 === 0b11  // true
9 === 0o11  // true
17 === 0x11  // true

2、新增極小常量(誤差上限)、新增安全數判斷(判斷是否支援)

Number.isFinite(15)  // true
Number.isNaN(3)  // false, Number.isNaN('3')為true, isNaN('3')為false

Number.parseInt('123.45#')  // '123', 行為不變
Number.parseFloat('123.45#')  // '123.45', 行為不變

Number.EPSILON  // 2.220446049250313e-16
Number.MAX_SAFE_INTEGER  // 9007199254740991
Number.MIN_SAFE_INTEGER  // -9007199254740991

Number.isInteger(15.0)  // true
Number.isSafeInteger(9007199254740992)  // false

// 按照ES6的思想構造一個判斷兩數值是否相等的函式,ES6中 Object.is(A,B) 能辨別所有型別的A、B是否相等 
function equalNum(A, B) {

    if (Math.abs(A) > Number.MAX_SAFE_INTEGER) {
        throw new Error(`${A} is outof safe range!`);
    } else if (Math.abs(B) > Number.MAX_SAFE_INTEGER) {
        throw new Error(`${B} is outof safe range!`);  
    } else if (Math.abs(A - B) < Number.EPSILON) {
        return A !== 0 || 1/A === 1/B;
    } else if (A !== A && B !== B) {
        return true;  // NaN
    }
}   

tips:新增若干Math方法,對指數運算、三角函式提供更多支援。Math.trunc()可對數值擷取整數

[String]

1、從語言層面,讓模板字串的構建和解析更直觀,一目瞭然,一步到位

let str;
str = 'I am me.You are you.';
// 等同於
const me = 'me', you = 'you';
str = `I am ${me}.You are ${you}.`;

反引號(`)中包裹的 ${param} 用來表示變數的值,是如下寫法的縮寫

let str = tag`I am ${me}.You are ${you}.`;

function tag(stringArr, value1, value2) {
    // stringArr -> ['I am ', 'You are ', '.']
    // stringArr.raw 指向前者中'\'被轉義後的陣列,若'\'已被轉義,不做處理
    // value1 -> 'me'
    // value2 -> 'you'
    let str = stringArr[0];
    for (let i=1, len=arguments.length; i<len; i++) {
        str += arguments[i] + stringArr[i];
    }

    return str;
}

通過顯式的指定函式,可以輕鬆完成字串的安全處理、模板解析等

let str = safeHtml`I am ${me}.You are ${you}.`;
// safeHtml函式 略

String.raw() 可以返回一個被轉義的字串,首引數為有raw屬性的陣列、類陣列

let str = String.raw`I am ${me}.\nYou are ${you}.`;
// 'I am me.\\nYou are you.'

// 等同於
let str = tag`I am ${me}.\nYou are ${you}.`;
function tag(stringArr, value1, value2) {
    let _stringArr = stringArr.raw,
        str = _stringArr[0];
    for (let i=1, len=arguments.length; i<len; i++) {
        str += arguments[i] + _stringArr[i];
    }

    return str;
}


2、通過新增API及寫法,全面支援32位utf16字元,同時向後相容(關鍵:寫法、length、遍歷、匹配(通過RegExp支援))

/* 1. \u{}的全新寫法,支援超過FFFF */

let s = "\uD842\uDFB7";   // markdown亂碼了,這貨是一個字
// 等同於
let s = "\u{20BB7}";

/**
 * 2. 全新方法取字元編碼 String.prototype.codePointAt()、把字元編碼轉字元 String.fromCodePoint()
 * 其實就是想了個新詞 codePoint 替代 charCode,我會亂說嗎
 */
"\uD842\uDFB7人".codePointAt(0)  // 134071, charCodeAt為55362
"\uD842\uDFB7人".codePointAt(1)  // 57271, 跟charCodeAt相同,length向下相容,未調整

for (let a of "\uD842\uDFB7人")  // ES6新增遍歷,自然是全面支援32位字元的 -> '\uD842\uDFB7','人'
[..."\uD842\uDFB7人"].length  // 2, 變通方法

// tips: 老方法 "\uD842\uDFB7人加"charAt(2) -> "加", ES6沒有對應API,目前ES7有一個提案,用String.prototype.at() 替代。可通過[...str](pos)取值


3、新增字串的String.prototype.repeat()重複、String.prototype.padStart()/padEnd()補位(未增加覆蓋功能,如陣列新增的copyWithin

'he'.repeat(3)  // 'hehehe'
'he'.padStart(9, 'ab')  // 'abababahe', 若(1, 'ab') -> 'he'
'he'.padEnd(9, 'ab')  // 'heabababa'

tips:

  • 新增String.prototype.includes(str)/startWith(str, pos)/endWith(str, pos)。由於增有concat()、+、刪改有replace()、查有indexOf()、search()、擷取有splice()、打斷成陣列split(),而新方法並沒有在length和查詢匹配上做修正以支援32位utf16字元(不理解原因),因此看起來倒不是很必要

  • ES6的思路是把32位utf16字元匹配,放在RegExp有關的方法上,新增全新的flag模式

[RegExp]

正則的作用是快速匹配字串

1、ES6把所有跟正則有關的核心程式碼,遷移到了RegExp.prototype

String.prototype.match 呼叫 RegExp.prototype[Symbol.match]
String.prototype.replace 呼叫 RegExp.prototype[Symbol.replace]
String.prototype.search 呼叫 RegExp.prototype[Symbol.search]
String.prototype.split 呼叫 RegExp.prototype[Symbol.split]

2、新增flag修飾符u(開啟32位編碼查詢支援)、 y(粘連式全域性匹配)。原有g全域性匹配、i忽略大小寫、m支援多行查詢

/* 開啟 32位UTF16字元 識別 */
/\uD83D/.test('\uD83D\uDC2A')  // true
/\uD83D/u.test('\uD83D\uDC2A')  // false, 能正確識別右側為一個字
/^.$/.test('\uD83D\uDC2A')  // false, 解讀成2個字元
/^.$/u.test('\uD83D\uDC2A')  // true

/* 粘連全域性匹配,有時易於發現非法字元 */
'aa_a_ba_'.match(/a+_/g)  // ['aa_', 'a_', 'a_']
'aa_a_ba_'.match(/a+_/y)  // ['aa_', 'a_'], 順序全域性匹配,一旦不符,返回
'#x#2'.split(/#/y)  // ['', 'x#2']
'aaxa'.replace(/a/y, '-') // '--xa'

/a/y.sticky  // true


*3、ES7提案:後行斷言(之前只有先行斷言支援)。斷言可以不捕獲,只匹配不包含斷言的部分

/* 先行斷言lookahead */
/x(?=y)/
/x(?!y)/

/* 後行斷言lookbehind */
/(?=x)y)/
/(?!x)y/

tips:

  • 新增RegExp.escape(),用於雙重轉義字串中的’\’,可用於new RegExp(RegExp.escape(str), flags)生成正則。/ab\nf/.source也會輸出雙重轉義的字串,可用於new RegExp()

  • 在正則匹配失敗的時候,經常會效能糟糕。因為正則中通常的貪婪或吝嗇匹配,都是在能匹配成功的情況下的。當不成功時,就會回溯,若正則複雜疊加了多層,就是效能災難。因此對於確定匹配的專案,可以使用 斷言+反向引用

[Symbol]

Symbol是ES6新增的基本資料型別,用來指定獨一無二值。

出現原因:

物件的屬性使用字串指定,但是很可能會跟原有的屬性名一致,或者後來者造成屬性覆蓋。以往通常是用 字串+Date毫秒(或隨機數)。為了解決這個頭疼的問題,引入了獨一無二值資料型別Symbol。再也不用絞盡腦汁的想奇奇怪怪的名字了。。┑( ̄Д  ̄)┍

Symbol()Symbol.for()Symbol.keyFor()

// Symbol(key) 每一個都不相同,不會註冊到全域性,不能被Symbol.for()使用
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2  // false
Symbol.keyFor(s2);  // undefined,未註冊,無法搜尋

// Symbol.for(key) 先搜尋全域性尋找key對應的Symbol,若無,生成一個Symbol並註冊到全域性
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2  // true
Symbol.keyFor(s2);  // 'foo'

ES6提供了11個內建Symbol值,指向語言內部使用的方法。之所以如此,是為了防止使用時人為的命名衝突,得用未註冊到全域性的Symbol值,必須把屬性名儲存起來

Symbol.iterator  // 最常用。帶有該介面,才能被 for...of 、...遍歷

// 如下,通常有才呼叫,沒有則預設行為
Symbol.hasInstance  // a instanceof MyClass,呼叫MyClass[Symbol.hasInstance](foo)
Symbol.isConcatSpreadable  // 使用 Array.prototype.concat() 時是否可被展開
Symbol.species  // 作為建構函式時,返回值
Symbol.match  // 被使用 String.prototype.match() 時
Symbol.replace
Symbol.search
Symbol.split
Symbol.toPrimitive  // 被轉為原始型別值時  obj[Symbol.toPrimitive](type)
Symbol.toStringTag  // "[object xxx]" 修改xxx部分的字串
Symbol.unscopables  // 指定使用with時,哪些屬性被排除 { propA: true }

[遍歷]

我們經常用for (var i=0; i<len; i++)的方式進行遍歷(如陣列)。ES5陣列例項支援filter、map等方法,使得基於遍歷的處理變得更簡單。其實這就是迭代器,但是ES5的迭代API都會跳過陣列空位,與ES6的思想不符,需要新的方法支援。

這裡說說迭代器本身。迭代器分為兩種:內部迭代器、外部迭代器。內部迭代器邏輯簡單卻不夠靈活,外部迭代器稍微複雜,但足夠靈巧。

// 內部迭代器 - 示例
Array.prototype.mapDemo = function(callback) {
    for (let i=0; i<this.length; i++) {
        callback(this[i], i);
    }
}

// 外部迭代器 - 示例
var Iterator = function(obj) {
    let current = 0;
    return {
        next() {
            return current < obj.length ?
                {value: obj[current++], done: false} :
                {value: undefined, done: true};
        }
    }
};

ES6新增了Set Map資料結構,為了給不同資料結構提供一個統一的訪問機制,並且更靈活的迭代。抽象出了Iterator(遍歷器),能夠細粒度的訪問元素,同時for...of提供自動的遍歷。所有物件必須有[Symbol.iterator]屬性,才能夠被for…of遍歷=右側的...擴充套件運算子用於陣列時,也會呼叫for…of。

物件的[Symbol.iterator]屬性被呼叫得到Iterator物件(在ES6的實現裡系統定義的Iterator物件(如Array例項的[Symbol.iterator]函式)的_proto_原型都會是一個有[Symbol.iterator]介面的物件,執行anIteratorSymbol.iterator會返回自身,因此Iterator物件本身也可以被遍歷,自己新增[Symbol.iterator]執行後返回的遍歷器若沒設定原型鏈為自身,自然就沒有這個待遇了),Iterator物件呼叫 next 要求返回 {value: contentHere, done: boolean} 帶有value和done的介面(見外部迭代器程式碼)。當done為true時表示結束(該項value不被計入)。使用 for (let x of anIterator),則 x 為每項的value。

for…of 是對值的遍歷(不是對index/key,能正確遍歷帶32位utf16字元的字串),若中途提前退出(通常是因為出錯,或者有break語句或continue語句),將觸發Iterator物件的return方法(與throw方法都是可選配置),必須返回一個物件,如 {done: true}。

for (let [x, y] of [[1,2], [3,4]]) {
    console.log(x, y);
}
// 1 2
// 3 4

// `Iterator`物件本身也可以被 for...of 遍歷
for (let [x, y] of [[1,2], [3,4]][Symbol.iterator]()) {
    console.log(x, y);
}

[列舉]

ES6新增了Symbol型別值可以作為物件屬性,for...in不能列舉Symbol屬性,提供了新的API支援。(列舉是對值key的遍歷,不是value。列舉相當於對遍歷的一種加工

(1) for...in
遍歷物件自身和繼承(__proto__)的可列舉屬性(不含Symbol屬性)

/* 下面均返回可遍歷的物件 */
(2) Object.keys(obj)
返回陣列,包括物件自身的可列舉屬性(不含Symbol屬性)

(3) Object.getOwnPropertyNames(obj)
返回陣列,包含物件自身的所有屬性(不含Symbol屬性)

(4) Object.getOwnPropertySymbols(obj)
返回陣列,包含物件自身的Symbol屬性

(5) Reflect.ownKeys(obj)
返回陣列,包含自身所有屬性

(6) Reflect.enumerate(obj)
返回Iterator物件,對其let...of遍歷,會與for (x in obj) 表現一致

[Array]

老問題:

  • Array構造陣列時單引數和多引數行為不一
  • 對陣列空項、ES5 API在操作時直接跳過,ES6改變了策略,新API都把空項當做undefined處理

新增Array.of(),與new Array()多引數時一致

Array.of()  // []
Array.of(undefined)  // [undefined]
Array.of(1, 2, 3)  // [1, 2, 3]

new Array(4)  // [,,,,]
new Array(1, 2)  // [1, 2]

“語法升級”中提到...擴充套件運算子,可以把帶有Symbol.iterator屬性的物件轉為陣列。在整個ES6體系裡已經拋棄了對類陣列(帶length)的眷顧(因為遍歷被進一步抽象,用於不只是數值索引的情況),但是它可以使用 Array.from(obj, map),同時支援類陣列。

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};
// ES5的寫法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ES6的寫法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

Array.from([1,2]);  // [1,2]  一模一樣的新陣列
Array.from({ length: 3 });  // [ undefined, undefined, undefinded ]

Array.from({ length: 3 }, (value, index) => index);  // [0, 1, 2],阿里面試題答案有木有!

// 類陣列如下方式可以支援`for...of`遍歷
arrayLike[Symbol.iterator] = Array.prototype[Symbol.iterator];
[...arrayLike]

ES7新增了一種陣列推導的方式從現有陣列生成新陣列,比Array.from()強大,非常簡潔!!可以替代map和filter方法。

let years = [ 1954, 1974, 1990, 2006, 2010, 2014 ];

[for (year of years) if (year > 2000) if(year < 2010) year];
// [ 2006]

Array.prototype.keys()/values()/entries() 分別返回名、值、名值對的遍歷器物件。使得只對value遍歷的for...of能夠對陣列完成多種遍歷

for (let index of ['a', 'b'].keys()) {
    console.log(index);
}
// 0
// 1

for (let value of ['a', 'b'].values()) {
    console.log(value);
}
// 'a'
// 'b'

for (let [index, value] of ['a', 'b'].entries()) {
    console.log(index, value);
}
// 0 'a'
// 1 'b'

新增Array.prototype.copyWithin(target, start, end)/fill(value),表示 移位覆蓋/填充

Array.prototype.copyWithin(target, start, end)
target(可選) -> 從該位置開始替換資料
start(可選) -> 從該位置開始讀取,預設0,負數表示倒數
end(可選) -> 到該位置前停止讀取資料,預設等於length,負數表示倒數

[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]

[1, 2, 3].fill(7)
// [7, 7, 7]

新增Array.prototype.includes(value)/find(value, index, arr)/findIndex(value, index, arr),取代indexOf(),可判斷NaN

[1, 5, 10, 15].find(function(value, index, arr) {
  return value > 9;
}) // 10

[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2

[Object]

物件的簡潔表示,已經在”語法升級”中做了說明。

ES6新增Objet.assign(),用於物件可列舉屬性合併(淺拷貝,一層)。類似於jq的extend。

var target = { a: 1, b: 1 };

var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

新增Object.is(A, B),用來判斷兩個值或物件是否相等。但能正確判斷NaN,+0和-0不等。

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

ES6開放了原型鏈的設定許可權。用可訪問的proto屬性存取物件原型鏈(不建議),也可用Object.setPrototypeOf(),Object.getPrototypeOf()方法存取(建議)。

var obj = {
  method: function() { ... },
  __proto__ : someOtherObj
}
Object.getPrototypeOf(obj);  //  someOtherObj
Object.setPrototypeOf(obj, anOtherObj);

物件沒有[Symbol.iterator]介面,不能直接實現對value的遍歷,可以通過Object.keys()得到陣列集合,然後通過obj[prop]求得值。

ES7提案,參考陣列,引入與Object.keys()配套的Object.values()、Object.entries(),返回陣列。三個API都只對自身可列舉的非Symbol屬性有效。

ES7提案,新增Object.getOwnPropertyDescriptors()(所有自身屬性),與ES5方法Object.getOwnPropertyDescriptor()配套。若加入標準,則會有相應的Reflect.getOwnPropertyDescriptors()方法。

const shallowClone = (obj) => Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
);

[Set/Map]

ES6新增資料結構Set Map ,和與之配套的WeakSet WeakMap。但只有Set、Map有[Symbol.iterator]介面,能被遍歷。

Set可以看做對陣列不能剔除重複項的補充(自動去重),Map可以看做對物件只能以字串為鍵的補充(還可以以物件為鍵)。

/* Set 和 Map 公有屬性方法*/

// size 成員數
// add(value)/Set(key, value) 新增,返回Set/Map物件本身
// delete(xx) 刪除,返回布林值,表示刪除是否成功
// has(xx) 返回布林值
// clear() 清空,無返回值

// Map專有方法:get(key) 獲取對應value

/* Set */

let set = new Set();

set.add({}).add({});  // 等同 new Set([{}, {}]);
set.size // 2

set.add(1).add(1);
set.size // 3,不會重複新增1

/* Map */

let map = new Map();

map.set(NaN, '111').set({}, '222');  // 等同 new Map([[NaN, '111'], [{}, '222']]);
map.get(NaN) // '111',若key為簡單型別值,除NaN外,只要===,將其視為一個鍵,NaN也都視為一個鍵

Set和Map不是陣列,沒有數字索引,需要遍歷支援。提供了4個公有方法

keys():返回一個鍵名的遍歷器
values():返回一個鍵值的遍歷器
entries():返回一個鍵值對的遍歷器
forEach():使用回撥函式遍歷每個成員

// 使用示例
let map = new Map();
// some code...
for (let a of map.entries()) {}

WeakSetWeakMap是ES6提供的兩種弱引用型別。分別只支援 value為物件/key為物件 的情形。與SetMap不同,儲存的資料如果在環境中不再被引用,則會被垃圾清理機制,WeakSet和WeakMap並不會引用到它們。沒有size、clear()和4個遍歷介面。

這讓我有一個簡單的猜測,每個WeakSet/WeakMap物件建立的時候,內部產生一個獨一無二的Symbol()值。對其add/set時,對value/key新增Symbol()屬性,這樣被呼叫has(obj)時直接根據obj是否有它內部的Symbol()值判斷是否包含在內。這樣就根本不存在引用了。但是對於WeakMap,key物件的Symbol()屬性上需要儲存value值,符合規範中說的key對value引用,但WeakMap不引用key。這也吻合為什麼儲存鍵必須要物件型別,而且沒有size、clear(),且不支援遍歷介面的說法。(但是我在chrome試了一下,但是並沒有發現多出來Symbol型別屬性。裝逼失敗,就當我什麼都沒說吧 2333333)。

語言層面

“型別升級”一節中對內建Symbol和Object原型鏈等進行了說明。

新增語言攔截