(九) js继承模式的简单理解
1. 有关原型链
**四句话: **
- 每个函数都有一个
prototype
属性, 指向一个对象, 这个对象我们称之为原型对象
- 原型对象中有一个属性
constructor
, 指向该原型对象的构造函数 - 每个对象都有一个
__proto__
属性 - 对象的
__proto__
属性指向其构造函数的prototype
属性
而原型链就是一系列的__proto__
的指向关系所构成的链式结构
2. 原型链继承
我们知道: 当实例调用方法时, 会先在自身属性中查找, 找不到则从__proto__中查找, 而__proto__
指向其构造函数的prototype
因此所谓的原型链继承, 其实就是对于prototype
上属性的继承
示例
function Father() {
this.hobby = ['唱歌', '跳舞']
}
function Child() { }
Child.prototype = new Father()
var son = new Child()
console.log(son.hobby); // ["唱歌", "跳舞"]
son.hobby.push('弹钢琴')
console.log(son.hobby); // ["唱歌", "跳舞", "弹钢琴"]
var daughter = new Child()
console.log(daughter.hobby); // ["唱歌", "跳舞", "弹钢琴"]
父亲有 唱歌
和 跳舞
的爱好, 也希望自己的孩子有跟自己一样的爱好, 于是父亲的期望诞生了 Child.prototype = new Father()
, 不久后, 父亲有了一个儿子, 儿子的爱好果然跟父亲一样, 随着儿子长大, 还有了自己的新爱好: 弹钢琴
, 这让父亲也很欣慰, 觉得单单培养儿子还不够, 还想再要一个女儿, 后来父亲如愿以偿, 有了一个女儿, 父亲高兴啊, 想着女儿也有跟自己一样的爱好, 但是父亲却发现: 女儿怎么多出一个弹钢琴
的爱好, 自己也没有这个爱好啊, 这是怎么回事 ? 原来啊, 在培养儿子的过程中, 儿子的爱好影响了父亲, 导致父亲也有了 弹钢琴
的技能 (虽然是被动的), 或许父亲觉得这是好事, 但是有些情况下, 这种改变却是一种错误
而这种错误, 也是原型链继承的一种缺陷: 引用类型的属性会被实例所改变
3. 借用构造函数继承
借用构造函数继承是为了解决原型链继承所带来的缺陷而被设计出来的
基本思路: 在子类构造函数中, 调用父类的构造函数
function Father() {
this.hobby = ['唱歌', '跳舞']
}
Father.prototype.surname = '猫'
function Child() {
// 继承 Father
Father.call(this)
}
var son = new Child()
son.hobby.push('弹钢琴')
console.log(son.hobby);
var daughter = new Child()
console.log(daughter.hobby);
果然, 解决了原型链继承
的缺陷, 但是我们再来关注另外一个问题: 孩子肯定是要跟父亲姓 (特殊情况咱们忽略) , 而姓氏也是一种固有属性, 那我们来看一看孩子的姓是不是跟随父亲:
function Father() {
// 自有属性
this.hobby = ['唱歌', '跳舞']
}
// 姓氏为固有属性
Father.prototype.surname = '猫'
function Child() {
Father.call(this)
}
var son = new Child()
var daughter = new Child()
竟然是 undefined
, 这显然是不合理的, 孩子不仅不随父亲姓, 还没有姓 !
而这也恰恰是借用构造函数继承
的一种缺陷: 无法访问父类原型上的属性, 这也就意味着: 所有的继承属性都需要在父类的构造函数中定义, 也就产生了无法复用的问题
本以为解决了问题, 却没想到又引来的新的问题, 因此我们就需要考虑新的解决办法
4. 组合继承
什么是组合继承?
既然 原型链继承
引用类型的属性会导致父类一起改变, 而 借用构造函数继承
又无法访问父类原型上的属性, 那我们干脆 取其精华, 剔其糟粕
: 使用原型链继承原型上的属性和方法, 通过借用构造函数继承实例属性 (也就是父类的自有属性)
示例
// 组合继承: 使用原型链继承原型上的属性和方法, 通过借用构造函数继承实例属性 (也就是父类的自有属性)
function Father() {
this.hobby = ['唱歌', '跳舞']
}
Father.prototype.surname = '猫'
function Child() {
// 通过借用构造函数继承实例属性
Father.call(this)
}
// 使用原型链继承原型上的属性和方法
Child.prototype = new Father()
var son = new Child()
var daughter = new Child()
// 继承父类的自有属性, 并在此基础上添加自己的属性
son.hobby.push('弹钢琴')
console.log('son hobby: ', son.hobby);
console.log('daughter hobby: ', daughter.hobby);
// 但是不会改变父亲
console.log(Child.prototype);
//
console.log('son surname: ', son.surname);
console.log('daughter surname: ', daughter.surname);
皆大欢喜, 这才是我们想要的 ~
总结一下:
- 原型链继承时子类会导致父类的属性值被改变
- 借用构造函数时无法继承父类原型上的属性和方法
- 组合式继承结合上面两者的优点, 完成了完美继承, 即:
- 使用原型链继承原型上的属性和方法, 通过借用构造函数继承实例属性 (也就是父类的自有属性)
其实, 这种看似完美的继承方法也有自己的缺点, 毕竟人无完人, 组合继承的缺点就是: 父类构造函数里面的代码会执行两次
, 第一次是在子类的构造函数里面借用父类的构造函数, 第二次是在原型继承的时候实例化父类
5. 原型式继承
根据高程第3版
里面对原型式继承的描述如下:
- 必须有一个对象可以作为另一个对象的基础
- 可以将这个基础对象传递给object()函数
- 然后根据具体需求对得到的对象加以修改
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
var person = {
name: "Nicholas",
friends: ["shelby", "Count", "Van"]
}
var person1 = object(person);
person1.name = "Gerg";
person1.friends.push("Job");
console.log(person1.name); // Gerg
console.log(person1.friends); // ["shelby", "Count", "Van", "Job"]
var person2 = object(person);
console.log(person2.name); // Nicholas
console.log(person2.friends); // ["shelby", "Count", "Van", "Job"]
相比于 原型链继承, 原型式继承让我们可以不必非得使用构造函数, 也可以是其他任何对象
本质上来说, 原型式继承就是对基础对象的一个 浅拷贝
而es5中的 Object.create() 函数就是基于原型式继承的, 因此我们也可以直接利用该函数
var person = {
name: "Nicholas",
friends: ["shelby", "Count", "Van"]
}
var person1 = Object.create(person);
person1.name = "Gerg";
person1.friends.push("Job");
console.log(person1.name); // Gerg
console.log(person1.friends); // ["shelby", "Count", "Van", "Job"]
var person2 = object(person);
console.log(person2.name); // Nicholas
console.log(person2.friends); // ["shelby", "Count", "Van", "Job"]
6. 寄生式继承
寄生式继承就是在原型式继承的基础上做了一些增强
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
function createAnother(original) {
var clone = object(original); //通过调用函数创建一个新对象
clone.sayHi = function () { //以某种方式来增强这个对象
alert("hi");
};
return clone; //返回这个对象
}
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var p1 = createAnother(person);
console.log(p1.sayHi);
简化一下
function cloneAndStrengthen(proto){
function F () {}
F.prototype = proto
let f = new F()
f.say = function() {
console.log('I am a person')
}
return f
}
缺点: 为实例对象添加的新属性和方法会类似构造函数继承一样, 无法做到复用
7. 寄生组合式继承
上面提到 组合继承
的一个缺陷: 父类构造函数里面的代码会执行两次
寄生组合式继承就是为了解决这个缺陷
实现的基本模式如下
// sub: 子类构造函数 sup: 超类构造函数
function inherit(sub, sup){
let prototype = clone(sup.prototype)
prototype.constructor = sub
sub.prototype = prototype
}
来使用一下
function object(proto) {
function F() { }
F.prototype = proto
return new F()
}
function inherit(sub, sup) {
let prototype = object(sup.prototype)
prototype.constructor = sub
sub.prototype = prototype
}
function Father() {
this.hobby = ['唱歌', '跳舞']
}
Father.prototype.surname = '猫'
function Child() {
Father.call(this)
}
inherit(Child, Father)
var son = new Child()
var daughter = new Child()
// 继承父类的自有属性, 并在此基础上添加自己的属性
son.hobby.push('弹钢琴')
console.log('son hobby: ', son.hobby);
console.log('daughter hobby: ', daughter.hobby);
// 但是不会改变父亲
console.log(Child.prototype);
//
console.log('son surname: ', son.surname);
console.log('daughter surname: ', daughter.surname);