前言
最近在排查巩固面试知识点的时候,发现继承,原型链这一块真是一生之敌,为了不让自己将来面临懵逼的困境,做个笔记总结一下。本文讲解JavaScript各种继承方式和优缺点,欢迎各位提出不同意见探讨。
什么是继承
继承是面向对象编程中的一个重要概念,通过继承可以使子类的实例使用在父类中定义的属性和方法。 面向对象的语言有一个标志,即拥有类的概念,抽象实例对象的公共属性与方法,基于类可以创建任意多个实例对象。在es6中,js终于引入了class类的概念(本质上还是对基于原型prototype的实现方式做了进一步的封装),现在的继承可以愈发贴近面向对象的写法。
//定义一个叫Animal的类
class Animal {
//构造函数constructor
constructor(color){
this.color = color;
}
}
在类语言中,对象基于模板来创建,然后由类来实例化对象。在没有引入类概念的时候,我们进行实例化对象
其实更像是对另一个对象的克隆,被克隆的母体称为原型对象。
生成实例的方式 -- 构造函数
为了解决从原型对象生成实例的问题,Javascript提供了一个构造函数(Constructor)模式。
所谓"构造函数",其实就是一个普通函数,但是内部使用了this
变量。对构造函数使用new
运算符,就能生成实例,并且this
变量会绑定在实例对象上。
如果这是一场面试,到这里的时候,面试官少不了要问你new
操作符干了什么事情,如何自己实现一个_new
来创建实例。不懂的同学可以回去复习这一块的知识~
function Cat(name,color){
this.name=name;
this.type = 'animal'
this.color=color;
}
var cat1 = new Cat("大毛","黄色");
var cat2 = new Cat("二毛","黑色");
alert(cat1.name); // 大毛
alert(cat1.color); // 黄色
仔细看上述代码,对于每一个实例对象,type属性都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存。怎么避免这个问题呢?
Prototype 与 原型链
Javascript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。 JavaScript深入之从原型到原型链 点我复习
这意味着,我们可以把那些不变的属性和方法,直接定义在prototype对象上,共享了同一份数据。
function Cat(name,color){
this.name = name;
this.color = color;
}
Cat.prototype.type = "animal";
var cat1 = new Cat("大毛","黄色");
var cat2 = new Cat("二毛","黑色");
alert(cat1.type); // animal
原型链继承
function Parent () {
this.name = 'kevin';
this.names = ['kevin', 'daisy'];
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
console.log(child1.getName()) // kevin
console.log(child1 instanceof Parent) // true
问题:
- 父类应用类型的属性会被所有实例共享
// child2 的 names 也被修改了 因为实例上没有 引用在原型链上找到父类的names,并且修改了值
// child1.names = ['aaaa'] 不会影响child2 在实例上新增了names属性
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]
var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy", "yayu"]
- 创建实例的同时,不能向父类传参
构造函数继承
function Parent (age) {
this.names = ['kevin', 'daisy'];
this.age = age
}
function Child (age) {
Parent.call(this,age);
}
var child1 = new Child(20);
child1.names.push('yayu');
console.log(child1.names,child1.age); // ["kevin", "daisy", "yayu"] 20
var child2 = new Child(30);
console.log(child2.names,child2.age); // ["kevin", "daisy"] 30
这种方式避免了原型链继承的引用类型属性被实例共享,同时也可以向父类传参数。 缺点:
- 方法定义在构造函数中,每次创建实例都会执行一遍。
组合继承
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name) {
Parent.call(this, name);
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
var child1 = new Child('kevin');
child1.colors.push('black');
console.log(child1.name); // kevin
console.log(child1.colors); // ["red", "blue", "green", "black"]
var child2 = new Child('daisy');
console.log(child2.name); // daisy
console.log(child2.colors); // ["red", "blue", "green"]
优点:
- 融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。
注意到多出了 Child.prototype.constructor = Child
更正了构造函数的指向。
- 任何一个prototype对象都有一个constructor属性,指向它的构造函数。
如果没有
Child.prototype = new Parent();
这一行,Child.prototype.constructor
是指向Child
的;加了这一行以后,Child.prototype.constructor
指向Parent
。
- 首先,每一个实例也有一个constructor属性,默认调用prototype对象的constructor属性,
child1.constructor == Child.prototype.constructor
- 这样一来,
child1.constructor == new Parent().constructor
,实际上就是:child1.constructor == Parent
,这显然会导致继承链的紊乱(child1是用构造函数Child生成的),因此我们必须手动纠正。
原型式继承
function createObj(o) {
function F(){}
F.prototype = o;
return new F();
}
利用空对象作为中介,就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。
缺点:
- 包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。
寄生组合式继承
组合式继承虽然解决了我们的一些痛点,但是也并非完美的方案。比如:
- 调用了两次父类构造函数
//一次是设置子类型实例的原型的时候:
Child.prototype = new Parent()
//一次在创建子类型实例的时候:
var child1 = new Child('kevin', '18');
等于 ==
Parent.call(this,name)
重复调用会带来什么问题呢?实例与原型上出现重复属性,比如child1
和Child.prototype
都有一个colors
属性,有没有办法可以避免这个问题呢,做到更加精简呢?
我们为了让子类能够访问父类原型上的属性,使子类原型指向了父类的一个实例,从而在原型链上关联。既然出现这个问题是因为设置子类型原型时的调用,那么可不可以间接的让
Child.prototype
访问到 Parent.prototype
。
为什么说间接关联呢?
因为Child.prototype = Parent.prototype
这样完成的继承是不合理的,父类子类的原型指向了同一个对象,任何对Child.prototype
的修改,都会反映到Parent.prototype
。
看看如何实现,我们可以借助一个空对象可以做到优化:
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
// 关键的三步
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
var child1 = new Child('kevin', '18');
console.log(child1);
改变后:
最后我们封装一下这个继承方法:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function prototype(child, parent) {
var prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
// 当我们使用的时候:
prototype(Child, Parent);
后记
欢迎各位共同讨论,本文参考了大佬的一些总结,如果觉得还行,请点赞讨论,谢谢@_@