本文主要讲解ECMAScript7
规范中的instanceof
操作符。
预备知识
有名的Symbols
“有名”的Symbols
指的是内置的符号,它们定义在Symbol
对象上。ECMAScript7
中使用了@@name
的形式引用这些内置的符号,比如下面会提到的@@hasInstance
,其实就是Symbol.hasInstance
。
InstanceofOperator(O, C)
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)
OrdinaryHasInstance(C, O)
抽象操作的步骤如下:
- 如果
C
不能被调用,返回false
; -
如果
C
有内部插槽[[BoundTargetFunction]]
,那么:- 让
BC
等于C
的内部插槽[[BoundTargetFunction]]
的值; - 返回
InstanceofOperator(O, BC)
的结果;
- 让
- 如果
O
的类型不是对象,返回false
; - 让
P
等于Get(C, "prototype")
,大概语义是获取C.prototype
的值; - 如果
P
的数据类型不是对象,抛出一个类型错误的异常; -
重复执行下述步骤:
- 让
O
等于O.[[GetPrototypeOf]]()
的结果,大概语义就是获取O
的原型对象; - 如果
O
等于null
,返回false
; - 如果
SameValue(P, O)
的结果是true
,返回true
。
- 让
SameValue
抽象操作参见JavaScript中的==,===和Object.js()中的Object.is()
,Object.is()
使用的就是这个抽象操作的结果。
由上述步骤2
可知,如果C
是一个bind
函数,那么会重新在C
绑定的目标函数上执行InstanceofOperator(O, BC)
操作。
由上述步骤6
可知,会重复地获取对象O
的原型对象,然后比较该原型对象和C
的prototype
属性是否相等,直到相等返回true
,或者O
变为null
,也就是遍历完整个原型链,返回false
。
Function.prototype[@@hasInstance](V)
由上面的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
操作符,希望大家能有所收获。如果本文有什么错误或者不严谨的地方,欢迎在评论区留言。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。