为什么 JavaScript 是基于原型的 OOP

2025-06-07

为什么 JavaScript 是基于原型的 OOP

在面向对象编程中,我们可以区分两种类型的语言:基于类的语言和基于原型的语言。

基于类的语言以类为中心,作为创建对象的蓝图。然而,基于原型的语言明确排除了类,对象使用原型从其他对象继承。

与 C++ 和 Java 等基于类的语言相比,JavaScript 被认为是一种基于原型的面向对象编程语言。

本文深入探讨了 JavaScript 中基于原型的面向对象编程。

阅读完本文后,您应该能够了解:

  • 基于类和基于原型的 OOP 之间的区别

  • 继承在基于类的编程中如何工作

  • 继承在基于原型的编程中如何运作

  • 原型和__proto__属性

  • 如何实现原型继承

让我们开始吧

介绍

在 Netscape 工作期间,Brendan Eich 开发了 JavaScript 作为脚本语言,用于该公司的旗舰网络浏览器 Netscape Navigator。

Netscape 最初与 Sun 合作,将 Java 编程语言融入 Navigator。然而,该语言存在一些复杂性(例如 JVM 消耗大量资源,启动 Java 小程序需要几秒钟等),对于需要为网页添加交互性的脚本编写者来说,并不具有吸引力。

为了纠正这个问题,Brendan Eich 开发了 JavaScript 作为针对脚本编写者和设计师的实用语言。

Netscape 的管理层指示 Brendan Eich 不要在 JavaScript 中包含类和模块等高级功能,以防止它看起来与 Java 竞争。

由于 Brendan 不允许使用类(类提供了一种创建对象以及继承属性和方法的便捷方法),他必须找到一种方法来在 JavaScript 中仍然保持面向对象模型。他选择了原型,并实现了原型继承。

Javascript 在设计时并没有采用 C++ 或 Java 所使用的面向对象模型,因为创建者没有时间复制任何经典的 OO 模型,而且 Sun 不希望 JS 中包含类,因为 JS 被认为只是 Java 的“配角”。

因此,JavaScript 是一种基于原型的面向对象语言,使用原型继承而不是经典继承。

简而言之,JavaScript 没有类,它通过克隆原型对象来创建新的对象。类只是原型的语法糖。

基于类的编程语言

在基于类的编程中,对象是基于类构建的。对象的属性和行为由class作为创建具体对象的蓝图的类定义。

打个比方,如果你要开发一款手机,你首先会设计一个蓝图,其中包含构建手机所需的所有结构和行为。例如外观、操作系统、电路板等等,然后根据该蓝图构建手机。

这个蓝图被称为class。它是一个用于创建对象的可扩展程序代码模板。object是现实世界中的实体(例如,一部手机)。

蓝图(class)可以扩展以构建其他类型的移动电话设备(基本型手机、功能型手机和智能手机)

简单地说,在基于类的编程中,必须基于类明确地创建对象。

基于原型的编程语言

基于原型的编程是一种面向对象编程风格,其中类没有明确定义。继承是通过重用作为原型的现有对象来实现的(对象通过原型属性从其他对象继承)。

它允许创建一个对象而无需先定义其类。

考虑在基于原型的编程中开发移动电话设备的相同类比。

首先,您需要创建对象(无需蓝图或类)。组装外部设备、操作系统、电路板等,然后启动一部手机。之后,我们可以克隆最初的手机,并根据所需的移动设备(例如,具有更大屏幕、触控功能、传感器等的智能手机)扩展其结构和行为。

使用这种方法,每部手机都可以从通用手机对象克隆出来。

在基于原型的 OOP 世界中,您可以构建一个对象并快速创建它的“克隆”,而基于类的方法则需要一个类来创建一个对象,然后扩展子类以从父类继承属性和方法。

理解基于原型的 OOP 概念

在基于类的 OOP 中,class是创建对象的蓝图。 Achild class将继承 中定义的属性和方法parent class,而 distinctobjects则使用 函数创建constructor

然而,在基于原型的 OOP 中,我们不需要定义 来class创建object。对象是直接创建的。

因为我们没有定义类,所以我们需要一种方法,使其他对象能够从初始对象继承属性和方法。

每个对象都有一个称为原型的隐藏属性,使我们能够实现这一点。

我们将在下一节进一步探讨这个问题。

什么是原型?

原型是一个指向另一个对象的内置(隐藏)属性,由 表示[[Prototype]]。 指向的值[[Prototype]]通常被称为“该对象的原型”。

请考虑以下示例:



//user object
const user = {
    name: "Emmanuel",
    age: 23,
    city: "Accra"
}

console.log(user)


Enter fullscreen mode Exit fullscreen mode

在控制台中,您可以查看的属性user以及[[Prototype]](隐藏属性)。

JavaScript 协议

因为 的值[[Prototype]]Object,我们说它是对象的原型 也有一个原型属性(表示为),其中包括其他内置方法。user Object__proto__

__proto__与隐藏的 不同[[Prototype]]。它允许我们读取并将详细信息分配给[[Prototype]]

让我们扩大Object调查范围

JS 原型

原型链

每当__proto__属性指向时object,该对象也会有一个原型,该原型也指向另一个对象。实际上,它创建了一个原型链。原型链以一个值为 的对象结尾null

Javascript 原型

原型的工作原理

考虑user下面的物体。

它有三个属性:nameagecity



const user = {
    name: "Emmanuel",
    age: 23,
    city: "Accra"
}


Enter fullscreen mode Exit fullscreen mode

我们可以通过点 ( obj.propName) 或括号 ( )访问这些属性obj['propName'])

让我们访问该name属性



user.name
// which returns 
Emmanuel


Enter fullscreen mode Exit fullscreen mode

我们立即可以访问该值(Emmanuel

现在,如果我们访问对象中不存在的属性会发生什么?

如果我们访问某个属性,而该对象不直接具有该属性,则 JS 引擎将通过首先搜索来查找具有该名称的属性[[Prototype]],并且由于它引用了一个对象,因此它会在该对象中查找该属性。

如果找不到该属性,[[prototype]]则搜索该对象。此搜索将依次继续,直到找到匹配项,或到达原型链的末尾。

原型链的顶端是null,现阶段未找到该属性,因此undefined将其返回。

原型链

下面的代码说明了原型链



let person  = {
  eats: true,
  hasLegs: 2,
  walks(){ console.log('I can walk')}
}
//define another object
let man = {
  hasBreast: false,
  hasBeard : true,

}
//set the prototype of man to person object
man.__proto__ = person;
//define a third object
let samuel = {
   age: 23
}
//set the prototype of samuel to man
samuel.__proto__ = man;
//access walk method from samuel
console.log(samuel.walks())
//access hasBeard from samuel
console.log(samuel.hasBeard)


Enter fullscreen mode Exit fullscreen mode
  • 我们定义一个具有和属性的person对象,包括一个方法eatshasLegswalks

  • 我们创建一个man对象,并person使用man.__proto__= person

  • 我们创建一个特定的人samuel,并将原型设置samuelman使用samuel.__proto__ = man

  • 现在,我们尝试访问samuel.walks()并且samuel.hasBeard从对象中可以清楚地看出samuel,没有walk方法和hasBeard属性。

那么,我们如何获得输出I can walktrue

  • 无论何时samuel.walks()调用,JavaScript 引擎都会首先在对象中搜索samuel该方法。

  • 如果未找到,则[[Prototype]]搜索samuel

  • [[Prototype]]设置为对象man,因此 JavaScript 引擎会检查man方法walk

  • 如果未找到,则[[Prototype]]搜索man

  • [[Prototype]]已被设置为person,因此我们检查person方法walk

  • 最后,在原型链的顶部,我们可以walk在对象中找到该方法person,并且 JavaScript 可以调用该方法,即使该方法未在对象中定义samuel

JavaScript 原型

我们再考虑一个例子。假设我们想转换user.name为大写,可以按如下方式操作:



//convert from Emmanuel to EMMANUEL
const user = {
    name: "Emmanuel",
    age: 23,
    city: "Accra"
}

console.log(user.name.toUpperCase())


Enter fullscreen mode Exit fullscreen mode

当我们调用方法时toUppercase()

  • toUpperCaseJavaScript 引擎在user对象中寻找

  • 如果找不到,它会搜索[[prototype]]属性usertoUpperCase

  • 在那里找到它并执行该方法。

JS原型

调光特性

每当您在对象中定义属性,并且该属性与中的属性具有相同的名称时[[prototype]],对象的原型就会变暗,而定义属性的原型将优先。

考虑这个例子



const myDate = new Date(1999,05,26)
console.log(myDate.getFullYear()) // 1999
//define a method for myDate
myDate.getFullYear = function(){
    console.log('changed the default getFullYear method')
}
myDate.getFullYear()


Enter fullscreen mode Exit fullscreen mode
  • getFullYear()是对象的内置方法myDate。执行时,返回完整年份

  • 我们给对象分配一个同名的属性myDate,该属性的值是一个控制台函数changed the default getFullYear method

  • 这种方法会覆盖内置方法,因为 JS 引擎会先检查myDate对象是否存在该getFullYear属性。既然已经找到了,就无需再检查对象的原型了。

这被称为“隐藏”财产

在对象上设置原型

设置对象原型有两种主要方法:

  • 对象.create()

  • 构造函数

让我们进一步探讨这个问题。

用于Object.create()设置原型

创建Object.create()一个新对象并允许您使用现有对象作为新对象的原型。

语法如下:



Object.create(proto) // proto is the existing object


Enter fullscreen mode Exit fullscreen mode

请参阅下面的示例:



const user = {
    isLoggedIn: false,
    greetUser(){
        console.log(`Howdy ${this.name} !`)
    }
}
//create a new object and set the prototype
const admin = Object.create(user)


Enter fullscreen mode Exit fullscreen mode
  • 我们定义了一个user对象,具有isLoggedIn属性和greetUser方法。

  • 创建Object.create()一个新admin对象。

  • 我们admin使用userObject.create(user)

admin以下是记录到控制台的结果

JS 原型

让我们检查一下上面的代码:

  • 当我们初次调查该对象的细节时admin,它看起来是空的。然而,我们注意到它有一个[[Prototype]]属性。这个属性是内置的,引用了该admin对象的原型。

  • 我们得出结论,原型已经设置为现有user对象。

我们还可以给对象赋值属性admin。见下面的示例:



const user = {
  isLoggedIn: false,
    greetUser(){
        console.log(`Howdy ${this.name} !`)
    }  
}

//create an object an set the prototype
const admin = Object.create(user)
console.log(admin)
//assign name property to the admin object
admin.name = "Emmanuel"


Enter fullscreen mode Exit fullscreen mode

nowadminname分配属性。此name属性已在 上设置,admin但未在 上设置user

现在我们已经设置了对象的原型,让我们尝试从访问对象greetUser()中定义的useradmin



const user = {
  isLoggedIn: false,
    greetUser(){
        console.log(`Howdy ${this.name} !`)
    }  
}

//create an object an set the prototype
const admin = Object.create(user)
console.log(admin)
//assign name property to the admin object
admin.name = "Emmanuel"
//access greetUser from admin 
admin.greetUser()
// output
Howdy Emmanuel !


Enter fullscreen mode Exit fullscreen mode

输出如下:

JS 原型

greetUser 当方法未在对象中定义时 ,如何访问该方法admn

让我们进一步研究一下

  • nameadmin是对象中唯一可见的属性

  • 还有一个隐藏[[Prototype]]属性,引用Object

  • 这就是我们传递给方法的Object现有内容userObject.create()

  • 如上所述,当我们访问对象的属性,而该属性不存在时,我们将查看对象的原型property

  • 在这个场景中,对象的原型admin已经被设置为user

  • user原型中,我们可以访问该greetUser方法

  • 现在我们可以调用greetUser()admin原型将处理实现。

  • 这解释了为什么admin.greetUser()即使方法未在中定义,也会执行该方法admin

  • 请注意,this中使用的关键字user将引用新创建的对象(在本例中admin)。因此,this.name引用Emmanuel

使用构造函数设置原型

在 JavaScript 中,每个函数都有一个prototype属性。原型使我们能够向构造函数添加属性和方法。

每当我们从构造函数创建一个对象时,该对象都可以从函数的原型中继承属性和方法。

这是一个例子



//constructor function
function User (name) {
  this.name =name,
  this.age = 34
}

//add greet method to the function's prototype
User.prototype.greet = function(){console.log(`Howdy ${this.name}`)}
//create a new object
const admin = new User("Jonas");
//access the greet method from the admin
console.log(admin.greet()) // Howdy Jonas


Enter fullscreen mode Exit fullscreen mode
  • 我们定义一个构造函数User,具有nameage属性

  • User一个prototype属性允许我们分配greet方法。

  • 我们adminUser

  • 现在,尽管greet尚未在中定义admin,但它从原型中继承了该方法。

  • 这就解释了为什么我们可以调用对象greet上的方法admin

从构造函数创建的新对象也将继承该greet方法以及分配给原型的附加属性和方法。

此外,如果原型值发生变化,所有新对象都将具有更新的值。

请参阅下面的示例:



function User (name) {
  this.name =name,
  this.age = 34
}
//create a new object
const admin = new User("Jonas");

//add greet method to the constructor function 
User.prototype.greet = function(){console.log(`Howdy ${this.name}`)}

//in effect extending the object
console.log(admin.greet())

//add additional method to the prototype 
User.prototype.userAction = function(){console.log(`${this.name} what do you like to learn today`)}

//access the new method  (*)
const user2 = new User('Clement')
user2.userAction() 
//output
Clement what do you like to learn today
//change the prototype property
User.prototype.greet = function(){console.log(`Hi ${this.name}`)}

//verify the update method
user2.greet() // Hi Clement


Enter fullscreen mode Exit fullscreen mode

了解__proto__

我们可以使用语法访问对象的原型obj.__proto__

__proto__是对象的内部属性,指向其原型。

在下面的例子中,我们读取对象的原型user2



console.log(user2.__proto__)


Enter fullscreen mode Exit fullscreen mode

JS 原型

使用 读取原型__proto__已过时,已被弃用。目前的方法是使用Object.getPrototypeOf(obj)



Object.getPrototypeOf(user2)


Enter fullscreen mode Exit fullscreen mode

我们还可以使用来设置对象的原型__proto__

请看下面的例子:



const obj = {
  greet: function(){console.log('How are you today')}
}

const obj2 = {

}
//set the prototype of obj2
obj2.__proto__ = obj;

//access the greet method
console.log(obj2.greet())


Enter fullscreen mode Exit fullscreen mode
  • obj我们用一个greet方法定义一个

  • 我们定义obj2with 是一个空对象字面量

  • 我们访问并分配 的详细信息__proto__这会将所有属性和方法复制obj2objobjobj2

  • 从此以后,即使该greet方法没有在那里定义,我们也可以调用它obj2

  • obj2继承greetobj

使用与 OOP 语言中的__proto__ 使用关键字相同。 extends

当前设置对象原型的方法是使用Object.setPrototypeOf(obj, proto)。这会将 的 设置为我们在参数中定义的[[Prototype]]内容objproto

user2下面的例子改变了对象的原型



//using user2 object created earlier
Object.setPrototypeOf(user2, {changeProto: function(){console.log('changing the prototype')}}); // change the prototype of user2


Enter fullscreen mode Exit fullscreen mode
  • 我们已经将原型user2greet一个方法更改为另一个changeProto方法

  • 我们可以通过使用以下方式读取原型来验证更改Object.getPrototypeOf(user2)

上述输出如下

Javascript 原型

原型在 OOP 中的重要性

原型是定义 JavaScript 中 OOP 模型的完美方法,classes只是原型的语法糖。

每当我们定义一个class时,它都会被转换为具有内置原型属性的构造函数

下面是一个例子



class User {
greetUser(){
console.log('Hello world')
}
}
//transpiled to
function User {
User.prototype.greetUser = function(){ console.log('Hello world')}
}

Enter fullscreen mode Exit fullscreen mode




原型继承

在编程中,当一个对象能够访问另一个对象中的所有属性和方法时,就会发生继承。

与实现经典继承的 C++ 和 Java 等其他语言不同,JavaScript 实现的是原型继承。

在经典继承中,子类扩展了父类,使其能够继承父类中定义的属性和方法。object然后可以从该类的实例构建一个确定的对象。

class然而,在处理原型继承时,我们不需要定义。我们定义一个object包含其属性和方法的对象,访问初始对象,并使用原型对其进行扩展。这个新对象将继承初始对象中定义的所有属性和方法。

原型继承是一种涉及对象从任何其他对象(而不是从类)继承的机制。

我们之前讨论的所有内容都使用了原型继承的概念。例如,我们定义了一个对象及其属性和方法,那么我们创建的新对象将继承该对象user中定义的属性和方法。user

为什么有这么多方法来管理原型

管理方法多种多样[[Prototype]],常常令人困惑。这是怎么回事?为什么?

JavaScript 语言自诞生之日起就包含原型继承。然而,管理原型继承的方式已经发生了变化。

  • 构造函数的属性prototype是使用给定原型创建对象的最古老的方法。

  • 2012 年末,Object.create浏览器提供了使用给定原型创建对象的功能,但缺乏获取或设置原型的功能。为了给开发者提供更大的灵活性,浏览器实现了__proto__访问器,允许用户获取或设置原型。

  • Object.setPrototypeOfObject.getPrototypeOf于 2015 年被添加到标准中,以处理与 相同的功能__proto__。与__proto__一样,它已被弃用并被推送到标准的附件 B,即:非浏览器环境下的可选

  • __proto__2022 年,它被正式允许在对象文字中使用{...},但不能作为 getter/setter(仍在附件 B 中)。

概括

  • 在基于类的编程中,对象是基于类构建的。

  • 基于原型的语言允许创建对象而无需先定义其类。

  • 每个对象都有一个隐藏属性,称为原型,它使我们能够继承属性和方法。

  • 指向的值[[Prototype]]通常被称为“该对象的原型”

  • 原型继承是一种涉及对象从任何其他对象(而不是从类)继承的机制。

如果您觉得本文有价值,请评论或分享到您的社交媒体账号。这或许能帮到一些人。关注我 https://twitter.com/emmanuelfkumah 获取开发技巧。

文章来源:https://dev.to/efkumah/why-javascript-is-a-prototype-based-oop-4b4g
PREV
所以我完成了 Replit 的免费 100 天 Python 课程
NEXT
效应器:我们需要更深入地研究