理解 JavaScript 原型

2025-06-10

理解 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')
Enter fullscreen mode Exit fullscreen mode

如果您使用console.log此数组,则看不到任何方法。但是,您可以使用诸如concatslicefilter和 之类的方法map

数组不包含方法。

为什么?

因为这些方法位于数组的原型中。您可以展开__proto__对象(Chrome Devtools)或<prototype>对象(Firefox Devtools),然后会看到一个方法列表。


Array.prototype 包含方法


Firefox 将原型记录为prototype

:::注意:Chrome 和Firefox 中
指向 Prototype 对象。只是在不同浏览器中的写法不同。 :::__proto__<prototype>

当你使用 时,JavaScript 会在对象本身中map查找。如果未找到,JavaScript 会尝试查找原型。如果找到原型,JavaScript 会继续在该原型中搜索mapmapmap

因此, Prototype 的正确定义是:实例在尝试查找属性时可以访问的对象。

原型链

以下是访问属性时 JavaScript 所做的事情:

步骤 1:JavaScript 检查对象内部是否存在该属性。如果存在,JavaScript 会立即使用该属性。

步骤 2:如果该属性不在对象内部,JavaScript 会检查是否存在可用的原型。如果存在,则重复步骤 1(并检查该属性是否在原型内部)。

步骤 3:如果没有剩余的原型,并且 JavaScript 找不到该属性,它将执行以下操作:

  • 返回undefined(如果您尝试访问某个属性)。
  • 引发错误(如果您尝试调用某个方法)。

以图表形式来看,该过程如下:

原型链。

原型链示例

假设我们有一个Human类。我们还有一个Developer继承自的子类HumanHuman有一个sayHello方法,Developers还有一个code方法。

这是代码Human

class Human {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastname = lastName
  }

  sayHello () {
    console.log(`Hi, I'm ${this.firstName}`)
  }
}
Enter fullscreen mode Exit fullscreen mode

:::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}`)
}
Enter fullscreen mode Exit fullscreen mode

:::

这是的代码Developer

class Developer extends Human {
  code (thing) {
    console.log(`${this.firstName} coded ${thing}`)
  }
}
Enter fullscreen mode Exit fullscreen mode

实例Developer可以同时使用code和,sayHello因为这些方法位于实例的原型链中。

const zell = new Developer('Zell', 'Liew')
zell.sayHello() // Hi, I'm Zell
zell.code('website') // Zell coded website
Enter fullscreen mode Exit fullscreen mode

如果您是console.log实例,您可以看到原型链中的方法。

原型链中的原始“code” endraw 和原始“sayHello” endraw。

原型委托/原型继承

原型委托和原型继承意思相同。

他们只是说我们使用原型系统——我们将属性和方法放入prototype对象中。

我们应该使用原型委托吗?

由于 JavaScript 是基于原型的语言,因此我们应该使用原型委托。对吗?

并不真地。

我认为这取决于你如何编写面向对象编程。如果你使用类,那么使用原型是合理的,因为它们更方便。

class Blueprint {
  method1 () {/* ... */}
  method2 () {/* ... */}
  method3 () {/* ... */}
}
Enter fullscreen mode Exit fullscreen mode

但是如果使用工厂函数,则不使用原型是有意义的。

function Blueprint {
  return {
      method1 () {/* ... */}
      method2 () {/* ... */}
      method3 () {/* ... */}
  }
}
Enter fullscreen mode Exit fullscreen mode

再次阅读本文,了解编写面向对象编程的四种不同方法。

性能影响

两种方法之间的性能差异并不大——除非你的应用需要数百万次操作。在本节中,我将分享一些实验来证明这一点。

设置

我们可以performance.now在运行任何操作之前记录时间戳。运行操作后,我们将performance.now再次记录时间戳。

然后我们将获得时间戳的差异来测量操作花费的时间。

const start = performance.now()
// Do stuff
const end = performance.now()

const elapsed = end - start
console.log(elapsed)
Enter fullscreen mode Exit fullscreen mode

我使用了一个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)
}
Enter fullscreen mode Exit fullscreen mode

注意:您可以在这篇文章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)
Enter fullscreen mode Exit fullscreen mode

平均结果总结于下表中:

测试 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)
Enter fullscreen mode Exit fullscreen mode

平均结果总结如下表:

测试 1,000,000 次操作 10,000,000 次操作
班级 5毫秒 18毫秒
工厂 6毫秒 18毫秒

结论:使用 Class 函数还是 Factory 函数都没关系。即使运行超过 100 万次操作,也不会有什么区别。

关于性能测试的结论

您可以使用类或工厂函数。您可以选择使用原型,也可以选择不使用。这完全取决于您。

无需担心性能。


感谢阅读。本文最初发布在我的博客上。如果您想阅读更多文章来帮助您成为更优秀的前端开发人员,请订阅我的新闻通讯。

鏂囩珷鏉ユ簮锛�https://dev.to/zellwk/understanding-javascript-prototype-5187
PREV
如何在 10 分钟内用 100 行代码构建可扩展的 SaaS 后端 🚀 构建可扩展的 SaaS 系统很难
NEXT
以不同的方式设置悬停、焦点和活动状态的样式