JS中的继承进化(多图理解)

11 篇文章 0 订阅



JS中的多种继承方式

  • 思考
  • 学习笔记 + 图片理解
  • 记录各种继承方式的优缺点







继承的思考

为什么要继承?

假设Parent类上有这些属性和方法

function Parent(name, son) {
   this.name = name;
   this.container = [1];
   this.son = son;
}

new Parent('father', 'Tom');

其他类有很多相同的添加操作,但又有自身独特的属性或方法

function Son(name,son,age) {
   this.name = name;
   this.container = [1];
   this.son = son;
   
   this.age = age; // 新增
}

new Son('son', null, 17);

重写的话十分麻烦,如果还有其他类似的构造函数,还得复制粘贴。
那么是不是可以直接借用一下呢?

function Parent(name) {
   this.name = name;
   this.container = [1];
   this.son = son;
}

function Son(name, ...) {
   Parent.call(this, name, ...);
   this.age = age
}
let son = new Son('son', ...);

//=> 通过这种方式就可以直接继承父类添加属性的操作
省去了很多冗余代码


还有一种情况是,Parent类上的prototype属性上有一些方法

function Parent() {
	this.name = 'zh';
	this.container = [1];
}
Parent.prototype.getName = function () {
	console.log(this.name);
}

复制粘贴显然不是什么好办法
那么是不是可以直接借用一下呢?

比如:
让Son的prototype直接指向Parent的prototype

function Son(name, age) {
   this.name = name;
   this.age = age;
   this.container = [1];
}
Son.prototype = Parent.prototype; // 省去了重写n种方法
Son.prototype.sonMethod = function () { // 还可以新增自己的方法
   console.log(this.age);
}

但是这会有个问题,就是Son.prototype如果写了Parent同样属性名的方法,会将Parent.prototype上的方法直接覆盖。

function Parent() {
   //...
}
Parent.prototype.getName = function () {
   console.log('parent');
}

function Son(name, age) {
}
Son.prototype = Parent.prototype; 
Son.prototype.getName = function () {
   console.log('son');
}

let son = new Son();
son.getName(); // => 'son'

let parent = new Parent()
parent.getName(); // => 'son'



继承方式

0.伪继承

这种方式是有问题的,刚刚提到过,写在这为了给后面的思考做参考。

	function Parent() {
	}
	Parent.prototype.getName = function () {
		console.log(this.name);
	}

	function Son(name) {
		this.name = name;
	}

	Son.prototype = Parent.prototype; // 伪继承操作

	var son1 = new Son('zh');
	var son2 = new Son('zh');

	son1.name = 'zh-son1';
	son1.getName(); //=> ''zh-son1'
	son2.getName(); //=> 'zh'



1.寄生式继承

🍪前置知识:
模拟实现Object.create()

function create(obj) {
	function F() {};
	F.prototype = obj;
	return new F();
}

let obj1 = {};
obj1.fn = function () { console.log('inherit')}:

let obj2 = Object.create(obj1); 
obj2.__proto__.fn === obj1.fn // => true

创造一个类F,让F的prototype指向传入的对象obj1
在这里插入图片描述



function Parent() {
// this.name = name;
}
Parent.prototype.getName = function () {
	console.log(this.name);
}

function Son(name) {
	this.name = name;
}
// 创建一个新对象,将新对象的原型指向Parent.prototype,并返回这个对象
Son.prototype = Object.create(Parent.prototype);

var son1 = new Son('zh');
var son2 = new Son('zh');

son1.name = 'zh-son1';
son1.getName(); //=> ''zh-son1'
son2.getName(); //=> 'zh'

传参: ❌ (实例化Son的时候无法向父类传参)
也就意味着无法借用父类添加属性的方式

继承
父类的属性 :❌
父类原型属性 ✅

思考
与伪继承相比,它的好处在于?

很重要的一幅图

在这里插入图片描述
寄生继承不仅避免了直接修改Parent.prototype的问题
这里我们应该有一个思考,也就是为Son.prototype以及Parent.prototype之间添加一个介质,是一个非常好的继承思路。




再用两幅图将过程具体化
在这里插入图片描述
change:
在这里插入图片描述
介质就是new F()


2.原型链继承

// 原型继承
	function Parent() {
		this.name = 'zh';
		this.container = [1];
	}
	Parent.prototype.getName = function () {
		console.log(this.name);
	}

	function Son() {}

	Son.prototype = new Parent(); //原型继承

	var son1 = new Son();
	var son2 = new Son();

	son1.name = 'zh-son1';
	son2.getName(); //=> 'zh'

	son1.container[0] = 0;
	console.log(son1.container); //=> [0]
	console.log(son2.container); //=> [0]

传参: ❌ (实例化Son的时候无法向父类传参)

继承
父类的属性 ✅
父类原型属性 ✅

缺点
1.父类的属性如果是引用数据类型,会被所有实例共享
2.子类实例化无法向父类传递参数

思路:
在这里插入图片描述
与寄生继承相比较,其实他们的实现思路是类似的,只不过是将介质改成new Parent

只是new Parent甚至可以继承父类的私有属性!



3.call继承(构造函数继承)

function Parent(name) {
	this.name = name;
	this.container = [1];
}
Parent.prototype.getName = function () {
	console.log(this.name);
}

function Son() {
	Parent.call(this, name);
}

var son1 = new Son('zh');
var son2 = new Son('zh');

son1.name = 'zh-son1';
// son2.getName(); //=> Uncaught TypeError: son2.getName is not a function

son1.container[0] = 0;
console.log(son1.container); //=> [0]
console.log(son2.container); // => [1] 

原理

var son1 = new Son();

new关键字实例化对象son1,会调用Son这个函数。(new原理)
Son中通过call又调用了Parent,将Parent中的this指向son1。(call原理)
于是son1相当于:

son1 =  {
	name = 'zh';
	container = [1];
}

son2同上

son2 =  {
	name = 'zh';
	container = [1];
}

我们可以看到,每次都会创建一个新的对象

传参:✅

继承
父类的属性 ✅
父类原型属性 ❌

优点
1.与原型继承相比,call继承可以传入参数
2.与原型继承相比,call继承子类实例化对象不会共享父类的引用类型属性

缺点
1.无法继承父类原型上的方法


继承的理想形式?

继承的理想形式应该是
1.父类的属性私有,子类实例化对象间不能共享(原型链继承的缺陷)
2.父类上prototype的方法公有,子类实例化对象都能调用(call继承的缺陷)

第二点可能会带来一个问题

问题思考

function Parent(name) {
	this.name = name;
}
Parent.prototype.getName = function () {
	console.log('Tom');
}

为什么不直接这样

function Parent(name) {
	this.name = name;
	this.getFatherName = function () {
		console.log('Tom');
	}
}
// 然后
function Son() {
	Parent.call(this,name);
}

let son = new Son('Jerry');
son.getFatherName(); // => 'Tom'

直接一个call打天下

这个问题也困扰了我很久…
我发现我一开始学习这些知识的时候往往都是拿别人直接给出的答案,却很少能说出为啥要这么做

例子:

function Parent(name) {
	this.name = name;
	this.getFatherName = function () {
		console.log('Tom');
	}
}
function Son() {
	Parent.call(this,name);
}

let son1 = new Son('a');
let son2 = new Son('b');

son1.getFatherName(); // => 'Tom'
son2.getFatherName(); // => 'Tom'

son1.getFatherName === son2.getFatherName // => false

参照物

function Parent(name) {
	this.name = name;
}
Parent.prototype.getFatherName = function () {
	console.log('Tom');
}

function Son() {
	Parent.call(this,name);
}
Son.prototype = new Parent(); // 原型继承

let son1 = new Son('a');
let son2 = new Son('b');

son1.getFatherName(); // => 'Tom'
son2.getFatherName(); // => 'Tom'

son1.getFatherName === son2.getFatherName // => true

关键最后一行代码。

大概就是这个意思吧

在这里插入图片描述
也就是如果都放在类的私有属性里,每次创建实例都会开辟一块新的内存空间存放这个属性中的getFatherName,这就浪费了内存空间。

也更好的说明了继承的理想形式

function Parent(name) {
	this.name = name; // 私有
}
Parent.prototype.getName = function (){}; // 公用



4.组合继承

  • 融合了上面两种继承的优点
function Parent(name) {
	this.name = name;
	this.container = [1];
}
Parent.prototype.getName = function () {
	console.log(this.name);
}

Son.prototype = new Parent(); // 继承父类方法

function Son(name) { // 继承父类属性
	Parent.call(this, name);
}

var son1 = new Son('zh');
var son2 = new Son('zh');

son1.name = 'zh-son1';
son2.getName(); // => 'zh'

son1.container[0] = 0;
console.log(son1.container); //=> [0]
console.log(son2.container); // => [1] 互不影响

传参:✅

继承
父类的属性 ✅
父类原型属性 ✅

融合了两种继承方式,是JS中最常用的继承方式。



它是很优秀的继承方法,却有点瑕疵。

Son.prototype = new Parent(); // 继承父类方法

原型链继承的时候我们说过,它不仅避免了直接修改Parent.prototype,还可以获取父类的私有属性。

图中
黄色的为call继承
红色为原型继承
在这里插入图片描述
红色框框的部分,属性如果是引用数据类型,所有Son实例对象共享这个属性。
因此我们需要使用call继承:Parent.call(this),这样就能够保证每个Son实例对象所继承的引用类型属性都来源于给各自的实例化对象。

但这只是一种覆盖,原来的Parent类上的共享属性依然存在!

function Parent(name) {
			this.name = 'Tom';
			this.container = [1];
		}
		Parent.prototype.getName = function () {
			console.log(this.name);
		}

		Son.prototype = new Parent(); // 继承父类方法

		function Son(name) { // 继承父类属性
			Parent.call(this, name);
		}

		var son1 = new Son('zh');
		var son2 = new Son('zh');

		console.log(son1.container[0]); // => 1
		son1.container[0] = 2; // 修改为2
		console.log(son2.container[0]); // => 1

		console.log(son1.__proto__.container[0]); // => 1
		son1.__proto__.container[0] = 2; // 修改为2
		console.log(son2.__proto__.container[0]); // => 2

结合这个例子
红色框框的部分显然是多余的,因为它不是理想的继承形式。
在这里插入图片描述
之前讲过,继承父类prototype公有属性的方法,就是寻找一块介质
new Parent这块介质能够虽然能够完成这个任务,但是有瑕疵。

寻找新的介质

在这里插入图片描述


回忆一下寄生式继承
在这里插入图片描述
注意这里
在这里插入图片描述
F这个方法里的类,是空的!



5.寄生组合式继承

function Parent(name) {
	this.name = name;
	this.container = [1];
}
Parent.prototype.getName = function () {
	console.log(this.name);
}


Son.prototype = Object.create(Parent.prototype); // 继承父类原型方法

function Son(name) { // 继承父类属性
	Parent.call(this, name);
}

var son1 = new Son('zh');
var son2 = new Son('zh');

son1.name = 'zh-son1';
son2.getName(); // => 'zh'

son1.container[0] = 0;
console.log(son1.container); // => [0]
console.log(son2.container); // => [1] 互不影响

传参:✅

继承
父类的属性 ✅
父类原型属性 ✅

相比组合继承,它更加优秀!
组合继承需要两次实例化Parent类,带来了赘余的部分。
在这里插入图片描述
而寄生组合式继承只需要实例化一次!

=============================================// 第二次修改于2020/7/1

遗留待解决问题

ES6中的class继承

实践检验真理
寄生组合继承

function Parent(name) {
			this.name = name;
			this.container = [1];
		}
		Parent.prototype.getName = function () {
			console.log(this.name);
		}

		Son.prototype = Object.create(Parent.prototype);

		function Son(name) { // 继承父类属性
			Parent.call(this, name);
		}

		var son1 = new Son('Tom');
		var son2 = new Son('Bob');

		son1.container[0] = 2;
		console.log(son2.container[0]);
		
		console.log(son1.getName === son2.getName); // true

完美的继承模式,私有属性互不干扰,公有方法共用。

class继承

class Parent {
			constructor(name) {
				this.name = name;
				this.container = [1];
			}

			getName () {
				console.log(this.name);
			}
		}

		class Son extends Parent {
			constructor(name) {
				super(name);
			}
		}

		let son1 = new Son('Tom');
		let son2 = new Son('Bob');

		son1.container[0] = 2;
		console.log(son2.container[0]);  // 1

		console.log(son1.getName === son2.getName); // true

寄生组合继承能够完成的,class继承也能够完美解决。


ES6中的class继承与寄生组合继承的区别

区别:
ES6语法规定super关键字必须在子类constructor最前面调用

class Parent {
	constructor(name) {
		this.name = name;
		this.container = [1];
	}

	getName () {
		console.log(this.name);
	}
}

class Son extends Parent {
	constructor(name) {
		super(name);
	}
}

let son1 = new Son('Tom');
son1.getName();	// 'Tom'																													

再为子类添加一个name属性,覆盖父类的name

class Parent {
	constructor(name) {
		this.name = name;
		this.container = [1];
	}

	getName () {
		console.log(this.name);
	}
}

class Son extends Parent {
	constructor(name) {
		super(name);
		this.name = 'Jerry';
	}
}

let son1 = new Son('Tom');
son1.getName();	// 'Jerry'																													

这里的super相当于

Parent.prototype.constructor.call(this);

类比一下寄生组合继承

function Parent(name) {
			this.name = name;
			this.container = [1];
		}
		Parent.prototype.getName = function () {
			console.log(this.name);
		}

		Son.prototype = Object.create(Parent.prototype);

		function Son(name) { // 继承父类属性
			this.name = 'Jerry';
			Parent.call(this, name);
		}

		var son1 = new Son('Tom');
		son1.getName(); // 'Tom'

父类的属性会覆盖子类的属性,因为call继承在后面调用。
而ES6是不允许这种情况出现的,super必须在所有语句之前调用,否则报错。
class更加严谨,保证了父类属性先于子类属性创建。

**寄生组合继承 constructor **

function Parent(name) {
	this.name = name;
	this.container = [1];
}
Parent.prototype.getName = function () {
	console.log(this.name);
}

Son.prototype = Object.create(Parent.prototype);

function Son(name) {
	Parent.call(this, name);
	console.log(this);
}

var son1 = new Son('Tom');
console.log(son1.constructor === Parent); // true

控制台结果
在这里插入图片描述
在这里插入图片描述
寄生组合继承的constructor指向的是Parent,而我们实例化的是Son这个类。

class继承

class Parent {
	constructor(name) {
		this.name = name;
		this.container = [1];
	}
	
	getName () {
		console.log(this.name);
	}
	}
	
	class Son extends Parent {
	constructor(name) {
		super(name);
		console.log(this);
	}
}

let son1 = new Son('Tom');
console.log(son1.constructor === Parent);
console.log(son1.constructor === Son);

控制台结果
在这里插入图片描述
在这里插入图片描述
class继承则不存在这个问题。

总结:

  1. class继承因为super关键字的语法,避免了父类属性覆盖子类属性的情况
  2. new Sonconstructor应该指向Son,而在寄生组合继承中指向了Parent,这是因为调用了Parent.call()的问题。
    class继承的constructor指向的是Son,更加符合逻辑。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值