JavaScript系列之实现继承的几种方式


一、为什么需要继承?

在实际编码的过程中,如果有很多类似的方法都存放于构造函数中,这样会导致内存的浪费(内存泄漏),在这种情况下我们就需要用到继承。

二、继承是什么?

所谓继承就是通过某种方式让一个对象可以访问到另一个对象中的属性和方法。

三、在JavaScript中常用的几种继承方式

1、原型链继承

原型链是针对原型对象的,在查找实例属性时,先在实例中查找,如果没有找到,再到obj.__proto__(=f1.prototype)原型对象,再到f1.prototype.__proto__(=f2.prototype),依次向上查找。

1.1、实现方式

将Person实例化给Son的原型对象,再实例化Son赋值给变量son,打印结果可见son通过prototype指向Father函数,Father函数通过prototype指向Object对象,让Son来继承其中的属性。

function Person(name) {
	this.name = name;
	this.song = function () {
		console.log("唱歌");
	};
}
Person.prototype.phone = function () {
	console.log("打电话"); // 继承不了
};
function Son(name) {}
Son.prototype = new Person();
let zhangsan = new Son("张三");
console.log(zhangsan);
console.log(zhangsan.phone());

在这里插入图片描述

1.2、优点

能够继承构造函数属性,也能够继承原型属性。

1.3、缺点

  1. 首先属性值为引用类型值的原型属性被修改后可能会影响到所有的实例,并在此继承方式中,构造函数中的属性会继承到子类型,成为子类的原型对象的属性,这也是要在构造函数中定义属性,在原型中定义公共方法的原因。
  2. 在创建子类的实例时,不能向父类的构造函数中传递参数。

2、借助父构造函数继承属性

2.1、实现方式

  1. 先定义一个父构造函数(this指向为window);
  2. 再定义一个子构造函数(this指向为new出的实例化对象);
  3. 子构造函数通过call()、apply()、bind()等方式改变父的this指向继承父构造函数属性;
function Person(name) {
	this.name = name;
	this.song = function () {
		console.log("唱歌");
	};
}
Person.prototype.phone = function () {
	console.log("打电话"); // 继承不了
};
function Son(name) {
	Person.call(this, name); // apply bind
}
let zhangsan = new Son("张三");
console.log(zhangsan);
console.log(zhangsan.phone());

在这里插入图片描述

2.2、优点

避免了原型链继承的两个缺点,可以向父类传参,且不会造成原型属性共享的问题(因为父类构造函数中的属性继承到子类构造函数中,而非原型对象中)。

2.3、缺点

不能继承原型属性,无法实现函数复用,所有方法都只能放在构造函数中。

3、组合继承

组合继承(combination inheritance),也叫做伪经典继承,指的是将原型链和借用父构造函数组合到一块,其思路是用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

function Person(name) {
	this.name = name;
	this.song = function () {
		console.log("唱歌");
	};
}
Person.prototype.phone = function () {
	console.log("打电话"); // 继承不了
};
function Son(name) {
	this.price = "price";
	this.setScore = function () {};
}
Son.prototype = new Person();
Son.prototype.constructor = Son;
let zhangsan = new Son("张三");
console.log(zhangsan);
console.log(zhangsan.phone());

在这里插入图片描述

4、共享原型继承

该继承方式使得子类和父类共用一个原型。

function Parent() {}
Parent.prototype.hobbies = ["sing", "dance", "rap"];

function Child(name, age) {
	this.name = name;
	this.age = age;
}
Child.prototype = Parent.prototype;

var c1 = new Child("c1", 20);
var c2 = new Child("c2", 24);

c1.hobbies.push("coding");
console.log(c1.hobbies); // ["sing", "dance", "rap", "coding"]
console.log(c2.hobbies); // ["sing", "dance", "rap", "coding"]
console.log(c1.name); // "c1"
console.log(c2.name); // "c2"

4.1、优点

简单

4.2、缺点

  • 只能继承父类原型属性方法,不能继承构造函数属性方法;
  • 与原型链继承一样0存在引用类型问题;

5、原型式继承

该继承方法多用于基于当前已有对象创建新对象。在另一个函数o1内部创建一个临时的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。本质上函数o1对传入的对象进行了一次浅拷贝。

ES5通过新增Object.create()方法将原型是继承进行了规范化。该方法接受两个参数:

第一个参数:作为新对象原型的对象
第二个参数(可选):给新对象定义额外属性的对象。

// 用法一:创建一个纯洁的对象:对象什么属性都没有
Object.create(null);

// 用法二:创建一个子对象,它继承自某个父对象
var o1 = {
	name: "父对象",
	say: function () {}
};
var o2 = Object.create(o1);

5.1、优点

不需要单独创建构造函数

5.2、缺点

属性中包含的引用值始终会在相关对象间共享。

6、寄生式继承

寄生式继承是原型式继承的加强版,它结合原型式继承和工厂模式,创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。

function createAnother(origin) {
	var clone = Object.create(origin); // 通过调用函数创建一个新对象
	clone.sayHi = function () {
		// 以某种方式来增强这个对象
		alert("Hi");
	};
	return clone; // 返回这个对象
}

var o1 = {
	name: "父对象",
	hobbies: ["sing", "dance", "rap"]
};
var o2 = createAnother(o1);
o2.sayHi();

上述代码中,createAnother函数接受了一个参数,也就是将要被继承的对象。
o2是基于o1创建的一个新对象,新对象不仅具有o1的所有属性和方法,还有自己的方法。简单来说,寄生式继承在产生了这个继承父类的对象之后,为这个对象添加了一些增强方法。

6.1、优点

写法简单,不需要单独创建函数。

6.2、缺点

通过该方式给对象添加函数会导致函数难以复用。

7、寄生组合式继承

通过借用构造函数来继承属性,通过原型链的混成形式来实现继承的方法。

本质上就是使用寄生式继承来继承超类的原型,然后再将结果指定给子类型的原型。

基本写法

function inheritPrototype(SubType, SuperType) {
	var prototype = Object.create(SuperType.prototype);
	prototype.constructor = SubType;
	SubType.prototype = prototype;
}

兼容写法

function object(o) {
	function W() {}
	W.prototype = o;
	return new W();
}
function inheritPrototype(SubType, SuperType) {
	var prototype;
	if (typeof Object.create === "function") {
		prototype = Object.create(SuperType.prototype);
	} else {
		prototype = object(SuperType.prototype);
	}
	prototype.constructor = SubType;
	SubType.prototype = prototype;
}

完美写法

function Parent(name) {
	this.name = name;
	this.hobbies = ["sing", "dance", "rap"];
}
Parent.prototype.getHobbies = function () {
	return this.hobbies;
};
function Child(name) {
	Parent.call(this, name);
	this.age = 24;
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

// 测试结果
var c1 = new Child("c1");
var c2 = new Child("c2");

console.log(c1 instanceof Child); // true
console.log(c1 instanceof Parent); // true
console.log(c1.constructor); // Child
console.log(Child.prototype.__proto__ === Parent.prototype); // true
console.log(Parent.prototype.__proto__ === Object.prototype); // true

c1.hobbies.push("coding");
console.log(c1.getHobbies()); // ["sing", "dance", "rap", "coding"]
console.log(c2.getHobbies()); // ["sing", "dance", "rap"]

7.1、优点

高效率只调用一次父构造函数,并且因此避免了在子原型上面创建的不必要、多余的属性。与此同时,原型链还保持不变。

7.2、缺点

代码复杂!!!

8、ES6继承的实现方式

通过关键字extends和super来实现

super特点

  • super只能在子类中使用,可以在constructor 及 函数或静态方法中使用
  • 不能单独使用super
  • super调用类似函数调用可以根据父类构造函数传参数
  • 如果子类中没有constructor,子类会自动调取super()且传入参数到父类
  • 子类中需要在调取super之后调用this
class Person {
	static height = "178cm";
	constructor(name) {
		this.name = name;
	}
	fn() {
		console.log("父类的fn");
	}
}
class Son extends Person {
	constructor(name) {
		super(name);
		this.age = 20;
	}
	fn() {
		// console.log(super);
		super.fn();
		console.log("子类fn");
	}
}
console.log(Son.height);
let zhangsan = new Son("张三");
console.log(zhangsan);
zhangsan.fn();

8.1、优点

语法简单易懂,操作更加方便

8.2、缺点

不是每个浏览器都支持class关键字

9、混入方式继承多个对象

在寄生组合方法中的Object.creat()的下一行使用Object.assin()混合其它的类,

Object.assin()会把所有可枚举的属性从一个或多个原对象复制到目标对象。

//父类
function ParentClass(name) {
	this.name = name;
}
ParentClass.prototype.eat = function () {
	console.log("这是一个" + this.name);
};

function ParentClass1(count) {
	this.count = count;
}
//子类
function ChildClass(name, age, count) {
	ParentClass.call(this, name);
	ParentClass1.call(this, count);
	this.age = age;
}

/* 
    这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
*/
ChildClass.prototype = Object.create(ParentClass.prototype);
//混合其它
Object.assign(ChildClass.prototype, ParentClass1.prototype);
ChildClass.prototype.constuctor = ChildClass;

console.log(ChildClass);
let obj = new ChildClass("西红柿", 22, 10);
obj.eat();
console.log(obj.age, obj.name, obj.count); // 22

参考

  1. 总结JS继承的六种方式

写在最后

如果你感觉文章不咋地//(ㄒoㄒ)//,就在评论处留言,作者继续改进;o_O???
如果你觉得该文章有一点点用处,可以给作者点个赞;\\*^o^*//
如果你想要和作者一起进步,可以微信扫描二维码,关注前端老L~~~///(^v^)\\\~~~
谢谢各位读者们啦(^_^)∠※!!!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老__L

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值