在JavaScript中,“new”关键字是什么?

1922

JavaScript中的new关键字在初次遇到时可能会让人感到困惑,因为人们往往认为JavaScript不是面向对象编程语言。

  • 它是什么?
  • 它解决了哪些问题?
  • 什么情况下使用它是适当的,什么情况下不是?

13
好的,我会尽力进行翻译。以下是需要翻译的内容:Also, related thread - https://dev59.com/MHRC5IYBdhLWcg3wMd9S - Chetan S
1
先阅读这些示例,https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new - MartianMartian
17个回答

2298

它有以下五个作用:

  1. 创建一个新对象,该对象的类型为 object
  2. 将这个新对象的内部、不可访问的 [[prototype]] 属性(即 __proto__)设置为构造函数的外部、可访问的 prototype 对象(每个函数对象都自动具有 prototype 属性)。
  3. 使 this 变量指向新创建的对象。
  4. 使用新创建的对象执行构造函数,每当提到 this 时都会使用该对象。
  5. 返回新创建的对象,除非构造函数返回一个非 null 的对象引用。在这种情况下,返回该对象引用。

注意:此处的 构造函数 指的是在 new 关键字后面的函数,如下所示:

new ConstructorFunction(arg1, arg2)

完成这一步,如果请求新对象的未定义属性,则脚本将检查该对象的[[prototype]]对象是否有该属性。这就是如何在JavaScript中获得类似传统类继承的方法。
其中最困难的部分是第2点。每个对象(包括函数)都有一个名为[[prototype]]的内部属性。它只能在对象创建时使用newObject.create或基于文字设置(函数默认为Function.prototype,数字默认为Number.prototype等)。只能使用Object.getPrototypeOf(someObject)来读取此值。没有其他获取或设置此值的方法。
除了隐藏的[[prototype]]属性外,函数还具有称为原型的属性,您可以访问并修改它,以为您创建的对象提供继承属性和方法。
以下是一个例子:
ObjMaker = function() { this.a = 'first'; };
// `ObjMaker` is just a function, there's nothing special about it
// that makes it a constructor.

ObjMaker.prototype.b = 'second';
// like all functions, ObjMaker has an accessible `prototype` property that 
// we can alter. I just added a property called 'b' to it. Like 
// all objects, ObjMaker also has an inaccessible `[[prototype]]` property
// that we can't do anything with

obj1 = new ObjMaker();
// 3 things just happened.
// A new, empty object was created called `obj1`.  At first `obj1` 
// was just `{}`. The `[[prototype]]` property of `obj1` was then set to the current
// object value of the `ObjMaker.prototype` (if `ObjMaker.prototype` is later
// assigned a new object value, `obj1`'s `[[prototype]]` will not change, but you
// can alter the properties of `ObjMaker.prototype` to add to both the
// `prototype` and `[[prototype]]`). The `ObjMaker` function was executed, with
// `obj1` in place of `this`... so `obj1.a` was set to 'first'.

obj1.a;
// returns 'first'
obj1.b;
// `obj1` doesn't have a property called 'b', so JavaScript checks 
// its `[[prototype]]`. Its `[[prototype]]` is the same as `ObjMaker.prototype`
// `ObjMaker.prototype` has a property called 'b' with value 'second'
// returns 'second'

这有点像类继承,因为现在使用 new ObjMaker() 创建的任何对象都将具有继承了 'b' 属性的外观。

如果你想要类似于子类的东西,那么可以这样做:

SubObjMaker = function () {};
SubObjMaker.prototype = new ObjMaker(); // note: this pattern is deprecated!
// Because we used 'new', the [[prototype]] property of SubObjMaker.prototype
// is now set to the object value of ObjMaker.prototype.
// The modern way to do this is with Object.create(), which was added in ECMAScript 5:
// SubObjMaker.prototype = Object.create(ObjMaker.prototype);

SubObjMaker.prototype.c = 'third';  
obj2 = new SubObjMaker();
// [[prototype]] property of obj2 is now set to SubObjMaker.prototype
// Remember that the [[prototype]] property of SubObjMaker.prototype
// is ObjMaker.prototype. So now obj2 has a prototype chain!
// obj2 ---> SubObjMaker.prototype ---> ObjMaker.prototype

obj2.c;
// returns 'third', from SubObjMaker.prototype

obj2.b;
// returns 'second', from ObjMaker.prototype

obj2.a;
// returns 'first', from SubObjMaker.prototype, because SubObjMaker.prototype 
// was created with the ObjMaker function, which assigned a for us

在找到这个页面之前,我读了很多关于这个主题的垃圾文章。在这个页面上,用漂亮的图解很好地解释了这个问题。


53
想要补充一下:事实上,有一种方法可以通过 __proto__ 访问内部 [[prototype]]。但是这种方法并不标准,并且只受到相对较新的浏览器支持(并非所有浏览器都支持)。即将推出一种标准化的方法,即 Object.getPrototypeOf(obj),但它是 Ecmascript3.1 规范,本身也仅在新浏览器上受支持。通常建议不要使用该属性,因为其中的东西会变得非常复杂。 - Blub
12
如果将ObjMaker定义为返回值的函数,会有什么不同? - Jim Blackler
15
new的存在是为了让你无需编写工厂方法来构造/复制函数/对象。它的意思是,“复制这个,使其与其父类完全一样;高效且正确地执行此操作,并存储继承信息,该信息仅对我(JS)内部可访问”。为此,它修改了新对象的原本不可访问的内部prototype,将继承成员不透明地封装起来,模拟经典的OO继承链(这些链是不能在运行时修改的)。你可以在没有new的情况下模拟这种效果,但继承会在运行时可修改。好还是坏?由你决定。 - Engineer
12
补充一点:使用new关键字调用构造函数时,会自动返回创建的对象;无需在构造函数内显式地进行返回。 - charlie roberts
7
有一张提示信息写着“注意,这个模式已经过时了!”。设置类的原型的正确、最新的模式是什么? - Tom Pažourek
显示剩余25条评论

454

假设你有以下函数:

var Foo = function(){
  this.A = 1;
  this.B = 2;
};

如果您像这样单独调用此函数:

Foo();

执行这个函数将向window对象添加两个属性(AB)。它添加到window对象中,因为当像这样执行函数时,window是调用函数的对象,在JavaScript中,函数中的this指向调用该函数的对象。

现在,通过使用new调用它:

var bar = new Foo();
当你在函数调用中添加new时,会创建一个新的对象(就像var bar = new Object()),并且函数内的this指向刚刚创建的新Object,而不是调用该函数的对象。因此,bar现在是具有属性AB的对象。任何函数都可以是构造函数;只是有时可能没有意义。

8
根据执行环境而定。在我的情况下(Qt脚本),它只是一个全局对象。 - MaksymB
4
这会导致更多的内存使用吗? - Jürgen Paul
3
因为窗口是调用函数的对象 - 必须更改为:因为窗口是包含该函数的对象。 - Dávid Horváth
3
在Web浏览器中,非方法函数会隐式地成为window的方法,即使在闭包中,甚至是匿名函数。然而,在示例中,它只是对窗口的简单方法调用:Foo(); => [默认上下文] .Foo(); => window.Foo();。在这个表达式中,window上下文(不仅是调用者,这并不重要)。 - Dávid Horváth
2
基本上是的。然而在ECMA 6和7中,事情更加复杂(例如lambda、类等)。 - Dávid Horváth
显示剩余4条评论

178

除了Daniel Howard的答案,这里是new的作用(或者至少看起来是这样):

function New(func) {
    var res = {};
    if (func.prototype !== null) {
        res.__proto__ = func.prototype;
    }
    var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
    if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
        return ret;
    }
    return res;
}

虽然

var obj = New(A, 1, 2);

等同于

var obj = new A(1, 2);

78
我发现 JavaScript 比英语更容易理解 :v - damphat
1
非常好的答案。我有一个小问题:func.prototype 怎么可能是 null?您能详细解释一下吗? - Tom Pažourek
7
你可以通过简单地编写A.prototype = null;来覆盖原型属性。在这种情况下,new A()将得到一个对象,其内部原型指向Object对象:http://jsfiddle.net/Mk42Z/ - basilikum
3
typeof检查可能不准确,因为主机对象可能会产生与“object”或“function”不同的结果。为了测试某个对象是否是对象,我更喜欢使用Object(ret) === ret - Oriol
3
谢谢您的留言,@Oriol。您所说的是正确的,任何实际测试都应以更强大的方式进行。然而,我认为对于这个概念性答案,typeof测试只是使我们更容易理解在幕后发生了什么。 - basilikum

139

为了初学者更好地理解

在浏览器控制台中尝试以下代码。

function Foo() {
    return this;
}

var a = Foo();       // Returns the 'window' object
var b = new Foo();   // Returns an empty object of foo

a instanceof Window;  // True
a instanceof Foo;     // False

b instanceof Window;  // False
b instanceof Foo;     // True

现在您可以阅读社区 Wiki 回答 :)


8
好的回答。同时,省略 return this; 也会产生相同的输出结果。 - Nelu
return this;不改变行为的解释是,运算符new在创建新对象和执行构造函数方面是神奇的,如果构造函数的返回值是undefined(没有返回语句或只有return;),或者是null(特殊情况:return null;),则新创建的对象(构造函数内部的this)将被用作new运算符的值,否则new的值是返回的值。我不知道这种行为的原理,但我猜测是“由于历史原因”。 - Mikko Rantalainen

41

所以它可能不是用来创建对象实例的。

它就是用来创建对象实例的。你可以像这样定义一个函数构造器:

function Person(name) {
    this.name = name;
}

var john = new Person('John');

然而 ECMAScript 的额外好处在于您可以使用 .prototype 属性进行扩展,因此我们可以做出这样的事情...

Person.prototype.getName = function() { return this.name; }

由于它们可以访问原型链,从该构造函数创建的所有对象现在都将具有getName


8
函数构造器就像类一样使用,虽然没有 "class" 关键字,但你几乎可以实现相同的功能。 - meder omuraliev
2
有一种类似于class关键字的东西 - class关键字是为将来使用而保留的。 - Greg
12
顺便提一下,这就是为什么你使用.className而不是.class来设置CSS类。 - Greg

30
JavaScript是一种面向对象的编程语言,它主要用于创建实例。它基于原型(prototype-based)而非类(class-based),但这并不意味着它不是面向对象的。

9
我想说JavaScript似乎比那些基于类的语言更加面向对象。在JavaScript中,你所写的一切都会立即成为一个对象,但在基于类的语言中,你首先编写声明,然后才创建特定实例(对象)的类。而JavaScript原型似乎模糊地提醒了所有基于类的语言的虚拟表(VTABLE)的内容。 - JustAMartin

27

概述:

new 关键字用于 JavaScript 中从构造函数创建对象。在调用构造函数之前,必须将 new 关键字放置在其前面,并执行以下操作:

  1. 创建一个新对象
  2. 将该对象的原型设置为构造函数的 prototype 属性
  3. this 关键字绑定到新创建的对象并执行构造函数
  4. 返回新创建的对象

示例:

function Dog (age) {
  this.age = age;
}

const doggie = new Dog(12);

console.log(doggie);
console.log(Object.getPrototypeOf(doggie) === Dog.prototype) // true

具体发生了什么:

  1. const doggie表示:我们需要内存来声明一个变量。
  2. 赋值操作符=表示:我们将使用=后面的表达式来初始化这个变量。
  3. 表达式是new Dog(12)。JavaScript引擎看到new关键字,创建一个新对象并将原型设置为Dog.prototype
  4. 构造函数使用this值设置为新对象来执行。在这一步中,年龄被分配给新创建的小狗对象。
  5. 新创建的对象被返回并赋值给变量doggie。

21
请看下面我对第 III 情况的观察。它涉及当你在一个函数中有一个显式的返回语句,而你正在创建一个函数时发生了什么。请查看以下情况:
第一种情况:
var Foo = function(){
  this.A = 1;
  this.B = 2;
};
console.log(Foo()); //prints undefined
console.log(window.A); //prints 1

上面是调用变量 Foo 指向的匿名函数的简单示例。当您调用此函数时,它返回 undefined 。由于没有明确的返回语句,JavaScript解释器会强制在函数末尾插入 return undefined; 语句。因此,上面的代码示例等同于:
var Foo = function(){
  this.A = 1;
  this.B = 2;
  return undefined;
};
console.log(Foo()); //prints undefined
console.log(window.A); //prints 1

当调用Foo函数时,window是默认的调用对象(上下文的this),会获得新的AB属性。 案例二:
var Foo = function(){
  this.A = 1;
  this.B = 2;
};
var bar = new Foo();
console.log(bar()); //illegal isn't pointing to a function but an object
console.log(bar.A); //prints 1

在这里,JavaScript解释器看到了new关键字,创建了一个新的对象作为由Foo指向的匿名函数的调用对象(上下文的this)。在这种情况下,AB成为新创建的对象的属性(取代了window对象)。由于没有明确的返回语句,JavaScript解释器强制插入一个返回语句来返回由于使用new关键字而创建的新对象。
var Foo = function(){
  this.A = 1;
  this.B = 2;
  return {C:20,D:30};
};
var bar = new Foo();
console.log(bar.C);//prints 20
console.log(bar.A); //prints undefined. bar is not pointing to the object which got created due to new keyword.

再次强调,JavaScript解释器看到new关键字时,会创建一个新的对象作为由Foo指向的匿名函数的调用对象(上下文的this)。同样,AB成为新创建对象的属性。但是这一次你有了明确的返回语句,所以JavaScript解释器不会自作主张。
需要注意的是,在第三种情况中,由于new关键字创建的对象丢失了。实际上,bar指向一个完全不同的对象,而不是JavaScript解释器由new关键字创建的对象。
引用David Flanagan的话(来自《JavaScript权威指南》第6版,第4章,第62页):
当对象创建表达式被评估时,JavaScript首先创建一个新的空对象,就像由对象初始化程序{}创建的对象一样。接下来,它使用指定的参数调用指定的函数,将新对象作为this关键字的值传递。函数可以使用this来初始化新创建的对象的属性。为用作构造函数编写的函数不返回值,对象创建表达式的值是新创建并初始化的对象。如果构造函数确实返回对象值,则该值成为对象创建表达式的值,并且新创建的对象被丢弃。 额外信息: 以上案例中代码片段中使用的函数在JavaScript世界中具有特殊名称,如下:
案例 # 名称
案例 I 构造函数
案例 II 构造函数
案例 III 工厂函数

您可以在此讨论串中了解构造函数和工厂函数的区别。

案例 III 中的代码异味 - 不应该使用 new 关键字来调用工厂函数,但我在上面的代码片段中故意这样做只是为了解释概念。


2
你的第三个案例是一个很棒的观察。 - appu

13
JavaScript是一种动态编程语言,支持面向对象编程范例,用于创建对象的新实例。
类对于对象来说并非必需品。JavaScript是一种基于原型prototype-based的语言。

10
new关键字会改变函数运行时的上下文,并返回一个指向该上下文的指针。
当不使用new关键字时,Vehicle()函数运行的上下文与调用Vehicle函数的上下文相同。关键字this将引用同一上下文。当使用new Vehicle()时,将创建一个新的上下文,因此函数内部的关键字this将引用新的上下文。你会得到新创建的上下文。

这是一个非常有洞察力的回答,就范围而言非常好。对回答来说是个很棒的补充。 - appu

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