继承 是 OO 语言中的一个最为人津津乐道的概念。许多 OO 语言都支持两种继承方式:接口继承 和 实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。如前所述,由于函数没有签名,在 ECMAScript 中无法实现接口继承。
ECMAScript 只支持实现继承,而且其 实现继承 主要依靠 原型链 来实现的。
ECMAScript 中将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
function SuperType(){ this.property=true; } SuperType.prototype.getSuperValue=function(){ return this.property; } function SubType(){ this.subproperty=false; } //通过创建SuperType的实例继承了SuperType SubType.prototype=new SuperType(); SubType.prototype.getSubValue=function(){ return this.subproperty; } var instance=new SubType(); alert(instance.getSuperValue()); //true
缺点:
(1)、包含引用类型值的原型属性会被所有实例共享,这会导致对一个实例的修改会影响另一个实例;
(2)、在创建子类型的实例时,不能向超类型的构造函数中传递参数。
由于这两个问题的存在,实践中很少单独使用原型链
下面例子清楚的说明了第一个问题
function SuperType(){ this.colors=["red", "blue", "green"]; } function SubType(){ } //继承了SuperType SubType.prototype=new SuperType(); var instance1=new SubType(); instance1.colors.push("black"); alert(instance1.colors); //red,blue,green,black var instance2=new SubType(); alert(instance2.colors); //red,blue,green,black
在解决原型中包含引用类型值所带来的问题中,使用借用构造函数技术来解决。
借用构造函数的基本思想,即在子类型构造函数的内部调用超类型构造函数。
函数只不过是在特定环境中执行代码的对象,因此通过使用 apply() 和 call() 方法可以在新创建的对象上执行构造函数。
function SuperType(name){ this.name=name; } function SubType(){ //继承了SuperType,同时还传递了参数 SuperType.call(this,"mary"); //实例属性 this.age=22; } var instance=new SubType(); alert(instance.name); //mary alert(instance.age); //29
缺点:
(1)、无法避免构造函数模式存在的问题,方法都在构造函数中定义,因此无法复用函数;
(2)、在超类型的原型中定义的方法,对子类型而言是不可见的。
因此这种技术很少单独使用。
组合继承,指的是将原型链和借用构造函数的技术组合到一起。
思路是使用原型链实现对原型方法的继承,而通过借用构造函数来实现对实例属性的继承。
这样,既通过在原型上定义方法实现了函数的复用,又能够保证每个实例都有它自己的属性。
function SuperType(name){ this.name=name; this.colors=["red", "blue", "green"]; } SuperType.prototype.sayName=function(){ alert(this.name); }; function SubType(name, age){ //继承属性 使用借用构造函数实现对实例属性的继承 SuperType.call(this,name); // 第一次调用 SuperType this.age=age; } //继承方法 使用原型链实现 SubType.prototype=new SuperType(); // 第二次调用 SuperType SubType.prototype.constructor=SubType; subType.prototype.sayAge=function(){ alert(this.age); }; var instance1=new SubType("mary", 22); instance1.colors.push("black"); alert(instance1.colors); //red,blue,green,black instance1.sayName(); //mary instance1.sayAge(); //22 var instance2=new SubType("greg", 25); alert(instance2.colors); //red,blue,green instance2.sayName(); //greg instance2.sayAge(); //25
组合继承避免了原型链和借用构造函数的缺点,融合了他们的优点,是JavaScript中最常用的继承模式。
缺点:无论在什么情况下,都会调用两次超类型构造函数,一次是在创建子类型原型的时候,一次是在子类型构造函数的内部
原型式继承是借助原型可以基于已有的对象创建新对象,同是还不比因此创建自定义类型。
function object(o) { function F() {} F.prototype = o return new F() }
在 object 函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。
本质上来说,object对传入其中的对象执行了一次浅复制。
var person = { name: 'Gaosirs', friends: ['Shelby', 'Court'] } var anotherPerson = object(person) console.log(anotherPerson.friends) // ['Shelby', 'Court']
这种模式要去你必须有一个对象作为另一个对象的基础。
在这个例子中,person作为另一个对象的基础,把person传入object中,该函数就会返回一个新的对象。
这个新对象将person作为原型,所以它的原型中就包含一个基本类型和一个引用类型。
所以意味着如果还有另外一个对象关联了person,anotherPerson修改数组friends的时候,也会体现在这个对象中。
Object.create()方法
ES5通过Object.create()方法规范了原型式继承,可以接受两个参数,一个是用作新对象原型的对象和一个可选的为新对象定义额外属性的对象,行为相同,基本用法和上面的object一样,除了object不能接受第二个参数以外。
var person = { name: 'Gaosirs', friends: ['Shelby', 'Court'] } var anotherPerson = Object.create(person) console.log(anotherPerson.friends) // ['Shelby', 'Court']
寄生式继承的思路寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程中的函数,该函数在内部已某种方式来增强对象,最后返回对象。
function createAnother(o) { var clone = Object.create(o) // 创建一个新对象 clone.sayHi = function() { // 添加方法 console.log('hi') } return clone // 返回这个对象 } var person = { name: 'GaoSirs' } var anotherPeson = createAnother(person) anotherPeson.sayHi()
缺点:使用寄生式继承来为对象添加函数,会因为做不到函数复用而降低效率,这个与构造函数模式类似。
在前面说的组合模式(原型链+构造函数)中,继承的时候需要调用两次父类构造函数。
使用寄生组合式继承,可以规避这些问题。
这种模式通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
本质上就是使用寄生式继承来继承父类的原型,在将结果指定给子类型的原型。
function inheritPrototype(subType, superType) { var prototype = Object.create(superType.prototype); // 创建对象 prototype.constructor = subType; // 增强对象 subType.prototype = prototype; // 指定对象 }
该函数实现了寄生组合模式的最简单形式。
这个函数接受两个参数,一个子类,一个父类。
第一步创建父类原型的副本,第二步将创建的副本添加constructor属性,第三部将子类的原型指向这个副本。
function SuperType(name) { this.name = name this.colors = ['red', 'blue', 'green'] } SuperType.prototype.sayName = function () { console.log(this.name) } function SubType(name, job) { // 继承属性 SuperType.call(this, name) this.job = job } // 继承 inheritPrototype(SubType, SuperType) var instance = new SubType('Gaosirs', 'student') instance.sayName()
补充:直接使用Object.create来实现,其实就是将上面封装的函数拆开,这样演示可以更容易理解。
function SuperType(name) { this.name = name this.colors = ['red', 'blue', 'green'] } SuperType.prototype.sayName = function () { console.log(this.name) } function SubType(name, job) { // 继承属性 SuperType.call(this, name) this.job = job } // 继承 SubType.prototype = Object.create(SuperType.prototype) // 修复constructor SubType.prototype.constructor = SubType var instance = new SubType('Gaosirs', 'student') instance.sayName()
优点:只调用了一次 supertype 构造函数,因此避免在subtype.prototype上创建不必要的,多余的属性,与此同时,原型链还能保持不变,还能正常使用instanceof 和isPrototypeOf(),因此,寄生组合式继承被认为是引用类型最理想的继承范式。
基本用法:Class之间通过使用extends关键字,这比通过修改原型链实现继承,要方便清晰很多
class Colorpoint extends Point { constructor(x,y,color){ super(x,y); //调用父类的constructor(x,y) this.color = color } toString(){ //调用父类的方法 return this.color + ' ' + super.toString(); } }
子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工,如果不调用super方法,子类就得不到this对象。因此,只有调用super之后,才可以使用this关键字。
prototype 和__proto__:
一个继承语句同时存在两条继承链:一条实现属性继承,一条实现方法的继承
class A extends B{} A.__proto__ === B; //继承属性 A.prototype.__proto__ == B.prototype;//继承方法