本文主要講解ECMAScript7
規範中的instanceof
操做符。chrome
「有名」的Symbols
指的是內置的符號,它們定義在Symbol
對象上。ECMAScript7
中使用了@@name
的形式引用這些內置的符號,好比下面會提到的@@hasInstance
,其實就是Symbol.hasInstance
。segmentfault
O instanceof C
在內部會調用InstanceofOperator(O, C)
抽象操做,該抽象操做的步驟以下:瀏覽器
C
的數據類型不是對象,拋出一個類型錯誤的異常;instOfHandler
等於GetMethod(C, @@hasInstance)
,大概語義就是獲取對象C
的@@hasInstance
屬性的值;若是instOfHandler
的值不是undefined
,那麼:函數
ToBoolean(? Call(instOfHandler, C, « O »))
的結果,大概語義就是執行instOfHandler(O)
,而後把調用結果強制轉化爲布爾類型返回。C
不能被調用,拋出一個類型錯誤的異常;OrdinaryHasInstance(C, O)
的結果。OrdinaryHasInstance(C, O)
抽象操做的步驟以下:測試
C
不能被調用,返回false
;若是C
有內部插槽[[BoundTargetFunction]]
,那麼:this
BC
等於C
的內部插槽[[BoundTargetFunction]]
的值;InstanceofOperator(O, BC)
的結果;O
的類型不是對象,返回false
;P
等於Get(C, "prototype")
,大概語義是獲取C.prototype
的值;P
的數據類型不是對象,拋出一個類型錯誤的異常;重複執行下述步驟:prototype
O
等於O.[[GetPrototypeOf]]()
的結果,大概語義就是獲取O
的原型對象;O
等於null
,返回false
;SameValue(P, O)
的結果是true
,返回true
。SameValue
抽象操做參見JavaScript中的==,===和Object.js()中的Object.is()
,Object.is()
使用的就是這個抽象操做的結果。code
由上述步驟2
可知,若是C
是一個bind
函數,那麼會從新在C
綁定的目標函數上執行InstanceofOperator(O, BC)
操做。對象
由上述步驟6
可知,會重複地獲取對象O
的原型對象,而後比較該原型對象和C
的prototype
屬性是否相等,直到相等返回true
,或者O
變爲null
,也就是遍歷完整個原型鏈,返回false
。繼承
由上面的InstanceofOperator(O, C)
抽象操做的步驟2
和3
能夠知道,若是C
上面定義或繼承了@@ hasInstance
屬性的話,會調用該屬性的值,而不會走到步驟4
和5
。步驟4
和5
的目的是爲了兼容沒有實現@@hasInstance
方法的瀏覽器。若是一個函數沒有定義或繼承@@hasInstance
屬性,那麼就會使用默認的instanceof
的語義,也就是OrdinaryHasInstance(C, O)
抽象操做描述的步驟。
ECMAScript7
規範中,在Function
的prototype
屬性上定義了@@hasInstance
屬性。Function.prototype[@@hasInstance](V)
的步驟以下:
F
等於this
值;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 A
,P
是A.prototype
,在第一次循環的時候,a
的原型對象a._proto__
是A.prototype
,也就是步驟中的O
是A.prototype
,因此返回了true
;a instanceof B
,P
是B.prototype
,在第一次循環的時候,a
的原型對象a._proto__
是A.prototype
,不等於P
;執行第二次循環,此時O
是a.__proto__.__proto__
,也就是Object.prototype
,不等於P
;執行第三次循環,此時O
是a.__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 B
在OrdinaryHasInstance(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.prototype
是undefined
。因此,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
操做符的行爲。
直到有一天看了MDN
上Symbol.hasInstance的兼容性部分,發現chrome
從51
版本就開始支持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
的屬性描述符的writable
是false
,也就是這個屬性是隻讀的,因此在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
操做符,但願你們能有所收穫。若是本文有什麼錯誤或者不嚴謹的地方,歡迎在評論區留言。