視撰寫的應用程式或程式庫的需求而定,我們對於型態的繼承,或者是物件判斷的型態,實作上,可以簡單,也可以朝更通用的方向,而這中間尺寸的拿捏依據,也在於開發者對JavaScript(JS)引擎的內部特性實作,有多少認識。

繼承標準API?

若單就繼承的實現而言,現今的JS開發者應該都知道,目前有兩種基本的機制,也就是實作原型鏈,或是透過ES6類別語法,而且,確實有不少繼承上的需求,使用原型鏈或類別語法都可以實現;然而,若對象是標準API就要注意──如果你想要繼承標準API,我不建議使用原型鏈,因為特殊行為不會被繼承。

以陣列為例,以原型鏈實作方式來繼承Array,雖然可以繼承Array.prototype本身定義的方法,instanceof判斷子型態實例時,也會被認為是Array的實例,然而陣列的特殊行為,length特性並不會隨著元素增減自行維護,將length設為比元素更少的值,也不會令元素被拋棄。

另一個例子是Error,早期JS程式庫之所以少見實現Error子型態,理由之一,是原型鏈繼承Error時,拋出自訂的Error子型態實例,JS引擎並不會有自動記錄堆疊追蹤的行為,這就造成了除錯時的麻煩。

在《Effective JavaScript》的〈條款40〉指出的「避免繼承標準類別」,其實,是指避免自行以原型鏈方式來實作繼承的意思。這是因為,標準API有一些特殊行為,是由JS引擎根據內部特性標示的值來決定。

例如,]被標示為'Array'的物件,length與元素之間才會有相互影響的行為,被標示為'Error'的物件,才會在拋出後自動記錄堆疊追蹤。

在ES5或早期版本,沒有標準方式,可以控制JS引擎中物件的內部特性;然而,ES6的類別語法在繼承標準API時,子類別實例會擁有標準API特殊行為,例如,Array子物件的length與元素之間,會相互影響;Error子物件被拋出後,會自動記錄堆疊追蹤,因此,ES6類別語法並不純粹是語法蜜糖,若要繼承標準API,同時需要內建的特殊行為,務必使用類別語法來實現。

它是哪種型態?

對於動態定型語言,基本上,建議針對行為而不是型態來撰寫程式。對於JS,要檢測API的版本或功能性時,也建議使用特性偵測(Feature detection)而不是判斷型態;然而,在某些場合,確實需要檢測型態,根據檢查對象的不同,以及需求的簡單或複雜,採用的方式也不盡相同。

面對基本型態、函式、undefined時,許多場合會使用typeof;面對其他物件時,常見使用instanceof,不過,在沒有進一步的定義下,instanceof其實是判斷原型鏈上是否有指定的原型,因此,若類陣列物件的原型被設為Array.prototype,instanceof也會認為該物件是Array實例;至於原型上的constructor特性,預設指向建構式,我們常見到作為型態檢測的依據,且constructor可以修改,事實上,使用原型鏈實現繼承時,我們也建議設定constructor指向子建構式的作法。

從ES6開始,開發者可以進一步控制instanceof的行為,透過在類別使用Symbol.hasInstance定義靜態方法,能夠決定物件使用instanceof判斷時,是否被視為此類別的實例。

因為,使用類別語法定義時,在類別上,會有預設的Symbol.hasInstance,行為預設為原型鏈查找,以符合instanceof傳統的行為。

所以,開發者可以結合typeof、Symbol.hasInstance、instanceof,來寫個更嚴格的_instanceof函式。例如,Babel轉換程式碼時,_instanceof函式就結合了三者(https://bit.ly/2YoyQJw)。

同時,ECMAScript規範要求Object.prototype.toString呼叫後,必須傳回'[object class]'格式的字串,而且,對於內建標準型態,Object實例會傳回'[object Object]'、陣列會傳回'[object Array]'、函式會傳回'[object Function]'等。因為基於對標準的支持,現今有不少程式庫也會使用這個,來作為型態判斷的依據。

在ES6以後,我們可以在類別的原型上,定義Symbol.toStringTag特性,而它的字串值,決定了Object.prototype.toString呼叫時,'[object class]'格式中class的實際字串。

使用類別語法定義時,類別的原型不會自動定義Symbol.toStringTag,若是採用Object.prototype.toString的結果,來作為型態判斷依據,此時,我們必須記得定義Symbol.toStringTag。

你的陣列不是陣列?

按照以上的說明,在ES5的環境中,想要判定某個物件是不是陣列,該使用哪個方式呢?typeof顯然不行,因為除了函式之外,其他物件都會傳回'object',若物件的原型被設為Array.prototype,instanceof也就破功了。

開發者應該使用的是,ES5特別針對陣列而設計的Array.isArray函式,而且,只有在JS引擎內部,]被標示為'Array'的物件,Array.isArray才會傳回true。

在ES6之後,若是使用類別語法繼承而來的Array子類別實例,Array.isArray也會判斷為true,這表示類別語法繼承時,子類別物件的]會標示為'Array'。

在ES5剛釋出的那個年代,若想修補Array.isArray該怎麼做呢?Object.prototype.toString傳回的字串中,class的部份其實是根據,因為沒有標準方式,可以干涉JS引擎實作中物件的,只有陣列實例的才會是'Array',因此,修補的方式可以是:

Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};

必須注意的是,真正的Array.isArray看的是,不是Object.prototype.toString的結果,因而這只是個近似的修補方式;進一步地,雖然ES6可以定義Symbol.toStringTag,然而,將它的值設為'Array',並不會令原生的Array.isArray在判斷時傳回true。

基於以上原因,如果要繼承陣列,我們就必須使用類別語法。這麼做還有個好處:如果SubArray繼承Array,filter、map等原本會傳回陣列實例的方法繼承下來之後,會傳回SubArray實例,也就是說,instanceof SubArray會是true。這是因為Array的相關方法,會使用Symbol.species協定傳回的建構式來建立實例。

釐清需求與實作

如果開發者是第一次接觸這些細節,也許會覺得不用考慮到這種程度吧!然而,如果應用程式或程式庫的開發中,有著繼承標準API或判斷型態的需求,就必須掌握細節;而對於繼承標準API,目前最好的選擇,就是使用類別語法,避免使用原型鏈來實現。

若是型態判斷,情況就更是複雜許多,因為不單要考量應用程式或程式庫打算採取哪個方式,若相依於第三方程式庫或工具,也必須確認它們採用了哪種型態判斷方式,此時,我們才能儘量採取一致的方式,並且選擇正確的方式來實作。

作者簡介


Advertisement

更多 iThome相關內容