首先先熟悉一下Symbol類型的定義及其做用:javascript
內置的Symbol值主要是用於ECMAScript規範算法擴展的。本文主要是經過對規範的解讀來了解Symbol內置的值是如何使用及其規範定義的。java
咱們先瀏覽一下規範裏的Symbol內置的值:es6
規範名稱 | Description | 值及其做用 |
---|---|---|
@@asyncIterator | "Symbol.asyncIterator" | 一個返回異步迭代器的方法,主要用於for await |
@@hasInstance | "Symbol.hasInstance" | 用於確認對象是否爲該構造函數實例的方法,主要用於instanceof |
@@isConcatSpreadable | "Symbol.isConcatSpreadable" | 一個Boolean值,標識是否能夠經過Array.prototype.concat進行扁平化處理 |
@@iterator | "Symbol.iterator" | 一個返回異步迭代器的方法,主要用於for of |
@@match | "Symbol.match" | 用於String.prototype.match調用 |
@@replace | "Symbol.replace" | 用於String.prototype.replace調用 |
@@search | "Symbol.search" | 用於String.prototype.search調用 |
@@species | "Symbol.species" | 一個用來返回建立派生對象的構造函數的方法 |
@@split | "Symbol.split" | 用於String.prototype.split調用 |
@@toPrimitive | "Symbol.toPrimitive" | 用於ToPrimitive抽象方法 |
@@toStringTag | "Symbol.toStringTag" | 用於描述一個對象的字符串,主要用於Object.prototype.toString調用 |
@@unscopables | "Symbol.unscopables" | 用於with環境綁定中排除的屬性名稱 |
上面有些描述比較抽象,不要急,咱們將逐個來仔細瞭解其規範定義和做用算法
和上面描述的同樣,用於確認對象是否爲該構造函數實例的方法,主要用於instanceof,當調用instanceof時,內部方法會調用對象上的Symbol.hasInstance方法。typescript
咱們來看一個例子json
class MyArray { static [Symbol.hasInstance](val){ return val instanceof Array; } } [1,2,3] instanceof MyArray; // true
在執行instanceof (V instanceof target) 操做時,Es6規範規定如下步驟:api
<font color="red">注意:這裏會將結果值進行隱式轉換</font>數組
緊接着咱們看一下OrdinaryHasInstance是怎麼規定的:promise
判斷是否有[[BoundTargetFunction]]內部屬性,若是有, let BC = target.[[BoundTargetFunction]],返回 V instanceof BC, 算法結束。app
<font color="red">注意: 這裏的[[BoundTargetFunction]]實際上是調用bind方法以前的原始方法</font>
看下面的例子說明:
function F1(){} const F2 = F1.bind({}); const obj = new F2(); obj instanceof F1 // true
let V = V.__proto__; if (V === null) { return false; } if(P === V){ return true; }
Function.prototype[@@hasInstance] = function(V) { return OrdinaryHasInstance(this, V); }
咱們能夠看到在es6規範中,先嚐試獲取對象上的@@hasInstance方法,若是有,先調用對象上的@@hasInstance方法並返回。
@@isConcatSpreadable用於在執行Array.prototype.concat時判斷對象是否可展開。
咱們先看兩個例子
class MyArray { constructor(){ this.length = 0; } push(val){ this[this.length++] = val; } [Symbol.isConcatSpreadable] = true; } const array = new MyArray(); array.push(1); array.push(2); Array.prototype.concat.call(array, []); //[1,2] 這裏自動展開array [].concat(array); // [1,2] 這裏自動展開array class MyArrayNotConcatSpreadable { constructor(){ this.length = 0; } push(val){ this[this.length++] = val; } } const array2 = new MyArrayNotConcatSpreadable(); array2.push(1); array2.push(2); [].concat(array2); // [MyArrayNotConcatSpreadable對象] 這裏不會自動展開array2
@@isConcatSpreadable用於IsConcatSpreadable抽象方法,先看一下IsConcatSpreadable(O)規範定義:
IsConcatSpreadable是抽象方法,不會暴露給javascript api,僅供內部調用,其用於Array.prototype.concat方法。
IsConcatSpreadable在Array.prototype.concat中會產生以下做用:
僞代碼以下
const O = ToObject(this.value); const A = ArraySpeciesCreate(O, 0); let n = 0; for(item of [O, ...arguments]){ if(IsConcatSpreadable(item)){ const length = item.length; let k = 0; while(k < length) { if(item.HasProperty(ToString(k))){ Object.defineProperty(A, ToString(k), { value: item[ToString(k)] }); } } } }
注意:上述僞代碼只是展現了IsConcatSpreadable的使用,並非所有的concat算法邏輯
@@match主要用於兩個地方
咱們仍是結合例子看:
const helloWorldStartMatcher = { toString(){ return 'Hello'; } } 'Hello World'.startsWith(helloWorldStartMatcher);// true // startsWith在這裏會調用helloWorldStartMatcher的toString方法進行判斷 helloWorldStartMatcher[Symbol.match] = function(){ return true; } 'Hello World'.startsWith(helloWorldStartMatcher);// throw TypeError // startsWith調用時會調用IsRegExp對helloWorldStartMatcher進行判斷,由於定義了Symbol.match,全部返回true,startsWith會對正則拋出TypeError
const helloWorldMatcher = { [Symbol.match](val){ return 'Hello World'.indexOf(val); } } 'Hello'.match(helloWorldMatcher); // 0 helloWorldMatcher[Symbol.match] = function(){ return /Hello/[Symbol.match](val); }; 'Hello World'.match(helloWorldMatcher); // 執行正則的match邏輯 等同於 'Hello World'.match(/Hello/);
IsRegExp(argument)規範定義以下:
IsRegExp主要用於String.prototype.startsWith和String.prototype.endsWith,在這兩個方法中會先經過IsRegExp對參數進行判斷,若是是true,會拋出typeError異常。
@@match被String.prototype.match ( regexp )調用規則以下:
注意:上述描述只是展現了@@match在規範中的做用,並非所有的String.prototype.match算法邏輯
@@replace用於String.prototype.replace,自定義replace邏輯
例子
const upperCaseReplacer = { [Symbol.replace](target, replaceValue){ return target.replace('hello', replaceValue.toUpperCase()); } } 'hello world'.replace(upperCaseReplacer, 'my');// MY world
@@replace被String.prototype.replace ( searchValue, replaceValue )調用規則以下:
注意:上述描述只是展現了@@replace在規範中的做用,並非所有的String.prototype.replace算法邏輯
@@search用於String.prototype.search,自定義search邏輯
例子
const upperCaseSearcher = { value: '', [Symbol.search](target){ return target.search(this.value.toUpperCase()); } } upperCaseSearcher.value = 'world'; 'hello WORLD'.search(upperCaseSearcher);// 6
@@search被String.prototype.search (regexp)調用規則以下:
注意:上述描述只是展現了@@search在規範中的做用,並非所有的String.prototype.search算法邏輯
@@split用於String.prototype.split,自定義split邏輯
例子
const upperCaseSplitter = { value: '', [Symbol.split](target, limit){ return target.split(this.value.toUpperCase(), limit); } } upperCaseSplitter.value = 'world'; 'hello WORLD !'.split(upperCaseSplitter);// ["hello ", " !"] 'hello WORLD !'.split(upperCaseSplitter, 1);// ["hello "]
@@split被String.prototype.split ( separator, limit )調用規則以下:
注意:上述描述只是展現了@@split在規範中的做用,並非所有的String.prototype.split算法邏輯
@@toStringTag經過Object.prototype.toString來調用的,用於描述對象。
例子
const obj = { [Symbol.toStringTag]: 'Hello' } Object.prototype.toString.call(obj); // "[object Hello]" class ValidatorClass {} Object.prototype.toString.call(new ValidatorClass()); // "[object Object]" 默認值 class ValidatorClass { get [Symbol.toStringTag]() { return "Validator"; } } Object.prototype.toString.call(new ValidatorClass()); // "[object Validator]" class ValidatorClass { get [Symbol.toStringTag]() { return {}; } } Object.prototype.toString.call(new ValidatorClass()); // "[object Object]"
@@toStringTag被Object.prototype.toString調用規則以下:
Es6新增的@@toStringTag以下:
對象 | 值 |
---|---|
Atomics | Atomics |
Math | Math |
JSON | JSON |
Symbol.prototype | Symbol |
Map.prototype | Map |
Set.prototype | Set |
WeakMap.prototype | WeakMap |
WeakSet.prototype | WeakSet |
Promise.prototype | Promise |
ArrayBuffer.prototype | ArrayBuffer |
Module Namespace Objects | Module |
SharedArrayBuffer.prototype | SharedArrayBuffer |
DataView.prototype | DataView |
GeneratorFunction.prototype | GeneratorFunction |
AsyncGeneratorFunction.prototype | AsyncGeneratorFunction |
Generator.prototype | Generator |
AsyncGenerator.prototype | AsyncGenerator |
AsyncFunction.prototype | AsyncFunction |
%StringIteratorPrototype% | String Iterator |
%ArrayIteratorPrototype% | Array Iterator |
%MapIteratorPrototype% | Map Iterator (new Map()[Symbol.iterator]()) |
%SetIteratorPrototype% | Set Iterator |
%AsyncFromSyncIteratorPrototype% | Async-from-Sync Iterator |
@@toPrimitive被ToPrimitive抽象方法調用,主要做用於類型轉換。
咱們仍是結合例子來看:
const obj = { [Symbol.toPrimitive](hint){ if(hint === 'number') { return 2; } return '1'; } } const keyObj = { '1': 1 }; console.log(1 - obj);// -1 調用ToNumber類型轉換 console.log(1 == obj); // true 抽象相等算法時調用 console.log(obj + 1); // 11 +號操做符時調用 console.log(keyObj[obj]); // 調用ToPropertyKey進行轉換 console.log(0 < obj); // 抽象比較算法時調用 obj[Symbol.toPrimitive] = function(){return '2017-05-31'}; console.log(new Date(obj)); // Date構造時調用 obj[Symbol.toPrimitive] = function(){return {}}; console.log(obj + 1);// throw type error
因爲ToPrimitive抽象方法是Es6底層最主要的抽象方法之一,調用點比較多,咱們先注重看一下它的實現。
ToPrimitive ( input [ , PreferredType ] )被定義爲以下:
let exoticToPrim = GetMethod(input, @@toPrimitive),若是exoticToPrim不是undefined進行以下操做
OrdinaryToPrimitive爲Es5規範定義的ToPrimitive方法,這裏順帶介紹一下:
其次咱們看一下ToPrimitive調用點:
在es規範中,不少的方法都須要獲取當前調用者的構造函數,而後根據此構造函數構造對象,可能這樣說比較抽象,咱們仍是先看例子吧。
class MyArray extends Array{ } const array = new MyArray(); array.push(1); array.push(2); console.log(array instanceof Array); // true console.log(array instanceof MyArray); // true const mapArray = array.map(item => item); console.log(mapArray instanceof Array); // true console.log(mapArray instanceof MyArray); // true
從上面的例子中咱們看到,map後的數組仍是經過MyArray構造的,有時咱們但願建立衍生對象時使用咱們指定的構造器。
class MyArray extends Array{ static [Symbol.species] = Array; // 等同於上面效果 //static get [Symbol.species](){ // return Array; //} } const array = new MyArray(); array.push(1); array.push(2); console.log(array instanceof Array); // true console.log(array instanceof MyArray); // true const mapArray = array.map(item => item); console.log(mapArray instanceof Array); // true console.log(mapArray instanceof MyArray); // false
在es6規範中,Symbol.species擴展屬性主要做用於兩個抽象動做中,分別是SpeciesConstructor,ArraySpeciesCreate,咱們先來看看這兩個抽象動做具體是如何執行的。
SpeciesConstructor ( O, defaultConstructor )定義以下:
其中O是當前的調用者,若是O中不存在@@species屬性就以defaultConstructor爲默認構造器
ArraySpeciesCreate ( originalArray, length )定義以下:
其中originalArray是當前的調用數組
若是C是Object
注:上述是規範的簡化過程,去除了一些斷言和判斷
咱們看一下SpeciesConstructor調用點:
例如
class MyPromise extends Promise { } const thenMyPromise = MyPromise.resolve().then(); console.log(thenMyPromise instanceof MyPromise); // true console.log(thenMyPromise instanceof Promise); // true class MyPromise2 extends Promise { static get [Symbol.species]() { return Promise; } } const thenMyPromise2 = MyPromise2.resolve().then(); console.log(thenMyPromise2 instanceof MyPromise); // false console.log(thenMyPromise2 instanceof Promise); // true
ArraySpeciesCreate調用點:
主要用於Array原型上的方法時調用觸發,包括concat, filter, flat,map,slice,splice方法
es6規範中定義的javascript原始類型的@@species默認值爲 Return the this value.
這個多是自定義時使用的最多的,它能夠幫助咱們自定義迭代器,並且ECMAScript規範中的Set,Map等迭代過程都是基於它實現的。
在Typescript的Es6簽名庫,咱們能夠看到迭代器的簽名以下:
interface IteratorReturnResult<TReturn> { done: true; value: TReturn; } interface IteratorYieldResult<TYield> { done?: false; value: TYield; } type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>; interface Iterator<T, TReturn = any, TNext = undefined> { next(...args: [] | [TNext]): IteratorResult<T, TReturn>; return?(value?: TReturn): IteratorResult<T, TReturn>; throw?(e?: any): IteratorResult<T, TReturn>; } interface Iterable<T> { [Symbol.iterator](): Iterator<T>; }
經過簽名咱們能夠看到實現自定義迭代器須要擴展[Symbol.iterator]方法,而該方法要返回一個Iterator,Iterator中的next方法接受一個值,返回IteratorResult。其中的return方法的使用場合是,若是for...of循環提早退出(一般是由於出錯,或者有break語句),就會調用return方法。
throw方法,能夠在函數體外拋出錯誤,而後在 Generator 函數體內捕獲,主要是配合Generator使用。
咱們先看兩個例子感覺一下。
function *iterable () { yield 1; yield 2; yield 3; }; // iterable()返回一個迭代器 for(const val of iterable()){ console.log(val); // 輸出1,2,3 } class EvenArray extends Array { [Symbol.iterator](){ const _this = this; let index = 0; return { next(){ if(index < _this.length){ const value = _this[index]; index += 2; return { done: false, value, } } return { done: true }; }, return() { this._index = 0; console.log('return iterator'); return { done: true } } } } } const array = new EvenArray(); for(let i = 0; i <= 100; i++){ array.push(i); } for(const val of array){ console.log(val); // 0, 2, 4, 6, ... , 98, 100 } for(const val of array){ console.log(val); // 0 // return iterator 調用了return 方法 break; } for(const val of array){ console.log(val); // 0 // return iterator 調用了return 方法 throw new Error(); } // //等同於上面代碼 // class EvenArray extends Array { // constructor(){ // super(); // this.index = 0; // } // [Symbol.iterator](){ // this.index = 0; // return this; // } // next(){ // if(this.index < this.length){ // const value = this[this.index]; // this.index += 2; // return { // done: false, // value, // } // } // return { // done: true // }; // } // } const myIterable = {} myIterable[Symbol.iterator] = function* () { yield 1; yield 2; yield 3; }; // 擴展默認調用迭代器 console.log([...myIterable]); // [1, 2, 3] function *iterable2 () { yield* myIterable; //懸停myIterable迭代器 }; for(const val of iterable2()){ console.log(val); // 1,2,3 } function consoleArgs(...args){ console.log(args); } consoleArgs(...myIterable);// 剩餘參數調用默認調用迭代器
先梳理一下@@iterator的調用點:
咱們一個個來剖析裏面的具體實現及其做用
GetIterator ( obj [ , hint [ , method ] ] )定義以下
若是method沒有提供,進行以下操做
若是hint爲async
若是method爲undefined,則
經過上述算法咱們能夠看到,GetIterator最終返回一個包裝好的迭代器對象。那麼都有那些地方調用GetIterator抽象方法呢?
因爲迭代器涉及的調用點比較多,可能須要單獨的一篇文檔介紹,這裏注重看一下for of的規範:
for of執行主要包含兩個部分:
接下來看一下ForIn/OfHeadEvaluation(TDZnames, expr, iterationKind )的規範定義:
說明:該抽象方法有三個參數,分別表示:綁定的環境變量名稱、of後面的語句、迭代的類型(包括enumerate、async-iterate、iterate)。具體含義及其做用咱們接着往下看。
若是TDZnames不爲空,執行以下操做
判斷iterationKind是否爲enumerate,若是是(這裏主要用於for in)
不然
上述方法返回的結果會傳入到ForIn/OfBodyEvaluation進行變量執行
ForIn/OfBodyEvaluation ( lhs, stmt, iteratorRecord, iterationKind, lhsKind, labelSet [ , iteratorKind ])規範定義以下:
參數比較多,咱們一個一個解釋:
算法執行邏輯以下:
開始進入循環
上述算法去除了規範裏的一些繁瑣的步驟,尤爲是lhs解析綁定的部分,若是想要深刻了解,建議查看ECMAScript規範文檔。
Es6內置的多數對象都實現來迭代器,具體以下:
Symbol.asyncIterator指定了一個對象的默認異步迭代器。若是一個對象設置了這個屬性,它就是異步可迭代對象,可用於for await...of循環。
接下來咱們看幾個例子:
const myAsyncIterable = new Object(); myAsyncIterable[Symbol.asyncIterator] = async function*() { yield 1; yield 2; yield 3; }; (async () => { for await (const x of myAsyncIterable) { console.log(x); // 輸出: // 1 // 2 // 3 } })();
固然也能夠經過它遍歷promise
const myAsyncIterable = new Object(); const promise1 = new Promise(resolve=>setTimeout(() => resolve(1), 500)); const promise2 = Promise.resolve(2); myAsyncIterable[Symbol.asyncIterator] = async function*() { yield await promise1; yield await promise2; }; (async () => { for await (const x of myAsyncIterable) { console.log(x); // 輸出: // 1 // 2 } })();
也能夠自定義異步迭代器
const myAsyncIterable = { promiseList:[ new Promise(resolve=>setTimeout(() => resolve(1), 500)), Promise.resolve(2) ], [Symbol.asyncIterator](){ const _this = this; let index = 0; return { next(){ if(index === _this.promiseList.length){ return Promise.resolve({done: true}); } return _this.promiseList[index++].then(value => ({done: false, value})) } } } }; (async () => { for await (const x of myAsyncIterable) { console.log(x); // 輸出: // 1 // 2 } })();
@@asyncIterator做用和@@iterator,在規範定義中也是統一處理的,只是在執行ForIn/OfBodyEvaluation時iteratorKind參數設置爲了async,執行函數時經過Await動做處理@@asyncIterator。
對象的Symbol.unscopables屬性,指向一個對象。該對象指定了使用with關鍵字時,哪些屬性會被with環境排除
const object1 = { property1: 42 }; object1[Symbol.unscopables] = { property1: true }; with (object1) { console.log(property1); // expected output: Error: property1 is not defined }
@@unscopables用於HasBinding調用
HasBinding查看對象是否綁定到當前的環境記錄項中,規範中的HasBinding最後會經過@@unscopables進行過濾。
規範中只有Array.prototype指定了@@unscopables
具體以下:
{ "copyWithin":true, "entries":true, "fill":true, "find":true, "findIndex":true, "flat":true, "flatMap":true, "includes":true, "keys":true, "values":true }