JavaScript中的“new”关键字是否被认为是有害的?

596
另一个问题中,一个用户指出使用new关键字是危险的,并提出了一种不使用new的对象创建解决方案。我并不相信这是真的,主要是因为我使用过PrototypeScript.aculo.us等优秀的JavaScript库,它们都使用了new关键字。
尽管如此,昨天我在YUI剧院观看了道格拉斯·克罗克福德的演讲,他说了完全相同的话,即他不再在自己的代码中使用new关键字(Crockford on JavaScript - Act III: Function the Ultimate - 50:23 minutes)。
使用new关键字是否“不好”?使用它的优缺点是什么?

97
使用new关键字并不是“不好的”。但如果你忘记使用它,就会将对象构造函数作为一个普通函数来调用。如果你的构造函数没有检查其执行上下文,那么它就不会注意到“this”指向不同的对象(通常是全局对象),而不是新实例。因此,你的构造函数会将属性和方法添加到全局对象(window)中。如果你总是在对象函数中检查“this”是否属于你的对象实例,那么你就永远不会遇到这个问题。 - Kristian B
5
我不理解这个。一方面,道格(Doug)反对使用“new”。但是当你看YUI库时,你必须到处使用“new”,比如像这样:var myDataSource = new Y.DataSource.IO({source:"./myScript.php"}); - aditya_gaur
71
我不认为这篇文章应该被关闭。是的,它可能会激起一些反对者的不满,但我们讨论的是一条流行建议,即基本上避免使用一种主要的语言特性。使每个jQuery对象几乎不占用内存的机制涉及使用“new”关键字。应优先使用工厂方法,而不是不断调用new,但不要仅使用对象文字大幅减少架构选项和性能潜力。它们各有其适用场景,构造函数也是如此。这是过时且很糟糕的建议。 - Erik Reppen
5
TLDR: 使用 new 不危险。省略 new 才是危险的,因此是不好的。但在 ES5 中,你可以使用 严格模式,它可以保护你免受这种危险和其他许多危险的影响。 - jkdev
3
整个线程已经过时。目前的最佳实践是使用ES6模块,并且在继承方面使用ES6类。它们默认处于严格模式,这意味着在函数调用中this不会是globalThis,但这不仅仅是这样:类根本不能在没有new的情况下使用。自ES6以来,语言已经发展到鼓励使用new,就像较新的构造函数例如MapSet不能再(不带)使用new一样。违反这一点似乎是不合理的。请注意,旧的构造函数,例如new Stringnew Datenew Array,具有旧规则。 - Sebastian Simon
显示剩余2条评论
13个回答

628
Crockford做了很多工作来普及良好的JavaScript技术。他在语言的关键要素上持有自己的观点,引发了许多有用的讨论。尽管如此,仍有太多人将每个“不好”或“有害”的声明视为真理,拒绝超越一个人的意见。有时这可能会有点令人沮丧。
使用new关键字提供的功能比从头开始构建每个对象具有几个优点:
  1. 原型继承。虽然那些习惯于基于类的面向对象语言的人经常被怀疑和嘲笑,但JavaScript的本地继承技术是一种简单而令人惊讶的有效的代码重用手段。而new关键字是使用它的规范(也是唯一可用的跨平台)方式。
  2. 性能。这是#1的副作用:如果我想给我创建的每个对象添加10个方法,我可以编写一个创建函数,手动将每个方法分配给每个新对象... 或者,我可以将它们分配给创建函数的prototype并使用new来生成新对象。这不仅更快(无需为原型上的每个方法编写每个方法的代码),而且避免了为每个方法分别创建单独属性的对象膨胀。在较慢的机器(或特别是较慢的JS解释器)上,当创建许多对象时,这可能意味着节省大量时间和内存。

是的,new 有一个关键的缺点,正如其他答案所描述的那样:如果你忘记使用它,你的代码将在没有警告的情况下崩溃。幸运的是,这个缺点很容易被缓解 - 只需在函数本身中添加一些代码:

function foo()
{
   // if user accidentally omits the new keyword, this will 
   // silently correct the problem...
   if ( !(this instanceof foo) )
      return new foo();
   
   // constructor logic follows...
}

现在你可以享受`new`的优势,而不必担心因意外误用而导致的问题。
John Resig在他的Simple "Class" Instantiation文章中详细介绍了这种技术,并包括一种将此行为默认构建到您的“类”中的方法。绝对值得一读……就像他即将出版的书Secrets of the JavaScript Ninja一样,在JavaScript语言的许多其他“有害”功能中发现了隐藏的黄金(关于with的章节对于那些最初将此功能视为噱头的人特别启发)。
一个通用的健全性检查
如果你担心破碎的代码会静默地工作,甚至可以添加一个断言来进行检查。或者,正如some所评论的那样,使用检查来引入运行时异常:
if ( !(this instanceof arguments.callee) ) 
   throw new Error("Constructor called as a function");

请注意,这个代码片段可以避免硬编码构造函数名称,因为与前面的例子不同,它没有实际实例化对象的需要——因此,它可以被复制到每个目标函数中而无需修改。

ES5 的限制

正如 Sean McMillanstephenbezjrh 所指出的那样,在 ES5 的 strict mode 中使用 arguments.callee 是无效的。因此,如果在该上下文中使用它,上述模式将抛出错误。

ES6 和完全无害的 new

ES6 在 JavaScript 中引入了 Classes - 不是以旧式 Crockford 那种奇怪的 Java 模仿方式,而是更像他(和其他人)后来采用的轻量级方式,将原型继承的最佳部分融入语言本身,并将常见模式固化到语言中。

...其中一部分包括安全的 new 操作:

class foo
{
  constructor()
  {
    // constructor logic that will ONLY be hit 
    // if properly constructed via new
  } 
}

// bad invocation
foo(); // throws, 
// Uncaught TypeError: class constructors must be invoked with 'new'

但是,如果你不想使用新的语法糖怎么办?如果你只是想更新你完全正常的原型式代码,并加入上述安全检查以使其在严格模式下继续工作呢?

好吧,正如Nick Parsons指出,ES6还提供了一个方便的检查方式,即new.target

function foo()
{
  if ( !(new.target) ) 
     throw new Error("Constructor called as a function");
   
  // constructor logic follows...
}

所以,无论您选择哪种方法,只要思考清楚并保持良好的卫生习惯,您就可以毫不担心地使用new


103
如果 (!(this instanceof arguments.callee)) 抛出错误("构造函数被当作函数调用");// 更通用,不需要知道构造函数的名称,让用户修复代码。 - some
5
如果您担心性能问题并且确实有理由担心,那么根本不要进行检查。要么记住使用 new,要么使用包装函数为您记住它。我猜测如果您到了这个问题很重要的地步,您已经耗尽了其他优化方法,比如记忆化,因此您的创建调用将会是局部的... - Shog9
66
使用 arguments.callee 来检查是否使用了 new 并不是一个好方法,因为在严格模式下 arguments.callee 是不可用的。更好的方法是使用函数名来代替。 - Sean McMillan
74
如果我拼错了标识符,我的代码就会出错。那我是不是应该不使用标识符?因为你可能会忘记在代码中写new,所以不使用它同样荒谬,就像我的例子一样。 - Thomas Eding
17
如果打开严格模式,如果忘记使用new关键字,当您尝试使用this时会出现异常:http://www.yuiblog.com/blog/2010/12/14/strict-mode-is-coming-to-town/ 这比将检查的实例添加到每个构造函数中要容易得多。 - stephenbez
显示剩余30条评论

190

我刚读了Crockford的书 "JavaScript: The Good Parts" 的部分内容。我感觉他认为曾经咬过他的一切都是有害的:

关于 switch fall through:

我从不允许 switch case 落入下一个 case。曾经在我慷慨陈词之后,发现我的代码中有一个意外的 fall through 导致了一个 bug,这让我深有体会:fall through 有时候是有用的。(第97页,ISBN 978-0-596-51774-8)

关于 ++ 和 --:

++(增量)和--(减量)运算符被认为鼓励编写过于巧妙的代码,导致病毒和其他安全隐患的出现,仅次于错误的架构。(第122页)

关于 new:

如果在调用构造函数时忘记包含 new 前缀,则 this 不会绑定到新对象。遗憾的是,this 将绑定到全局对象,因此你将破坏全局变量而不是增强你的新对象。这真的很糟糕。没有编译警告,也没有运行时警告。(第49页)

还有更多,但我希望你已经明白了。

我的回答是: 不,它不会有害。但如果你在应该使用它的时候忘记了,可能会出现一些问题。如果你在良好的环境中进行开发,你会注意到这一点。

在 ECMAScript 的第五版中,支持严格模式。在严格模式下,this 不再绑定到全局对象,而是绑定到 undefined


4
我完全同意。解决方案是:始终记录用户如何实例化你的对象。使用示例,用户很可能会剪切/复制。每种语言都有可以被误用导致不寻常/意外行为的结构/特性。这并不意味着它们有害。 - nicerobot
48
惯例是构造函数的首字母应该大写,而其他所有函数的首字母应该小写。 - some
64
我刚意识到Crockford认为WHILE循环不会有害...我不知道有多少次因为忘记增加一个变量而创建了一个无限循环... - some
5
同样适用于++、--运算符。它们用最清晰明了的语言准确地表达了我的意图。我喜欢它们!对于switch fall throughs(开关穿透),有些人可能很清楚,但我对它们感到疲倦。当它们显然更清晰(请原谅双关语,我忍不住)时,我会使用它们。 - PEZ
33
我对Crockford的回答:编程很难,我们去购物吧。天哪! - Jason Jackson
显示剩余16条评论

93

JavaScript是一种动态语言,有很多方法可以搞砸程序而其他语言则不会让你这么做。

因为害怕出错就避免使用像new这样的基本语言特性,就好像在地雷区走路之前脱下你崭新的鞋子,以防弄脏它们。

我使用一个惯例,即函数名称以小写字母开头,而实际上是类定义的“函数”以大写字母开头。 这样做的结果是一个非常引人注目的视觉提示,“语法”不正确:

var o = MyClass();  // This is clearly wrong.

此外,良好的命名习惯也很重要。毕竟,函数会执行某些操作,因此其名称中应该包含一个动词,而类则代表对象,并且是没有动词的名词和形容词。

var o = chair() // Executing chair is daft.
var o = createChair() // Makes sense.

有趣的是,Stack Overflow 的语法高亮显示如何解释上面的代码。


8
没问题,我刚刚也在想语法颜色的问题。 - BobbyShaftoe
5
有一次,我忘记打单词“function”。这个单词应该尽量避免使用。另外有一次,在多行if/then语句中我没有使用花括号。所以现在我不再使用花括号,只写单行的条件语句。 - Hal50000
1
"这显然是错误的"。为什么?对于我的类(它只是执行if (! this instanceof MyClass) return new MyClass()),我实际上更喜欢没有new的语法。为什么?因为函数构造函数更通用。省略new使得将构造函数转换为常规函数变得容易...或者反过来。添加new使代码比必要的更具体。因此不够灵活。与Java相比,建议接受List而不是ArrayList,以便调用者可以选择实现。 - Stijn de Witt
我不了解JavaScript,但对于Java而言,新的语法高亮器肯定期望遵循Java代码约定,包括标识符的大小写和等号前后的空格。 - Peter Mortensen

42

我是JavaScript的新手,也许我没有足够的经验来提供一个好的观点。但是我想分享一下我的看法关于这个“新”东西。

我从C#世界来,使用关键字“new”是如此自然,以至于工厂设计模式对我来说看起来很奇怪。

当我第一次编写JavaScript代码时,我不知道有“new”关键字,像YUI模式中的代码,并且我不需要花费太多时间就遇到了灾难。当我回顾我写的代码时,我无法跟踪特定行应该做什么。更混乱的是,当我在“干运行”代码时,我的头脑不能真正地过渡到对象实例的边界上。

然后,我发现了“new”关键字,它对我来说是“分离”的东西。使用new关键字,它创建了东西。如果没有new关键字,除非我调用的函数给出强烈的线索,否则我不会将其与创建东西混淆。

例如,对于var bar=foo();,我不知道bar可能是什么...它是一个返回值还是一个新创建的对象?但对于var bar = new foo();,我可以确定bar是一个对象。


4
同意,我认为工厂模式应该遵循像 makeFoo() 这样的命名约定。 - pluckyglen
3
+1 - “new”的存在使意图更加明确。 - belugabob
4
这个回答有点奇怪,因为在JS中几乎所有的东西都是对象。既然所有的函数都是对象,为什么你需要确切地知道一个函数是一个对象呢? - Joshua Ramirez
@JoshuaRamirez 关键不在于 typeof new foo() == "object"。而是 new 返回了一个 foo 的实例,你知道可以调用 foo.bar()foo.bang()。然而,使用 JsDoc 的 @return 可以轻松地缓解这个问题。并不是我主张过程式代码(避免使用 new 这个词)。 - Ruan Mendes
1
@JuanMendes 嗯...你的帖子让我觉得你喜欢new,因为它在你的代码库中有明确的关键字。我可以理解。这就是为什么我使用模块模式。我会有一个名为createFoo或NewFoo或MakeFoo的函数,只要它是明确的就行。在其中,我声明变量,这些变量作为闭包在对象文字内部使用,并从函数返回。该对象文字最终成为您的对象,而该函数只是一个构造函数。 - Joshua Ramirez

40

另一个需要使用new的情况是我称之为Pooh CodingWinnie-the-Pooh跟随他的肚子。我建议跟随你正在使用的语言,而不是反其道而行。

很有可能,语言的维护者会针对他们试图鼓励的习惯用法来优化语言。如果他们在语言中加入了一个新关键字,他们可能认为在创建新实例时清晰明了是有意义的。

遵循语言意图编写的代码将随着每个版本的发布而效率提高。而避免使用语言的关键结构的代码将随时间而受到影响。

这远远不止于性能问题。我无法数清有多少次我听到(或说过)“为什么他们要做那个?”当发现奇怪的代码时。通常情况下,当编写代码时,有一些“好”的理由。遵循语言的道路是您避免在未来几年内被人嘲笑的最佳保险。


3
谢谢您对Pooh Coding链接的点赞,现在我需要找借口把它融入我的谈话中... - Shog9
哈哈!我在那个特定领域有多年的经验,我可以向你保证你不会遇到任何困难。他们经常在robwiki.net上叫我小熊维尼。=) - PEZ
1
不知道你是否会看到这条评论,但是那个 Pooh Coding 的链接已经失效了。 - Calvin
谢谢提醒。上周我们将robowiki.net迁移到了一个新的维基,旧内容目前无法访问。我会加快使旧链接正常工作的速度。 - PEZ

25
我写了一篇关于如何缓解调用构造函数时不使用new关键字的问题的文章。
这篇文章主要是教学性质的,但它展示了如何创建既可以使用new也可以不使用new的构造函数,并且不需要在每个构造函数中添加样板代码来测试this不使用"new"的构造函数 以下是该技术的要点:
/**
 * Wraps the passed in constructor so it works with
 * or without the new keyword
 * @param {Function} realCtor The constructor function.
 *    Note that this is going to be wrapped
 *    and should not be used directly
 */
function ctor(realCtor) {
  // This is going to be the actual constructor
  return function wrapperCtor() {
    var obj; // The object that will be created
    if (this instanceof wrapperCtor) {
      // Called with new
      obj = this;
    } else {
      // Called without new. Create an empty object of the
      // correct type without running that constructor
      surrogateCtor.prototype = wrapperCtor.prototype;
      obj = new surrogateCtor();
    }
    // Call the real constructor function
    realCtor.apply(obj, arguments);
    return obj;
  }

  function surrogateCtor() {}
}

以下是使用方法:

// Create our point constructor
Point = ctor(function(x, y) {
  this.x = x;
  this.y = y;
});

// This is good
var pt = new Point(20, 30);
// This is OK also
var pt2 = Point(20, 30);

6
避免使用 "arguments.callee" 真是太好了! - Gregg Lind
我维护了一组类似 Crockford 风格的实用程序集合,并发现每次启动新项目时,我都需要去查找这些实现。而这正是编程中如此基础的部分:对象的实例化。所有这些工作,只为了使随意的实例化代码能够正常运行?我真的很欣赏这个包装器的创意,但它似乎只会导致不负责任的编码方式。 - Joe Coder
1
@joecoder 我提到这只是为了教学目的,我不赞同这种偏执式编码风格。然而,如果我正在编写一个类库,那么我会以对调用者透明的方式添加此功能。 - Ruan Mendes

22
不使用"new"关键字的原因很简单:完全避免了意外遗漏它所带来的问题。YUI所使用的构造模式是一个避免使用"new"关键字的例子。
var foo = function () {
    var pub = { };
    return pub;
}
var bar = foo();

或者,您也可以这样做:

function foo() { }
var bar = new foo();

但是这样做会有遗漏使用new关键字和this运算符出现FUBAR的风险。据我所知,这样做没有任何优势(除了你习惯于这样做)。
归根结底:它是为了防御。 你能使用new语句吗?可以。它是否使你的代码更加危险?是的。
如果您曾经编写过C ++,那么就类似于在删除指针后将其设置为NULL。

6
不行。使用 "new foo()" 会在返回的对象上设置一些属性,如构造函数。 - some
37
所以,为了明确一点:你不应该使用“新的”,因为你可能会忘记它?你在开玩笑吧? - Bombe
18
在其他语言中,如果忘记了“new”,会导致错误。但在JavaScript中,程序会继续运行而你可能察觉不到这个问题。查看有错误的代码也不一定能明显看出问题所在。请注意,这里不是指解决问题。 - Kent Fredric
4
@Greg:我不明白第一种技术如何允许使用原型链 - 对象字面量很好,但因为害怕而放弃原型所提供的性能和其他优势似乎有点傻。 - Shog9
5
@Bombe - 你应该使用 "new",因为你(和使用你代码的任何人)永远不会犯错? 你在开玩笑吧? - Greg Dean
显示剩余7条评论

20

我认为"new"可以使代码更加清晰易懂,而清晰度是至关重要的。了解可能存在的问题固然很好,但是通过避免清晰度来避免问题似乎不是我的选择。


15

情况一:不需要使用new,应当避免使用

var str = new String('asd');  // type: object
var str = String('asd');      // type: string

var num = new Number(12);     // type: object
var num = Number(12);         // type: number

情况二:必须使用new,否则会出现错误

new Date().getFullYear();     // correct, returns the current year, i.e. 2010
Date().getFullYear();         // invalid, returns an error

5
需要注意的是,在情况1中,仅当你打算进行类型转换(并且不知道外壳对象是否有转换方法,比如toString())时,将构造函数作为函数调用才有意义。在所有其他情况下,请使用字面量。不是 String('asd'),而只是 'asd',也不是 Number(12),而只是 12 - PointedEars
1
@PointedEars 这个规则的一个例外是 ArrayArray(5).fill(0) 显然比 [undefined, undefined, undefined, undefined, undefined].fill(0)(var arr = []).length = 5; arr.fill(0); 更易读。 - yyny
@YoYoYonnY 我的说法是针对答案中情况1进行的,该情况涉及将new与构造函数一起使用以创建与原始类型对应的对象类型。您建议的文字量_并不_等同于Array调用(尝试0 in…),其余部分是语法错误:)否则您是正确的,但需要注意Array(5)可能会引起歧义,如果您考虑不符合规范的实现(因此是一个模拟的Array.prototype.fill(…))。 - PointedEars
请注意,Date有特殊规则来区分是否使用new。它有点不同寻常。此外,所有这些构造函数都是最古老的语言规范的一部分。更新或删除所有这里的答案...因为新的语言特性更加规范。 - Sebastian Simon

12

以下是两种最有力的支持和反对使用 new 运算符的简要概述:

反对 new 的观点

  1. 如果将设计成使用 new 运算符实例化为对象的函数错误地作为普通函数调用,将会产生灾难性后果。在这种情况下,函数的代码将在调用函数的作用域中执行,而不是在局部对象的作用域中执行,从而覆盖全局变量和属性,造成灾难性后果。
  2. 最后,编写 function Func(),然后调用 Func.prototype 并向其中添加内容,以便可以调用 new Func() 构建对象,这个过程对一些程序员来说可能显得丑陋,他们更想使用另一种对象继承风格,出于架构和风格原因。

有关此论点的更多信息,请查看道格拉斯·克罗克福德的精炼著作 JavaScript: The Good Parts。事实上,无论如何都应该查看。

支持 new 的观点

  1. 使用 new 运算符与 原型分配 结合使用是快速的。
  2. 如果始终在构造函数中包含一些代码来检查它们是否被正确调用,并在不正确调用的情况下按预期方式处理调用,则可以轻松地防止意外运行构造函数的代码在全局命名空间中执行。

请参考John Resig的文章,了解这种技术的简单说明以及他所提倡的继承模型的更深入说明。


1
反对观点的最新进展:#1可以通过使用“'use strict';”模式(或您链接中的方法)来减轻;#2在ES6中有语法糖,但行为与以前相同。 - ninMonkey

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