JavaScript 可视化:原型继承
.length
有没有想过,为什么我们可以在字符串、数组或对象上使用诸如、 之.split()
类的内置方法.join()
?我们从未明确指定过它们,它们从何而来?别说“这是 JavaScript,哈哈,没人知道,这太神奇了🧚🏻♂️”,这实际上是因为一种叫做原型继承 的东西。它非常棒,而且你使用它的频率比你意识到的还要高!
我们经常需要创建许多相同类型的对象。假设我们有一个网站,人们可以在这里浏览狗狗!
对于每只狗,我们都需要一个代表它的对象!🐕 我不会每次都写一个新对象,而是使用构造函数(我知道你在想什么,我稍后会介绍 ES6 类!),我们可以使用关键字从中创建 Dog实例new
(不过这篇文章并不是真正解释构造函数,所以我就不多说了)。
每只狗都有名字、品种、颜色和吠叫的功能!
当我们创建Dog
构造函数时,它并不是我们创建的唯一对象。我们还自动创建了另一个对象,称为原型!默认情况下,此对象包含一个构造函数属性,在本例中,它只是对原始构造函数的引用Dog
。
Dog 构造函数上的属性prototype
是不可枚举的,这意味着当我们尝试访问对象属性时,它不会显示出来。但它仍然在那里!
好吧,那么……我们为什么要有这个属性对象呢?首先,让我们创建一些想要展示的狗狗。为了简单起见,我把它们命名为dog1
……是 Daisy,一只可爱的黑色拉布拉多犬!是 Jack,一只勇敢无畏的白色杰克罗素梗犬dog2
😎dog1
dog2
让我们登录dog1
到控制台,并扩展其属性!
我们看到了添加的属性,比如name
、breed
、color
和bark
.. 但哇,这是什么__proto__
属性!它是不可枚举的,这意味着当我们尝试获取对象的属性时,它通常不会显示出来。让我们扩展它吧!😃
哇,它看起来和那个对象一模一样Dog.prototype
!你猜怎么着,它其实__proto__
是一个对象的引用Dog.prototype
。这就是原型继承的精髓:构造函数的每个实例都可以访问构造函数的原型!🤯
那么为什么这很酷呢?有时我们有一些所有实例都共享的属性。例如bark
本例中的函数:它对于每个实例都是完全相同的,为什么每次创建新狗时都要创建一个新函数,每次都消耗内存呢?相反,我们可以将它添加到Dog.prototype
对象中!🥳
每当我们尝试访问实例上的属性时,引擎首先会在本地搜索该属性是否在对象本身上定义。但是,如果找不到我们要访问的属性,引擎就会沿着原型链向下查找该__proto__
属性!
现在这只是一个步骤,但它可以包含多个步骤!如果你继续往下看,你可能已经注意到,当我展开__proto__
对象显示时,我没有包含一个属性Dog.prototype
。Dog.prototype
本身就是一个对象,这意味着它实际上是构造函数的一个实例Object
!这意味着Dog.prototype
还包含一个__proto__
属性,它是对 的引用Object.prototype
!
终于,我们终于知道所有内置方法的来源了:它们都在原型链上!😃
比如.toString()
方法。它是在对象上本地定义的吗dog1
?嗯,不是。它是dog1.__proto__
在引用它的对象上定义的吗Dog.prototype
?同样不是!它是在Dog.prototype.__proto__
引用它的对象上定义的吗Object.prototype
?是的!🙌🏼
现在,我们刚刚使用了构造函数(function Dog() { ... }
),这仍然是有效的 JavaScript。然而,ES6 实际上为构造函数和原型引入了一种更简单的语法:类!
类只是构造函数的语法糖。一切仍然以相同的方式工作!
我们用关键字来编写类class
。类有一个constructor
函数,它基本上就是我们在 ES5 语法中编写的构造函数!我们想要添加到原型的属性,是在类主体本身中定义的。
类的另一个优点是,我们可以轻松地扩展其他类。
假设我们想展示几只同一品种的狗,也就是吉娃娃!吉娃娃(不知何故……😐)仍然是一只狗。为了简化示例,我name
现在只将 属性传递给 Dog 类,而不是name
、breed
和color
。但这些吉娃娃还可以做一些特别的事情,它们会轻轻地叫。Woof!
吉娃娃不仅可以说 ,还可以说Small woof!
🐕
在扩展类中,我们可以使用关键字 : 访问父类的构造函数。在本例中super
,我们必须将父类构造函数所需的参数传递给super
: 。name
myPet
可以访问Chihuahua.prototype
和Dog.prototype
(并且自动访问Object.prototype
,因为Dog.prototype
是对象)。
由于Chihuahua.prototype
具有smallBark
函数,并且Dog.prototype
具有bark
函数,我们可以同时访问smallBark
和!bark
myPet
现在你可以想象,原型链不会永远延伸。最终会有一个原型等于的对象null
:Object.prototype
在本例中就是这个对象!如果我们尝试访问一个在本地或原型链上都找不到的属性,undefined
就会被返回。
虽然我在这里解释了构造函数和类的全部内容,但另一种为对象添加原型的方法是使用Object.create
方法。通过这个方法,我们可以创建一个新对象,并可以精确指定该对象的原型应该是什么!💪🏼
我们通过将一个现有对象作为参数传递给Object.create
方法来实现这一点。该对象是我们创建的对象的原型!
让我们记录me
刚刚创建的对象。
我们没有给me
对象添加任何属性,它只是简单地包含一个不可枚举的__proto__
属性!该__proto__
属性保存着我们定义为原型的对象的引用:该person
对象拥有一个name
和一个age
属性。由于该对象是一个对象,因此该对象上person
该属性的值就是(但为了更容易阅读,我没有在动图中添加该属性!)__proto__
person
Object.prototype
希望你现在明白了为什么原型继承在精彩的 JavaScript 世界中如此重要!如有任何疑问,请随时联系我!😊
文章来源:https://dev.to/lydiahallie/javascript-visualized-prototypal-inheritance-47co