«
如何通俗解释 JavaScript 中的原型概念?

时间:2023-7   


为什么需要原型

首先,JS早期并没有类这个概念,直到ES6才引入class(本质还是function)。没有类,却有构造函数,并且能通过构造函数new一个对象。这种写法实在太简单:

null

但这种简单未必是好事。比如,我要在Son构造器中定义一个方法:

null

结果我发现,bravo和jordan虽然都是通过Son构造器new出来的,两个人的jump()方法却不相等。也即是说,如果把方法定义在构造器中,new对象时,其实会重复创建自己的方法,造成内存浪费。

而Java、Python等语言的做法是,方法仅有一份,靠隐式传递this来辨别需要处理的数据。

null

p1和p2共用一份changeUser(),但调用时传递this,这样changeUser()就知道处理哪个对象的数据那JS如何改进这个问题呢?原型就出来了。就我学了几天的感受来看,原型的作用其实就是一块共享空间,是和构造器深度绑定的(不是和对象)。

null

构造器、原型对象、实例对象,三足鼎立原型对象,可以简单理解为与构造器深度绑定的一块空间,在原型对象中定义的属性和方法,都是被实例对象(son1、son2、son3)共享的。

原型对象(prototype)的简单使用

构造器:Son.prototype,可以得到原型对象

实例对象:son.proto,可以得到原型对象

接着,我准备把jump()方法挪到Son的原型对象中。

null

因为原型中定义的属性和方法是共享的,所以此时bravo和jordan两个对象的jump()都来自Son的原型对象,是同一份,所以是相等的。

JS对原型的定义有一个要求:

null

原型继承

先撇开原型,谈谈为什么需要继承?很简单,最直观的作用的就是复用:寻找一个点,存储共通的属性和方法,避免重复。

在Java或其他面向对象语言中,继承是很简单的,就是通过extends关键字,子类可以沿袭父类定义的属性和方法。那对于上面JS中只有构造器没有类的情况,如何完成继承呢?

现在我们已经有Son了,等会儿我们会让Son继承Father。但在此之前,我们先来观察一下Son。

null

在这幅图中,除了构造器和原型对象之间的关系外,实例对象和原型对象之间也有关系:实例对象可以通过proto属性访问原型对象。

而且,在JS中,只要是对象,都有proto属性。那么,我不禁好奇:原型对象是不是对象?如果是,那么原型对象.proto得到的是什么呢?

null

null

观察第三个打印,通过constructor,我们发现这似乎是一个Object?难道JS中也有Object?如果我继续深入一级呢?

null

null

好,走到尽头了。我们来梳理一下整个过程,大概是这样的:

null

在JS中,它的继承不是传统意义上的“类继承”,而是偏向“对象继承”,而且这个对象不是实例对象,而是原型对象。

假设现在有一个Father对象。

null

现在,这个Father和上面的Son完全一样,就是名字不同。

null

假设现在Father上面定义了我很想要的一个方法,我如何继承过来呢?

null

注意,倒数第4行代码需要覆盖constructor为Son(否则就指向Father,而不是Son),为的是符合约定:原型对象constructor必须指向对应构造器null

马老师,发什么肾摸事了?哦,有个Father对象覆盖了Son的原型对象。

null

null

最终变成:

null

原型链

null

原型链在原型继承以后,自然就产生了。它的作用就是提供一条依赖查找的路径,沿着原型链,对象能找到继承的方法并调用。

最后补充前端流传最广的一张图:

null

比我上面画的详细(我没涉及到Function.proto