# 继承
孩子
复制
父亲的全部能力,并在此基础上对复制的能力进行修改而不影响父亲的能力,我们称之为继承
。
我们为什么要学习继承
呢?因为这是我们实现前端工程化
的过程中,一定绕不开的话题,下面我们一步步来了解一下如何实现继承
。
要理解继承
,这里就要先知道,类
、面向对象
是什么意思?
# 面向对象 到底什么意思?
将
对象
作为程序的基本单元
。将数据
和操作数据的函数
封装成对象,以提高软件的重用性、灵活性和扩展性。
在这个程序里,万物皆是对象
,所以就被称为面向对象
。
面向对象
是一种代码规范
,你实际开发中可以遵循,也可以不遵循。也不是所有语言都支持面向对象的形式进行开发。
但可以按照面向对象规范
进行开发的语言,都能被成为面向对象语言
。
所以JS
也是面向对象语言
。
# 类 到底是什么意思?
类
实际上是一种数据类型
(对象
的抽象形式)。每个类包含数据
和操作数据的函数
。类的实例
称为对象。
并不是所有的面向对象语言
都具有类
这种数据类型(比如JS
就没有...JS在ES6中新增的class只是一种语法糖,不是真的类),而继承
则是类设计模式
中的一种基本特征
。
类设计模式的基本特征包括:继承、封装、多态。
JS
底层没有类
这种数据类型
,所以也就不是具备类设计模式的特征
(所以标题的继承加了引号),JS是一套基于原型链的委托
模式(当前没有的功能委托给原型链的上一层,层层委托)。
但由于我们知道了
类
的表现形式
和设计思路,所以使用一些方法,可以让JS
实现近似继承
的功能(但会很别扭)。
# JS如何实现“继承”?
JS要实现"继承"
,需要满足下面几个条件:
- 子类是父类的一个
复本
(可以理解为一个快照) - 子类修改继承过来的功能,不影响父类(因为子类是快照,所以当然不能影响父类)
- 子类可以
重写/新增
功能(随便改写快照)
类
是图纸,实例
是建筑物。
复制一份图纸
起到参考作用
,具体建筑物造成什么样子,是根据具体的施工条件
来进行的。具体施工时会在图纸的基础上进行调整和优化,不会完全按照图纸去实施(当然也可以完全按照图纸去实施,看具体的施工条件)。
下面就来具体实现下JS中的继承
吧
直接上代码(可以直接打开浏览器控制台,直接复制粘贴看结果)
// 方法1 构造函数继承
function Parent() {
this.name = 'Jack';
}
Parent.prototype.sayHi= function() {
console.log(`hi! I am ${this.name}~`);
}
function Child() {
Parent.call(this); // 通过this绑定,把Parent里的属性“继承”到child
}
var child = new Child();
console.log(child.name); // Jack
child.sayHi(); // Uncaught TypeError: child.sayHi is not a function
// 遗留问题:无法继承原型链上的方法
// 方法2 构造函数 + 原型链连接 继承
function Parent() {
this.name = 'Jack';
}
Parent.prototype.sayHi= function() {
console.log(`hi! I am ${this.name}~`);
}
function Child() {
Parent.call(this);
}
Child.prototype = Parent.prototype;
var child = new Child();
console.log(child.name); // Jack
child.sayHi(); // hi! I am Jack~
// 这里有个问题,如果Child修改自己的原型链,会影响Parent
Child.prototype.run = function() {
console.log('run');
}
Parent.prototype.run(); // run
// 遗留问题:修改Child的原型链,会影响Parent
// 方法3 构造函数 + 原型链连接 继承(解决原型链相互影响)
function Parent() {
this.name = 'Jack';
}
Parent.prototype.sayHi= function() {
console.log(`hi! I am ${this.name}~`);
}
function Child() {
Parent.call(this);
}
// 创建一个对象,这个对象的[[prototype]]关联到Parent.prototype,返回这个对象给Child.prototype(创建的对象内容是空的,所以也就没有了constructor)
Child.prototype = Object.create(Parent.prototype);
// 此时Child修改自己的原型链,不会影响Parent
Child.prototype.run = function() {
console.log('run');
}
Parent.prototype.run(); // Uncaught TypeError: Parent.prototype.run is not a function
var child = new Child();
console.log(child.name); // Jack
child.sayHi(); // hi! I am Jack~
child.run(); // run
// 此时又产生一个新问题...不能准确知道child继承自谁。js实现继承就是有这么多问题...
console.log(child.constructor === Child); // false 此时Child的constructor为空,会顺着原型链往上找,然后针找到了Parent
console.log(child.constructor === Parent); // true
// 遗留问题:Child的实例child,继承信息为Parent
// 方法4 构造函数 + 原型链连接 继承(解决继承信息不准确)
function Parent() {
this.name = 'Jack';
}
Parent.prototype.sayHi= function() {
console.log(`hi! I am ${this.name}~`);
}
function Child() {
Parent.call(this);
}
Child.prototype = Object.create(Parent.prototype);
// 手动给Child添加constructor
Child.prototype.constructor = Child;
Child.prototype.run = function() {
console.log('run');
}
var child = new Child();
console.log(child.name); // Jack
child.sayHi(); // hi! I am Jack~
child.run(); // run
console.log(child.constructor === Child); // true
console.log(child.constructor === Parent); // false
// 此时使用JS实现了 类似 类设计模式的继承
// 但和真正的继承还是有区别的
// 比如顺着Child的原型链往上,还是能修改Parent
// 方法5 ES6 class 语法糖 版
class Parent {
constructor() {
this.name = 'Jack';
}
sayHi() {
console.log(`hi! I am ${this.name}~`);
}
}
class Child extends Parent {
constructor() {
super();
}
run() {
console.log('run');
}
}
var child = new Child();
console.log(child.name); // Jack
child.sayHi(); // hi! I am Jack~
child.run(); // run
console.log(child.constructor === Child); // true
console.log(child.constructor === Parent); // false
// 可以说class的出现,极大的方便了我们去实现继承
// 但他掩盖了 JS 不是类设计模式的本质...
// 但只要我们知道这个问题存在,就不怕去使用。我们能根据现实情况去权衡
// 低版本不支持class的浏览器,polyfill是使用构造函数来实现的
// 方法6 使用JS的链式委托来实现 类似继承
var Parent = {
name: 'Jack',
sayHi: function() {
console.log(`hi! I am ${this.name}~`);
}
}
var Child = Object.create(Parent);
Child.run = function() {
console.log('run');
}
var child = Object.create(Child);
console.log(child.name); // Jack
child.sayHi(); // hi! I am Jack~
child.run(); // run
// 判断继承自谁
console.log(Object.getPrototypeOf(child) === Child); // true
console.log(Object.getPrototypeOf(child) === Parent); // false
// 不清楚为啥的,可以复习下this啦~
child.name = 'Tony';
child.sayHi(); // hi! I am Tony~
// 不会影响Parent
console.log(Parent.name);
Parent.sayHi();// hi! I am Jack~
// 使用链式委托的方式,实现更简洁,也更符合JS的底层实现
// 熟悉原型链的话,会发现这种方法,相比构造函数,在原型链连接结构上也更简单且清晰(大家可以根据上一章原型链中的思路,动手画一下这两种的链路图)
// 在了解了内部的各种实现原理后,最后如何使用还是看大家喜好
如果觉得内容对你有帮助,请点个赞和关注,你们的鼓励是我持续更新下去的动力,比心。
如需转载请注明出处,感恩。
更多内容可先关注GitHub (opens new window)