js中的继承
1. 原型链继承
原型链继承的主要思想是:重写子类的prototype属性,将其指向父类的实例。我们定义一个子类Cat,用于继承父类Animal,子类Cat的实现代码如下。
// 子类Cat
function Cat(name) {
this.name = name;
}
// 原型继承
Cat.prototype = new Animal();
// 很关键的一句,将Cat的构造函数指向自身
Cat.prototype.constructor = Cat;
var cat = new Cat('加菲猫');
console.log(cat.type); // Animal
console.log(cat.name); // 加菲猫
console.log(cat.sleep()); // 加菲猫正在睡觉!
console.log(cat.eat('猫粮')); // 加菲猫正在吃:猫粮
在子类Cat中,我们没有增加type属性,因此会直接继承父类Animal的type属性,输出字符串“Animal”。在子类Cat中,我们增加了name属性,在生成子类Cat的实例时,name属性值会覆盖父类Animal的name属性值,因此输出字符串“加菲猫”,而并不会输出父类Animal的name属性“动物”。
同样因为Cat的prototype属性指向了Animal类型的实例,因此在生成实例cat时,会继承实例函数和原型函数,在调用sleep()函数和eat()函数时,this指向了实例cat,从而输出“加菲猫正在睡觉!”和“加菲猫正在吃:猫粮”。需要注意其中有很关键的一句代码,如下所示。
Cat.prototype.constructor = Cat;
这是因为如果不将Cat原型对象的constructor属性指向自身的构造函数的话,那将会指向父类Animal的构造函数。
Cat.prototype.constructor === Animal; // true
所以在设置了子类的prototype属性后,需要将其constructor属性指向Cat。原型链继承有什么优点和缺点呢?
- 原型链继承的优点通过原型链实现继承有以下几个优点。
(1)简单,易于实现只需要设置子类的prototype属性为父类的实例即可,实现起来简单。
(2)继承关系纯粹生成的实例既是子类的实例,也是父类的实例。
console.log(cat instanceof Cat); // true,是子类的实例
console.log(cat instanceof Animal); // true,是父类的实例
(3)可通过子类直接访问父类原型链属性和函数通过原型链继承的子类,可以直接访问到父类原型链上新增的函数和属性。继续沿用前面的代码,我们通过在父类的原型链上添加属性和函数进行测试,代码如下。
// 父类原型链上增加属性
Animal.prototype.bodyType = 'small';
// 父类原型链上增加函数
Animal.prototype.run = function () {
return this.name + '正在奔跑';
};
// 结果验证
console.log(cat.bodyType); // small
console.log(cat.run()); // 加菲猫正在奔跑
- 原型链继承的缺点通过原型链实现继承同样存在一些缺点。
(1)子类的所有实例将共享父类的属性
Cat.prototype = new Animal();
在使用原型链继承时,是直接改写了子类Cat的prototype属性,将其指向一个Animal的实例,那么所有生成Cat对象的实例都将共享Animal实例的属性。以上描述可以理解为如下所码。
// 生成一个Animal的实例animal
var animal = new Animal();
// 通过改变Cat的原型链,所有的Cat实例将共享animal中的属性
Cat.prototype = animal;
这就会带来一个很严重的问题,如果父类Animal中有个值为引用数据类型的属性,那么改变Cat某个实例的属性值将会影响其他实例的属性值。
// 定义父类
function Animal() {
this.feature = ['fat', 'thin', 'tall'];
}
// 定义子类
function Cat() {}
// 原型链继承
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// 生成
var cat1 = new Cat();
var cat2 = new Cat();
// 先输出两个实例的feature值
console.log(cat1.feature); // [ 'fat', 'thin', 'tall' ]
console.log(cat2.feature); // [ 'fat', 'thin', 'tall' ]
// 改变cat1实例的feature值
cat1.feature.push('small');
// 再次输出两个实例的feature值,发现cat2实例也受到影响
console.log(cat1.feature); // [ 'fat', 'thin', 'tall', 'small' ]
console.log(cat2.feature); // [ 'fat', 'thin', 'tall', 'small' ]
(2)在创建子类实例时,无法向父类的构造函数传递参数在通过new操作符创建子类的实例时,会调用子类的构造函数,而在子类的构造函数中并没有设置与父类的关联,从而导致无法向父类的构造函数传递参数。
(3)无法实现多继承由于子类Cat的prototype属性只能设置为一个值,如果同时设置为多个值的话,后面的值会覆盖前面的值,导致Cat只能继承一个父类,而无法实现多继承。
(4)为子类增加原型对象上的属性和函数时,必须放在new Animal()函数之后实现继承的关键语句是下面这句代码,它实现了对子类的prototype属性的改写。
Cat.prototype = new Animal();
如果想要为子类新增原型对象上的属性和函数,那么需要在这个语句之后进行添加。因为如果在这个语句之前设置了prototype属性,后面执行的语句会直接重写prototype属性,导致之前设置的全部失效。
// 先设置prototype属性
Cat.prototype.introduce = 'this is a cat';
// 原型链继承
Cat.prototype = new Animal();
// 生成子类实例
var cat1 = new Cat();
console.log(cat1.introduce); // undefined
访问子类实例的introduce属性为“undefined”。
2. 构造继承
构造继承的主要思想是在子类的构造函数中通过call()函数改变this的指向,调用父类的构造函数,从而能将父类的实例的属性和函数绑定到子类的this上。
// 父类
function Animal(age) {
// 属性
this.name = 'Animal';
this.age = age;
// 实例函数
this.sleep = function () {
return this.name + '正在睡觉!';
}
}
// 父类原型函数
Animal.prototype.eat = function (food) {
return this.name + '正在吃:' + food;
};
// 子类
function Cat(name) {
// 核心,通过call()函数实现Animal的实例的属性和函数的继承
Animal.call(this);
this.name = name || 'tom';
}
// 生成子类的实例
var cat = new Cat('tony');
// 可以正常调用父类实例函数
console.log(cat.sleep()); // tony正在睡觉!
// 不能调用父类原型函数
console.log(cat.eat()); // TypeError: cat.eat is not a function
上的函数,这是因为子类并没有通过某种方式来调用父类原型对象上的函数。那么构造继承有什么优点和缺点呢?
- 构造继承的优点
(1)可解决子类实例共享父类属性的问题call()函数实际是改变了父类Animal构造函数中this的指向,调用后this指向了子类Cat,相当于将父类的type、age和sleep等属性和函数直接绑定到了子类的this中,成了子类实例的属性和函数,因此生成的子类实例中是各自拥有自己的type、age和sleep属性和函数,不会相互影响。
(2)创建子类的实例时,可以向父类传递参数在call()函数中,我们可以传递参数,这个时候参数是传递给父类的,我们就可以对父类的属性进行设置,同时由子类继承下来。我们稍微改写下上面的代码。
function Cat(name, parentAge) {
// 在子类生成实例时,传递参数给call()函数,间接地传递给父类,然后被子类继承
Animal.call(this, parentAge);
this.name = name || 'tom';
}
// 生成子类实例
var cat = new Cat('tony', 11);
console.log(cat.age); // 11,因为子类继承了父类的age属性
(3)可以实现多继承在子类的构造函数中,可以通过多次调用call()函数来继承多个父对象,每调用一次call()函数就会将父类的实例的属性和函数绑定到子类的this中。
- 构造继承的缺点
(1)实例只是子类的实例,并不是父类的实例因为我们并未通过原型对象将子类与父类进行串联,所以生成的实例与父类并没有关系,这样就失去了继承的意义。
var cat = new Cat('tony');
console.log(cat instanceof Cat); // true,实例是子类的实例
console.log(cat instanceof Animal); // false,实例并不是父类的实例
(2)只能继承父类实例的属性和函数,并不能继承原型对象上的属性和函数与缺点(1)的原因相同,子类的实例并不能访问到父类原型对象上的属性和函数。
(3)无法复用父类的实例函数由于父类的实例函数将通过call()函数绑定到子类的this中,因此子类生成的每个实例都会拥有父类实例函数的引用,这会造成不必要的内存消耗,影响性能。
3. 复制继承
复制继承的主要思想是首先生成父类的实例,然后通过for...in遍历父类实例的属性和函数,并将其依次设置为子类实例的属性和函数或者原型对象上的属性和函数。
// 父类
function Animal(parentAge) {
// 实例属性
this.name = 'Animal';
this.age = parentAge;
// 实例函数
this.sleep = function () {
return this.name + '正在睡觉!';
}
}
// 原型函数
Animal.prototype.eat = function (food) {
return this.name + '正在吃:' + food;
};
// 子类
function Cat(name, age) {
var animal = new Animal(age);
// 父类的属性和函数,全部添加至子类中
for (var key in animal) {
// 实例属性和函数
if (animal.hasOwnProperty(key)) {
this[key] = animal[key];
} else {
// 原型对象上的属性和函数
Cat.prototype[key] = animal[key];
}
}
// 子类自身的属性
this.name = name;
}
// 子类自身原型函数
Cat.prototype.eat = function (food) {
return this.name + '正在吃:' + food;
};
var cat = new Cat('tony', 12);
console.log(cat.age); // 12
console.log(cat.sleep()); // tony正在睡觉!
console.log(cat.eat('猫粮')); // tony正在吃:猫粮
在子类的构造函数中,对父类实例的所有属性进行for...in遍历,如果animal.hasOwnProperty(key)返回“true”,则表示是实例的属性和函数,则直接绑定到子类的this上,成为子类实例的属性和函数;如果animal.hasOwnProperty(key)返回“false”,则表示是原型对象上的属性和函数,则将其添加至子类的prototype属性上,成为子类的原型对象上的属性和函数。生成的子类实例cat可以访问到继承的age属性,同时还能够调用继承的sleep()函数与自身原型对象上的eat()函数。那么复制继承有什么优缺点呢?
- 复制继承的优点
(1)支持多继承只需要在子类的构造函数中生成多个父类的实例,然后通过相同的for...in处理即可。
(2)能同时继承实例的属性和函数与原型对象上的属性和函数因为对所有的属性进行for...in处理时,会通过hasOwnProperty()函数判断其是实例的属性和函数还是原型对象上的属性和函数,并根据结果进行不同的设置,从而既能继承实例的属性和函数又能继承原型对象上的属性和函数。
(3)可以向父类构造函数中传递值
在生成子类的实例时,可以在构造函数中传递父类的属性值,然后在子类构造函数中,直接将值传递给父类的构造函数。
function Cat(name, age) {
var animal = new Animal(age);
// 代码省略
}
// 以下的参数12就是传递给父类的参数
var cat = new Cat('tony', 12);
- 复制继承的缺点
(1)父类的所有属性都需要复制,消耗内存对于父类的所有属性都需要复制一遍,这会造成内存的重复利用,降低性能。
(2)实例只是子类的实例,并不是父类的实例实际上我们只是通过遍历父类的属性和函数并将其复制至子类上,并没有通过原型对象串联起父类和子类,因此子类的实例不是父类的实例。
console.log(cat instanceof Cat); // true
console.log(cat instanceof Animal);// false
4. 组合继承
组合继承的主要思想是组合了构造继承和原型继承两种方法,一方面在子类的构造函数中通过call()函数调用父类的构造函数,将父类的实例的属性和函数绑定到子类的this中;另一方面,通过改变子类的prototype属性,继承父类的原型对象上的属性和函数。
// 父类
function Animal(parentAge) {
// 实例属性
this.name = 'Animal';
this.age = parentAge;
// 实例函数
this.sleep = function () {
return this.name + '正在睡觉!';
};
this.feature = ['fat', 'thin', 'tall'];
}
// 原型函数
Animal.prototype.eat = function (food) {
return this.name + '正在吃:' + food;
};
// 子类
function Cat(name) {
// 通过构造函数继承实例的属性和函数
Animal.call(this);
this.name = name;
}
// 通过原型继承原型对象上的属性和函数
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat = new Cat('tony');
console.log(cat.name); // tony
console.log(cat.sleep()); // tony正在睡觉!
console.log(cat.eat('猫粮')); // tony正在吃:猫粮
那么组合继承有什么优缺点呢?
- 组合继承的优点
(1)既能继承父类实例的属性和函数,又能继承原型对象上的属性和函数一方面,通过Animal.call(this)可以将父类实例的属性和函数绑定到Cat构造函数的this中;另一方面,通过Cat.prototype = new Animal()可以将父类的原型对象上的属性和函数绑定到Cat的原型对象上。
(2)既是子类的实例,又是父类的实例
console.log(cat instanceof Cat); // true
console.log(cat instanceof Animal);// true
(3)不存在引用属性共享的问题因为在子类的构造函数中已经将父类的实例属性指向了子类的this,所以即使后面将父类的实例属性绑定到子类的prototype属性中,也会因为构造函数作用域优先级比原型链优先级高,所以不会出现引用属性共享的问题。
(4)可以向父类的构造函数中传递参数通过call()函数可以向父类的构造函数中传递参数。
- 组合继承的缺点
组合继承的缺点为父类的实例属性会绑定两次。在子类的构造函数中,通过call()函数调用了一次父类的构造函数;在改写子类的prototype属性、生成父类的实例时调用了一次父类的构造函数。通过两次调用,父类实例的属性和函数会进行两次绑定,一次会绑定到子类的构造函数的this中,即实例属性和函数,另一次会绑定到子类的prototype属性中,即原型对象上的属性和函数,但是实例属性优先级会比原型对象上的属性优先级高,因此实例属性会覆盖原型对象上的属性。
5. 寄生组合继承
事实上4.5.4组合继承的方案已经足够好,但是针对其存在的缺点,我们仍然可以进行优化。在进行子类的prototype属性的设置时,可以去掉父类实例的属性和函数。
// 子类
function Cat(name) {
// 继承父类的实例属性和函数
Animal.call(this);
this.name = name;
}
// 立即执行函数
(function () {
// 设置任意函数Super()
var Super = function () {};
// 关键语句,Super()函数的原型指向父类Animal的原型,去掉父类的实例属性
Super.prototype = Animal.prototype;
Cat.prototype = new Super();
Cat.prototype.constructor = Cat;
})();
其中最关键的语句为如下所示的代码。
Super.prototype = Animal.prototype;
只取父类Animal的prototype属性,过滤掉Animal的实例属性,从而避免了父类的实例属性绑定两次。寄生组合继承的方式是实现继承最完美的一种,但是实现起来较为复杂,一般不太容易想到。在大多数情况下,使用组合继承的方式就已经足够,当然能够使用寄生组合继承更好。
备注:摘自于书籍:《Javasript 重难点实例精讲》