在JavaScript中,对象的建立能够脱离类型(class free),经过字面量的方式能够很方便的建立出自定义对象。函数
另外,JavaScript中拥有原型这个强大的概念,当对象进行属性查找的时候,若是对象自己内找不到对应的属性,就会去搜索原型链。因此,结合原型和原型链的这个特性,JavaScript就能够用来实现对象之间的继承了。this
下面就介绍一下JavaScript中的一些经常使用的继承方式。spa
因为原型链搜索的这个特性,在JavaScript中能够很方便的经过原型链来实现对象之间的继承。prototype
下面看一个例子:code
function Person(name, age){ this.name = name; this.age = age; } Person.prototype.getInfo = function(){ console.log(this.name + " is " + this.age + " years old!"); } function Teacher(staffId){ this.staffId = staffId; } Teacher.prototype = new Person(); var will = new Teacher(1000); will.name = "Will"; will.age = 28; will.getInfo(); // Will is 28 years old! console.log(will instanceof Object); // true console.log(will instanceof Person); // true console.log(will instanceof Teacher); // true console.log(Object.prototype.isPrototypeOf(will)); // true console.log(Person.prototype.isPrototypeOf(will)); // true console.log(Teacher.prototype.isPrototypeOf(will)); // true
在这个例子中,有两个构造函数"Person"和"Teacher",经过"Teacher.prototype = new Person()"语句建立了一个"Person"对象,而且设置为"Teacher"的原型。对象
经过这种方式,就实现了"Teacher"继承"Person","will"这个对象能够成功的调用"getInfo"这个属于"Person"的方法。blog
在这个例子中,还演示了经过"instanceof"操做符和"isPrototypeOf()"方法来查看对象和原型之间的关系。继承
对于原型链继承,下面看看其中的一些细节问题。ip
对于全部的JavaScript原型对象,都有一个"constructor"属性,该属性对应用来建立对象的构造函数。ci
对于"constructor"这个属性,最大的做用就是能够帮咱们标明一个对象的"类型"。
在JavaScript中,当经过"typeof"查看Array对象的时候,返回的结果是"object"。这个咱们的预期结果,因此若是要判对一个对象究竟是不是Array类型,就能够结合"constructor"属性获得想要的结果。
function isArray(myArray) { return myArray.constructor.toString().indexOf("Array") > -1; } var arr = [] console.log(typeof arr); // object console.log(isArray(arr)); // true
如今回到前面的例子,查看一下对象"will"的原型和构造函数:
从这个结果能够看到,"will"的原型是"Person {name: undefined, age: undefined}"(经过new Person()构造出来的对象),"will"的构造函数是"function Person"。
等等,"will"不是经过"Teacher"建立出来的对象么?为何构造函数对于的是"function Person",而不是"function Teacher"?
下面,根据前面的例子绘制一张对象关系图,从而分析一下继承关系以及"constructor"属性:
图中给出了各类对象之间的关系,有几点须要注意的是:
为了解决上面的问题,让子类对象的"constructor"属性对应正确的构造函数,咱们能够重设子类原型对象的"constructor"属性。
通常来讲,能够简单的使用下面代码来重设"constructor"属性:
Teacher.prototype.constructor = Teacher;
可是经过这种方式重设"constructor"属性会致使它的[[Enumerable]]特性被设置为 true。默认状况下,原生的"constructor"属性是不可枚举的。
所以若是使用兼容 ECMAScript 5 的 JavaScript 引擎,就可使用"Object.defineProperty()":
Object.defineProperty(Teacher.prototype, "constructor", { enumerable: false, value: Teacher });
经过下面的结果能够看到:
经过这个设置,对象"will" 的"constructor"属性就指向了正确的"function Teacher"。
这时的对象关系图就变成了以下,跟前面的关系图比较,惟一的区别就是"Teacher.prototype"对象多了一个"constructor"属性,而且这个属性指向"function Teacher":
原型对象是能够修改的,因此,当建立了继承关系以后,咱们能够经过更新子类的原型对象给子类添加特有的方法。
例如经过下面的方式就给子类添加了一个特有的"getId"方法。
Teacher.prototype.getId = function(){ console.log(this.name + "'s staff Id is " + this.staffId); } will.getId(); // Will's staff Id is 1000
可是,必定要区分原型的修改和原型的重写。若是对原型进行了重写,就会产生彻底不一样的效果。
下面看看若是对"Teacher"的原型重写会产生什么效果,为了分清跟前面代码的顺序,这里贴出了完整的代码:
function Person(name, age){ this.name = name; this.age = age; } Person.prototype.getInfo = function(){ console.log(this.name + " is " + this.age + " years old!"); } function Teacher(staffId){ this.staffId = staffId; } Teacher.prototype = new Person(); Object.defineProperty(Teacher.prototype, "constructor", { enumerable: false, value: Teacher }); var will = new Teacher(1000); will.name = "Will"; will.age = 28;
// 更新原型 Teacher.prototype.getId = function(){ console.log(this.name + "'s staff Id is " + this.staffId); } will.getId(); // Will's staff Id is 1000
// 重写原型 Teacher.prototype = { getStaffId: function(){ console.log(this.name + "'s staff Id is " + this.staffId); } } will.getInfo(); // Will is 28 years old! will.getId(); // Will's staff Id is 1000 console.log(will.__proto__); // Person {name: undefined, age: undefined} console.log(will.__proto__.constructor); // function Teacher var wilber = new Teacher(1001); wilber.name = "Wilber"; wilber.age = 28; // wilber.getInfo(); // Uncaught TypeError: wilber.getInfo is not a function(…) wilber.getStaffId(); // Wilber's staff Id is 1001 console.log(wilber.__proto__); // Object {} console.log(wilber.__proto__.constructor); // function Object() { [native code] }
通过重写原型以后状况更加复杂了,下面就来看看重写原型以后的对象关系图:
从关系图能够看到:
在经过原型链方式实现的继承中,父类和子类的构造函数相对独立,若是子类构造函数能够调用父类的构造函数,而且进行相关的初始化,那就比较好了。
这时就想到了JavaScript中的call方法,经过这个方法能够动态的设置this的指向,这样就能够在子类的构造函数中调用父类的构造函数了。
这样就有了组合继承这种方式:
function Person(name, age){ this.name = name; this.age = age; } Person.prototype.getInfo = function(){ console.log(this.name + " is " + this.age + " years old!"); } function Teacher(name, age, staffId){ Person.call(this, name, age); // 经过call方法来调用父类的构造函数进行初始化 this.staffId = staffId; } Teacher.prototype = new Person(); Object.defineProperty(Teacher.prototype, "constructor", { enumerable: false, value: Teacher }); var will = new Teacher("Will", 28, 1000); will.getInfo(); console.log(will.__proto__); // Person {name: undefined, age: undefined} console.log(will.__proto__.constructor); // function Teacher
在这个例子中,在子类构造函数"Teacher"中,直接经过"Person.call(this, name, age);"的方式调用了父类的构造函数,进而设置了"name"和"age"属性(但这里依旧是覆盖了父类的"name"和"age"属性)。
组合式继承是比较经常使用的一种继承方法,其背后的思路是使用原型链实现对原型属性和方法的继承,而经过借用构造函数来实现对实例属性的继承。这样,既经过在原型上定义方法实现了函数复用,又保证每一个实例都有它本身的属性。
虽然组合继承是 JavaScript 比较经常使用的继承模式,不过经过前面组合继承的代码能够看到,它也有一些小问题。
首先,子类会调用两次父类的构造函数:
子类型最终会包含超类型对象的所有实例属性,但咱们不得不在调用子类型构造函数时重写这些属性,从下图能够看到"will"对象中有两份"name"和"age"属性。
后面,咱们会看到如何经过"寄生组合式继承"来解决组合继承的这个问题。
在前面两种方式中,都须要用到对象以及建立对象的构造函数(类型)来实现继承。
可是在JavaScript中,建立对象彻底不须要定义一个构造函数(类型),经过字面量的方式就能够建立一个自定义的对象。
为了实现对象之间的直接继承,就有了原型式继承。
这种继承方式方法并无使用严格意义上的构造函数,而是直接借助原型基于已有的对象建立新对象,同时还没必要建立自定义类型(构造函数)。为了达到这个目的,咱们能够借助下面这个函数:
function object(o){ function F(){} F.prototype = o; return new F(); }
在 "object()"函数内部,先建立了一个临时性的构造函数,而后将传入的对象做为这个构造函数的原型,最后返回了这个临时类型的一个新实例。
下面看看使用"object()"函数实现的对象之间的继承:
var utilsLibA = { add: function(){ console.log("add method from utilsLibA"); }, sub: function(){ console.log("sub method from utilsLibA"); } } var utilsLibB = object(utilsLibA); utilsLibB.add = function(){ console.log("add method from utilsLibB"); } utilsLibB.div = function(){ console.log("div method from utilsLibB"); } utilsLibB.add(); // add method from utilsLibB utilsLibB.sub(); // sub method from utilsLibA utilsLibB.div(); // div method from utilsLibB
经过原型式继承,基于"utilsLibA"建立了一个"utilsLibB"对象,而且能够正常工做,下面看看对象之间的关系:
经过"object()"函数的帮助,将"utilsLibB"的原型赋值为"utilsLibA",对于这个原型式继承的例子,对象关系图以下,"utilsLibB"的"add"方法覆盖了"utilsLibA"的"add"方法:
ECMAScript 5 经过新增 "Object.create()"方法规范化了原型式继承。这个方法接收两个参数:
在传入一个参数的状况下,"Object.create()"与 上面的"object"函数行为相同。关于更多"Object.create()"的内容,请参考MDN。
继续上面的例子,此次使用"Object.create()"来建立对象"utilsLibC":
utilsLibC = Object.create(utilsLibA, { sub: { value: function(){ console.log("sub method from utilsLibC"); } }, mult: { value: function(){ console.log("mult method from utilsLibC"); } }, }) utilsLibC.add(); // add method from utilsLibA utilsLibC.sub(); // sub method from utilsLibC utilsLibC.mult(); // mult method from utilsLibC console.log(utilsLibC.__proto__); // Object {add: (), sub: (), __proto__: Object} console.log(utilsLibC.__proto__.constructor); // function Object() { [native code] }
寄生式继承是与原型式继承紧密相关的一种思路,寄生式继承的思路与寄生构造函数和工厂模式相似,即建立一个仅用于封装继承过程的函数,该函数在内部以某种方式来加强对象,最后再像真地是它作了全部工做同样返回对象。
如下代码示范了寄生式继承模式,其实就是封装"object()"函数的调用,以及对新的对象进行自定义的一些操做:
function create(o){ var f= object(o); // 经过原型式继承建立一个新对象 f.run = function () { // 以某种方式来加强这个对象 return this.arr; }; return f; // 返回对象 }
所谓寄生组合式继承,即经过借用构造函数来继承属性,经过原型链的混成形式来继承方法。
其背后的基本思路是:没必要为了指定子类型的原型而调用超类型的构造函数,咱们所须要的无非就是父类型原型的一个副本而已。本质上,就是使用寄生式继承来继承父类型的原型,而后再将结果指定给子类型的原型。
注意在寄生组合式继承中使用的“inheritPrototype()”函数。
function object(o) { function F() {} F.prototype = o; return new F(); } function inheritPrototype(subType, superType) { var prototype = object(superType.prototype); // 建立对象 prototype.constructor = subType; // 加强对象,设置constructor属性 subType.prototype = prototype; // 指定对象 } function Person(name, age){ this.name = name; this.age = age; } Person.prototype.getInfo = function(){ console.log(this.name + " is " + this.age + " years old!"); } function Teacher(name, age, staffId){ Person.call(this, name, age) this.staffId = staffId; } inheritPrototype(Teacher, Person); Teacher.prototype.getId = function(){ console.log(this.name + "'s staff Id is " + this.staffId); } var will = new Teacher("Will", 28, 1000); will.getInfo(); // Will is 28 years old! will.getId(); // Will's staff Id is 1000 var wilber = new Teacher("Wilber", 29, 1001); wilber.getInfo(); // Wilber is 29 years old! wilber.getId(); // Wilber's staff Id is 1001
代码中有一处地方须要注意,给子类添加"getId"方法的代码("Teacher.prototype.getId")必定要放在"inheritPrototype()"函数调用以后,由于在“inheritPrototype()”函数中会重写“Teacher”的原型。
下面继续查看一下对象"will"的原型和"constructor"属性。
这个示例中的" inheritPrototype()"函数实现了寄生组合式继承的最简单形式。这个函数接收两个参数:子类型构造函数和父类型构造函数。
在函数内部,第一步是建立超类型原型的一个副本。第二步是为建立的副本添加 "constructor" 属性,从而弥补因重写原型而失去的默认的 "constructor" 属性。最后一步,将新建立的对象(即副本)赋值给子类型的原型。这样,咱们就能够用调用 "inheritPrototype()"函数的语句,去替换前面例子中为子类型原型赋值的语句了("Teacher.prototype = new Person();")。
对于这个寄生组合式继承的例子,对象关系图以下:
本文介绍了JavaScirpt中的 几种经常使用继承方式,咱们能够经过构造函数实现继承,也能够直接基于现有的对象来实现继承。
不管哪一种继承的实现,本质上都是经过JavaScript中的原型特性,结合原型链的搜索实现继承。
与其说"JavaScript是一种面向对象的语言",更恰当的能够说"JavaScript是一种基于对象的语言"。
经过了这些介绍,相信你必定对JavaScript的继承有了一个比较清楚的认识了。