在JS中,构造函数其实就是一个使用new操作符调用的函数,和普通函数的区别是函数名的首字母大写(不是强制规定)。当使用new调用时,构造函数内用到的this对象会指向新创建的对象实例:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
}
var person = new Person('Nicholas',29,'software engineer');
console.log(person.name); // 'Nicholas'
console.log(person.age); //29
console.log(person.job); //'software engineer'
在这个例子中,Person构造函数内部的this指针指向新对象person,所以person对象会被赋值name,age,job属性。但是如果没有使用new操作符来调用该构造函数时会发生什么情况呢:
var person = Person('Nicholas',29,'software engineer');
console.log(person.name); //TypeError: person is undefined
console.log(person.age);
console.log(person.job);
会发现person报错,未定义,在来看一下window属性:
var person = Person('Nicholas',29,'software engineer');
console.log(window.name); //'Nicholas'
console.log(window.age); //29
console.log(window.job); //'software engineer'
结果发现Person构造函数的给window对象添加了属性。这是因为,this对象是在运行时绑定的,直接调用Person()时,this会映射到window对象上,导致错误对象属性的意外添加。
导致上述错误的原因是把构造函数当做普通函数调用,忽略了new操作符。这个问题是由this对象的晚绑定造成的,在这里this被解析成了window对象。上面这个直接调用可以理解成下面这样:
var person = Person('Nicholas',29,'software engineer');
等价于:
var person = window.Person('Nicholas',29,'software engineer');
所以出现上面的错误就不足为奇了。
发生window的属性错误赋值可能会出现其他错误的出现,例如上面的window.name属性被覆盖了。为了解决这个问题,我们可以更改一下构造函数,创建一个作用域安全的构造函数。
作用域安全的构造函数
作用域安全的构造函数在进行任何更改前,首先确认this对象是否是正确的类型的实例对象。如果不是,创建新的实例并返回。看下面的例子:
function Person(name,age,job){
if(this instanceof Person){
this.name = name;
this.age = age;
this.job = job;
}else{
return new Person(name,age,job);
}
}
var person1 = Person('nicholas',29,'software engineer');
console.log(window.name); //''
console.log(person1.name); //'nicholas'
我们在Person构造函数中添加了一个this对象的类型检查,要么new操作符,要么在现有Person实例环境中调用构造函数。在这种情况下,即使不适用new操作符调用构造函数也可以正常运行。
带来的问题
使用了这种模式后,你就锁定了可以调用构造函数的环境。如果使用构造函数的窃取模式实现继承而不使用原型链,那么这个继承很可能出现问题:
function Polygon(sides){
if (this instanceof Polygon) {
this.sides = sides;
this.getArea = function(){
return 0;
};
} else {
return new Polygon(sides);
}
}
function Rectangle(width, height){
Polygon.call(this, 2);
this.width = width;
this.height = height;
this.getArea = function(){
return this.width * this.height;
};
}
var rect = new Rectangle(5, 10);
console.log(rect.sides); //undefined
这个例子中,构造函数使用了作用域安全的模式,Rectangle构造则不是。新创建一个Rectangle实例后,这个实例应该通过Polygon.call来继承Polygon的sides属性,但是,由于Polygon构造函数是作用域安全的,this对象并非Polygon的实例,所以会创建一个Polygon对象,Rectangle 构造函数中的this 对象并没有得到增长,同时Polygon.call()返回的值也没有用到,所以Rectangle 实例中就不会有sides 属性。
解决方案
如果构造函数窃取结合使用原型链或者寄生组合则可以解决这个问题。考虑以下例子:
function Polygon(sides){
if (this instanceof Polygon) {
this.sides = sides;
this.getArea = function(){
return 0;
};
} else {
return new Polygon(sides);
}
}
function Rectangle(width, height){
Polygon.call(this, 2);
this.width = width;
this.height = height;
this.getArea = function(){
return this.width * this.height;
};
}
Rectangle.prototype = new Polygon();
var rect = new Rectangle(5, 10);
console.log(rect.sides); //2
上面这段重写的代码中,一个Rectangle 实例也同时是一个Polygon 实例,所以Polygon.call()会照原意执行,最终为Rectangle 实例添加了sides 属性。