Skip to content

JS构造函数的一个问题以及三个解决方案

jnotnull edited this page Jul 19, 2014 · 8 revisions

前言 大家都知道,我们可以使用构造函数来创建对象,想这样:

function Fubar (foo, bar) { this._foo = foo; this._bar = bar; }

var snafu = new Fubar("Situation Normal", "All Fsked Up");

当你使用new来调用构造函数时候,你得到了一个新分配的对象,同时构造函数在当前上下文中被新的对象调用。即使你没有显式的返回什么,但是你得到了一个新的对象作为结果。

因此,构造函数体是用来初始化新创建的对象的。另一个注意的事情是:新创建的对象会有一个prototype属性。protytype是什么呢?它是构造函数的原型属性。因此我们可以这样写:

Fubar.prototype.concatenated = function () { return this._foo + " " + this._bar; }

snafu.concatenated() //=> 'Situation Normal All Fsked Up'

非常感谢JS的内部操作符instanceof,我们可以使用它来判断一个对象是否由指定的构造函数来创建。

snafu instanceof Fubar //=> true

问题

如果我们有时不带有new去调用构造函数的话会发生什么呢?

var fubar = Fubar("Fsked Up", "Beyond All Recognition");

fubar //=> undefined

我们呢调用了函数,但是并没有获得返回的东东,因此subar是undefined。这个不是我们想要的。事实上,问题更加严重:

_foo //=> 'Fsked Up'

当我们调用一个普通函数时候,JS会默认设置this到global环境中,因此我们错误的设置变量到global上下文中了。我们可以坐下如下动作去修复下:

function Fubar (foo, bar) { "use strict"

this._foo = foo; this._bar = bar; }

Fubar("Situation Normal", "All Fsked Up"); //=> TypeError: Cannot set property '_foo' of undefined

虽然“use strict”可能已经在博客和书中被忽略,但是在生产环境中可能因为各种原因,它是近乎必须使用的了。但是无论如何,不适用new来调用构造函数都是一个潜在的风险。

那我们该如何做呢?

解决方案:自动实例化

在Effective JavaScript中,David Herman描述了自动实例化。当我们使用new去调用构造函数的时候,this变量就指向了我们类的实例。我们可以使用this去验证我们的构造函数是否被new调用。

function Fubar (foo, bar) { "use strict"

var obj, ret;

if (this instanceof Fubar) { this._foo = foo; this._bar = bar; } else return new Fubar(foo, bar); }

Fubar("Situation Normal", "All Fsked Up"); //=> { _foo: 'Situation Normal', _bar: 'All Fsked Up' }

为何要这么麻烦而不用new呢?因为这个方案解决了不能使用new Fubar(...)来构造对象的问题。

function logsArguments (fn) { return function () { console.log.apply(this, arguments); return fn.apply(this, arguments) } }

function sum2 (a, b) { return a + b; }

var logsSum = logsArguments(sum2);

logsSum(2, 2) //=> 2 2 4

logsArguments封装了一个方法,这个方法在输出参数后返回。让我们在原始Fubar上做下试验。

function Fubar (foo, bar) { this._foo = foo; this._bar = bar; } Fubar.prototype.concatenated = function () { return this._foo + " " + this._bar; }

var LoggingFubar = logsArguments(Fubar);

var snafu = new LoggingFubar("Situation Normal", "All Fsked Up"); //=> Situation Normal All Fsked Up

snafu.concatenated() //=> TypeError: Object [object Object] has no method 'concatenated'

这个没有用,因为snafu事实上是LoggingFubar而不是Fubar的实例。但是,如果我们使用Fubar的自动初始化 function Fubar (foo, bar) { "use strict"

var obj, ret;

if (this instanceof Fubar) { this._foo = foo; this._bar = bar; } else { obj = new Fubar(); ret = Fubar.apply(obj, arguments); return ret === undefined ? obj : ret; } } Fubar.prototype.concatenated = function () { return this._foo + " " + this._bar; }

var LoggingFubar = logsArguments(Fubar);

var snafu = new LoggingFubar("Situation Normal", "All Fsked Up"); //=> Situation Normal All Fsked Up

snafu.concatenated() //=> 'Situation Normal All Fsked Up'

现在它可以实现了,但是显然snafu是Fubar的一个实例,而不是LoggingFubar的。那是你想要的吗?谁知道呢!这不是标准的模式,只能解释为他是一个有用的但是有漏洞的抽象化。

解决方案:重载

如果能有一个方法去判断对象的类型,那将非常的方便。如果我们赞成一个方法能做两个不同事情,我们就可以让我们的构造函数拥有自己的instanceof验证功能。 function Fubar (foo, bar) { "use strict"

if (this instanceof Fubar) { this._foo = foo; this._bar = bar; } else return arguments[0] instanceof Fubar; }

var snafu = new Fubar("Situation Normal", "All Fsked Up");

snafu //=> { _foo: 'Situation Normal', _bar: 'All Fsked Up' }

Fubar({}) //=> false Fubar(snafu) //=> true

这就允许我们使用构造函数作为参数来进行消息发送,或者作为过滤器

var arrayOfSevereProblems = problems.filter(Fubar);

解决方案:抛出异常

如果你不需要自动实例化和函数重载,那我们建议你不要直接不带new去调用构造函数。我们看到“use strict”有帮助,但是它不是万能的。如果我们不把变量设定到global环境中,它不会抛出错误。我们应该尽可能的在赋值之前做好事情。

也许我们最好自己掌控局势。Olivier Scherrer给出了下面的模式:

function Fubar (foo, bar) { "use strict"

if (!(this instanceof Fubar)) { throw new Error("Fubar needs to be called with the new keyword"); }

this._foo = foo; this._bar = bar; }

Fubar("Situation Normal", "All Fsked Up"); //=> Error: Fubar needs to be called with the new keyword

这个比仅仅依赖“use strict”更加简单安全。如果你喜欢这个简单的instance判断,你还可以把这个判断方法传递到构造函数中。

Fubar.is = function (obj) { return obj instanceof Fubar; }

var arrayOfSevereProblems = problems.filter(Fubar.is);

这里总结下:如果不适用new去调用构造函数都会有潜在的问题。这里有三个解决方法,分别是:自动实例化、重载构造函数、抛出错误。

Clone this wiki locally