强制使用new的模式
我们知道,构造函数和普通的函数本质一样,只是通过new
调用而已。那么如果调用构造函数时忘记new
会发生什么呢?漏掉new
不会产生语法错误也不会有运行时错误,但可能会造成逻辑错误,导致执行结果不符合预期。这是因为如果不写new
的话,函数内的this
会指向全局对象(在浏览器端this
指向window
)。
当构造函数内包含this.member
之类的代码,并直接调用这个函数(省略new
),实际上会创建一个全局对象的属性member
,可以通过window.member
或member
访问到。这不是我们想要的结果,因为我们要努力确保全局命名空间干净。
// 构造函数
function Waffle() {
this.tastes = "yummy";
}
// 新对象
var good_morning = new Waffle();
console.log(typeof good_morning); // "object"
console.log(good_morning.tastes); // "yummy"
// 反模式,漏掉new
var good_morning = Waffle();
console.log(typeof good_morning); // "undefined"
console.log(window.tastes); // "yummy"
ECMAScript5中修正了这种出乎意料的行为逻辑。在严格模式中,this
不再指向全局对象。如果在不支持ES5的JavaScript环境中,也有一些方法可以确保有没有new
时构造函数的行为都保持一致。
命名规范
一种简单解决上述问题的方法就是命名规范,前面的章节已经讨论过,构造函数首字母大写(MyConstructor()
),普通函数和方法首字母小写(myFunction
)。
使用that
遵守命名规范有一定的作用,但规范毕竟不是强制,不能完全避免出现错误。这里给出了一种模式可以确保构造函数一定会按照构造函数的方式执行,那就是不要将所有成员添加到this
上,而是将它们添加到that
上,并返回that
。
function Waffle() {
var that = {};
that.tastes = "yummy";
return that;
}
如果要创建更简单一点的对象,甚至不需要局部变量that
,直接返回一个对象字面量即可,就像这样:
function Waffle() {
return {
tastes: "yummy"
};
}
不管用什么方式调用它(使用new
或直接调用),它都会返回一个实例对象:
var first = new Waffle(),
second = Waffle();
console.log(first.tastes); // "yummy"
console.log(second.tastes); // "yummy"
这种模式的问题是会丢失原型,因此在Waffle()
的原型上的成员不会被继承到这些对象中。
需要注意的是,这里用的
that
只是一种命名规范,that
并不是语言特性的一部分,它可以被替换为任何你喜欢的名字,比如self
或me
。
调用自身的构造函数
为了解决上述模式的问题,能够让对象继承原型上的属性,我们使用下面的方法:在构造函数中首先检查this
是否是构造函数的实例,如果不是,则通过new
再次调用自己:
function Waffle() {
if (!(this instanceof Waffle)) {
return new Waffle();
}
this.tastes = "yummy";
}
Waffle.prototype.wantAnother = true;
// 测试
var first = new Waffle(),
second = Waffle();
console.log(first.tastes); // "yummy"
console.log(second.tastes); // "yummy"
console.log(first.wantAnother); // true
console.log(second.wantAnother); // true
还有一种比较通用的用来检查实例的方法是使用arguments.callee
,而不是直接将构造函数名写死在代码中:
if (!(this instanceof arguments.callee)) {
return new arguments.callee();
}
这种模式利用了一个事实,即在任何函数内部都会创建一个arguments
对象,它包含函数调用时传入的参数。同时arguments
包含一个callee
属性,指向正在被调用的函数。需要注意,ES5严格模式中已经禁止了arguments.callee
的使用,因此最好对它的使用加以限制,并尽可能删除现有代码中已经用到的地方。