深入理解JavaScript原型与原型链:附详细代码解析

发表时间: 2023-08-09 09:58

JavaScript 是一种基于原型的面向对象语言。虽然你经常会看到class关键字,但它的底层本质还是用作原型。

在本文中,我们将了解 JavaScript 的原型性质,以及对象中的原型链。

首先检查以下代码:

const animals = {

name: "animal",

type: "object",

}

animals.hasOwnProperty("name")// true

但是我们并没有给animals定义方法hasOwnProperty,可它为什么可以调用该方法?在本文结束时,我们将了解其工作原理以及更多内容。

一、原型的概念

根据查阅得到的字面意思的解释是:

指一个词语或一个类型意义的所有典型模型或原形象,是一个类型的典型特征,我们可以用下图中的车辆示例很好地解释这一点。

原型“A”是创建其他版本(如“B”和“C”)的第一个版本。“A”包含车辆应具备的最基本功能,而“B”和“C”将包含更多的功能。

这意味着,“B”和“C”是“A”的改进版本,但仍包含“A”的特征。“A”有四个轮胎,“B”也有四个轮胎,但是可以飞,“C”同样有四个轮胎,但是可以在水上行驶。

JavaScript 在原型的基础上工作。在每个函数的声明中,JavaScript 引擎将prototype属性添加到该函数,这使该函数成为可以创建其他版本的原型。我们可以通过打印其属性确认:

function hello() {

console.log("hello")

}

console.dir(hello)

结果:

如上图所示,显示了函数的属性,hello函数中包括了prototype属性, 以及另一个名为__proto__的属性。本文稍后会详细介绍。

该prototype对象有两个属性:一个名为constructor以及另一个同样名为__proto__的属性。前者指向hello函数,后者指向Object。

二、原型的好处

说原型的好处之前我们先说一下构造函数,这是创建对象的一种方式,如下所示:

function Hello() {

console.log("hello")

}

const anotherVersion = new Hello()

anotherVersion.type = "new"

console.log(anotherVersion)

Hello首字母大写是一种约定,表示该函数可以用作构造对象,这个函数也被称为构造函数。

结果:

结果现在向我们展示了这个anotherVersion对象是一个从Hello函数通过new关键字而变化来的。你可以通过这种方式去创建类似具有相同功能的对象,例如:

function Obj(name) {

this.name = name;

this.printName = function () {

console.log(this.name)

}

}

const javascript = new Obj("javascript")

const java = new Obj("java")

console.log(javascript)// Obj {name: 'javascript', printName: f}

console.log(java)// Obj {name: 'java', printName: f}

构造函数中的this变量指向构造函数new出来的实例化对象(在上面的代码中是javascript和java)。

我们可以看到,虽然javascript和java具有不同的名称值,但它们具有相同的功能代码。

使用原型的好处就是,你可以通过一个构造函数去创建很多具有相同功能的对象,并且这些对象都具有不同的名字。

还记得上面hello函数有两个属性:prototype和__proto__。prototype还有两个属性:constructor和__proto__。使用构造函数创建对象时,使用了prototype属性的constructor属性,让我们用下面的代码检查一下:

function Obj(name) {

this.name = name

this.printName = function () {

console.log(this.name)

}

}

const javascript = new Obj("javascript")

console.log(javascript)

结果:

从上图中,你会看到__proto__属性连接到我们的构造函数Obj。

三、与原型共享功能

现在我们知道函数的prototype属性使该函数成为可用于创建其他对象的原型。

如果该prototype属性有其他属性呢?我们知道,JavaScript 对象可以在任何时候添加新的属性,让我们来看看:

function Obj(name) {

this.name = name

this.printName = function () {

console.log(this.name)

}

}

const javascript = new Obj("javascript")

Obj.prototype.printType = function () {

console.log(this.type)

}

console.log(javascript)

结果:

如上图所示,__proto__属性现在有一个printType方法,但对象javascript本身没有printType方法。由上面所述结果我们可以知道,__proto__属性连接我们的构造函数,由于javascript在默认情况下可以访问__proto__属性中的构造函数,因此它也可以访问printType。因此,以下操作将正常工作:

javascript.printType()// undefined

javascript.type = "language"

javascript.printType()// language

JavaScript 是如何做到这一点的呢?首先它检查对象是否存在该方法,如果不存在,它检查__proto__属性。

四、原型链

我们看最后一张图片,你会注意到车辆B和C也有自己的原型,这意味着Obj用作原型的对象也继承了另一个原型的一些特性,这称为原型链。

这说明,一个对象可以是原型的新版本,同时也是另一个对象的原型。因此,当你尝试访问对象上的属性时,JavaScript引擎开始从对象自身中查找该属性,如果没有,它会继续检查__proto__,一直到没有__proto__或者找到该属性。如果找到最后,此属性不存在时,返回undefined

五、总 结

回到第一个代码块:

const animals = {

name: "animal",

type: "object",

}

animals.hasOwnProperty("name")// true

到现在你应该清楚了,对吧?

当你的animals在控制台打印的时候,您会注意到它有一个__proto__指向Object的原型。并且,Object的原型具有hasOwnProperty属性。animals继承了该属性,这使得它可以使用该属性。

Object在 JavaScript 中有一个所有对象都能继承的原型。Function、String等构造函数也从Object继承了属性。

这也就是为什么string.toLowerCase()也可以直接使用的原因。构造函数的原型对象String具有所有这些属性,因此字符串可以使用它们。