本文主要讲解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
操做符,但愿你们能有所收获。若是本文有什么错误或者不严谨的地方,欢迎在评论区留言。