instanceof函数内部机制探析
instanceof函数是javascript语言中用来判断对象是否是某个构造函数实例的函数,但是它其实是一个比较奇葩的函数,有时的表现并不像其字面上的意思这么直观,经常容易造成各种困惑和不解。不相信?先来看下面的这段代码:
1 function foo() { 2 3 } 4 5 console.log(foo instanceof Function); // true 6 console.log(foo instanceof Object); // true 7 console.log(Function instanceof Object); // true 8 console.log(Object instanceof Function); // true
有趣的事情出现了,foo函数既是Function构造函数的实例,也是Object构造函数的实例,同时Function也是Object构造函数的实例,Object又是Function构造函数的实例。怎么样?乍一看是不是很困惑?这个例子确实比较奇葩,本文会在后面详细讲解产生这些结果的原理。接下来还是先来看一些不是那么奇葩的例子好了:
1 function Animal(height, weight) { 2 if( this instanceof Animal ) { 3 this.height = height; 4 this.weight = weight; 5 this.getHeight = function() { 6 return this.height; 7 } 8 this.getWeight = function() { 9 return this.weight; 10 } 11 } else { 12 return new Animal(height, weight); 13 } 14 } 15 16 function Dog(height, weight, race) { 17 this.race = race; 18 19 Animal.call(this, height, weight); 20 } 21 22 Dog.prototype = Animal.prototype; 23 24 var dog = new Dog(100, 20, 200); 25 26 console.log(dog instanceof Dog); // true 27 console.log(dog instanceof Animal); // true
上面这段代码的意思就比较明确了,首先有一个Animal基类,然后从这个基类派生出一个Dog类,有Dog类型对象会拥有所有Animal类型对象的属性和方法。其中Animal构造函数是作用域安全的,它会检查this对象是否是当前Animal构造函数的实例,保证最后返回的对象一定是Animal构造函数的实例。但是在19行中我们传入的this对象明明是在Dog构造函数中生成的,应该是Dog构造函数的实例,怎么会变成了Animal的实例呢?
玄机就隐藏在 Dog.prototype = Animal.prototype; 这一句当中,该语句表明了Animal和Dog的继承关系,如果把这一句注释掉,那么输出结果将会变成:
console.log(dog instanceof Dog); // true console.log(dog instanceof Animal); // false
唔,果然注释掉了这一句之后dog对象就不是Animal的实例了,看来instanceof函数的判断方法跟构造函数的prototype属性有关。
对js的对象继承机制有一定了解的人都知道,当对构造函数调用new操作符时,实际中发生的事情可以归结为下面的伪代码:
1 var dog = (function(/*params*/) { 2 var obj = {}; 3 obj.__proto__ = Dog.prototype; 4 Dog.call(obj); 5 6 /* do something */ 7 8 return obj; 9 }(/*params*/));
js中的每个对象都会带有一个隐藏的__proto__属性,该属性在某些浏览器中是不可访问的,它指向了构造函数的prototype对象。这个对象有两个属性,一个显式属性为constructor,指向该prototype对象所在的构造函数。另一个属性__proto__是隐式的,指向再上一层的构造函数的prototype对象。以此类推层层往上,最终指向的就是Object构造函数的prototype对象(Object是javascript中所有对象的基类)。
记得有次曾经在某个论坛上看到有人说instanceof操作符是根据constructor属性来判断对象是否是某个构造函数的实例的,当初还相信了好久(年幼无知 = =|||)。唔,话不多说先来试试看,把声明了Animal和Dog构造函数的代码后半部分修改如下:
1 var dog = new Dog(100, 20, 200); 2 3 console.log(dog.__proto__.constructor) // Animal 4 dog.__proto__.constructor = ""; 5 console.log(dog.__proto__.constructor) // "" 6 7 console.log(dog instanceof Dog); // true 8 console.log(dog instanceof Animal); // true
由上述代码可知,在修改了dog对象__proto__对象的constructor属性之后,对dog对象使用instanceof操作符的结果显示它还是Animal的实例。其实想想也对,即使在修改之前,dog对象的整条原型链上也没有任何指向Dog构造函数本身的constructor对象,可是检查结果依旧显示dog对象是Dog构造函数的实例,这明显于理不符,所以可以断定instanceof操作符的作用与constructor属性无关
instanceof操作符的结果与constructor属性没有关系,可是要能判断dog对象是基类Animal构造函数的实例,就必定要从原型链上入手。原型链上的必备属性除了constructor就是__proto__了,而__proto__又是一个完整的prototype对象,莫非instanceof操作符是与整个__proto__对象相关的么?再来看看下面的示例:
1 function Dog() { 2 this.name = "dog"; 3 } 4 5 Dog.prototype = { 6 race: 100 7 }; 8 9 var dog_1 = new Dog(); 10 console.log(dog_1 instanceof Dog); // true 11 12 Dog.prototype = { 13 race: 200 14 }; 15 16 var dog_2 = new Dog(); 17 console.log(dog_1 instanceof Dog); // false 18 console.log(dog_2 instanceof Dog); // true
由上述代码可知,dog_1和dog_2其实都是Dog构造函数的实例。不过在实例化dog_1之后,构造函数Dog的prototype对象被修改了,然后再实例化dog_2。之后检测的结果却显示只有dog_2才是Dog构造函数的实例,而dog_1的Dog身份则被剥夺了。而如果将对prototype属性的修改写在构造函数内部,那么生成的所有对象都无法被检测为Dog的实例:
1 function Dog() { 2 this.name = "dog"; 3 4 Dog.prototype = { 5 race: 100 6 }; 7 8 Dog.prototype.constructor = Dog; 9 } 10 11 var dog_1 = new Dog(); 12 var dog_2 = new Dog(); 13 14 console.log(dog_1.constructor); // Dog 15 console.log(dog_1 instanceof Dog); // false 16 console.log(dog_2 instanceof Dog); // false
真相大白了!instanceof操作符判断对象是否是某个构造函数的实例的方法就是检测该对象的__proto__对象是否指向的是该构造函数的prototype对象!如果原型链当前节点的__proto__属性指向的不是要检查的构造函数的prototype对象,就会沿着原型链一直往上搜索比对直至尽头。
于是就可以解释本文开头的那一段代码了,Function和Object都是构造函数,因为在javascript中函数也是对象,Object是Function的基类,所以foo函数既是Function构造函数的实例也是Object构造函数的实例。输出Function和Object构造函数的prototype和__proto__属性如下:
1 console.log(Object.__proto__); // function Empty() {} 2 console.log(Object.prototype); // Object {} 3 4 console.log(Function.__proto__); // function Empty() {} 5 console.log(Function.prototype); // function Empty() {} 6 console.log(Function.__proto__.__proto__);// Object {}
由上面的代码可知,Object.__proto__与Function.prototype相等,因此 Object instanceof Function 的结果为 true。 而Function.__proto__与Object.prototype虽然不相等,但是Function.__proto__.__proto__却与Object.prototype相等,因此 Function instanceof Object的结果同样也为true。
所以instanceof真心是一个奇葩的函数,了解了它的工作机制之后,来用它做点奇葩的事情吧o(∩_∩)o :
1 function Animal() { 2 this.run = function() { 3 console.log("I'm running!"); 4 } 5 }; 6 function Plant() { 7 this.ColorOfFlower = "red"; 8 }; 9 10 var plant = new Plant(); 11 plant.__proto__ = Animal.prototype; 12 console.log(plant instanceof Animal); // true 13 console.log(plant instanceof Plant); // false 14 15 16 function Person() {}; 17 var dog = {}; 18 var obj = {}; 19 20 Person.prototype = obj; 21 dog.__proto__ = obj; 22 23 console.log(dog instanceof Person); // true
另附上参考资料,感谢网上所有高手的详细解读:
《javascript内部机制探析》--- http://www.jb51.net/article/25010.htm
《js中的Object Function奇怪现象》--- http://www.iteye.com/problems/30452
《javascript中prototype属性继承原理详解》 --- http://blog.csdn.net/liuqiwen0512/article/details/8095306
另外我发现网上对于原型链的解读的文章都比较笼统,而且千篇一律翻来覆去说的都是那几句话(就像所有参考书和授课的老师在向初学者讲解局部变量和全局变量的区别时都是说:“在函数中加var声明的是局部变量, 不加var声明的是全局变量” = =),有些东西还说得不是很清楚,过段时间我想想清楚再写篇关于原型链的文章好了。。