原文:https://hackernoon.com/javascript-for-beginners-the-new-operator-cee35beb669e
四条规则
理解 new
运算符的最简单的方式就是去理解它做什么。当我们用 new
时,发生了四件事:
- 创建了一个新的空对象。
- 把
this
绑定到新创建的对象。 - 给新创建的对象添加了一个名为
__proto__
的属性,该属性指向构造器函数的原型对象。 - 给函数末尾添加一条
return this
语句,这样被创建的对象就被从函数返回。
等等,这是啥玩意啊?
如果还不明白这些规则,没关系。我们从简单的开始。创建一个名为 Student
的构造器函数。该函数带有两个参数,name
和 age
。然后,将这些属性设置在 this
的值上面。
1 | function Student(name, age) { |
不错。下面我们用 new
运算符调用我们的构造器函数。我们打算传递进两个参数:'John'
和 26
。
1 | var first = new Student('John', 26); |
那么,当执行上述代码时,会发生什么呢?
- 创建了一个新对象 —
first
对象。 this
被绑定到first
对象。于是引用this
就会指向first
。__proto__
被添加到first
上。现在first.__proto__
会指向Student.prototype
。- 一切都做完后,崭新的
first
对象被返回给新的first
变量。
现在我们可以运行几个简单的 console.log
语句来检测一下它是否起作用了:
1 | console.log(first.name); |
棒极了。下面我们更深入研究一下 new 关键字的 __proto__
部分。
原型
每个 JavaScript 对象都有一个原型(prototype)。JavaScript 中所有对象都从它们的原型中继承方法和属性。
下面我们用示例验证一下。打开 Chrome 开发者控制台(Windows: Ctrl + Shift + J)(Mac: Cmd + Option + J),键入本文早前的 Student
函数:
1 | function Student(name, age) { |
为证实每个对象都有一个原型,现在我们键入:
1 | Student.prototype; |
酷!返回了一个对象。现在我们试着创建一个新学生:
1 | var second = new Student('Jeff', 50); |
我们用 Student
构造器函数来创建第二个名为 Jeff
的学生。并且,因为我们使用 new
运算符,那么 __proto__
也应该已经被添加到 second
对象上。它应该指向父构造器。我们来试试看它们是否相等:
1 | second.__proto__ === Student.prototype; |
最后,Student.prototype.constructor
应该指向 Student
构造器函数:
1 | Student.prototype.constructor; |
这很快就变得复杂了。我们来看看能否用一个图像帮助表示发生了什么:
如上所见,Student
构造器函数(以及所有其它构造器函数)有一个名为 .prototype
的属性。这个原型上有一个对象叫 .constrcutor
,往回指向构造器函数。这是一个漂亮的小回环。然后,在我们使用 new
运算符创建新对象时,每个对象都有 .__proto__
属性将新对象与 Student.prototype
连接起来。
那么,为什么这很重要呢?
因为继承,所以它重要。原型对象是在用该构造器函数创建的所有对象之间共享的。也就是说可以给原型添加所有对象都可以用的函数和属性。
在上例中,我们只创建了两个 Student
对象,但是如果不是两个对象,而是两万个对象呢?把共享的函数放在原型上,而不是在每个学生对象上,一下子就省了大把力气。
下面我们看一个例子,让这种理念落地。在控制台中,添加如下行:
1 | Student.prototype.sayInfo = function(){ |
我们在这里所做的,还是给 Student
原型添加一个函数 - 每个我们要创建的或者已经创建的任何学生都可以访问这个崭新的 .sayInfo
函数了!我们来检验一下:
1 | second.sayInfo(); |
加一个新学生,再试试:
1 | var third = new Student('Tracy', 15); |
起作用了!而且它能起作用是因为继承。使用 JavaScript 对象时,对象会先找找它本身是否有我们调用的属性。如果没有,就会向上,对它的原型说 ‘喂,你有没有这个属性啊?’。这种同样的模式一直持续到要么找到该属性,要么到达全局对象上原型链的末尾。
过去我们能使用 .toString()
这种函数,也是因为有了继承!想一想,我们从来就没有写过 toString()
方法,却依然可以很好地用它。这是因为该方法,以及其它 JS 内置的方法都定义在 Object 对象的原型上。我们创建的每个对象最终都会委托给 Object 的原型。当然,我们可以像下面这样重写这些方法:
1 | var name = { |
对象会在转到原型之前,先检查它自己是否有这个方法。既然我们确实有这个方法,那就执行它好了,不需要继承。不过,这并不是个好主意。最好保留全局方法,把自己的函数改个名字。
总结
对于开发新手来说,这可能是一个很难理解的概念,不过一旦你掌握了,就可以编写更好更干净的代码。通过使用原型,我们可以快速而高效地在成百上千个对象之间共享相同的代码。跟我所有其它文章一样,本文的理念是让你掌握能让你起步的基础知识,然后自己去继续学习更多东西。