一、constructor
我们创建的每个函数都有一个prototype(原型)对象,这个属性是一个指针,指向一个对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针。
1 function Person () {} 2 3 console.log(Person.prototype)
打印结果如下:
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针,指向构造函数的原型对象。ECMA-262第5版中管这个指针叫[[Prototype]],双中括号表示该属性为内部属性,在JavaScript中不能直接访问。虽然在脚本中没有标准的方式访问[[Prototype]],但Firefox、Safari和Chrome在每个对象上都支持一个属性__proto__来访问[[Prototype]],但__proto__的[[Enumerable]]值为false,不可以通过for-in或Object.keys()枚举出来。
1 function Person (name, age) { 2 this.name = name 3 this.age = age 4 } 5 6 var person = new Person('Jim', 21) 7 8 console.log(person) 9 10 console.log(Object.keys(person)) 11 12 console.log(person.__proto__) 13 14 console.log(person.__proto__ === Person.prototype)
打印结果如下:
因为constructor属性在构造函数的原型里,并且指向构造函数,那我们就可以利用constructor属性来判断一个实例对象是由哪个构造函数构造出来的,也可以说判断它属于哪个类。
1 function Person (name, age) { 2 this.name = name 3 this.age = age 4 } 5 6 function Car () {} 7 8 var person = new Person('Jim', 21) 9 10 console.log(person.constructor) //Person 11 12 console.log(person.constructor === Person) //true 13 14 console.log(person.constructor === Car) // false
但有一点我们是要注意的,当我们将Person.prototype设置为等于一个以对象字面量形式创建的新对象时,constructor属性不再指向Person。因为上述做法把Person默认的prototype覆盖掉,指向Person的constructor就不复存在。
1 function Person () {} 2 3 var person1 = new Person() 4 5 Person.prototype = { 6 name: 'Jim', 7 age: '21' 8 } 9 10 var person2 = new Person() 11 12 console.log(person1) 13 14 console.log(person2) 15 16 console.log(person1.constructor) 17 18 console.log(person2.constructor)
打印结果如下:
访问person2时,Person.prototype里已经没有了constructor属性,所以会继续沿着原型链往上找到Person.prototype.prototype中的constructor属性,它是指向Object的,因此person2.constructor指向Object。
那如果我们对 Person.prototype重新赋值后希望constructor仍指向Person的话,我们可以在字面对象里加一个constructor属性让它指向Person
1 function Person () {} 2 3 var person1 = new Person() 4 5 Person.prototype = { 6 constructor: Person, 7 name: 'Jim', 8 age: '21' 9 } 10 11 var person2 = new Person() 12 13 console.log(person1) 14 15 console.log(person2) 16 17 console.log(person1.constructor) 18 19 console.log(person2.constructor) 20 21 console.log(Object.keys(Person.prototype))
打印结果如下:
我们发现person2.constructor重新指向Person,但同时我们也发现constructor变成了可枚举属性,上文说到constructor属性默认是不可枚举的,即[[Enumerable]]的值为false
我们可以通过Object.defineProperty()把constructor定义为不可枚举属性
1 function Person () {} 2 3 var person1 = new Person() 4 5 Person.prototype = { 6 name: 'Jim', 7 age: '21' 8 } 9 // 重设构造函数,只使用与ECMAScript5兼容的浏览器 10 Object.defineProperty(Person.prototype, 'constructor', { 11 enumerable: false, 12 value: Person 13 }) 14 15 var person2 = new Person() 16 17 console.log(person1) 18 19 console.log(person2) 20 21 console.log(person1.constructor) 22 23 console.log(person2.constructor) 24 25 console.log(Object.keys(Person.prototype))
打印结果如下:
我们可以看到constructor已经变成了不可枚举属性
二、instanceof
instanceof
运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype
属性。通俗来将就是判断一个实例对象是否由某个构造函数构造而来。
1 function Person () {} 2 3 function Car () {} 4 5 var person = new Person() 6 7 console.log(person instanceof Person) //true 8 9 console.log(person instanceof Car) //false
这一点与constructor有着相同的作用,但instanceof相对于constructor更为可靠
1 function Person () {} 2 3 Person.prototype = { 4 name: 'Jim', 5 age: '21' 6 } 7 8 var person = new Person() 9 10 console.log(person instanceof Person) //true 11 12 console.log(person.constructor === Person) //false
可见Person.prototype对象被重写并不影响instanceof的判断,因为instanceof是根据原型链来判断构造函数的,只要对象实例的原型链不发生变化,instanceof便可以正确判断
1 function Person () {} 2 3 var person = new Person() 4 5 console.log(person instanceof Person) //true 6 7 console.log(person instanceof Object) //true 8 9 person.__proto__ = {} 10 11 console.log(person instanceof Person) //false 12 13 console.log(person instanceof Object) //true
instanceof不仅可以判断实例对象直接的构造函数,而且还能判断原型链上所有的构造函数,上面代码中Object在person的原型链中,所以返回true。
当我们把空字面对象{}赋值给person.__proto__后,相当于切断了person原来的原型链(person -> Person -> Object),所以person instanceof Person返回false,那为什么person instanceof Object会返回true呢,因为空字面对象是Object的实例,即原型链修改为(person -> Object)。这里顺带说一下我面试时被问到的一道面试题,就是Object的原型是什么?答案是null,因为Object是万物之源,所以对象都是Object的实例,处于原型链的末端,而它没有原型。