.length
、.split()
、.join()
这些内置方法呢?我们从来没有明确指定过它们,它们到底是从哪里来的呢?现在别说“哈哈,没人知道,这就是神奇的JavaScript🧚🏻♂️”。这实际上是因为一种叫做原型继承(prototypal inheritance)的玩意儿。它很棒,而且我们用到它的次数比意识到它存在的次数要多得多!
我们经常要创建很多相同类型的对象。假设我们有一个网站,在这个网站上,人们可以浏览狗!
对每一只狗,我们都需要对象来表示它!🐕 我们用不着每次都写一个新对象,而是用一个构造器函数(我知道你在想什么,稍后我将介绍ES6类!),用new
关键字创建Dog实例(不过,本文并非是要解释构造器函数,所以我不想谈太多)。
每只狗都有名字(name)、品种(breed)、颜色(color)以及一个bark()函数!
image.png <br />当我们创建Dog
构造器函数时,它并不是我们创建的唯一对象。我们还自动创建了另一个对象,称为prototype(原型)!默认情况下,这个对象包含一个constructor属性,它只是对原始构造器函数的引用,在本例中是Dog
。<br />
p1.gif
<br />图1.当我们创建一个构造器函数时,同时也创建了一个prototype对象。构造器的prototype有一个对原始构造器函数的引用。
Dog构造器函数上的prototype
属性是不可枚举的,也就是说,当我们试图访问对象的属性时,它是不会出现。但它依然存在!
好吧,那么为什么我们会有这个属性对象呢?首先,我们来创建一些我们想展示的狗。为简单起见,我叫它们dog1
和dog2
。dog1
是Daisy,一只可爱的黑色拉布拉多犬!dog2
是Jack,一只无畏的白色杰克罗素犬!😎
dog1
输出到控制台,并展开其属性!<br />
p2.gif
<br />我们可以看到添加的属性,如name
、breed
、color
和bark
。但是__proto__
属性是什么玩意!它是不可枚举的,也就是说当我们试图获取对象的该属性时,它通常不会出现。下面我们把它展开!😃<br />
p3.gif
<br />哇哦,它看起来就像Dog.prototype
对象!你猜怎么着,__proto__
就是对Dog.prototype
对象的一个引用。这就是原型继承的目的:构造器的每个实例都可以访问构造器的原型!🤯<br />
p4.gif
<br />图3.实例也包含一个proto属性,这是对实例的构造器的原型的引用,在本例中是Dog.prototype。<br />为什么这很酷呢?有时我们有一些所有实例都共享的属性。比如本例中的bark
函数:它对每个实例都是完全相同的,那么与其每次创建一个新的dog时都创建一个新函数,每次都消耗内存,还不如将其添加到Dog.prototype
对象!🥳<br />
image.gif
<br />图4.我们可以通过将属性添加到所有实例都可以共享的原型上,而不是每次创建该属性的新副本。<br />每当我们试图访问实例上的属性时,引擎首先在本地搜索,看看该属性是否在对象本身上定义。不过,如果找不到我们要访问的属性,那么引擎就会通过__proto__
属性沿着原型链遍历!<br />
image.gif
<br />图5.当试图访问一个对象上某个属性时,引擎首先在本地搜索。然后,通过__proto__
属性沿着原型链遍历。<br />现在这只是一个步骤,但它可以包含几个步骤!如果继续往下看,我们就可能会注意到,当展开__proto__
对象时,并没有包含一个显示Dog.prototype
的属性。Dog.prototype
本身是一个对象,也就是说它实际上是Object
构造器的一个实例!这意味着Dog.prototype
也包含一个__proto__
属性,这个属性是对Object.prototype
的一个引用!<br />
p7.gif
<br />最后,我们就有了所有内置方法来自何方的答案:它们在原型链上!😃
比如.toString()
方法。它是在dog1
对象上本地定义的吗?嗯,不是的。。它是定义在dog1.__proto__
的引用,即Dog.prototype
对象上的吗?也不是!它是定义在Dog.prototype.__proto__
的引用,即Object.prototype
对象上的吗?是的!🙌🏼<br />
function Dog() { ... }
),它仍然是有效的JavaScript。不过,ES6实际上为构造器函数以及处理原型引入了一种更简单的语法:类!
类只是构造器函数的语法糖。其工作机制还是一样的!
我们用class
关键字编写类。类有一个constructor
函数,它基本上就是我们用ES5语法编写的构造器函数!我们要添加到原型中的属性是在类主体本身上定义的。<br />
假设我们要展示几只相同品种的狗,即吉娃娃狗(chihuahua)!不管咋样,吉娃娃依然是狗。为简单起见,我们现在只保留一个name
属性给Dog类。不过这些吉娃娃也可以做些特别的事情,它们的叫声很小(smallBark),它们的叫声不是Woof!
,而是Small woof!
。🐕
在继承的类中,我们可以使用super
关键字访问父类的构造器。父类的构造器期望的参数,我们必须传递给super
:在本例中是name
。<br />
myPet
既可以访问Chihuahua.prototype
,又可以访问Dog.prototype
(并且由于Dog.prototype
是个对象,又可以自动访问Object.prototype
)。<br />
p10.gif
<br />图8.原型继承在类与ES5构造器上的工作机制是一样的。<br />由于Chihuahua.prototype
有smallBark
函数,而Dog.Prototype
有bark
函数,因此在myPet
上,我们既可以访问smallBark
,也可以访问bark
!
现在我们可以料想得到,原型链不会永远持续下去。最终有一个原型等于null
的对象:在本例中就是Object.prototype
对象!如果我们尝试访问在本地或原型链上找不到的属性,就会返回undefined
。<br />
Object.create()
方法。用这个方法,我们可以创建一个新对象,并可以准确指定该对象的原型!💪🏼
为此,我们将已有对象作为参数传递给Object.create
方法。该对象就是我们创建的对象的原型!<br />
me
对象。<br />
p12.gif
我们没有向对象me
添加任何属性,它仅包含不可枚举的__proto__
属性!__proto__
属性引用了我们定义为原型的对象:person
对象,它有一个name
和一个age
属性。由于person
对象是一个对象,因此person
对象上的__proto__
属性值就是Object.prototype
(不过为了使更容易阅读,我没有在gif上展开该属性!)。
希望你现在了解了为什么原型继承在JavaScript的美好世界中如此重要!如有疑问,请随时与我联系!😊
原文 by Lydia Hallie:https://dev.to/lydiahallie/javascript-visualized-prototypal-inheritance-47co
网友评论