ES6 系列之模擬實現 Symbol 型別

冴羽發表於2018-06-12

前言

實際上,Symbol 的很多特性都無法模擬實現……所以先讓我們回顧下有哪些特性,然後挑點能實現的……當然在看的過程中,你也可以思考這個特性是否能實現,如果可以實現,該如何實現。

回顧

ES6 引入了一種新的原始資料型別 Symbol,表示獨一無二的值。

1. Symbol 值通過 Symbol 函式生成,使用 typeof,結果為 "symbol"

var s = Symbol();
console.log(typeof s); // "symbol"
複製程式碼

2. Symbol 函式前不能使用 new 命令,否則會報錯。這是因為生成的 Symbol 是一個原始型別的值,不是物件。

3. instanceof 的結果為 false

var s = Symbol('foo');
console.log(s instanceof Symbol); // false
複製程式碼

4. Symbol 函式可以接受一個字串作為引數,表示對 Symbol 例項的描述,主要是為了在控制檯顯示,或者轉為字串時,比較容易區分。

var s1 = Symbol('foo');
console.log(s1); // Symbol(foo)
複製程式碼

5. 如果 Symbol 的引數是一個物件,就會呼叫該物件的 toString 方法,將其轉為字串,然後才生成一個 Symbol 值。

const obj = {
  toString() {
    return 'abc';
  }
};
const sym = Symbol(obj);
console.log(sym); // Symbol(abc)
複製程式碼

6. Symbol 函式的引數只是表示對當前 Symbol 值的描述,相同引數的 Symbol 函式的返回值是不相等的。

// 沒有引數的情況
var s1 = Symbol();
var s2 = Symbol();

console.log(s1 === s2); // false

// 有引數的情況
var s1 = Symbol('foo');
var s2 = Symbol('foo');

console.log(s1 === s2); // false
複製程式碼

7. Symbol 值不能與其他型別的值進行運算,會報錯。

var sym = Symbol('My symbol');

console.log("your symbol is " + sym); // TypeError: can't convert symbol to string
複製程式碼

8. Symbol 值可以顯式轉為字串。

var sym = Symbol('My symbol');

console.log(String(sym)); // 'Symbol(My symbol)'
console.log(sym.toString()); // 'Symbol(My symbol)'
複製程式碼

9. Symbol 值可以作為識別符號,用於物件的屬性名,可以保證不會出現同名的屬性。

var mySymbol = Symbol();

// 第一種寫法
var a = {};
a[mySymbol] = 'Hello!';

// 第二種寫法
var a = {
  [mySymbol]: 'Hello!'
};

// 第三種寫法
var a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上寫法都得到同樣結果
console.log(a[mySymbol]); // "Hello!"
複製程式碼

10. Symbol 作為屬性名,該屬性不會出現在 for...in、for...of 迴圈中,也不會被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。但是,它也不是私有屬性,有一個 Object.getOwnPropertySymbols 方法,可以獲取指定物件的所有 Symbol 屬性名。

var obj = {};
var a = Symbol('a');
var b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

var objectSymbols = Object.getOwnPropertySymbols(obj);

console.log(objectSymbols);
// [Symbol(a), Symbol(b)]
複製程式碼

11. 如果我們希望使用同一個 Symbol 值,可以使用 Symbol.for。它接受一個字串作為引數,然後搜尋有沒有以該引數作為名稱的 Symbol 值。如果有,就返回這個 Symbol 值,否則就新建並返回一個以該字串為名稱的 Symbol 值。

var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');

console.log(s1 === s2); // true
複製程式碼

12. Symbol.keyFor 方法返回一個已登記的 Symbol 型別值的 key。

var s1 = Symbol.for("foo");
console.log(Symbol.keyFor(s1)); // "foo"

var s2 = Symbol("foo");
console.log(Symbol.keyFor(s2) ); // undefined
複製程式碼

分析

看完以上的特性,你覺得哪些特性是可以模擬實現的呢?

如果我們要模擬實現一個 Symbol 的話,基本的思路就是構建一個 Symbol 函式,然後直接返回一個獨一無二的值。

不過在此之前,我們先看看規範中呼叫 Symbol 時到底做了哪些工作:

Symbol ( [ description ] )

When Symbol is called with optional argument description, the following steps are taken:

  1. If NewTarget is not undefined, throw a TypeError exception.
  2. If description is undefined, var descString be undefined.
  3. Else, var descString be ToString(description).
  4. ReturnIfAbrupt(descString).
  5. Return a new unique Symbol value whose [[Description]] value is descString.

當呼叫 Symbol 的時候,會採用以下步驟:

  1. 如果使用 new ,就報錯
  2. 如果 description 是 undefined,讓 descString 為 undefined
  3. 否則 讓 descString 為 ToString(description)
  4. 如果報錯,就返回
  5. 返回一個新的唯一的 Symbol 值,它的內部屬性 [[Description]] 值為 descString

考慮到還需要定義一個 [[Description]] 屬性,如果直接返回一個基本型別的值,是無法做到這一點的,所以我們最終還是返回一個物件。

第一版

參照著規範,其實我們已經可以開始寫起來了:

// 第一版
(function() {
    var root = this;

    var SymbolPolyfill = function Symbol(description) {

        // 實現特性第 2 點:Symbol 函式前不能使用 new 命令
        if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');

        // 實現特性第 5 點:如果 Symbol 的引數是一個物件,就會呼叫該物件的 toString 方法,將其轉為字串,然後才生成一個 Symbol 值。
        var descString = description === undefined ? undefined : String(description)

        var symbol = Object.create(null)

        Object.defineProperties(symbol, {
            '__Description__': {
                value: descString,
                writable: false,
                enumerable: false,
                configurable: false
            }
        });

        // 實現特性第 6 點,因為呼叫該方法,返回的是一個新物件,兩個物件之間,只要引用不同,就不會相同
        return symbol;
    }

    root.SymbolPolyfill = SymbolPolyfill;
})();
複製程式碼

只是參照著規範,我們已經實現了特性的第 2、5、6 點。

第二版

我們來看看其他的特性該如何實現:

1. 使用 typeof,結果為 "symbol"。

利用 ES5,我們並不能修改 typeof 操作符的結果,所以這個無法實現。

3. instanceof 的結果為 false

因為不是通過 new 的方式實現的,所以 instanceof 的結果自然是 false。

4. Symbol 函式可以接受一個字串作為引數,表示對 Symbol 例項的描述。主要是為了在控制檯顯示,或者轉為字串時,比較容易區分。

當我們列印一個原生 Symbol 值的時候:

console.log(Symbol('1')); // Symbol(1)
複製程式碼

可是我們模擬實現的時候返回的卻是一個物件,所以這個也是無法實現的,當然你修改 console.log 這個方法是另講。

8. Symbol 值可以顯式轉為字串。

var sym = Symbol('My symbol');

console.log(String(sym)); // 'Symbol(My symbol)'
console.log(sym.toString()); // 'Symbol(My symbol)'
複製程式碼

當呼叫 String 方法的時候,如果該物件有 toString 方法,就會呼叫該 toString 方法,所以我們只要給返回的物件新增一個 toString 方法,即可實現這兩個效果。

// 第二版

// 前面面程式碼相同 ……

var symbol = Object.create({
    toString: function() {
        return 'Symbol(' + this.__Description__ + ')';
    },
});

// 後面程式碼相同 ……
複製程式碼

第三版

9. Symbol 值可以作為識別符號,用於物件的屬性名,可以保證不會出現同名的屬性。

看著好像沒什麼,這點其實和第 8 點是衝突的,這是因為當我們模擬的所謂 Symbol 值其實是一個有著 toString 方法的 物件,當物件作為物件的屬性名的時候,就會進行隱式型別轉換,還是會呼叫我們新增的 toString 方法,對於 Symbol('foo') 和 Symbol('foo')兩個 Symbol 值,雖然描述一樣,但是因為是兩個物件,所以並不相等,但是當作為物件的屬性名的時候,都會隱式轉換為 Symbol(foo) 字串,這個時候就會造成同名的屬性。舉個例子:

var a = SymbolPolyfill('foo');
var b = SymbolPolyfill('foo');

console.log(a ===  b); // false

var o = {};
o[a] = 'hello';
o[b] = 'hi';

console.log(o); // {Symbol(foo): 'hi'}
複製程式碼

為了防止不會出現同名的屬性,畢竟這是一個非常重要的特性,迫不得已,我們需要修改 toString 方法,讓它返回一個唯一值,所以第 8 點就無法實現了,而且我們還需要再寫一個用來生成 唯一值的方法,就命名為 generateName,我們將該唯一值新增到返回物件的 __Name__ 屬性中儲存下來。

// 第三版
(function() {
    var root = this;

    var generateName = (function(){
        var postfix = 0;
        return function(descString){
            postfix++;
            return '@@' + descString + '_' + postfix
        }
    })()

    var SymbolPolyfill = function Symbol(description) {

        if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');

        var descString = description === undefined ? undefined : String(description)

        var symbol = Object.create({
            toString: function() {
                return this.__Name__;
            }
        })

        Object.defineProperties(symbol, {
            '__Description__': {
                value: descString,
                writable: false,
                enumerable: false,
                configurable: false
            },
            '__Name__': {
                value: generateName(descString),
                writable: false,
                enumerable: false,
                configurable: false
            }
        });

        return symbol;
    }


    root.SymbolPolyfill = SymbolPolyfill;

})()
複製程式碼

此時再看下這個例子:

var a = SymbolPolyfill('foo');
var b = SymbolPolyfill('foo');

console.log(a ===  b); // false

var o = {};
o[a] = 'hello';
o[b] = 'hi';

console.log(o); // Object { "@@foo_1": "hello", "@@foo_2": "hi" }
複製程式碼

第四版

我們再看看接下來的特性。

** 7.Symbol 值不能與其他型別的值進行運算,會報錯。**

+ 操作符為例,當進行隱式型別轉換的時候,會先呼叫物件的 valueOf 方法,如果沒有返回基本值,就會再呼叫 toString 方法,所以我們考慮在 valueOf 方法中進行報錯,比如:

var symbol = Object.create({
    valueOf: function() {
        throw new Error('Cannot convert a Symbol value')
    }
})

console.log('1' + symbol); // 報錯
複製程式碼

看著很簡單的解決了這個問題,可是如果我們是顯式呼叫 valueOf 方法呢?對於一個原生的 Symbol 值:

var s1 = Symbol('foo')
console.log(s1.valueOf()); // Symbol(foo)
複製程式碼

是的,對於原生 Symbol,顯式呼叫 valueOf 方法,會直接返回該 Symbol 值,而我們又無法判斷是顯式還是隱式的呼叫,所以這個我們就只能實現一半,要不然實現隱式呼叫報錯,要不然實現顯式呼叫返回該值,那……我們選擇不報錯的那個吧,即後者。

我們迫不得已的修改 valueOf 函式:

// 第四版
// 前面面程式碼相同 ……

var symbol = Object.create({
    toString: function() {
        return this.__Name__;
    },
    valueOf: function() {
        return this;
    }
});
// 後面程式碼相同 ……
複製程式碼

第五版

10. Symbol 作為屬性名,該屬性不會出現在 for...in、for...of 迴圈中,也不會被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify() 返回。但是,它也不是私有屬性,有一個 Object.getOwnPropertySymbols 方法,可以獲取指定物件的所有 Symbol 屬性名。

嗯,無法實現。

11. 有時,我們希望重新使用同一個Symbol值,Symbol.for方法可以做到這一點。它接受一個字串作為引數,然後搜尋有沒有以該引數作為名稱的Symbol值。如果有,就返回這個Symbol值,否則就新建並返回一個以該字串為名稱的Symbol值。

這個實現類似於函式記憶,我們建立一個物件,用來儲存已經建立的 Symbol 值即可。

12. Symbol.keyFor 方法返回一個已登記的 Symbol 型別值的 key。

遍歷 forMap,查詢該值對應的鍵值即可。

// 第五版
// 前面程式碼相同 ……
var SymbolPolyfill = function() { ... }

var forMap = {};

Object.defineProperties(SymbolPolyfill, {
    'for': {
        value: function(description) {
            var descString = description === undefined ? undefined : String(description)
            return forMap[descString] ? forMap[descString] : forMap[descString] = SymbolPolyfill(descString);
        },
        writable: true,
        enumerable: false,
        configurable: true
    },
    'keyFor': {
        value: function(symbol) {
            for (var key in forMap) {
                if (forMap[key] === symbol) return key;
            }
        },
        writable: true,
        enumerable: false,
        configurable: true
    }
});
// 後面程式碼相同 ……
複製程式碼

完整實現

綜上所述:

無法實現的特性有:1、4、7、8、10

可以實現的特性有:2、3、5、6、9、11、12

最後的實現如下:

(function() {
    var root = this;

    var generateName = (function(){
        var postfix = 0;
        return function(descString){
            postfix++;
            return '@@' + descString + '_' + postfix
        }
    })()

    var SymbolPolyfill = function Symbol(description) {

        if (this instanceof SymbolPolyfill) throw new TypeError('Symbol is not a constructor');

        var descString = description === undefined ? undefined : String(description)

        var symbol = Object.create({
            toString: function() {
                return this.__Name__;
            },
            valueOf: function() {
                return this;
            }
        })

        Object.defineProperties(symbol, {
            '__Description__': {
                value: descString,
                writable: false,
                enumerable: false,
                configurable: false
            },
            '__Name__': {
                value: generateName(descString),
                writable: false,
                enumerable: false,
                configurable: false
            }
        });

        return symbol;
    }

    var forMap = {};

    Object.defineProperties(SymbolPolyfill, {
        'for': {
            value: function(description) {
                var descString = description === undefined ? undefined : String(description)
                return forMap[descString] ? forMap[descString] : forMap[descString] = SymbolPolyfill(descString);
            },
            writable: true,
            enumerable: false,
            configurable: true
        },
        'keyFor': {
            value: function(symbol) {
                for (var key in forMap) {
                    if (forMap[key] === symbol) return key;
                }
            },
            writable: true,
            enumerable: false,
            configurable: true
        }
    });

    root.SymbolPolyfill = SymbolPolyfill;

})()
複製程式碼

ES6 系列

ES6 系列目錄地址:github.com/mqyqingfeng…

ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級作用域、標籤模板、箭頭函式、Symbol、Set、Map 以及 Promise 的模擬實現、模組載入方案、非同步處理等內容。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。

相關文章