继承目的:不重复写类的相同属性和方法
摘自JavaScript高级程序设计:
继承是OO语言中的一个最为人津津乐道的概念。许多OO语言都支持两种继承方式: 接口继承 和 实现继承 。接口继承只继承方法签名,而实现继承则继承实际的方法。由于
js
中方法没有签名,在ECMAScript中无法实现接口继承.ECMAScript只支持实现继承,而且其实现继承
主要是依靠原型链来实现的。
原型链的问题
原型链并非十分完美,它包含如下两个问题。
问题一: 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;
问题二: 在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数。
有鉴于此, 实践中很少会单独使用原型链。
为此,下面将有一些尝试以弥补原型链的不足。
常见的几种继承方式
在开始之前,为了方面后面使用,我们创建一个 Box
类,作为后面被继承的父类。
function Box(_a) {
this.a = _a;
this.play();
// 静态属性
Box.static = "static";
}
Box.prototype.aa = 10;
Box.prototype.play = function () {
// 这里的this指向实例化对象
console.log("play")
}
let b = new Box(1);
console.log(b)
/* 打印结果
{
a: 1,
__proto__: {
aa: 10,
play: ƒ (),
constructor: ƒ Ball(_a),
__proto__: Object
}
}
*/
一、冒充式继承(借用构造函数继承)
做法:在子类构造函数的内部调用父类构造函数
-
优点:一举解决了原型链的两大问题:
- 其一, 保证了原型链中引用类型值的独立,不再被所有实例共享;
- 其二, 子类型创建时也能够向父类型传递参数.
缺点:这样父类会丢失传入的参数; 还会让父类构造函数重复执行,其中的方法也会重复执行(如果不
new Ball
,也会执行一次play()
)
function Ball(_a) {
Box.call(this, _a);
}
let b1 = new Ball(20); // TypeError: this.play is not a function
console.log(b1);
console.log(b1.aa); // ===> undefined
/* 注释Box中的this.play()后打印
{
a: 20,
__proto__: {
constructor: ƒ Ball(_a)
__proto__: Object
}
}
*/
可见,b1
的原型链上没有找到 play()
这个方法,因此报错。
二、组合式继承
将原型链和借用构造函数的技术组合到一块,从而发挥两者之长的一种继承模式。把子类的 prototype
属性,指向实例化的父类。
- 做法:使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。
- 优点:组合继承避免了原型链和借用构造函数的缺陷
-
缺点:这样父类会丢失传入的参数; 还会让父类构造函数重复执行,其中的方法也会重复执行(如果不
new Ball
,也会执行一次play()
)
function Ball(_a) {
Box.call(this, _a);
}
Ball.prototype = new Box();
// 原型链上没有constructor,需要加上
Ball.prototype.constructor = Ball;
var b2 = new Ball(10);
console.log(b2);
/*
{
a: 10,
__proto__: Box{
a: undefined, // 不是我们所需要的
constructor: ƒ Ball(_a),
__proto__: {
aa: 10,
play: ƒ(),
constructor: ƒ Box(_a),
__proto__: Object
}
}
}
*/
这里有一个需要明确的点:为什么要
new Box()
,而不是Box()
或者Box
?
Box()
:这样会直接执行Box()
,this
指向window
,而window
下没有play()
这个方法,并不能执行。Ball.prototype = Box(); // TypeError: this.play is not a function
Box
: 在Box
中刚开始打印this
可以发现,实例化的Ball
的原型是Box
(这似乎满足我们的需求)。但是aa
和play()
并不在原型链上,而在Box的prototyoe
上,并不能直接访问到。Ball.prototype = Box; // TypeError: this.play is not a function
三、原型式继承
- 做法:创建一个临时性的构造函数,然后将父类的原型对象作为这个构造函数的原型,最后将临时类的一个新实例赋值给子类的原型。
-
优点:解决了组合式继承执行两次
constructor
的问题。 -
缺点:
中间类.prototype = 父类.prototype
这个过程相当于做了一次浅复制,父类上的引用类型的属性值,会被实例化的子类修改。
function Ball(_a) {
Box.call(this, _a);
}
function F() { }
F.prototype = Box.prototype;
Ball.prototype = new F();
Ball.prototype.constructor = Ball;
let b3 = new Ball(10);
console.log(b3);
/*
{
a: 10,
__proto__: Box {
constructor: ƒ Ball(_a)
__proto__:{
aa: 10,
play: ƒ (),
constructor: ƒ Box(_a),
__proto__: Object
}
}
}
*/
缺点 - 引用类型被修改的例子:
Box.prototype.arr = [1, 2, 3];
b3.arr.push(4);
let b4 = new Ball(30);
b4.arr.push(5);
console.log(Box.prototype.arr); // [1, 2, 3, 4, 5]
这里有一个需要明确的点:为什么不直接
Ball.prototype = Box.prototype;
?因为这样不是真正意义上的继承,
Ball
与Box
二者在原型链上没有了任何联系。验证:
Ball.prototype = Box.prototype; Ball.prototype.constructor = Ball; let b5 = new Ball(10); console.log(b5); /* { a: 10, __proto__: { aa: 10, play: ƒ (), constructor: ƒ Ball(_a), __proto__: Object } } */
四、寄生式继承(常用!推荐!)
做法:创建一个仅用于封装继承过程的函数,该函数在内部来实现类的继承。
优点:寄生组合式继承、集寄生式继承和组合继承的优点于一身,是ES5实现基于类型继承的最有效方法。
// 参数: sub: 子类 sup: 父类
function extend(sub, sup) {
// 创建一个中间类
function F() { }
// 将父类的原型赋值给这个中间替代类
F.prototype = sup.prototype;
// 将原子类的原型保存
let subProto = sub.prototype;
// 将子类的原型设置为中间替代类的实例对象
sub.prototype = new F();
// 将原子类的原型复制到子类原型上,合并超类原型和子类原型的属性方法
Object.assign(sub.prototype, subProto);
// 设置子类的构造函数时自身的构造函数,以防止因为设置原型而覆盖构造函数
sub.prototype.constructor = sub;
// 给子类的原型中添加一个属性,可以快捷的调用到父类的原型方法(目的只是为了让子类刚方便访问父类的属性和方法,类似于ES6的super())
sub.prototype.sup = sup.prototype;
// 如果父类的原型构造函数指向的不是父类构造函数,重新指向
if (sup.prototype.constructor !== sup) {
sup.prototype.constructor = sup;
}
}
/* ===== 使用 extend() ===== */
function Ball(_a) {
this.sup.constructor.call(this, _a);
}
extend(Ball, Box);
let b6 = new Ball(10);
console.log(b6);
/*
{
a: 10,
__proto__: Box {
constructor: ƒ Ball(_a),
sup: {aa: 10, play: ƒ, constructor: ƒ}
__proto__: {
aa: 10,
play: ƒ (),
constructor: ƒ Box(_a),
__proto__: Object
}
}
}
*/
4.1 如果需要改写父类的方法,可以对同名方法进行覆盖
Ball.prototype.play = function () {
this.sup.play.call(this);// 如果需要给父类的方法增加内容,则先执行父类的同名方法
console.log("end");
}
4.2 Object.assign(sub.prototype, subProto)
的进阶写法
我们知道,constructor
应该是不可枚举的,而使用上面的constructor
是可枚举的,所以这一行代码,我们可以通过下面的方式进行改写。
var names = Object.getOwnPropertyNames(subProto);
for (var i = 0; i < names.length; i++) {
var desc = Object.getOwnPropertyDescriptor(subProto, names[i]);
Object.defineProperty(sub.prototype, names[i], desc);
}
4.3 也可以封装到Function上,只需要把sub
换成this
就可以了
Function.prototype.extend = function (sup) {
function F() { }
F.prototype = sup.prototype;
let subProto = this.prototype;
this.prototype = new F();
var names = Object.getOwnPropertyNames(subProto);
for (var i = 0; i < names.length; i++) {
var desc = Object.getOwnPropertyDescriptor(subProto, names[i]);
Object.defineProperty(this.prototype, names[i], desc);
}
this.prototype.constructor = this;
this.prototype.sup = sup.prototype;
if (sup.prototype.constructor !== sup) {
sup.prototype.constructor = sup;
}
}
应用:
function Ball(_a) { this.sup.constructor.call(this, _a); } Ball.prototype.play = function () { this.sup.play.call(this); console.log("end"); } Ball.extend(Box); let b7 = new Ball(10); console.log(b7);
参考资料:
JavaScript中的继承 - MDN: https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Inheritance
JS原型链与继承别再被问倒了:https://juejin.im/post/58f94c9bb123db411953691b#heading-0