继承思想源于面向对象编程语言,一般情况下支持两种继承方式:接口继承和实现继承。前者只继承方法签名,后者继承实际方法。
而在JS中,接口继承是不可能实现的,因为JS中函数并没有签名。实现继承是JS唯一支持的方式,这主要通过原型链实现的。
原型链
预备知识
每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。
实例对象可以调用原型对象的属性和方法。
原型链的使用
实现思想:“子类”实例对象的原型是“父类”的实例对象(在es5中并没有子类和父类这一概念 只是为了方便读者理解),实现“子类”的实例对象有一指针指向“子类”原型,而“子类”原型又有一指针指向“父类”原型。
大致图解如下
实现代码:
// 创建Animal
function Animal() {
this.name = 'animal';
}
Animal.prototype.getAnimalName = function () {
console.log(this.name + 'getAnimalName');
}
// 创建Dog
function Dog() {
this.name = 'dog';
}
// Dog继承自Animal 将Animal的实例赋值给Dog的原型对象,相当于将Animal的实例中的__proto__赋值给了Dog的原型对象
// 如此 Dog原型对象 就能通过 Animal 对象的实例中的[[prototype]](__proto__) 来访问到 Animal原型对象 中的属性和方法了。
Dog.prototype = new Animal();//核心思想体现代码
console.log(Dog.prototype.__proto__ === Animal.prototype );
// 在使用原型链继承的时候,要在继承之后再去原型对象上定义自己所需的属性和方法
Dog.prototype.getDogName = function () {
console.log(this.name + 'getDogName');
}
var d1 = new Dog();
d1.getAnimalName()
d1.getDogName()
原型链拼接的核心代码就是使“子类”原型对象称为“父类”构造函数构造的实例对象。
原型链的破坏
如果以字面量的形式重写“子类”原型对象,会导致其不再属于“父类”构造函数构造出来的实例,从而破坏原型链。
原型链的缺陷
此类继承思想有部分缺陷:
- 众所周知,原型对象的constructor指向他的构造函数,即“子类”的原型对象指向“子类”构造函数,而通过原型链继承后,“子类”的原型对象成为了“父类”构造函数的实例化对象,导致“子类”的原型对象中的constructor属性指向了“父类”构造函数,这显然不符合语义,可以通过子类.prototype.constructor=子类 来修正指针
- 由于引用数据类型的存储方式比较特殊,在子类实例中修改父类中的属性(引用数据类型)时,另一个子类也会收到影响。
function Animal() {
this.categorys = ["cat", "rabbit"];
}
function Dog() { }
// 继承 Animal
Dog.prototype = new Animal();
var d1 = new Dog();
d1.categorys.push("dog");
console.log(d1.categorys); // [ 'cat', 'rabbit', 'dog' ]
var d2 = new Dog();
console.log(d2.categorys); // [ 'cat', 'rabbit', 'dog' ]
经典继承
为了解决原型中包含引用数据类型的属性继承问题,一种“盗用构造函数”的继承思想应运而生。
实现思想:在子类构造函数中调用父类构造函数。
function Animal() {
this.categorys = ["cat", "rabbit"];
}
function Dog() {
// 继承 Animal
Animal.call(this);
}
在以上代码中,子类dog构造函数创建出来的实例对象也拥有父类构造函数Animal中的所有属性。
实际上,这并不是一种继承,而是一种盗用,本质上并没有拼接原型链。
经典继承的缺陷
对于私有属性来说,这种继承方式会将属性分别存储在各个实例对象中,互不干扰;可对于一些我们想要共有的属性,方法,这种继承方式就无法实现了,函数的特点就是复用性,一次声明多次调用,不会占用多份内存,而这种经典继承方式将相同的成员函数分别声明在各个实例中,浪费了内存,影响性能。
组合继承
这是一种综合了原型链和经典继承函数的继承方式,将两者的优点结合了起来。
实现思路:将需要私有化的属性使用经典继承的方式继承,使用拼接原型链的方式继承共有的原型方法和属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
function Animal(name) {
this.name = name;
this.categorys = ["cat", "rabbit"];
}
Animal.prototype.sayName = function () {
console.log(this.name);
};
function Dog(name, age) {
// 继承属性
Animal.call(this, name);
this.age = age;
}
// 继承方法
Dog.prototype = new Animal();
Dog.prototype.sayAge = function () {
console.log(this.age);
};
var d1 = new Dog("zhangsan", 29);
d1.categorys.push("dog");
console.log(d1.categorys); // [ 'cat', 'rabbit', 'dog' ]
d1.sayName(); // zhangsan
d1.sayAge(); // 29
var d2 = new Dog("lisi", 27);
console.log(d2.categorys); // [ 'cat', 'rabbit' ]
d2.sayName(); // lisi
d2.sayAge(); // 27