ES6之前,JS的继承
继承的概念
谈到继承,就不得不谈到类和对象的概念。
类是抽象的,它是拥有共同的属性和行为的抽象实体。
对象是具体的,它除了拥有类共同的属性和行为之外,可能还会有一些独特的属性和行为。
打个比方:
人类,就是一个抽象类,假设人类的共同属性是有手、有脚、有嘴巴,共同的行为是说话。
小明,就是一个具体的对象,他是人类,因此他拥有人类的共同属性和行为(有手有脚有嘴巴+会说话),除此之外,他还拥有一些独特的个性化的属性和行为,比如说他喜欢唱跳rap篮球。
那么什么是继承呢?
类的继承,子类继承父类之后,子类就拥有了父类的属性和行为。
在代码层面,就是子类通过继承来复用父类的代码。
打个比方:
人类,就是一个抽象类,假设人类的共同属性是有手、有脚、有嘴巴,共同的行为是说话。
新人类,继承了人类,它就拥有了人类的共同属性和行为(有手有脚有嘴巴+会说话),可以复用人类的代码。除此之外,他还可以拥有一些自己的独特属性和行为,比如说超能力之类的。
在es6之前,js的构造函数式面向对象语法与传统面向对象编程语言有所区别,js未在语法层面支持继承的操作。因此,js需要通过原型链特性,call、apply等的应用来实现继承。
js继承的最佳实践是“寄生组合式继承”,它结合了原型继承和借用构造函数继承。
原型继承(继承私有属性和原型属性,但无法传参)
根据原型链的特性:在访问实例对象属性的时候,如果没有找到这个属性,就会顺着__proto__去原型中查找。
因此,我们利用这一点,可以将子类构造函数的prototype(原型)指向父类的实例,当子类对象中找不到属性的时候,就会去父类实例中查找,从而让子类继承复用了父类的属性。
// 父类构造函数
function Father() {
this.name = 'father';
}
// 子类构造函数
function Child() {
this.age = 16;
}
// 子类的prototype指向父类的实例
Child.prototype = new Father();
var child = new Child();
console.log(child.name); // father
console.log(child.age); // 16
原型继承的实现:子类的prototype = 父类的实例。
原型继承特点:可以继承父类的私有属性和原型属性。
原型继承的缺点:无法向父类构造函数传参。
借用构造函数继承(可以传参,继承私有属性,但不能继承原型属性)
为了解决原型继承不能向父类构造函数传参的缺点。
借用构造函数继承,使用call或apply方法在子类构造函数中调用父类的构造函数,从而让子类继承父类的私有属性。
// 父类构造函数
function Father(name) {
this.name = name;
}
// 往父类原型上放一个say方法
Father.prototype.say = function() {
return 'i am your father';
}
// 子类构造函数
function Child(name) {
Father.call(this, name);
this.age = 16;
}
Child.prototype.sex = 'man';
var child = new Child('child');
// 调用父类的私有属性name
console.log(child.name); // child
// 调用父类的原型属性say
// console.log(child.say()); // child.say is not a function
// 调用子类的私有属性age
console.log(child.age); // 16
// 调用子类的原型属性sex
console.log(child.sex); // man
借用构造函数继承的实现:构造函数的this指向是指向实例,而在子类构造函数中使用call/apply,将父类构造函数的this指向了子类实例。
借用构造函数继承的特点:1. 在子类中可以给父类传参。2. 可以继承父类的私有属性。
借用构造函数继承的缺点:只能继承父类的私有属性,不能继承父类的原型属性。
组合继承(可以传参,继承私有属性和原型属性,但造成冗余)
借用构造函数继承解决了子类不能给父类传参的问题,但是又带来了无法继承父类原型属性的问题。那如何让子类既能继承父类的原型属性又能继承父类的私有属性呢?
将原型继承和借用构造函数继承结合起来,就是组合继承。
// 父类构造函数
function Father(name) {
this.name = name;
}
// 往父类原型上放一个say方法
Father.prototype.say = function() {
return 'i am your father';
}
// 子类构造函数
function Child(name) {
// 借用构造函数继承
Father.call(this, name);
this.age = 16;
}
// 原型继承
Child.prototype = new Father();
Child.prototype.sex = 'man';
var child = new Child('child');
// 调用父类的私有属性name
console.log(child.name); // child
// 调用父类的原型属性say
console.log(child.say()); // i am your father
// 调用子类的私有属性age
console.log(child.age); // 16
// 调用子类的原型属性sex
console.log(child.sex); // man
上述组合继承看似完美实现了继承,实则还存在有隐患。
那就是父类的name属性,既存在于Father类中,作为它的私有属性。又存在于Child类的prototype中,作为它的原型属性。
组合继承的实现:同时使用原型继承和借用构造函数继承。
组合继承的特点:1. 子类可以继承父类原型属性和私有属性。2. 子类可以给父类传参。
组合继承的缺点:对于父类的私有属性,子类继承时候同时存在于私有属性和原型属性中,造成了冗余。
寄生组合式继承(最佳实践)
解决组合式继承的冗余问题。
在使用借用构造函数继承之后,不用原型继承,而是用其他方法让子类只继承父类的原型属性而不继承父类的私有属性,就可以避免冗余了。
下面说明如何让子类只继承父类的原型属性而不继承父类的私有属性:
如果我们不给子类构造函数的prototype赋值为父类对象,而是赋值为一个只有父类原型属性而没有父类私有属性的对象,那么子类就不会继承到父类的私有属性,只会继承父类的原型属性了。如何生成这样一个对象呢?
// 父类构造函数
function Father(name) {
this.name = name;
}
// 往父类原型上放一个say方法
Father.prototype.say = function() {
return 'i am your father';
}
// 创建一个只拥有父类原型属性的实例对象
function getFatherProtoType(Father) {
function Func() {}
Func.prototype = Father.prototype;
return new Func();
}
// 子类构造函数
function Child(name) {
// 借用构造函数继承
Father.call(this, name);
this.age = 16;
}
Child.prototype = getFatherProtoType(Father);
Child.prototype.sex = 'man';
var child = new Child('child');
// 调用父类的私有属性name
console.log(child.name); // child
// 调用父类的原型属性say
console.log(child.say()); // i am your father
// 调用子类的私有属性age
console.log(child.age); // 16
// 调用子类的原型属性sex
console.log(child.sex); // man
这样就实现了:1. 子类继承父类的私有属性和原型属性。2. 子类可以向父类传递参数。3. 继承后没有冗余属性。
寄生组合继承是js继承的最佳实践。