BubblyPoker's Blog

JS继承之对象冒充继承,原型链继承,组合继承

开门见山


先来个总结:

原型链继承满足需要多个实例中共享公共属性和函数的场景

冒充继承则满足每个实例都有其独有的属性的场景.

简单来说就是前者利于属性共享,后者利于属性私有


好的,正文开始前先说一下我自己理解的两个知识,不对之处还请不吝赐教。


1.js中共有四种调用模式:方法调用,函数调用,构造器调用,call/apply调用(这一点找个时间好好研究一下),

  • 方法调用:当一个函数被保存为对象的属性时调用

  • 函数调用:最为简单的调用,函数名前没有任何引导内容

  • 构造器调用:当函数被当作构造器用来创建对象时

  • call/apply:改变函数的this指向,例如fn.apply(obj [, params]),fn.call(obj [, param1] [, param2])。其实就是将fn里的this指针指向改为obj

2.new 出一个实例对象时,new到底干了什么?
其实new可以用三行代码概括

1
2
3
4
5
var sub = new Base(args); //等价于下面三行
var sub = {}; //新建一个空对象
sub.__proto__ = Base.prototype; //将sub的原型链上层__proto__指向Base的原型prototype
Base.call(sub, args); //如1中apply调用所述,将Base的this指向替换为sub

大致了解这两个概念后,下面的东西就好理解多了。


1.冒充继承

冒充继承又称为借用构造函数继承,有两种方式:临时属性方式和call/apply方式,具体可参考以下代码

实现原理:将父类构造函数转换为子类的方法,然后调用子类的方法,将父类构造函数的this指针改为子类对象

  1. 临时属性方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Base(name){
this.name = name;
this.hello = function(){
console.log("hello world");
};
}
function Sub(name){
this.tmp = Base;
this.tmp(name);
delete this.tmp;
this.getName = function(){
console.log(this.name)
};
}
var a = new Sub("bubblypoker");
a.hello(); // "hello world"
a.getName(); // "bubbly poker"

在Sub里先是定义了个临时的属性tmp,然后将Base函数赋值给tmp,然后再执行Base函数。

当new Sub(…)时,Sub函数里的this指向由原本的window(浏览器里是window,node里是global)转换为对象a,
Base函数也理所当然地被当成a的方法来执行了,所以Base的作用域也就是a对象了,即Base里的this也是由window转换为a,
所以,当执行a.hello()和a.getName()时,就会输出代码中的相应的值了。

  1. call/apply方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Base(name){
this.name = name;
this.hello = function(){
console.log("hello world");
};
}
function Sub(name){
Base.apply(this, [name]); //或者Base.call(this, name)
this.getName = function(){
console.log(this.name)
};
}
var a = new Sub("bubblypoker");
a.hello(); // "hello world"
a.getName(); // "bubbly poker"

正如上面第二点知识点里的call方法一样,这里是也是将Base的this指向替换为a对象.

其实看下来,冒充继承的一个关键点就是改变this指针的指向,将Base里的属性转换为Sub的属性以达到继承的目的。

可是,稍微细心点的同学就会发现了,那如果Base的原型prototype定义了一个方法或者属性,那Sub能获取到吗?就像下面代码所示

1
Base.prototype.getAge = function(){...}; //Sub是否能获取到getAge

答案是不能,这就是冒充继承的一个主要的缺点:父类中所有定义在原型prototype上的方法和属性,子类都无法获得。坑爹了

而且可以发现,冒称继承中,所有成员变量都是指向this的,new出一个实例后,每个实例都会拥有这些成员变量,并且是私有的,这对于那些子类实例有着共同特征属性和方法的场景无疑会占用较多的内存。


2.原型链继承

实现原理:将子类的原型指向父类的实例对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Base(){
this.name = "bubblypoker";
}
Base.prototype.hello = function(name) {
console.log("hello, " + (name || this.name || "world"));
};
function Sub(age){
this.age = age;
}
Sub.prototype = new Base();
Sub.prototype.getAge = function(){
console.log(this.age);
}
var a = new Sub(22);
a.hello(); // "hello, bubblypoker"
a.getAge(); // 22

聪明的你又发现了,为什么Base构造函数没有参数?

其实,这也是原型继承的一个缺点:实例化子类时,不能将子类的参数传给父类。


3.组合继承

实现原理:将上述两种方式组合起来(掩笑)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Base(name){
this.name = name;
}
Base.prototype.hello = function(name) {
console.log("hello, " + (name || this.name || "world"));
};
function Sub(name, age){
Base.call(this, name);
this.age = age;
}
Sub.prototype = new Base();
Sub.prototype.getAge = function(){
console.log(this.age);
}
var a = new Sub("bubblypoker", 22);
a.hello(); // "hello, bubblypoker"
a.getAge(); // 22

将上面两种方式组合起来,私有动态变量使用冒充方式,共享变量使用原型链继承,


总结

冒充继承

缺点:1.父类定义在原型上的属性和方法,子类无法获取
     2.每个子类实例都拥有很多相同的成员变量,这些变量都是私有的,造成了空间浪费

原型链继承

缺点:1.父类包含引用类型的原型变量会被所有子类实例共享,这在某些场景下是很棘手的
     2.子类实例的参数不能传递给父类,降低了灵活性

所以,组合继承就是为了弥补上述两种方式的缺点而诞生的。

综上,以后遇到需要继承实现的场景时,需要先想清楚哪些是公共变量,哪些是私有变量,记住:多个实例共享的属性和方法在原型链中定义
例如hello方法,而不需要子类实例共享的属性和方法要放在构造函数中定义。

以上就是我的关于冒充,原型链及组合继承的观点,如有不妥之处,望赐教。

坚持原创技术分享,您的支持将鼓励我继续创作!