本文系统梳理JavaScript中传统的继承实现方式,从基础的构造函数调用与原型链修改出发,逐步推导至原型式、借用构造函数、组合继承及寄生组合式继承。详细解析每种模式的原理、优缺点及适用场景,重点探讨如何解决参数传递与原型丢失问题,最终导向相对完善的寄生组合式继承方案。
前言
JavaScript作为一种灵活的语言,其继承机制的实现方式比Java等强类型语言更为多样。这种多样性意味着涉及的知识点丰富,也是面试中常见的考察重点。抛开ES6中引入的class语法不谈,你是否了解传统的继承方式有哪些?它们的实现原理是什么,各自的优势与劣势又在哪里?本文将结合具体示例,按照渐进式的发展脉络,深入探讨JavaScript继承的演变过程。
准备
在探讨JavaScript继承之前,有必要先回顾一下JS中实例化对象的基本实现方式。
构造函数是一种可以通过new关键字来实例化对象的函数,其主要目的是实现代码复用,避免每次都需要手动声明对象实例。
new操作符的简单实现逻辑如下:
function my_new(func){
var obj = {}
obj._proto_ = func.prototype // 修改原型链指向,拼接至func原型链
func.call(obj) // 实例属性赋值
return obj
}
从上述实现可以看出,通过调用构造函数,可以将实例属性赋值到目标对象上。
由此可以推断,如果在子类中调用父类的构造函数,同样可以达到继承的目的。
这提供了一种JavaScript继承的思路,即通过调用构造函数来实现继承。
至于原型属性,则是通过修改原型指向,来实现原型属性的共享。
因此,在继承过程中,同样可以通过修改原型链的方式来实现原型属性的继承。
总结
基于构造函数和原型链这两种核心特性,结合JavaScript语言的灵活性,
尽管继承的实现方式繁多且多变,但其核心原理始终不离其宗。
继承的n种方式
原型式继承
定义:这种继承方式借助原型,并基于已有的对象创建新对象,同时无需创建自定义类型,这种方式称为原型式继承。
直接看代码更清晰:
function createObj(o) {
function F() { }
F.prototype = o;
return new F();
}
var parent = {
name: 'trigkit4',
arr: ['brother', 'sister', 'baba']
};
var child1 = createObj(parent);
从表面看,这种方式基于对象创建,不需要显式的构造函数(尽管内部封装了构造函数)。它仅借助原型对象,因此被称为原型式继承。
缺点:
比较明显的优点在于实现简单,
但无法复用该继承模式,因为每个子类的实例都需要执行完整的createObj流程。
对于子类对象而言,
由于构造函数被封装在createObj内部,子类本身没有构造函数,导致无法在初始化时传递参数。 补充:其中createObj对应ES6中常用的Object.create()方法,不过Object.create进行了完善,允许通过额外参数来增强功能。
解决思路:
既然问题在于没有构造函数,那么大胆猜测,更进一步的方式就是涉及构造函数的原型链继承。
原型链式继承
定义:为了让子类继承父类的属性(包括方法),首先需要定义一个构造函数。然后,将父类的新实例赋值给构造函数的原型。
function Parent() {
this.name = 'mike';
}
function Child() {
this.age = 12;
}
Child.prototype = new Parent();
child.prototype.contructor = child // 原型属性被覆盖,所以要修正回来。
var child1 = new Child();
也就是直接修改子类的原型对象指向父构造函数的实例,这样将父类的实例属性和原型属性都挂载到子类的原型链上。
缺点
执行Child.prototype = new Parent()后,子类自身的原型属性会被覆盖,如果需要保留,必须在后面手动补充。
子对象实例化时,无法向父类构造函数传递参数。 例如在new Child()执行时,想要覆盖name属性,只能在Child.prototype = new Parent()时进行。而在new Child()时统一传参初始化是更常规的需求。
解决思路
如何在子类初始化时调用父类构造函数?结合前面的基础知识,答案已呼之欲出。
借用构造函数(类式继承)
类式继承:是在子类型构造函数的内部调用超类型的构造函数。
思路比较清晰,由问题驱动。
既然原型链式继承中子类不能向父类传参,那么在子类初始化时调用父类构造函数,不就能满足目的了吗?
示例如下:
function Parent(age) {
this.name = ['mike', 'jack', 'smith'];
this.age = age;
}
Parent.prototype.run = function () {
return this.name + ' are both' + this.age;
};
function Child(age) {
// 调用父类
Parent.call(this, age);
}
var child1 = new Child(21);
这样满足了初始化时传参的需求,但问题也比较明显。
child1.run //undefined
问题
父类原型属性丢失
父类初始化只继承了实例属性,原型属性在子类的原型链上丢失。
解决思路
丢失的原因在于原型链没有修改指向,那么修改下指向不就行了吗?
组合继承
定义:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
示例:
function Parent(age) {
this.name = ['mike', 'jack', 'smith'];
this.age = age;
}
Parent.prototype.run = function () {
return this.name + ' are both' + this.age;
};
function Child(age) {
// 调用父类构造函数
Parent.call(this, age);
}
Child.prototype = new Parent();//原型属性继承
Child.prototype.contructor = Child
var child1 = new Child(21);
这样问题就避免了:
child1.run() // "mike,jack,smith are both21"
问题
功能满足之后,就该关注性能了。这种继承方式的问题在于父类构造函数执行了两次。
分别是:
function Child(age) {
// 调用父类构造函数,第二次
Parent.call(this, age);
}
Child.prototype = new Parent();//修改原型链指向,第一次
解决思路
解决自然是取消一次构造函数调用,要取消自然要分析这两次执行,功能上是否有重复。
第一次同样继承了实例和原型属性,第二次执行同样继承了父类的实例属性。
因此第二次满足对父类传参的不可获取性,因此只能思考能否第一次不调用父类构造函数,只继承原型属性。
答案自然是能,前面原型式继承就是这个思路。
寄生组合式继承
顾名思义,寄生指的是将继承原型属性的方法封装在特定方法中,组合的是将构造函数继承组合起来,补充原型式继承的不足。
饶了点,直接看:
function createObj(o) {
function F() { }
F.prototype = o;
return new F();
}
//继承原型属性 即原型式继承
function create(parent, child) {
var f = createObj(parent.prototype);//获取原型对象
child.prototype = f
child.prototype.constructor = child;//增强对象原型,即保持原有constructor指向
}
function Parent(name) {
this.name = name;
this.arr = ['brother', 'sister', 'parents'];
}
Parent.prototype.run = function () {
return this.name;
};
function Child(name, age) {
// 示例属性
Parent.call(this, name);
this.age = age;
}
// 原型属性继承寄生于该方法中
create(Parent.prototype,Child);
var child1 = new Child('trigkit4', 21);
这样沿着发现问题、解决问题的思路直到相对完善的继承方式。至于ES6的方式本篇就不涉及了。
结束语
唯有厚积,才能薄发。想要心仪的offer,就得准备充裕,夯实基础,切忌似是而非,道理我都懂就是答得不完全,这样跟不懂差别也不太大。不算新的日子里立个flag,每周三个知识点回顾。要去相信,你若盛开蝴蝶自来。
以上就是深入JS继承的详细内容,更多关于深入JS继承的资料请关注本站其它相关文章!