Skip to content

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

jnotnull edited this page Jul 19, 2014 · 8 revisions

原文链接:http://raganwald.com/2014/07/09/javascript-constructor-problem.html 翻译:jnotnull


前言

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

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