JavaScript | 继承
—————————————————————————————————————————————————————————
继承 - ECMAScript只支持实现继承(依靠原型链),不支持接口继承(函数没有签名)
原型链
- 利用原型让一个引用类型继承另一个引用类型的属性和方法,
- 构造函数、原型、实例的关系:每个构造函数都有一个原型对象,原型对象包含一个指向构造函数的指针。实例包含一个指向原型对象的内部指针,在创建实例之后即指向原型对象
- 而当A原型对象的指针指向B个原型对象时(此时A原型对象与B实例同级),就形成了一条原型链。
-
图解:
原型搜索机制:当读取模式访问一个实例属性时,首先会在实例中搜索该属性,如果没有找到该属性则沿着原型链向上查找
在例子<Demo-1>中,调用instance.getSuperValue(),先搜索实例instance,再搜索SubType.prototype,再搜索SuperType.protorype,最后一步才找到该方法。
默认的原型:所有的引用类型默认都继承了Object,所以默认原型的指针都会指向Object.prototype,完整的原型链如下:
instance → SubType.prototype → SuperType.prototype → Object.prototype
-
p.s.
必须替换掉实例的原型后才能给实例添加方法
不能使用对象字面量创建原型方法,这样做会重写原型链,如<Demo-3>
-
缺点:
包含引用类型值(Function Object Array)的原型属性会被所有实例共享,在通过原型来实现继承时,原型实际上会变成另一个类型的实例,所以原先的实例属性就变成了现在的原型属性了。<Demo-4>
在创建子类型的实例时,不能向超类型的构造函数中传递参数。
// "use strict"; // Demo - 1 // SuperType 拥有一个属性和一个方法 // SubType 拥有一个属性和一个方法,又从SuperType那里继承了一个属性一个方法 function SuperType(){ this.property = "111"; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subproperty = "222"; } // p.s.new操作之前,SubType.prototype指向的是function,不允许为function()定义.getSubValue方法,所以要将添加方法放在修改原型指向之后 // 操作之后SubType.prototype指向SuperType SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function(){ // 必须在SubType替换原型之后才能定义 return this.subproperty; } var instance = new SubType(); console.log(instance.property); // 111 console.log(instance.getSuperValue()); // 111 console.log(instance.subproperty); // 222 console.log(instance.getSubValue()); // 222 console.log(instance.constructor); // f SuperType(){} 原本SubType中的constructor属性被重写 // 重写SuperType.getSuperValue() // 如果要重写这个方法,会屏蔽原来的方法 // 换句话说,当通过SubType的实例调用getSuperValue时调用的就是这个重新定义的方法,但通过SuperType的实例调用时还会继续调用原来的方法 var beforeReWrite = new SuperType(); SuperType.prototype.getSuperValue = function(){ console.log("rewrite"); } console.log(instance.getSuperValue()); // rewrite,this.property = undefined console.log(SuperType.prototype.getSuperValue()); // rewrite,this.property = undefined console.log(beforeReWrite.getSuperValue()); // Demo - 2 // 确认原型和实例的关系 console.log(instance instanceof Object); // true console.log(instance instanceof SuperType); // true console.log(instance instanceof SubType); // true // 另一种方法 console.log(Object.prototype.isPrototypeOf(instance)); // true console.log(SuperType.prototype.isPrototypeOf(instance)); // true console.log(SubType.prototype.isPrototypeOf(instance)); // true // Demo - 3 function SuperType2(){ this.property = "1111"; } SuperType2.prototype.getSuperValue = function(){ return this.property; } function SubType2(){ this.subproperty = "2222"; } SubType2.prototype = new SuperType2(); SubType2.prototype = { getSubValue:function(){ return this.subproperty; }, someOtherMethod:function(){ return false; } } var instance2 = new SubType2(); console.log(instance2 instanceof Object); // true console.log(instance2 instanceof SuperType2); // false,原型链被切断 console.log(instance2 instanceof SubType2); // true // console.log(instance2.getSuperValue()); // error // Demo - 4 function SuperType3(){ this.colors = ["red","blue","green"]; } function SubType3(){} SubType3.prototype = new SuperType3(); var instance3 = new SubType3(); instance3.colors.push("black"); console.log(instance3.colors); // ["red", "blue", "green", "black"] var instance4 = new SubType3(); console.log(instance4.colors); // ["red", "blue", "green", "black"]
借用构造函数(伪造对象 / 经典继承)
- 在子类型构造函数的内部调用超类型构造函数
-
优点:
解决了单独使用原型链共享引用类型值属性的问题
可以在子类型构造函数中向超类型构造函数传递参数
-
缺点:
无法避免构造函数模式存在的问题:方法都在构造函数中定义,无法实现函数复用
// "use strict"; function SuperType(name) { this.name = name; this.colors = ["111", "222", "333"]; } function SubType() { SuperType.call(this, "name1"); this.age = 20; } var instance = new SubType(); instance.colors.push("444"); console.log(instance.colors); // ["111", "222", "333", "444"] console.log(instance.name); // name1 console.log(instance.age); // 20 var instance2 = new SubType(); console.log(instance2.colors); // ["111", "222", "333"]
组合继承(伪经典继承)
- 将原型链和借用构造函数组合,使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承
- 对应创建对象 <组合使用构造函数模式和原型模式>
- 优点:最常用
-
缺点:需要调用两次超类型构造函数,一次在创建子函数原型时,另一次在子函数构造函数内部。调用子类型构造函数时需要重写属性
// "use strict"; function SuperType(name) { this.name = name; this.colors = ["111", "222", "333"]; } SuperType.prototype.sayName = function() { console.log(this.name); } function SubType(name, age) { SuperType.call(this, name); // 继承属性 this.age = age; } SubType.prototype = new SuperType(); // 继承方法 SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function() { console.log(this.age); } var instance1 = new SubType("hugh", 20); instance1.colors.push("444"); console.log(instance1.colors); // ["111", "222", "333", "444"] instance1.sayName(); // hugh instance1.sayAge(); // 20 var instance2 = new SubType("dong", 21); console.log(instance2.colors); // ["111", "222", "333"] instance2.sayName(); // dong instance2.sayAge(); // 21
原型式继承
- 对应创建对象 <动态原型模式>
- 没有使用严格意义上的构造函数,借助已有的对象创建新对象
-
优点:
在不想创建构造函数,只想让一个对象与另一个对象保持类似的情况下,原型式继承完全可以胜任
-
缺点:
包含引用类型值的属性始终都会共享,就像原型模式一样
// "use strict"; function object(o){ function F(){} // 创建临时性构造函数 F.prototype = o; // 将传入的对象作为构造函数的原型 return new F(); // 返回临时类型的一个新实例 } var person = { name:"hugh", friends:["111",'222','333'] }; var anotherPerson = object(person); anotherPerson.name = "dong"; anotherPerson.friends.push("444"); var yetAnotherPerson = object(person); yetAnotherPerson.name = "hehe"; yetAnotherPerson.friends.push("555"); console.log(person.friends); // ["111", "222", "333", "444", "555"] console.log(person.name); // hugh console.log(anotherPerson.friends); // ["111", "222", "333", "444", "555"] console.log(anotherPerson.name); // dong console.log(yetAnotherPerson.friends); // ["111", "222", "333", "444", "555"] console.log(yetAnotherPerson.name); // hehe // 使用Object.create()规范化原型式继承 // 以这种方式指定的任何属性都会覆盖原型对象上的同名属性 var otherPerson1 = Object.create(person); otherPerson1.friends.push("666"); console.log(yetAnotherPerson.friends); // ["111", "222", "333", "444", "555", "666"] var otherPerson2 = Object.create(person,{ name:{ value:"test" } }); console.log(otherPerson2.name);
寄生式继承
- 对应创建对象 <寄生构造函数 / 工厂模式>
- 创建一个仅用于封装继承过程的函数,在内部增强对象,最后返回对象
- 示范集成模式时使用的object()函数不是必须的,任何能够返回新对象的函数都适用于此模式
- 使用场景:在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式
-
缺点:无法做到函数复用,类似于构造函数模式
// "use strict"; function object(o) { function F() {} // 创建临时性构造函数 F.prototype = o; // 将传入的对象作为构造函数的原型 return new F(); // 返回临时类型的一个新实例 } function createAnother(original) { // 接收的函数作为新对象基础的对象 var clone = object(original); clone.sayHi = function() { // 添加新方法 console.log('hi'); }; return clone; } var person = { name: "hugh", friends: ['111', '222', '333'] }; var person1 = createAnother(person); person1.sayHi(); console.log(person1.name); console.log(person1.friends);
寄生组合式继承
-
优点:
最理想的继承范式
解决组合继承重写属性的问题,只调用了一次SuperType构造函数
避免了在SubType.prototype上创建不必要的属性
原型链保持不变
能够正常使用instanceof和isPrototypeOf()
"use strict"; function object(o) { function F() {} F.prototype = o; return new F(); } // 1.创建超类型原型的一个副本 // 2.为创建的副本添加constructor属性,弥补因重写原型而失去的属性 // 3.将新创建的对象(即副本)赋值给子类型的原型 function inheritProtoType(subType,superType){ var prototype = object(superType.prototype); // 创建对象 prototype.constructor = subType; // 增强对象 subType.prototype = prototype; // 指定对象 } function SuperType(name){ this.name = name; this.colors= [1,2,3,4]; } SuperType.prototype.sayName = function(){ console.log(this.name); } function SubType(name,age){ SuperType.call(this,name); this.age = age; } inheritProtoType(SubType,SuperType); SubType.prototype.sayAge = function(){ console.log(this.age); }