首先,对constructor属性有以下几点了解:
- constructor属性是原型对象具有的属性,指向通过prototype户型链接它的构造函数
- 由于实例对象继承自原型对象,所以实例对象中也具有constructor属性,指向与原型对象中的constructor一样;
- 其实构造函数(无论是原生的还是自定义的)也有constructor属性,它们统统指向原生的Function构造函数,就连Function自己的构造函数也是它自己
一、发现
对于一些公共的属性和方法,我么可以通过原型对象,把它们定义在构造函数的外部,使构造函数成为一个空函数:
function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
但是,这样的话,每增加一个公共的属性或方法都得写上Person.prototype。为了减少重复的书写,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象
function Person(){}
Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
经过这样重写之后,从视觉上更好的封装原型的功能,我们相当于把Person.prototype设置成了一个以对象字面量形式创建的新对象。
==但是,这样设置有一个问题:constructor属性不再指向person了==
var friend = new Person();
alert(friend instanceof Person); //true
alert(friend.constructor == Person); //false
我们可以看到,instanceof表示friend仍然是Person的实例,但是constructor却表明friend的构造函数(父类)不再是Person了,constructor与instanceof的结果不一致这就造成了instanceof的失真。那么对象的constructor到底指向谁呢?
二、解答
var friend = new Person();
alert(friend.constructor == Person); //false
alert(friend.constructor == Object); //true
==没错,constructor 属性指向了Object== ,也就是说通过这种对象字面量方式改变原型对象之后,原型/实例对象的constructor属性指向了Object,它们的构造函数(父类)变成了Object。
这是为什么呢?
每创建一个函数就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。而通过对象字面量形式改写原型对象,本质上算是完全重写了默认的原型对象,也即是说我们写了一个新的对象,它是个新对象,因此它的constructor属性也就变成了新对象的constructor属性,指向Object构造函数,不再指向Person函数了。
因此,通过constructor操作符还能返回正确的结果,但是通过instanceof不能准确确定对象的类型
var friend = new Person();
alert(friend instanceof Object); //true
alert(friend instanceof Person); //true
alert(friend.constructor == Person); //false
alert(friend.constructor == Object); //true
三、带来的后果&根本原因
通过改写原型对象的方式改变属性和方法,不仅使instanceof操作符失真,如果实例对象定义在修改之前,还会导致实例对象无法访问新加的属性和方法:
var friend = new Person();
Person.prototype.sayHi = function(){
alert("hi");
};
friend.sayHi(); //"hi"(没有问题!)
这段代码显示,通过使用Person.prototype.……
的方式逐个向原型对象上添加的属性,可以被实例对象成果访问;但是,通过改写原型对象的形式添加新属性和方法并非如此:
function Person(){}
var friend = new Person();
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
friend.sayName(); //error
显然,通过改写原型对象的方式,实例对象不能访问新添加的属性和方法。导致这一现象的根本原因和instanceof
操作符失真的原因一致:constructor
的指向发生了变化
请参考以下图示:
简单来讲,调用构造函数时,会为实例添加一个指向最初原型的_proto_
指针,重写原型对象成为一个新对象,就等于:
- 切断了构造函数与最初原型之间的联系,切断后constructor默认指向Object,但可以自定义修改;
- 切断了新原型与之前已经存在的任何实例对象之间的联系,即新原型不是任何已有实例的原型对象;
- 实例对象的
_proto_
属性指向的仍然是原有的原型对象。
四、小结
1.常规写法,但是比较啰嗦,每次都要重复写Person.prototype
function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
//...
2.使用对象字面量方式改写原型对象,但是这样会改变constructor的指向
function Person(){}
Person.prototype = {
name : "Nicholas",
age : 29,
//...
};
3.如果 constructor 的值真的很重要,可以像下面这样特意将它设置回适当的值。
function Person(){}
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
//...
};
在代码中特意包含一个 constructor属性,并将它的值设置为Person,从而确保了通过该属性能够访问到适当的值
4.但是以上述形式重新指定constructor属性,会使得constructor属性的[[Enumerable]]特性被设置为 true,也就是变成了可枚举类型的属性,但是原生的constructor属性是不可枚举类型,[[Enumerable]]特性为 false,因此可使用下列语句修改它的[[Enumerable]]属性,仍符合原生的设定:
function Person(){}
Person.prototype = {
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
//重设构造函数,只适用于 ECMAScript 5 兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
Reference:
- 《JavaScript高级程序设计(第三版)》