一文读懂JS中类、原型和继承
很多前端小伙伴,包括我自己在开始学习JS时对__proto__和ptototype这两个概念时都是一脸懵逼,面试时遇到原型链的问题总是瑟瑟发抖;不过真正的勇士敢于直面难题,经过对原型链不断的探索,本文对JS中类和原型的概念进行了深入的讲解,同时从原型方面来了解JS中继承是什么。
构造函数和对象
首先让我们看一下,在其他语言中是怎么来定义类的。在JAVA中类可以看出是创建对象的模板,我们可以这样定义类:
1 |
|
但是ES6之前都没有class,那么JS怎么定义类呢?在JS中函数是一等公民,我们可以通过构造函数(即JAVA中的类)来创建对象。所谓构造函数,就是提供了一个生成对象的模板并描述对象的基本结构的函数。一个构造函数,可以生成多个对象,每个对象都有相同的结构。总的来说,构造函数就是对象的模板,对象就是构造函数的实例。
1 |
|
我们通过new来构建实例化对象,类函数中的this
总是指向实例化的对象,每一个实例对象都有一个不可枚举的属性constructor
属性来指向构造函数,即Person。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
我们把person1
打印出来看一下到底有什么:
实例对象中可以看到我们在类中定义的name属性和sayName方法都有了,但是constructor
属性并没有,但是却能取到值。
构造函数缺点
所有的实例对象都会单独创建自己的属性和方法,不同实例对象之间无法共享通用的属性。
1 |
|
但是有的属性或者方法是共有的,我们希望每个实例对象创建的时候就能有,比如说每个人天生就会哭(cry),不用一出生的时候还要“手把手教”。
原型对象prototype
为了解决实例对象之间共享属性的问题,JS提供了prototype属性。
1 |
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
prototype是从一个函数指向一个对象
,即函数才有prototype
属性。它的作用是让该构造函数创建的所有实例对象们都能找到公用的属性和方法。任何函数在创建实例对象的时候,其实会关联该函数的prototype对象。因此我们继续把原型图补充完整:
需要注意的是,我们可以修改原型对象的引用,但是仍需要把constructor
属性指向回构造函数;上面的cry
函数绑定我们可以这样改写:
1 |
|
原型链
有了原型对象,我们知道了,实例对象的属性和方法,有可能是定义在自身,也有可能是定义在他的原型对象上。通过上面的cry
函数我们可以看出,实例对象能够直接获取原型对象上的属性和方法,那么它是怎么获取的呢?在上面打印的person1
中我们发现有一个特别的属性__proto__
展开看一下:
因此__proto__
指向了实例对象的原型对象;当你访问一个对象上没有的属性时,对象就会去__proto__
上面找,如果还是找不到,就会继续找原型对象的__proto__
,直到原型对象为null;因此__proto__
构成了一条原型链。
同时我们也解答了上面实例对象上没有constructor
属性的问题,constructor
属性真正存在于原型对象上,所以实例对象才能获取到,我们继续完善原型图(虚线表示该属性或方法并不是真正存在):
同时,原型对象也是一个对象,既然是对象,那么肯定也有它自己的原型对象,那么它的原型对象是谁呢?我们知道,JS中所有的对象都是Object
的实例,并继承Object.prototype
的属性和方法;字面量var a = {}
实际上也是new Object()
的语法糖,因此:
1 |
|
我们继续完善原型图:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
constructor
我们说过constructor
用来指向构造函数;同时,constructor
真正存在于原型对象上,因此,我们可以得到下面的等式关系:
1 |
|
在学数据类型判断的时候学过,constructor
可以用来进行数据类型的判断:
1 |
|
这种方式看起来能判断所有类型,但是一旦我们更改了原型对象,这种方式就不可靠了。
1 |
|
在JS中,函数本身也可以看成是对象,对这种又是函数,又是对象,有一个特殊的称呼:函数对象
;我们调用函数的fn.call
和fn.apply
其实调用的是继承自其原型对象上的Function.prototype.call
和Function.prototype.apply
,因此函数都是Function函数的实例对象;既然是实例对象,所以Person
函数也拥有__proto__
和constructor
属性,我们来看一下函数的属性:
1 |
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
可以看出来,Person构造函数和JS普通的对象没有任何区别,有自己的constructor
属性,指向Function函数
,说明Person函数是Function函数
的实例对象;而且constructor
属性不在Person本身,而在其原型对象Function.prototype
上,因此我们再次完善一下原型图:
鸡生蛋蛋生鸡
到这里,我们发现最终原型图指向了四个基本的东西:Object
、Object.prototype
、Function
和Function.prototype
,他们之间的关系是整个原型关系里面最难理解的,为了避免干扰,我们给他们四个单独开个图:
我们知道Object
函数和Person
函数一样,都是函数对象,因此都是Function
函数的实例对象。
1 |
|
因此我们完善Object和Function的关系:
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
既然Object
是构造函数,我们又想起Function
也能通过new Function()
来构造匿名函数,同时自己又是自己的constructor
。
1 |
|
同时我们猜测Function.prototype
和Person.prototype
一样是个对象,因此它的原型对象肯定就是Object.prototype
。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
1 |
|
我们继续完善原型图:
这样,整个原型链最有意思的一幕出现了;Object
是构造函数,继承了Function.prototype
;Function
函数也是对象,继承了Object.prototype
,那么到底是先有了Object
,还是先有Function
?这似乎是一个无解的悖论。
我们发现导致鸡和蛋问题的根本原因在于Function.__proto__
指向了Function.prototype
,让Function
继承了Object.prototype
上的方法,因此我们需要对Function.prototype
来进一步的了解:
1 |
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
我们发现Function.prototype
是个特殊的函数对象,但是没有prototype属性;针对上面的代码,我们梳理了以下几点:
Function.prototype
像普通函数一样可以调用,但总是返回undefined
Function.prototype
继承于Object.prototype
,并且没有prototype
这个属性
因此Function.prototype
是个标准的内置对象,它继承于Object.prototype
,而我们知道Object.prototype===null
,说明原型链到Object.prototype
就终止了。
结论:先有
Object.prototype
(原型链顶端),Function.prototype
继承Object.prototype
而产生,最后,Function
和Object
和其它构造函数继承Function.prototype
而产生。
静态属性和方法
所谓的静态方法,是指不需要声明类的实例就可以使用的方法。在JAVA中我们可以直接在类中加一个static定义静态方法
1 |
|
在ES5中,我们直接将它作为类函数的属性即可:
1 |
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
静态方法和实例方法最主要的区别就是实例方法可以访问到实例对象,可以对实例进行操作,而静态方法一般用于跟实例无关的操作。静态方法最常见的是在jQuery的一些工具函数中,比如$.ajax()、$.trim(),可以看出来这两个函数也是直接定义在jQuery对象(即$对象)上的,因为其不需要获取DOM元素$(‘div’)。
手写instanceof
除了constructor
,我们还有instanceof
来进行数据类型的判断;instanceof
主要用来判断一个实例是否属于某种类型,让我们先看一下instanceof的简单用法:
1 |
|
instanceof
第一个变量是一个对象A,第二个变量是一个函数B,沿着A的原型链__proto__
一直向上找,如果能找到一个__proto__
等于B的prototype,则返回true;如果找到终点还没找到则返回false。
1 |
|
手写new
通过上面的的原型链,我们知道了new本质上就是调用构造函数生成一个对象,这个对象能够访问构造函数的的原型对象,因为我们来尝试模拟一下new的实现。
1 |
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
我们首先构建了一个空对象;然后将空对象作为this,调用构造函数绑定参数;最后将该对象的__proto__指向构造函数的原型对象。
可以看到生成出来的对象该有的属性都有了,原型链也绑定成功了,但是存在的问题就是不能进行传参,因此我们进行一下改进:
1 |
|
可以看到返回的对象已经和原生new生成出来的几乎一模一样了。但是我们对构造函数进行一些修改:
1 |
|
我们在构造函数中返回了多种类型,经过测试发现:如果构造函数返回引用类型,new生成的就是返回的对象;如果返回基本数据类型,new生成新的对象。因此我们终极版的new函数如下:
1 |
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
ES5继承
所谓的继承,就是把子类继承父类所有的属性和方法;同时我们也知道父类上的属性和方法不仅在自身构造函数,原型链上也会有属性和方法,因此我们也需要继承过来。
既然继承是继承父类的属性和方法,那么我们上面的myNew
函数也相当于是一种继承;让我们再看看看还有哪些继承的方式。
原型链继承
1 |
|
我们把父类的实例挂载到子类的原型上,那么所有的子类就能访问到父类的属性和方法了,但是由于所有子类共享原型对象,所以会存在以下问题:
- 问题1:父类引用类型的属性被子类共享,一旦改变,所有子类实例引用的都将改变。
1 |
|
- 问题2:创建子类实例的时候,不能向父类传参数。
- 问题3:不能继承静态方法。
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
构造函数继承
1 |
|
每次创建子类实例的时候调用父类的构造函数,避免了引用类型的属性被所有实例共享,也可以向父类传参数;但是没有继承父类原型上的属性和方法。
组合继承
1 |
|
融合了原型链继承和构造函数继承的优点,是JS中常用的继承方式。
ES6继承
ES6新增了class
关键词,用来定义一个类,和JAVA中的有种似曾相识的感觉;但是本质上其实是ES5构造函数的语法糖,大多部分功能ES5都能实现:
1 |
|
ES6的继承可以通过extends
关键词实现,比ES5的修改原型链实现继承要更清晰和方便:
1 |
|
谢小飞博客专用防爬虫链接,想要看最新的前端博客请点这里
可以很清晰的看出来子类继承了父类本身以及原型上的属性和方法。同时,在ES5中所有的继承我们发现都不支持静态函数的继承,但是在ES6中支持。
参考
从proto和prototype来深入理解JS对象和原型链
本网所有内容文字和图片,版权均属谢小飞所有,任何媒体、网站或个人未经本网协议授权不得转载、链接、转贴或以其他方式复制发布/发表。如需转载请关注公众号【前端壹读】后回复【转载】。
目录