ECMAScript7規範中的instanceof操做符

本文主要講解ECMAScript7規範中的instanceof操做符。chrome

預備知識

有名的Symbols

「有名」的Symbols指的是內置的符號,它們定義在Symbol對象上。ECMAScript7中使用了@@name的形式引用這些內置的符號,好比下面會提到的@@hasInstance,其實就是Symbol.hasInstancesegmentfault

InstanceofOperator(O, C)

O instanceof C在內部會調用InstanceofOperator(O, C)抽象操做,該抽象操做的步驟以下:瀏覽器

  1. 若是C的數據類型不是對象,拋出一個類型錯誤的異常;
  2. instOfHandler等於GetMethod(C, @@hasInstance),大概語義就是獲取對象C@@hasInstance屬性的值;
  3. 若是instOfHandler的值不是undefined,那麼:函數

    1. 返回ToBoolean(? Call(instOfHandler, C, « O »))的結果,大概語義就是執行instOfHandler(O),而後把調用結果強制轉化爲布爾類型返回。
  4. 若是C不能被調用,拋出一個類型錯誤的異常;
  5. 返回OrdinaryHasInstance(C, O)的結果。

OrdinaryHasInstance(C, O)

OrdinaryHasInstance(C, O)抽象操做的步驟以下:測試

  1. 若是C不能被調用,返回false
  2. 若是C有內部插槽[[BoundTargetFunction]],那麼:this

    1. BC等於C的內部插槽[[BoundTargetFunction]]的值;
    2. 返回InstanceofOperator(O, BC)的結果;
  3. 若是O的類型不是對象,返回false
  4. P等於Get(C, "prototype"),大概語義是獲取C.prototype的值;
  5. 若是P的數據類型不是對象,拋出一個類型錯誤的異常;
  6. 重複執行下述步驟:prototype

    1. O等於O.[[GetPrototypeOf]]()的結果,大概語義就是獲取O的原型對象;
    2. 若是O等於null,返回false
    3. 若是SameValue(P, O)的結果是true,返回true

SameValue抽象操做參見JavaScript中的==,===和Object.js()中的Object.is()Object.is()使用的就是這個抽象操做的結果。code

由上述步驟2可知,若是C是一個bind函數,那麼會從新在C綁定的目標函數上執行InstanceofOperator(O, BC)操做。對象

由上述步驟6可知,會重複地獲取對象O的原型對象,而後比較該原型對象和Cprototype屬性是否相等,直到相等返回true,或者O變爲null,也就是遍歷完整個原型鏈,返回false繼承

Function.prototype[@@hasInstance](V)

由上面的InstanceofOperator(O, C)抽象操做的步驟23能夠知道,若是C上面定義或繼承了@@ hasInstance屬性的話,會調用該屬性的值,而不會走到步驟45。步驟45的目的是爲了兼容沒有實現@@hasInstance方法的瀏覽器。若是一個函數沒有定義或繼承@@hasInstance屬性,那麼就會使用默認的instanceof的語義,也就是OrdinaryHasInstance(C, O)抽象操做描述的步驟。

ECMAScript7規範中,在Functionprototype屬性上定義了@@hasInstance屬性。Function.prototype[@@hasInstance](V)的步驟以下:

  1. F等於this值;
  2. 返回OrdinaryHasInstance(F, V)的結果。

因此,你能夠看到在默認狀況下,instanceof的語義是同樣的,都是返回OrdinaryHasInstance(F, V)的結果。爲何說默認狀況下?由於你能夠覆蓋Function.prototype[@@hasInstance]方法,去自定義instanceof的行爲。

例子

function A () {}
function B () {}

var a = new A
a.__proto__ === A.prototype // true
a.__proto__.__proto__ === Object.prototype // true
a.__proto__.__proto__.__proto__ === null // true

a instanceof A // true
a instanceof B // false

OrdinaryHasInstance(C, O)的第6步可知:

  • 對於a instanceof APA.prototype,在第一次循環的時候,a的原型對象a._proto__A.prototype,也就是步驟中的OA.prototype,因此返回了true
  • 對於a instanceof BPB.prototype,在第一次循環的時候,a的原型對象a._proto__A.prototype,不等於P;執行第二次循環,此時Oa.__proto__.__proto__,也就是Object.prototype,不等於P;執行第三次循環,此時Oa.__proto__.__proto__.__proto__,也就是null,也就是原型鏈都遍歷完了,因此返回了false

接着上面的例子:

A.prototype.__proto__ = B.prototype

a.__proto__ === A.prototype // true
a.__proto__.__proto__ === B.prototype // true
a.__proto__.__proto__.__proto__ === Object.prototype // true
a.__proto__.__proto__.__proto__.__proto__ === null // true

a instanceof B // true

在上面的例子中,咱們把B.prototype設置成了a的原型鏈中的一環,這樣a instanceof BOrdinaryHasInstance(C, O)的第6步的第2次循環的時候,返回了true

OrdinaryHasInstance(C, O)的第2步,咱們知道bind函數的行爲和普通函數的行爲是不同的:

function A () {}
var B = A.bind()

B.prototype === undefined // true

var b = new B
b instanceof B // true
b instanceof A // true

由上面的例子可知,B.prototypeundefined。因此,instanceof做用於bind函數的返回結果實際上是做用於綁定的目標函數的返回值,和bind函數基本上沒有什麼關係。

InstanceofOperator(O, C)步驟2和步驟3可知,咱們能夠經過@@hasInstance屬性來自定義instanceof的行爲:

function A () {}
var a = new A
a instanceof A // true

A[Symbol.hasInstance] = function () { return false }
a instanceof A // ?

chrome瀏覽器測試了一下,發現仍是輸出true。而後看了一下ECMAScript6的文檔,ECMAScript6文檔裏面尚未規定能夠經過@@hasInstance改變instanceof的行爲,因此應該是目前chrome瀏覽器尚未實現ECMAScript7中的instanceof操做符的行爲。

直到有一天看了MDNSymbol.hasInstance的兼容性部分,發現chrome51版本就開始支持Symbol.hasInstance了:

class MyArray {  
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance)
  }
}
console.log([] instanceof MyArray) // true

那麼爲何我那樣寫不行呢?直到我發現:

function A () {}
var fun = function () {return false}
A[Symbol.hasInstance] = fun
A[Symbol.hasInstance] === fun // false
A[Symbol.hasInstance] === Function.prototype[Symbol.hasInstance] // true
A[Symbol.hasInstance] === A.__proto__[Symbol.hasInstance] // true

由上面的代碼可知,A[Symbol.hasInstance]並無賦值成功,並且始終等於Function.prototype[Symbol.hasInstance],也就是始終等於A的原型上的Symbol.hasInstance方法。那是否是由於原型上的同名方法?

Object.getOwnPropertyDescriptor(Function.prototype, Symbol.hasInstance)
// Object {writable: false, enumerable: false, configurable: false, value: function}

由上面的代碼可知,Function.prototype上的Symbol.hasInstance的屬性描述符的writablefalse,也就是這個屬性是隻讀的,因此在A上面添加Symbol.hasInstance屬性失敗了。可是爲啥沒有失敗的提示呢?

'use strict'
function A () {}
var fun = function () {return false}
A[Symbol.hasInstance] = fun
// Uncaught TypeError: Cannot assign to read only property 'Symbol(Symbol.hasInstance)' of function 'function A() {}'

錯誤提示出來了,因此之後仍是儘可能使用嚴格模式。非嚴格模式下有些操做會靜默失敗,也就是即便操做失敗了也不會有任何提示,致使開發人員認爲操做成功了。

var a = {}
a[Symbol.hasInstance] = function () {return true}
new Number(3) instanceof a // true

由於能夠經過自定義Symbol.hasInstance方法來覆蓋默認行爲,因此用instanceof操做符判斷數據類型並不必定是可靠的。

還有一個問題:爲何上面MDN文檔的例子能夠成功,我最初的例子就不行呢,目的不都是寫一個構造函數,而後在構造函數上添加一個屬性嗎?
我的分析的結果是:雖然你們都說Class是寫構造函數的一個語法糖,可是其實仍是和使用function的方式有差異的,就好比上面的例子。使用Class的時候,會直接在構造函數上添加一個靜態屬性,不會先檢查原型鏈上是否存在同名屬性。而使用function的方式的時候,給構造函數添加一個靜態方法,至關於給對象賦值,賦值操做會先檢查原型鏈上是否存在同名屬性,因此就會有賦值失敗的風險。因此,就給構造函數添加Symbol.hasInstance屬性來講,Class能作到,使用Function的方式就作不到。

更新於2018/11/20
上面總結到:

因此,就給構造函數添加 Symbol.hasInstance屬性來講, Class能作到,使用 Function的方式就作不到。

可是,給對象添加屬性除了直接賦值以外,還可使用Object.defineProperty方法:

function A () {}
var a = new A
a instanceof A // true

Object.defineProperty(A, Symbol.hasInstance, {
    value: function () { return false }
})
a instanceof A // false

使用Object.defineProperty方法添加或者修改對象屬性的時候不會檢查原型鏈,因此就能夠成功了。因此上面的總結也就不成立了,也就是:

因此,就給構造函數添加 Symbol.hasInstance屬性來講, Class能作到,使用 Function的方式 也能夠作到。

總結

本文主要講解ECMAScript7規範中的instanceof操做符,但願你們能有所收穫。若是本文有什麼錯誤或者不嚴謹的地方,歡迎在評論區留言。

相關文章
相關標籤/搜索