Skip to content

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

jnotnull edited this page Jul 19, 2014 · 8 revisions

preamble 前言 As you know, you can create new objects in JavaScript using a Constructor Function, like this: 大家都知道,我们可以使用构造函数来创建对象,想这样:

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

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

When you “call” the constructor with the new keyword, you get a new object allocated, and the constructor is called with the new object as the current context. If you don’t explicitly return anything from the constructor, you get the new object as the result. 当你使用new来调用构造函数时候,你得到了一个新分配的对象,同时构造函数在当前上下文中被新的对象调用。即使你没有显式的返回什么,但是你得到了一个新的对象作为结果。

Thus, the body of the constructor function is used to initialize the newly created object. There’s another thing: The newly created object is initialized to have a prototype. What prototype? The contents of the constructor’s prototype property. So we can write: 因此,构造函数体是用来初始化新创建的对象的。另一个注意的事情是:新创建的对象会有一个prototype属性。protytype是什么呢?它是构造函数的原型属性。因此我们可以这样写:

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

snafu.concatenated() //=> 'Situation Normal All Fsked Up' Thanks to the internal mechanics of JavaScript’s instanceof operator, we can use it to test whether an object is likely to have been created with a particular constructor: 非常感谢JS的内部操作符instanceof,我们可以使用它来判断一个对象是否由指定的构造函数来创建。

snafu instanceof Fubar //=> true (It’s possible to “fool” instanceof when working with more advanced idioms, or if you’re the kind of malicious troglodyte who collects language corner cases and enjoys inflicting them on candidates in job interviews. But it works well enough for our purposes.)

the problem 问题 What happens if we call the constructor, but accidentally omit the new keyword? 如果我们有时不带有new去调用构造函数的话会发生什么呢?

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

fubar //=> undefined William-Thomas-Fredreich!? We’ve called an ordinary function that doesn’t return anything. so fubar is undefined. That’s not what we want. Actually, it’s worse than that:

_foo //=> 'Fsked Up' JavaScript sets this to the global environment by default for calling an ordinary function, so we’ve just blundered about in the global environment. We can fix that somewhat: 当我们调用一个普通函数时候,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

Although "use strict" might be omitted from code in blog posts and books (mea culpa!), in production it is very nearly mandatory for reasons just like this. But nevertheless, constructors that do not take into account the possibility of being called without the new keyword are a potential problem. 虽然“use strict”可能已经在博客和书中被忽略,但是在生产环境中可能因为各种原因,它是近乎必须使用的了。但是无论如何,不适用new来调用构造函数都是一个潜在的风险。

So what can we do? 那我们该如何做呢?

solution: auto-instantiation 解决方案:自动实例化 In Effective JavaScript, David Herman describes auto-instantiation. When we call a constructor with new, The pseudo-variable this is set to a new instance of our “class,” so-to-speak. We can use this to detect whether our constructor has been called with new: 在Effective JavaScript中,David Herman描述了自动实例化。

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' } Why bother making it work without new? One problem this solves is that new Fubar(...) does not compose. Consider:

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 decorates a function by returning a version of the function that logs its arguments. Let’s try it on the original 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' This doesn’t work because snafu is actually an instance of LoggingFubar, not of Fubar. But if we use the auto-instantiating version of 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' Now it works, but of course snafu is an instance of Fubar, not of LoggingFubar. Is that what you want? Who knows!? This isn’t a justification for the pattern, as much as an explanation that it is a useful, but leaky abstraction. It’s doesn’t “just work,” but it can make certain things possible (like decorating constructors) that are otherwise even more awkward to implement.

solution: overload its meaning It can be very handy to have a function that tests for an object being an instance of a particular class. If we can stomach the idea of one function doing two different things, we can make the constructor its own instanceof test:

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 This allows us to use the constructor as an argument in predicate and multiple dispatch, or as a filter:

var arrayOfSevereProblems = problems.filter(Fubar); solution: kill it with fire If we don’t have some pressing need for auto-instantiation, and if we care not for overloaded functions, we may wish to avoid accidentally calling a constructor without using new. We saw that "use strict" can help, but it’s not a panacea. It won’t throw an error if we don’t actually try to assign a value to the global environment. And if we try to do something before assigning a value, it will do that thing no matter what.

Perhaps it’s better to take matters into our own hands. Olivier Scherrer suggests the following pattern:

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 Simple and safer than only relying on "use strict". If you like having a simple instanceof test, you can bake it into the constructor as a function method:

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

var arrayOfSevereProblems = problems.filter(Fubar.is); There you have it: Constructors that fail when called without new are a potential problem, and three solutions we can use are, respectively, auto-instantiation, overloading the constructor, or killing such calls with fire.

Clone this wiki locally