1. 概述
JavaScript面向对象比较难理解的点是类的继承。不管是es5写法还是es6写法,JavaScript继承的本质是原型链。具体可看我的上一篇文章:
田浩:详解原型、原型链、构造函、实例、newzhuanlan.zhihu.com因为es6有专门的语法,写法很简单且容易理解,所以本文将着重介绍es5的实现继承不同方式及其原理。
再阅读本文之前,一定要注意区分几个名词:父类、子类、子类的实例。
2.类的声明和实例化:
/*** 类的声明*/
var Animal = function () {this.name = 'Animal';
};/*** es6中class的声明*/
class Animal2 {constructor () {this.name = 'Animal2';}
}/*** 实例化方式相同*/
let dog = new Animal();
let dog2 = new Animal2();
2. ES5中类的继承:
2.1 借助构造函数实现继承(部分继承)
function Parent1 () {this.name = 'parent1';
}Parent1.prototype.say = function () {
}; // 不会被子类继承function Child1 () {Parent1.call(this);this.type = 'child1';
}
原理:
核心在于:Parent1.call(this)
改变Parant1运行时的this的指向,指向子构造函数,所以父类中有的属性,子类中也有。
缺点:
父类原型链上的属性方法并没有被子类继承。 所以这种方式不能成为继承,只能叫做部分继承。
2.2 借助原型链实现继承
function Parent2 () {this.name = 'parent2';this.array = [1,2,3];
}
function Child2 () {this.type = 'child2';
}
Child2.prototype = new Parent2();
原理:
核心在于: Child2.prototype =new Parent2();
将Parent2的一个实例作为Child2的prototype。访问Child2的实例时候,实例可以通过__proto__访问Parent2中的属性。
缺点:
试想,如果Child2实例两个对象s1、s2,他们通过__proto__访问的父类上的属性实际上是同一个引用(s1.__proto__ === s2.__proto__)。这样,比如s1修改array,s2的array也会跟着变。这是我们所不想看到的。
2.3 组合方式
将上两种组合:
function Parent3 () {this.name = 'parent3';this.array = [1, 2, 3];
}
function Child3 () {Parent3.call(this); this.type = 'child3';
}
Child3.prototype = new Parent3();
原理:
核心在于: 将上两种方式结合。实际上,子类的实例中,Parent3上的属性会存在两份,一份在实例中,一份在实例的__proto__上:
缺点:
实例化子类的时候,父类执行了两次,且子类实例中存在冗余数据。这些都是没有必要的。
2.4 组合继承的优化1
function Parent4 () {this.name = 'parent4';this.play = [1, 2, 3];
}
function Child4 () {Parent4.call(this);this.type = 'child4';
}
Child4.prototype = Parent4.prototype;
原理:
核心在于: 将 Parent4的prototype直接赋给Child4.prototype。此时父类只执行一次。
缺点:
子类实例的constructor会指向父类:a的constructor会指向Parent4,不符合预期,无法区分这个实例是父类创造的还是子类创造的。当然这个缺点在2.3中也存在。原因是constructor属性来自原型对象中,上述方法子类的实例访问constructor 实际访问的事父类的prototype中的constructor。
2.5 组合继承的优化2
function Parent5 () {this.name = 'parent5';this.play = [1, 2, 3];
}
function Child5 () {Parent5.call(this);this.type = 'child5';
}
Child5.prototype = Object.create(Parent5.prototype);
// 手动修改constructor指向,由于父类的prototype和子类的prototype已经隔离开,可以放心修改。
Child5.prototype.constructor = Child5;
原理:
核心在于: Object.create(Parent5.prototype);
Object.create()
方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。