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具有所有这些属性,因此字符串可以使用它们。