刚好学到了ES6中的继承,记录一下自己的理解(参考b站pink老师的前端教程)
在JS里,class、extends等关键字是实现继承的语法糖,让编程更加快捷、简单,但是其实现的原理是什么呢?
我个人觉得最核心的一句便是:类的本质就是构造函数。
我们只需要通过观察类与类之间的联系,找到正确的联系方式,将两个类关联,理论上就可以实现继承。而JS为我们提供了__proto__和prototype这两个神奇的东西,帮助我们建立该联系。
那么这两个东西具体是什么呢?
首先我们来写一段代码,通过函数新建两个类
<script>
//1、定义一个父类
function Father() {
}
//2、通过原型对象prototype给父类添加一个方法。
Father.prototype.sing = function () {
console.log("唱歌");
}
// 3、定义一个子类
function Son() {
}
// 4、实例化一个子类对象
var son = new Son();
// 5、实例化一个父类对象
var father = new Father();
</script>
此时,JS内部会为每个类自动生成一个对应的原型对象(prototype),每个实例化的对象会自动生成一个原型属性__proto__,如下图所示。
这个时候Father和Son之间只是互相独立、无关联的两个类。
那么如何使得我们创建的son实例对象可以使用父类中的方法呢?我们可以让Son(类)的原型对象prototype指向father实例对象。
(先分析上述方案原理,后续再分析为什么是指向father的实例对象,而不是其他的对象,比如 Father的原型对象prototype等)
代码如下:
<script>
//1、定义一个父类
function Father() {
}
//2、通过原型对象prototype给父类添加一个方法。
Father.prototype.sing = function () {
console.log("唱歌");
}
// 3、定义一个子类
function Son() {
}
// 4、实例化一个子类对象
var son = new Son();
// 5、实例化一个父类对象
var father = new Father();
Son.prototype = father; //6、子类原型对象 指向 父类实例对象
</script>
现在的关系如下图:
【分析】此时,我们再通过子类对象去调用父类方法时,程序内部会顺着原型链一级一级向上遍历,先找到自身的__proto__属性指向的类原型对象prototype中,看看有没有对应的方法,如果没有再进一步向上遍历,一直遍历到所有类的祖先——Object类(这个图里没有画)。在本例子中,子类的prototype指向了父类实例对象father,在调用sing方法时程序会顺着son的__proto__找到Son的prototype,而Son的prototype刚好就是father,此时程序又会顺着father的__proto__继续向上找到Father的prototype,从而找到我们前面定义的sing方法,从而成功调用,实现继承。
我们可以通过子类对象调用父类方法、进一步验证上述分析。
<script>
//1、定义一个父类
function Father() {
}
//2、通过原型对象prototype给父类添加一个方法。
Father.prototype.sing = function () {
console.log("唱歌");
}
// 3、定义一个子类
function Son() {
}
// 4、实例化一个子类对象
// var son = new Son();
// 5、实例化一个父类对象
var father = new Father();
Son.prototype = father; //6、子类原型对象 指向 父类实例对象
var son = new Son(); //重新实例化一个子类对象
son.sing(); //调用Father中的sing方法
</script>
结果如下,调用成功:
综上,我们成功通过函数实现了继承(也就是ES6出来之前的做法)
那么回到前面曾提出的一个问题,为什么还要单独创建一个Father的实例化对象,让子类的原型原型对象去指向这个父类的实例化对象,而不是直接让子类的原型对象指向父类的原型对象,如下图,这样貌似更省事?
代码如下:
<script>
//1、定义一个父类
function Father() {
}
//2、通过原型对象prototype给父类添加一个方法。
Father.prototype.sing = function () {
console.log("唱歌");
}
// 3、定义一个子类
function Son() {
}
// 4、直接让子类原型对象指向父类对象
Son.prototype = Father.prototype
var son = new Son(); //重新实例化一个子类对象
son.sing(); //调用Father中的sing方法
</script>
这里就不演示浏览器端的结果了,结果是也可以成功调用了sing方法
但是这样会出现一个问题,这个问题也非常容易理解,就是当你此时想通过子类的原型对象给子类添加一些独有方法的时候,父类也会跟随着改变,因为子类的原型直接指向了父类,是同一个地址。改变子类的同时也改变了父类,这显然不是我们继承的初衷。
所以自然而然,想到的解决办法就是单独创建一个父类的实例化对象,这个对象和父类原型对象是在两个地址下,不会互相干扰,但是又存在一定的联系,也就是我们在片头就提到的父类实例化对象中会含有一个原型属性__proto__,通过这个属性可以找到父类的原型对象 prototype。