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支援;新增資料結構Set
和Map
及弱引用的WeakSet
和WeakMap
,可去重儲存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提案 ‘::’簡化bind
、apply
、call
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^53
到2^53
之間的數無法正確表示、運算
1、優化了二進位制、八進位制的表示,分別為0b/0B
、0o/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()) {}
WeakSet
和WeakMap
是ES6提供的兩種弱引用型別。分別只支援 value為物件/key為物件 的情形。與Set
和Map
不同,儲存的資料如果在環境中不再被引用,則會被垃圾清理機制,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原型鏈等進行了說明。
新增語言攔截