JS中从构造函数返回this

3
如果我在JavaScript中有一个构造函数,我希望能够明确地返回某些内容,这样在团队协作时一切都会变得清晰明了。
我读到过,在JS中如果你使用一个函数作为构造函数,那么如果你返回任何东西,使用new关键字将是徒劳无功的。但我想知道是否可以从构造函数中返回this并且是安全的,例如:
function MyConstructor(val){

      this.val = val;
      return this;   //attention here

}

var example = new MyConstructor(val);

这样做是否会得到期望的结果?


我认为这不是一个好主意。构造函数之所以与其他函数不同,是有其独特的原因的。如果你最终从构造函数中显式返回,则使用寄生继承,本质上并非使用构造函数而是普通函数。为什么要混淆两者,并通过打破事实上的约定来困惑未来的开发人员呢? - Sukima
坦白地说,你假设自己比团队中的其他人更懂。如果我在你的团队中,我会非常困惑为什么需要return this,并尝试删除冗余和令人困惑的语句。如果那听起来有点刻薄,我很抱歉,但我个人认为可读性很重要,而熟悉度是可读性的一部分。如果没有一个非常好的理由改变众所周知的模式,最好不要这样做。如果您确实要更改,请使用注释明确说明更改。这一切都是基于你的整个团队没有要求return this模式,如果有的话,请忘记我说的一切。 - Sukima
我认为需要有一些元数据来标记函数是否为构造函数,其中之一可以是明确地返回this,以某种形式或另一种方式 :) - Alexander Mills
1
如果您希望在使用new关键字或不使用时都获得一致的行为,请不要使用this,而是使用其他变量并返回它,例如 var self = {}; ... return self - Matt Browne
为了使原型继承按预期工作,可能需要使用“var self = this;”。 - Alexander Mills
@AlexMills,你所说的元数据通常是通过名称中的大写字母来实现的。function functionNamesLikeThis()只是一个函数,而function ClassNamesLikeThis()则是一个构造函数。这与使用下划线表示私有属性this.publicPropertythis._privateProperty的约定没有什么不同。许多样式指南建议像Google的https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml?showone=Naming#Naming一样。 - Sukima
5个回答

3

让我们先从JavaScript的正常行为开始。如果您没有从构造函数中返回任何内容,它将按预期工作(当然了)。

因此,

var Dog = function (name) {
  this.name = name;
};

var alice = new Dog('Alice');

这将创建一个新对象,其name属性设置为Alice

现在,如果您尝试通过显式调用构造函数来覆盖隐式的return语句会发生什么呢?

让我们添加一个返回其他内容的return语句:

var Dog = function (name) {
  this.name = name;
  return 23;
};

var alice = new Dog('Alice');

现在alice的值是什么?也许你期望它是23,但实际上不是。它仍然是一个对象,其中name属性被设置为Alice
问题在于JavaScript比你聪明:它看到了你的return,但意识到你想要返回的类型与使用new调用函数的事实不匹配。因此return会被忽略。
那么如果我们试图比JavaScript更聪明,并返回一个类型匹配的对象呢?
var Dog = function (name) {
  this.name = name;
  return { foo: 'bar' };
};

var alice = new Dog('Alice');

在这种情况下,JavaScript认为您有足够的理由覆盖隐式的return并使用您自己的return。这意味着alice现在指向一个对象,该对象具有设置为'bar'foo属性。

简而言之:如果您明确调用了return,它将覆盖隐式的return,并且实际使用您自己的return。请保留HTML标记。

return this;

你最终会得到最后一种情况。你重写了隐式返回的对象。但由于你返回的实际上与隐式返回的相同,因此没有区别。
所以:是的,它们是相同的,但不需要调用return this;
一些开发者利用这种行为来欺骗JavaScript始终返回一个新对象,无论你是否使用new调用构造函数:
var Dog = function (name) {
  return {
    name: name
  };
};

var alice1 = new Dog('Alice');
var alice2 = Dog('Alice');

两个调用都会创建一个新对象,其中一个 name 属性设置为 Alice,但与先前的示例有一些差异:
  • 两个对象只是对象字面量,其 constructor 属性未设置为 Dog,也没有使用预期的原型链。
  • 在使用 new 时实际上创建了两个对象:一个由 new 创建,另一个由您使用对象字面量语法创建。这意味着垃圾收集器需要更多的工作。
因此,我认为您应该避免使用此技术,而要么正确地使用构造函数,要么坚持使用工厂函数。
这与 @maboiteaspam 的答案 中利用的效果相同:
function DHT (opts) {
  var self = this
  if (!(self instanceof DHT)) return new DHT(opts)
  // ctor body
}

如果你用new调用这个函数,this会被设置为一个对象,它的constructor属性指向DHT函数。因此,self instanceof DHT返回true,实际的构造函数就会运行。
如果你不使用new调用这个函数,if语句会检测到这一点,并自动为你运行new(这至少比上述解决方案好,在那里你最终会遇到两个缺点)。
请注意,在严格模式下,不允许从没有在对象上调用的函数中访问this,因此如果你尝试访问this而没有使用new关键字调用构造函数,就会立即出现错误。

你提到的所有选项中,严格模式似乎是防止误用的最简单方法... +1 - Matt Browne

2
您可以在构造函数中返回this,但与不返回任何内容效果相同。
来自MDN文档
执行代码new Foo(...)时,会发生以下事情:
  1. 创建一个新对象,该对象继承自Foo.prototype
  2. 使用指定的参数调用构造函数Foo,将this绑定到新创建的对象。 new Foo等价于new Foo(),即如果不指定参数列表,则使用没有参数的Foo调用构造函数。
  3. 构造函数返回的对象成为整个new表达式的结果。如果构造函数没有显式返回对象,则使用步骤1中创建的对象。(通常构造函数不返回值,但如果它们想要覆盖正常的对象创建过程,则可以选择这样做。)

2

2

我很确定你所需要的是命名规则而不是语言特性。这种方法的问题在于你会掩盖意图。想象一下,如果你的团队采用了这种编程风格,那么如果我参与其中,我可能每天在编写代码时都会多次在脑海中进行以下对话:

var foo - foobar(); // Um wait is this a constructor or a function?
// I guess I'll have to look it up:
function foobar() {
  // skip a bunch of code.
  return this;
}
// Huh? Why do I need to return this? And what is this supposed to refer to?
// Do I need to always provide a context when calling this function?
// Oh wait that some weird convention I was told about during employee initiation.
// Guess I have to deal with this for now and note to self:
//   Brush up my resume, gonna need it soon.
var foo = new foobar();

因此,使用函数时程序员没有任何信息可用来调用该函数。

更好的方法是使用命名约定,即以大写字母开头的每个函数都是构造函数,而小写字母则只是普通函数。

这种约定被广泛认知,并在许多团队中使用:

function FooBar() {}

var foo = new FooBar(); // Capital letter need to use new
var bar = fooBar(); // lowercase means a normal function.

+1. 命名约定确实可以在这里非常有帮助……我在我的回答中发布了一些关于命名约定的建议。 - Matt Browne

1
更新:实际上,在严格模式下,this是未定义的,因此忘记使用this的程序员会立即收到错误提示 - 这将是他们注意到错误的最快方式。因此,简单地使用严格模式可能是最佳解决方案:
"use strict";
function MyConstructor(val){
    this.val = val;
}

(如果您在同一脚本中有其他不想在严格模式下运行的代码,也可以将"use strict"放在函数内部。)
这是一个更详细的示例,展示了如何使其无论是否使用new都能正常工作:
function MyConstructor(val) {
    var self = Object.create(MyConstructor.prototype);
    self.constructor = MyConstructor;
    self.val = val;
    return self;
}

var example = new MyConstructor(val);
var example2 = MyConstructor(val); //same result

当然,这有点啰嗦,所以在实践中,使用 @maboiteaspam 的答案可能更简单。就个人而言,我不担心这个问题;如果程序员未使用 new 关键字,则应该注意到错误,因为他们将只会得到 window 对象(或在严格模式下为 null),而不是一个新的对象实例。但是,如果您喜欢不需要使用 new 关键字的风格,则可以考虑使用旨在更具原型使用的库,例如: 或者您可以使用命名约定,使预期的用途更加清晰,正如@Sukima所建议的那样。您可以在制造函数前缀中加上“make”或“create”,以使其更清晰,例如makeUser()createUser()。或者使用上面“自私”库的样式:User.new()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接