作為協定的符號

August 2, 2022

ES6 以後,基本資料型態除了數字、字串與布林值外,還多了一個符號(symbol)。

為何是符號?

為什麼需要符號?主要是為了建立獨一無二的行為象徵而存在!ES6 之前沒有符號,物件特性名稱是使用字串,若要建立物件協定,就得慎選特性名稱,並保證其他地方不會採用了相同的名稱,卻賦予了不同的協定意義,例如 valueOftoString,必須得保證開發者都知道,valueOftoString 這兩個特性名稱的意義,不要實作出非預期的行為。

因此 valueOftoString 具有特定意義,與其說是名稱,不如說希望它們代表著某些獨一無二的行為象徵,既然需要的是獨一無二的行為象徵,ES6就令建立這種獨一無二的象徵變得容易,因此就制定了符號的相關規範。

ES6 以後可以使用 Symbol 函式來建立符號型態的值,建立時可以使用字串指定說明文字,對符號值使用 typeof,會傳回 'symbol'。例如:

> let x = Symbol()
undefined
> let y = Symbol('Protocol.iterable')
undefined
> typeof x
'symbol'
>

建立符號時指定的字串,純綷就只是個說明文字罷了,在執行時期,使用 Symbol 建立的符號都是獨一無二的,不會有兩個符號值相同,被指定的說明文字,ES10 以後可以從 Symboldescription 特性取得,而且會成為 toString 方法的傳回字串內容(Symbol 值雖是基本型態,然而這時會自動包裹為 Symbol 實例,才有方法可以呼叫):

> let a = Symbol()
undefined
> let b = Symbol()
undefined
> a === b
false
> Symbol('Justin.iterator') === Symbol('Justin.iterator')
Falsefalse
> Symbol('Justin.iterator').description
'Justin.iterator'
>  Symbol('Justin.iterator').toString()
'Symbol(Justin.iterator)'
>

全域註冊表

既然每個符號值都是獨一無二的,若要作為協定象徵,不就要在建立符號後小心保存?使用上不會很不方便嗎?對於保存符號值這件事,ES6 以後有個符號全域註冊表,要將建立的符號值保存到註冊表,或者從註冊表取得已建立的符號,可以透過 Symbol.for 函式:

> let iteratorSymbol = Symbol.for('Protocol.iterator')
undefined
> Symbol.for('Protocol.iterator') === iteratorSymbol
true
>

Symbol.for 會採用指定的字串作為依據,如果字串沒有對應的符號存在,就會建立新符號並存入註冊表,若有的話就會傳回已建立的符號。Symbol.for 主要是為了開發者自訂協定而存在,務必小心謹慎,制定好名稱規範,避免協定衝突問題。

如果手邊已經有個符號值,可以使用 Symbol.keyFor 傳回它在全域符號註冊表中的鍵,也就是符號描述字串,如果傳回字串就表示已登錄符號,否則傳回 undefined,這是確認符號存在與否,避免協定衝突的一種方式,例如若接續上例來查詢iteratorSymbol,可以得到 'Protocol.iterator' 字串:

> Symbol.keyFor(iteratorSymbol)
'Protocol.iterator'
>

運用標準符號

ES6 以後內建了一些標準符號,可透過 Symbol 的特性來取得,在試圖使用 Symbol.for 建立自訂協定前,應該先看看是否有適用的標準符號。

例如,Symbol.iterator 就代表用於迭代器協定的符號:

> Symbol.iterator
Symbol(Symbol.iterator)
>

如果希望可以迭代某物件,可以定義方法傳回迭代器,該方法使用 Symbol.iterator作為特性儲存,例如,陣列本身就定義了 Symbol.iterator 方法:

> let arr = [1, 2, 3];
undefined
> let iterator = arr[Symbol.iterator]();
undefined
> iterator.next();
{ value: 1, done: false }
> iterator.next();
{ value: 2, done: false }
> iterator.next();
{ value: 3, done: false }
> iterator.next();
{ value: undefined, done: true }
>

迭代器是擁有 next 方法的物件,每次呼叫會傳回一個迭代結果,當中包含了 valuedonevalue 是當次迭代的值,done 表示迭代是否結束,當迭代結束時,value 會是 undefineddone 會是 true

後續的文件,會談到 ES6 以後,可以更簡單地定義物件方法,也會談到怎麼使用符號定義方法。

對於 JavaScript 內建的標準物件,符號定義的特性或方法是不可列舉的,例如,陣列的 Symbol.iterator 方法就不可列舉,因此若自訂符號的特性或方法時,建議將之設為不可列舉,這可以使用 Object.defineProperty 函式來達成。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter