为什么 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)
在控制台中,您可以查看的属性user
以及[[Prototype]]
(隐藏属性)。
因为 的值[[Prototype]]
是Object
,我们说它是对象的原型 。也有一个原型属性(表示为),其中包括其他内置方法。user
Object
__proto__
这__proto__
与隐藏的 不同[[Prototype]]
。它允许我们读取并将详细信息分配给[[Prototype]]
让我们扩大Object
调查范围
原型链
每当__proto__
属性指向时object
,该对象也会有一个原型,该原型也指向另一个对象。实际上,它创建了一个原型链。原型链以一个值为 的对象结尾null
原型的工作原理
考虑user
下面的物体。
它有三个属性:name
、age
和city
。
const user = {
name: "Emmanuel",
age: 23,
city: "Accra"
}
我们可以通过点 ( obj.propName
) 或括号 ( )访问这些属性obj['propName'])
。
让我们访问该name
属性
user.name
// which returns
Emmanuel
我们立即可以访问该值(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)
-
我们定义一个具有和属性的
person
对象,包括一个方法eats
hasLegs
walks
-
我们创建一个
man
对象,并person
使用man.__proto__= person
-
我们创建一个特定的人
samuel
,并将原型设置samuel
为man
使用samuel.__proto__ = man
-
现在,我们尝试访问
samuel.walks()
并且samuel.hasBeard
从对象中可以清楚地看出samuel
,没有walk
方法和hasBeard
属性。
那么,我们如何获得输出I can walk
和true
?
-
无论何时
samuel.walks()
调用,JavaScript 引擎都会首先在对象中搜索samuel
该方法。 -
如果未找到,则
[[Prototype]]
搜索samuel
。 -
已
[[Prototype]]
设置为对象man
,因此 JavaScript 引擎会检查man
方法walk
。 -
如果未找到,则
[[Prototype]]
搜索man
。 -
它
[[Prototype]]
已被设置为person
,因此我们检查person
方法walk
。 -
最后,在原型链的顶部,我们可以
walk
在对象中找到该方法person
,并且 JavaScript 可以调用该方法,即使该方法未在对象中定义samuel
。
我们再考虑一个例子。假设我们想转换user.name
为大写,可以按如下方式操作:
//convert from Emmanuel to EMMANUEL
const user = {
name: "Emmanuel",
age: 23,
city: "Accra"
}
console.log(user.name.toUpperCase())
当我们调用方法时toUppercase()
-
toUpperCase
JavaScript 引擎在user
对象中寻找 -
如果找不到,它会搜索
[[prototype]]
属性user
toUpperCase
-
在那里找到它并执行该方法。
调光特性
每当您在对象中定义属性,并且该属性与中的属性具有相同的名称时[[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()
-
getFullYear()
是对象的内置方法myDate
。执行时,返回完整年份 -
我们给对象分配一个同名的属性
myDate
,该属性的值是一个控制台函数changed the default getFullYear method
-
这种方法会覆盖内置方法,因为 JS 引擎会先检查
myDate
对象是否存在该getFullYear
属性。既然已经找到了,就无需再检查对象的原型了。
这被称为“隐藏”财产
在对象上设置原型
设置对象原型有两种主要方法:
-
对象.create()
-
构造函数
让我们进一步探讨这个问题。
用于Object.create()
设置原型
创建Object.create()
一个新对象并允许您使用现有对象作为新对象的原型。
语法如下:
Object.create(proto) // proto is the existing object
请参阅下面的示例:
const user = {
isLoggedIn: false,
greetUser(){
console.log(`Howdy ${this.name} !`)
}
}
//create a new object and set the prototype
const admin = Object.create(user)
-
我们定义了一个
user
对象,具有isLoggedIn
属性和greetUser
方法。 -
创建
Object.create()
一个新admin
对象。 -
我们
admin
使用user
Object.create(user)
admin
以下是记录到控制台的结果
让我们检查一下上面的代码:
-
当我们初次调查该对象的细节时
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"
nowadmin
已name
分配属性。此name
属性已在 上设置,admin
但未在 上设置user
。
现在我们已经设置了对象的原型,让我们尝试从访问对象greetUser()
中定义的。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"
//access greetUser from admin
admin.greetUser()
// output
Howdy Emmanuel !
输出如下:
greetUser
当方法未在对象中定义时 ,如何访问该方法admn
?
让我们进一步研究一下
-
name
admin
是对象中唯一可见的属性 -
还有一个隐藏
[[Prototype]]
属性,引用Object
-
这就是我们传递给方法的
Object
现有内容。user
Object.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
-
我们定义一个构造函数
User
,具有name
和age
属性 -
有
User
一个prototype
属性允许我们分配greet
方法。 -
我们
admin
从User
-
现在,尽管
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
了解__proto__
我们可以使用语法访问对象的原型obj.__proto__
。
__proto__
是对象的内部属性,指向其原型。
在下面的例子中,我们读取对象的原型user2
console.log(user2.__proto__)
使用 读取原型__proto__
已过时,已被弃用。目前的方法是使用Object.getPrototypeOf(obj)
Object.getPrototypeOf(user2)
我们还可以使用来设置对象的原型__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())
-
obj
我们用一个greet
方法定义一个 -
我们定义
obj2
with 是一个空对象字面量 -
我们访问并分配 的详细信息
__proto__
。这会将所有属性和方法复制到obj2
obj
obj
obj2
-
从此以后,即使该
greet
方法没有在那里定义,我们也可以调用它obj2
-
obj2
继承greet
了obj
使用与 OOP 语言中的__proto__
使用关键字相同。 extends
当前设置对象原型的方法是使用Object.setPrototypeOf(obj, proto)
。这会将 的 设置为我们在参数中定义的[[Prototype]]
内容。obj
proto
user2
下面的例子改变了对象的原型
//using user2 object created earlier
Object.setPrototypeOf(user2, {changeProto: function(){console.log('changing the prototype')}}); // change the prototype of user2
-
我们已经将原型
user2
从greet
一个方法更改为另一个changeProto
方法 -
我们可以通过使用以下方式读取原型来验证更改
Object.getPrototypeOf(user2)
上述输出如下
原型在 OOP 中的重要性
原型是定义 JavaScript 中 OOP 模型的完美方法,classes
只是原型的语法糖。
每当我们定义一个class
时,它都会被转换为具有内置原型属性的构造函数
下面是一个例子
class User {
greetUser(){
console.log('Hello world')
}
}
//transpiled to
function User {
User.prototype.greetUser = function(){ console.log('Hello world')}
}
原型继承
在编程中,当一个对象能够访问另一个对象中的所有属性和方法时,就会发生继承。
与实现经典继承的 C++ 和 Java 等其他语言不同,JavaScript 实现的是原型继承。
在经典继承中,子类扩展了父类,使其能够继承父类中定义的属性和方法。object
然后可以从该类的实例构建一个确定的对象。
class
然而,在处理原型继承时,我们不需要定义。我们定义一个object
包含其属性和方法的对象,访问初始对象,并使用原型对其进行扩展。这个新对象将继承初始对象中定义的所有属性和方法。
原型继承是一种涉及对象从任何其他对象(而不是从类)继承的机制。
我们之前讨论的所有内容都使用了原型继承的概念。例如,我们定义了一个对象及其属性和方法,那么我们创建的新对象将继承该对象user
中定义的属性和方法。user
为什么有这么多方法来管理原型
管理方法多种多样[[Prototype]]
,常常令人困惑。这是怎么回事?为什么?
JavaScript 语言自诞生之日起就包含原型继承。然而,管理原型继承的方式已经发生了变化。
-
构造函数的属性
prototype
是使用给定原型创建对象的最古老的方法。 -
2012 年末,
Object.create
浏览器提供了使用给定原型创建对象的功能,但缺乏获取或设置原型的功能。为了给开发者提供更大的灵活性,浏览器实现了__proto__
访问器,允许用户获取或设置原型。 -
Object.setPrototypeOf
并Object.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