理解 JavaScript 原型
JavaScript 被认为是一种基于原型的语言。所以“原型”一定是一个重要的概念,对吧?
今天我将解释什么是原型,您需要了解什么以及如何有效地使用原型。
什么是原型?
首先,不要被“原型”这个词误导。JavaScript 中的“原型”与英语中的“prototype”不同。它不是指快速拼凑起来的产品初始版本。
相反,JavaScript 中的原型只是一个毫无意义的词。我们可以用橙子代替原型,它的意思是一样的。
比如苹果。在苹果电脑流行之前,你可能会觉得苹果是红色的水果。“苹果电脑”中的“苹果”最初并没有什么含义,但现在有了。
在 JavaScript 中,原型指的是一个系统。这个系统允许你定义对象的属性,并通过对象的实例来访问这些属性。
:::注意:
Prototype 与面向对象编程密切相关。如果你不理解面向对象编程的含义,那么理解它就毫无意义。
我建议您在继续学习之前先熟悉一下面向对象编程的入门系列
。 :::
例如,Array
是数组实例的蓝图。您可以使用[]
或创建一个数组实例new Array()
。
const array = ['one', 'two', 'three']
console.log(array)
// Same result as above
const array = new Array('one', 'two', 'three')
如果您使用console.log
此数组,则看不到任何方法。但是,您可以使用诸如concat
、slice
、filter
和 之类的方法map
!
为什么?
因为这些方法位于数组的原型中。您可以展开__proto__
对象(Chrome Devtools)或<prototype>
对象(Firefox Devtools),然后会看到一个方法列表。
prototype
:::注意:Chrome 和Firefox 中
都指向 Prototype 对象。只是在不同浏览器中的写法不同。 :::__proto__
<prototype>
当你使用 时,JavaScript 会在对象本身中map
查找。如果未找到,JavaScript 会尝试查找原型。如果找到原型,JavaScript 会继续在该原型中搜索。map
map
map
因此, Prototype 的正确定义是:实例在尝试查找属性时可以访问的对象。
原型链
以下是访问属性时 JavaScript 所做的事情:
步骤 1:JavaScript 检查对象内部是否存在该属性。如果存在,JavaScript 会立即使用该属性。
步骤 2:如果该属性不在对象内部,JavaScript 会检查是否存在可用的原型。如果存在,则重复步骤 1(并检查该属性是否在原型内部)。
步骤 3:如果没有剩余的原型,并且 JavaScript 找不到该属性,它将执行以下操作:
- 返回
undefined
(如果您尝试访问某个属性)。 - 引发错误(如果您尝试调用某个方法)。
以图表形式来看,该过程如下:

原型链示例
假设我们有一个Human
类。我们还有一个Developer
继承自的子类Human
。Human
有一个sayHello
方法,Developers
还有一个code
方法。
这是代码Human
class Human {
constructor(firstName, lastName) {
this.firstName = firstName
this.lastname = lastName
}
sayHello () {
console.log(`Hi, I'm ${this.firstName}`)
}
}
:::note Human
(及Developer
下文)可以用构造函数来编写。如果使用构造函数,代码prototype
会更清晰,但创建子类会更困难。这就是为什么我使用类来举例。(请参阅这篇文章,了解使用面向对象编程的四种不同方法)。
Human
如果您使用构造函数,则可以这样写。
function Human (firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
Human.prototype.sayHello = function () {
console.log(`Hi, I'm ${this.firstName}`)
}
:::
这是的代码Developer
。
class Developer extends Human {
code (thing) {
console.log(`${this.firstName} coded ${thing}`)
}
}
实例Developer
可以同时使用code
和,sayHello
因为这些方法位于实例的原型链中。
const zell = new Developer('Zell', 'Liew')
zell.sayHello() // Hi, I'm Zell
zell.code('website') // Zell coded website
如果您是console.log
实例,您可以看到原型链中的方法。
原型委托/原型继承
原型委托和原型继承意思相同。
他们只是说我们使用原型系统——我们将属性和方法放入prototype
对象中。
我们应该使用原型委托吗?
由于 JavaScript 是基于原型的语言,因此我们应该使用原型委托。对吗?
并不真地。
我认为这取决于你如何编写面向对象编程。如果你使用类,那么使用原型是合理的,因为它们更方便。
class Blueprint {
method1 () {/* ... */}
method2 () {/* ... */}
method3 () {/* ... */}
}
但是如果使用工厂函数,则不使用原型是有意义的。
function Blueprint {
return {
method1 () {/* ... */}
method2 () {/* ... */}
method3 () {/* ... */}
}
}
再次阅读本文,了解编写面向对象编程的四种不同方法。
性能影响
两种方法之间的性能差异并不大——除非你的应用需要数百万次操作。在本节中,我将分享一些实验来证明这一点。
设置
我们可以performance.now
在运行任何操作之前记录时间戳。运行操作后,我们将performance.now
再次记录时间戳。
然后我们将获得时间戳的差异来测量操作花费的时间。
const start = performance.now()
// Do stuff
const end = performance.now()
const elapsed = end - start
console.log(elapsed)
我使用了一个perf
函数来帮助我的测试:
function perf (message, callback, loops = 1) {
const startTime = performance.now()
for (let index = 0; index <= loops; index++) {
callback()
}
const elapsed = performance.now() - startTime
console.log(message + ':', elapsed)
}
注意:您可以在这篇文章performance.now
中了解更多信息。
实验 #1:使用原型 vs 不使用原型
首先,我测试了通过原型访问一个方法与访问对象本身中的另一个方法需要多长时间。
代码如下:
class Blueprint () {
constructor () {
this.inObject = function () { return 1 + 1 }
}
inPrototype () { return 1 + 1 }
}
const count = 1000000
const instance = new Blueprint()
perf('In Object', _ => { instance.inObject() }, count)
perf('In Prototype', _ => { instance.inPrototype() }, count)
平均结果总结于下表中:
测试 | 1,000,000 次操作 | 10,000,000 次操作 |
---|---|---|
在对象中 | 3毫秒 | 15毫秒 |
在原型中 | 2毫秒 | 12毫秒 |
注:结果来自 Firefox 的 Devtools。阅读此文可以了解为什么我只使用 Firefox 进行基准测试。
结论:使用与否原型都无所谓。除非运行超过 100 万次操作,否则不会有任何区别。
实验 #2:类 vs 工厂函数
我必须运行这个测试,因为我建议在使用类时使用原型,而在使用工厂函数时不要使用原型。
我需要测试创建工厂函数是否比创建类慢得多。
这是代码。
// Class blueprint
class HumanClass {
constructor (firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
sayHello () {
console.lg(`Hi, I'm ${this.firstName}}`)
}
}
// Factory blueprint
function HumanFactory (firstName, lastName) {
return {
firstName,
lastName,
sayHello () {
console.log(`Hi, I'm ${this.firstName}}`)
}
}
}
// Tests
const count = 1000000
perf('Class', _ => { new HumanClass('Zell', 'Liew') }, count)
perf('Factory', _ => { HumanFactory('Zell', 'Liew') }, count)
平均结果总结如下表:
测试 | 1,000,000 次操作 | 10,000,000 次操作 |
---|---|---|
班级 | 5毫秒 | 18毫秒 |
工厂 | 6毫秒 | 18毫秒 |
结论:使用 Class 函数还是 Factory 函数都没关系。即使运行超过 100 万次操作,也不会有什么区别。
关于性能测试的结论
您可以使用类或工厂函数。您可以选择使用原型,也可以选择不使用。这完全取决于您。
无需担心性能。
感谢阅读。本文最初发布在我的博客上。如果您想阅读更多文章来帮助您成为更优秀的前端开发人员,请订阅我的新闻通讯。
鏂囩珷鏉ユ簮锛�https://dev.to/zellwk/understanding-javascript-prototype-5187