你不懂的JS子型態
<div class="field field-name-body field-type-text-with-summary field-label-hidden"><div class="field-items"><div class="field-item even"> <p>視撰寫的應用程式或程式庫的需求而定,我們對於型態的繼承,或者是物件判斷的型態,實作上,可以簡單,也可以朝更通用的方向,而這中間尺寸的拿捏依據,也在於開發者對JavaScript(JS)引擎的內部特性實作,有多少認識</p> <p><span style="font-size:28px;"><strong>繼承標準API?</strong></span></p> <p>若單就繼承的實現而言,現今的JS開發者應該都知道,目前有兩種基本的機制,也就是實作原型鏈,或是透過ES6類別語法,而且,確實有不少繼承上的需求,使用原型鏈或類別語法都可以實現;然而,若對象是標準API就要注意──如果你想要繼承標準API,我不建議使用原型鏈,因為特殊行為不會被繼承。</p> <p>以陣列為例,以原型鏈實作方式來繼承Array,雖然可以繼承Array.prototype本身定義的方法,instanceof判斷子型態實例時,也會被認為是Array的實例,然而陣列的特殊行為,length特性並不會隨著元素增減自行維護,將length設為比元素更少的值,也不會令元素被拋棄。</p> <p>另一個例子是Error,早期JS程式庫之所以少見實現Error子型態,理由之一,是原型鏈繼承Error時,拋出自訂的Error子型態實例,JS引擎並不會有自動記錄堆疊追蹤的行為,這就造成了除錯時的麻煩。</p> <p>在《Effective JavaScript》的〈條款40〉指出的「避免繼承標準類別」,其實,是指避免自行以原型鏈方式來實作繼承的意思。這是因為,標準API有一些特殊行為,是由JS引擎根據內部特性標示的值來決定。</p> <p>例如,]被標示為'Array'的物件,length與元素之間才會有相互影響的行為,被標示為'Error'的物件,才會在拋出後自動記錄堆疊追蹤。</p> <p>在ES5或早期版本,沒有標準方式,可以控制JS引擎中物件的內部特性;然而,ES6的類別語法在繼承標準API時,子類別實例會擁有標準API特殊行為,例如,Array子物件的length與元素之間,會相互影響;Error子物件被拋出後,會自動記錄堆疊追蹤,因此,ES6類別語法並不純粹是語法蜜糖,若要繼承標準API,同時需要內建的特殊行為,務必使用類別語法來實現。</p> <p><span style="font-size:28px;"><strong>它是哪種型態?</strong></span></p> <p>對於動態定型語言,基本上,建議針對行為而不是型態來撰寫程式。對於JS,要檢測API的版本或功能性時,也建議使用特性偵測(Feature detection)而不是判斷型態;然而,在某些場合,確實需要檢測型態,根據檢查對象的不同,以及需求的簡單或複雜,採用的方式也不盡相同。</p> <p>面對基本型態、函式、undefined時,許多場合會使用typeof;面對其他物件時,常見使用instanceof,不過,在沒有進一步的定義下,instanceof其實是判斷原型鏈上是否有指定的原型,因此,若類陣列物件的原型被設為Array.prototype,instanceof也會認為該物件是Array實例;至於原型上的constructor特性,預設指向建構式,我們常見到作為型態檢測的依據,且constructor可以修改,事實上,使用原型鏈實現繼承時,我們也建議設定constructor指向子建構式的作法。</p> <p>從ES6開始,開發者可以進一步控制instanceof的行為,透過在類別使用Symbol.hasInstance定義靜態方法,能夠決定物件使用instanceof判斷時,是否被視為此類別的實例。</p> <p>因為,使用類別語法定義時,在類別上,會有預設的Symbol.hasInstance,行為預設為原型鏈查找,以符合instanceof傳統的行為。</p> <p>所以,開發者可以結合typeof、Symbol.hasInstance、instanceof,來寫個更嚴格的_instanceof函式。例如,Babel轉換程式碼時,_instanceof函式就結合了三者(<a href="https://bit.ly/2YoyQJw">https://bit.ly/2YoyQJw</a>)。</p> <p>同時,ECMAScript規範要求Object.prototype.toString呼叫後,必須傳回'[object class]'格式的字串,而且,對於內建標準型態,Object實例會傳回'[object Object]'、陣列會傳回'[object Array]'、函式會傳回'[object Function]'等。因為基於對標準的支持,現今有不少程式庫也會使用這個,來作為型態判斷的依據。</p> <p>在ES6以後,我們可以在類別的原型上,定義Symbol.toStringTag特性,而它的字串值,決定了Object.prototype.toString呼叫時,'[object class]'格式中class的實際字串。</p> <p>使用類別語法定義時,類別的原型不會自動定義Symbol.toStringTag,若是採用Object.prototype.toString的結果,來作為型態判斷依據,此時,我們必須記得定義Symbol.toStringTag。</p> <p><span style="font-size:28px;"><strong>你的陣列不是陣列?</strong></span></p> <p>按照以上的說明,在ES5的環境中,想要判定某個物件是不是陣列,該使用哪個方式呢?typeof顯然不行,因為除了函式之外,其他物件都會傳回'object',若物件的原型被設為Array.prototype,instanceof也就破功了。</p> <p>開發者應該使用的是,ES5特別針對陣列而設計的Array.isArray函式,而且,只有在JS引擎內部,]被標示為'Array'的物件,Array.isArray才會傳回true。</p> <p>在ES6之後,若是使用類別語法繼承而來的Array子類別實例,Array.isArray也會判斷為true,這表示類別語法繼承時,子類別物件的]會標示為'Array'。</p> <p>在ES5剛釋出的那個年代,若想修補Array.isArray該怎麼做呢?Object.prototype.toString傳回的字串中,class的部份其實是根據,因為沒有標準方式,可以干涉JS引擎實作中物件的,只有陣列實例的才會是'Array',因此,修補的方式可以是:</p> <pre> Array.isArray = function(arg) { return Object.prototype.toString.call(arg) === '[object Array]'; };</pre><p>必須注意的是,真正的Array.isArray看的是,不是Object.prototype.toString的結果,因而這只是個近似的修補方式;進一步地,雖然ES6可以定義Symbol.toStringTag,然而,將它的值設為'Array',並不會令原生的Array.isArray在判斷時傳回true。</p> <p>基於以上原因,如果要繼承陣列,我們就必須使用類別語法。這麼做還有個好處:如果SubArray繼承Array,filter、map等原本會傳回陣列實例的方法繼承下來之後,會傳回SubArray實例,也就是說,instanceof SubArray會是true。這是因為Array的相關方法,會使用Symbol.species協定傳回的建構式來建立實例。</p> <p><span style="font-size:28px;"><strong>釐清需求與實作</strong></span></p> <p>如果開發者是第一次接觸這些細節,也許會覺得不用考慮到這種程度吧!然而,如果應用程式或程式庫的開發中,有著繼承標準API或判斷型態的需求,就必須掌握細節;而對於繼承標準API,目前最好的選擇,就是使用類別語法,避免使用原型鏈來實現。</p> <p>若是型態判斷,情況就更是複雜許多,因為不單要考量應用程式或程式庫打算採取哪個方式,若相依於第三方程式庫或工具,也必須確認它們採用了哪種型態判斷方式,此時,我們才能儘量採取一致的方式,並且選擇正確的方式來實作。</p> </div></div></div><div class="field field-name-field-tags field-type-taxonomy-term-reference field-label-above"><div class="field-label">Tags: </div><div class="field-items"><div class="field-item even"><a href="/tags/javascript">JavaScript</a></div><div class="field-item odd"><a href="/tags/%E7%B9%BC%E6%89%BF%E6%A8%99%E6%BA%96api">繼承標準API</a></div><div class="field-item even"><a href="/tags/%E9%A1%9E%E5%88%A5%E8%AA%9E%E6%B3%95">類別語法</a></div><div class="field-item odd"><a href="/tags/%E5%9E%8B%E6%85%8B%E5%88%A4%E6%96%B7">型態判斷</a></div><div class="field-item even"><a href="/tags/%E7%AC%AC%E4%B8%89%E6%96%B9%E7%A8%8B%E5%BC%8F%E5%BA%AB">第三方程式庫</a></div><div class="field-item odd"><a href="/tags/%E7%B9%BC%E6%89%BF">繼承</a></div><div class="field-item even"><a href="/tags/%E7%89%A9%E4%BB%B6%E5%88%A4%E6%96%B7">物件判斷</a></div><div class="field-item odd"><a href="/tags/%E5%85%A7%E9%83%A8%E7%89%B9%E6%80%A7%E5%AF%A6%E4%BD%9C">內部特性實作</a></div></div></div>