在ECMAScript 6中,特別增加了一個新型態symbol,與1、''、true等的地位相同,是構建語言基礎的基本型態,它既不是字串,也不是變數,然而功能上乍看又與字串或變數重疊?

如果第一次接觸到symbol,難免搞不清楚這看似抽象的型態,實際上,Ruby中也有Symbol型態,而在Lisp中,symbol的發展歷史還比字串來得早。

沒有符號型態的語言

絕大多數的語言都沒有符號這類的型態,我是在Ruby中第一次接觸到符號型態。

Ruby以Symbol實例代表程式使用的變數名稱、方法名稱、類別名稱、模組名稱等,如果使用某個物件的methods方法,可以取得Symbol實例組成的陣列,例如,"".methods可以取得一個Array實例,裏頭包含了:<=>、:==、:eql?這些冒號開頭的Symbol實例。而通常談到何時該使用Symbol實例而不是字串,不可變動特性與效能考量,通常是個很好的區別方式。

可以把Symbol實例當成是Ruby中的不可變動字串,這樣的粗淺認知,到了我第二次接觸到符號型態時,就行不通了,那是在Lisp中面臨的狀況。

例如,'a會建立一個符號A,在Lisp裡,符號跟列表(list)一樣重要,然而,經常地,我總是在納悶,為什麼這個列表中使用符號,而不是字串?為什麼在那個列表求值中,使用的是符號,而不是變數?

絕大多數的語言都沒有符號這類的型態,那麼,有使用符號的需求嗎?舉Python為例好了,若想如同方才Ruby那樣,知道某個物件有哪些方法,可以使用dir來查詢物件,這會傳回一個清單(list),包含'capitalize'、'casefold'、'center'之類的字串。在Python中,字串不可變動,不需要像Ruby再創造一個符號型態來達到相同的目的;就這點來看,符號與字串的功能是似乎是重疊的,那麼進一步來想想,為什麼Python在這個需求下要使用字串?

Python中可以使用變數參考一個值,例如x=10,這樣變數x與10這個值就有了關聯,值並不一定是數字,也有可能是函式或其他物件,變數有生命週期的限制,因而有時會想要將變數的「名稱」儲存下來,以便後續透過名稱參考到一開始關聯的值,於是,就用'x'字串寫下來,代表著名稱「x」(這邊使用方括號強調,是為了避免與變數及字串混淆),並透過字典的資料結構來對應至值10。

於是,一開始的變數x <--> 10,就變成了字串'x' <--> 10,在Python中,可以使用物件的__dict__取得dict實例,以取得{'x': 10}這類的對應物件,那麼,x <--> 'x' <--> 10這樣的對應方式,是個好主意嗎?

字串本質上是個字元序列,通常的目的是用來進行描述,雖然也可以描述一個名稱,然而並不是字串原本的目的。實際上,不一定要使用字串,只要有個專用值代表著名稱,可以居中做為關聯,這樣一來,就有別於字串原本的用途與場合,也就能賦予名稱應有的職責,使用上也就能更有彈性一些吧!

Ruby的符號

若從以上概念來看Ruby,那個專用值的型態就是Symbol。在Ruby中,字串是可變動的,或許是因為這樣,Symbol型態的存在就更加必要了,畢竟使用可變動的字串作為字典的鍵,若某些場合更動了該字串的內容,那麼,對應的值就再也找不到了。

在Ruby中,可以使用:name方式建立Symbol實例,實際上,在建立變數、方法、類別、模組等的名稱時,就會自動建立一個與名稱對應的Symbol實例,你甚至可以使用Symbol.all_symbols,取得目前程式中全部的Symbol實例清單。

在Ruby中,要表示變數、方法、類別、模組等名稱時,慣例上也是指定Symbol實例。例如,想得知物件是否有名稱為「upcase」的方法,可以用"".methods.include? :upcase,想要加總數字,則用[1, 2, 3, 4, 5].reduce(:+),然而,字串包含的文字經常也代表名稱,或許因為這樣,Ruby也允許[1, 2, 3, 4, 5].reduce("+"),此時:+與"+"都表示「+」這個名稱。

實際上,Ruby中Symbol實例與字串也因此經常互換,可以使用:+.to_s取得字串"+",也可以使用"+".to_sym取得:+。許多文件都會從物件內容可否變動與效能等兩個方面,來考量何時使用字串,以及何時使用Symbol實例。實際上,在考量到要透過一個名稱來參考至某個值時,應該使用的就是Symbol實例,而不是字串。

Lisp的符號

Lisp經常被推崇的原因之一,在於它雖是歷史第二悠久的語言,卻早已包含著許多現代語言的特性,符號的概念也在其中。而Matz在設計Ruby時,混合了一些語言特性,Lisp是其中之一,不知是否Ruby中的符號概念也是來自於此。

在Lisp中,可以使用'abc會來創建一個符號ABC,代表著「ABC」這個名稱(Lisp中符號總是轉為大寫),在閱讀Lisp相關文件時,總是會看到列表操作著一串符號,這顯示了符號在Lisp中的重要性。

另一個事實是,符號比字串更早存在於Lisp之中。在許多Lisp文件中,在討論變數之前,也一定先討論符號,就像《ANSI Common Lisp》第二章中,在介紹過符號之後談及變數時就寫到:「符號是變數的名字」,而後,馬上寫到:「符號必須要被引用,不然會被當作變數」這是什麼意思?

舉個例子來說,(defparameter *glob* 99)建立了一個全域變數*glob*,它的值是99,變數的名稱是「*glob*」,如果想知道一個名稱是否爲全局變數或常數,可以使用boundp函數,那麼要怎麼呼叫呢?

你不能寫成(boundp *glob*),這樣*glob*會被當作是變數而求值,結果就是(boundp 99),然而,我們並不是想判斷99這個值,因此,你必須引用名稱「*glob*」,也就是(boundp '*glob*)。

如果沒接觸過Lisp,想想方才Ruby範例"".methods.include? :upcase,若寫成"".methods.include? upcase會怎麼樣?注意!upcase前少了個冒號!

ECMAScript 6的符號

JavaScript一開始並沒有符號,若想在物件添加一個特性,是透過obj[name]這樣的方式,其中name是個字串(string基本型態),概念跟Python有點像。

而在ECMAScript 6中,彷彿更進一步地回歸Lisp,在name的部份,也可以使用新增的symbol型態了。而且,你可以使用Symbol('x')這樣的方式,建立一個symbol值,同樣都是代表名稱,但是,有什麼理由改用symbol值,而不是字串?常見的理由之一是,使用symbol作為的特性名稱,該名稱不會被當成物件的一般特性被列舉,而必須透過Object.getOwnPropertySymbols來取得。

更重要的是,這種方式所建立的symbol值,都是獨一無二的(沒有任何方式能取得內部值),Symbol('x')===Symbol('x')會是false,「symbol值本身就是名稱」,呼叫Symbol時傳入的字串,只是輔助說明symbol值(然而諷刺地,也作為查找全域註冊表的依據),清楚地區分了名稱與說明是不一樣的概念,若不需要說明,Symbol()也能創造一個symbol值,Symbol()===Symbol()也仍是false。

這表示你不用擔心物件的特性上,會發生名稱衝突問題,相對地,如果想要建立獨一無二的API掛勾(hook),也可使用symbol,你必須建立一個symbol,讓其他開發者可以取得這個symbol,例如ECMAScript 6定義的標準symbol,就透過Symbol.iterator這類方式提供掛勾,如果想要讓某個symbol值成為全域,則可以透過Symbol.for(),向全域symbol表註冊(須考慮清楚這是不是個好主意)。

某些程度上,這跟Python中使用__xxx__,提供API掛勾又避免名稱衝突的概念類似,然而,ECMAScript 6的作法更進一步,不只是依賴名稱慣例,也就能夠如一開始探討的「有別於字串原本的用途與場合,也就能賦予名稱應有的職責,使用上也就能更有彈性一些」。

作者簡介


Advertisement

更多 iThome相關內容