1、前言html
继承是面向对象(OOP)语言中的一个最为人津津乐道的概念。许多面对对象(OOP)语言都支持两种继承方式::接口继承 和 实现继承 。java
接口继承只继承方法签名,而实现继承则继承实际的方法。因为js中方法没有签名,在ECMAScript中没法实现接口继承。ECMAScript只支持实现继承,并且其 实现继承
主要是依靠原型链来实现的。数组
2、概念浏览器
2.1简单回顾下构造函数,原型和实例的关系:app
每一个构造函数(constructor)都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针,而实例(instance)都包含一个指向原型对象的内部指针。函数
关系以下图所示:测试
2.2什么是原型链ui
每个对象拥有一个原型对象,经过__proto__指针指向上一个原型,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层层的,最终指向null。这种关系成为原型链(prototype chain)。this
假若有这样一个要求:instance实例经过原型链找到了Father原型中的getFatherValue方法.spa
function Father(){ this.property = true; } Father.prototype.getFatherValue = function(){ return this.property; } function Son(){ this.sonProperty = false; } //继承 Father Son.prototype = new Father();//Son.prototype被重写,致使Son.prototype.constructor也一同被重写 Son.prototype.getSonVaule = function(){ return this.sonProperty; } var instance = new Son(); console.log(instance.getFatherValue());//true
注意: 此时instance.constructor指向的是Father,这是由于Son.prototype中的constructor被重写的缘故.
2.3肯定原型和实例之间的关系
使用原型链后, 咱们怎么去判断原型和实例的这种继承关系呢? 方法通常有两种.
一、第一种是使用 instanceof 操做符。只要用这个操做符来测试实例(instance)与原型链中出现过的构造函数,结果就会返回true. 如下几行代码就说明了这点。
console.log(instance instanceof Object);//true console.log(instance instanceof Father);//true console.log(instance instanceof Son);//true
因为原型链的关系, 咱们能够说instance 是 Object, Father 或 Son中任何一个类型的实例. 所以, 这三个构造函数的结果都返回了true.
二、第二种是使用 isPrototypeOf() 方法,。一样只要是原型链中出现过的原型,isPrototypeOf() 方法就会返回true
console.log(Object.prototype.isPrototypeOf(instance));//true console.log(Father.prototype.isPrototypeOf(instance));//true console.log(Son.prototype.isPrototypeOf(instance));//true
2.4原型链的问题
原型链并不是十分完美, 它包含以下两个问题:
问题一: 当原型链中包含引用类型值的原型时,该引用类型值会被全部实例共享;
问题二: 在建立子类型(例如建立Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数.
有鉴于此, 实践中不多会单独使用原型链。为此,下面将有一些尝试以弥补原型链的不足。
3、借用构造函数(经典继承)
为解决原型链中上述两个问题, 咱们开始使用一种叫作借用构造函数(constructor stealing)的技术(也叫经典继承).
基本思想:即在子类型构造函数的内部调用超类型构造函数。
function Father(){ this.colors = ["red","blue","green"]; } function Son(){ Father.call(this);//继承了Father,且向父类型传递参数 } var instance1 = new Son(); instance1.colors.push("black"); console.log(instance1.colors);//"red,blue,green,black" var instance2 = new Son(); console.log(instance2.colors);//"red,blue,green" 可见引用类型值是独立的
很明显,借用构造函数一举解决了原型链的两大问题:
其一, 保证了原型链中引用类型值的独立,再也不被全部实例共享;
其二, 子类型建立时也可以向父类型传递参数.
随之而来的是, 若是仅仅借用构造函数,那么将没法避免构造函数模式存在的问题–方法都在构造函数中定义, 所以函数复用也就不可用了.并且超类型(如Father)中定义的方法,对子类型而言也是不可见的. 考虑此,借用构造函数的技术也不多单独使用。
4、组合继承
组合继承, 有时候也叫作伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式.
基本思路: 使用原型链实现对原型属性和方法的继承,经过借用构造函数来实现对实例属性的继承.
这样,既经过在原型上定义方法实现了函数复用,又能保证每一个实例都有它本身的属性. 以下所示.
function Father(name){ this.name = name; this.colors = ["red","blue","green"]; } Father.prototype.sayName = function(){ alert(this.name); }; function Son(name,age){ Father.call(this,name);//继承实例属性,第一次调用Father() this.age = age; } Son.prototype = new Father();//继承父类方法,第二次调用Father() Son.prototype.sayAge = function(){ alert(this.age); } var instance1 = new Son("louis",5); instance1.colors.push("black"); console.log(instance1.colors);//"red,blue,green,black" instance1.sayName();//louis instance1.sayAge();//5 var instance1 = new Son("zhai",10); console.log(instance1.colors);//"red,blue,green" instance1.sayName();//zhai instance1.sayAge();//10
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优势,成为 JavaScript 中最经常使用的继承模式. 并且, instanceof 和 isPrototypeOf( )也能用于识别基于组合继承建立的对象.
同时咱们还注意到组合继承其实调用了两次父类构造函数, 形成了没必要要的消耗, 那么怎样才能避免这种没必要要的消耗呢, 这个咱们将在后面讲到。
5、原型继承
该方法最初由道格拉斯·克罗克福德于2006年在一篇题为 《Prototypal Inheritance in JavaScript》(JavaScript中的原型式继承) 的文章中提出. 他的想法是借助原型能够基于已有的对象建立新对象, 同时还没必要所以建立自定义类型. 大意以下:
在object()函数内部, 先建立一个临时性的构造函数, 而后将传入的对象做为这个构造函数的原型,最后返回了这个临时类型的一个新实例。
function object(o){ function F(){} F.prototype = o; return new F(); }
从本质上讲, object() 对传入其中的对象执行了一次浅复制. 下面咱们来看看为何是浅复制.
在 ECMAScript5 中,经过新增 object.create() 方法规范化了上面的原型式继承.
object.create() 接收两个参数:一、一个用做新对象原型的对象;二、(可选的)一个为新对象定义额外属性的对象
var person = { friends : ["Van","Louis","Nick"] }; var anotherPerson = Object.create(person); anotherPerson.friends.push("Rob"); var yetAnotherPerson = Object.create(person); yetAnotherPerson.friends.push("Style"); alert(person.friends);//"Van,Louis,Nick,Rob,Style"
object.create() 只有一个参数时功能与上述object方法相同, 它的第二个参数与Object.defineProperties()方法的第二个参数格式相同: 每一个属性都是经过本身的描述符定义的.以这种方式指定的任何属性都会覆盖原型对象上的同名属性.例如:
var person = { name : "Van" }; var anotherPerson = Object.create(person, { name : { value : "Louis" } }); alert(anotherPerson.name);//"Louis"
目前支持 Object.create() 的浏览器有 IE9+, Firefox 4+, Safari 5+, Opera 12+ 和 Chrome.
提醒: 原型式继承中, 包含引用类型值的属性始终都会共享相应的值, 就像使用原型模式同样.
6、寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路, 一样是克罗克福德推而广之.
寄生式继承的思路与(寄生)构造函数和工厂模式相似, 即建立一个仅用于封装继承过程的函数,该函数在内部以某种方式来加强对象,最后再像真的是它作了全部工做同样返回对象. 以下.
function createAnother(original){ var clone = object(original);//经过调用object函数建立一个新对象 clone.sayHi = function(){//以某种方式来加强这个对象 alert("hi"); }; return clone;//返回这个对象 }
这个例子中的代码基于person返回了一个新对象–anotherPerson. 新对象不只具备 person 的全部属性和方法, 并且还被加强了, 拥有了sayH()方法.
注意: 使用寄生式继承来为对象添加函数, 会因为不能作到函数复用而下降效率;这一点与构造函数模式相似.
7、寄生组合式继承
前面讲过,组合继承是 JavaScript 最经常使用的继承模式; 不过, 它也有本身的不足. 组合继承最大的问题就是不管什么状况下,都会调用两次父类构造函数: 一次是在建立子类型原型的时候, 另外一次是在子类型构造函数内部.
寄生组合式继承就是为了下降调用父类构造函数的开销而出现的
其背后的基本思路是: 没必要为了指定子类型的原型而调用超类型的构造函数
function extend(subClass,superClass){ var prototype = object(superClass.prototype);//建立对象 prototype.constructor = subClass;//加强对象 subClass.prototype = prototype;//指定对象 }
extend的高效率体如今它没有调用superClass构造函数,所以避免了在subClass.prototype上面建立没必要要,多余的属性. 于此同时,原型链还能保持不变; 所以还能正常使用 instanceof 和 isPrototypeOf() 方法。
综上所述有:原型链继承,构造函数继承(经典继承),组合继承,寄生继承,寄生组合继承五种方法,寄生组合式继承,集寄生式继承和组合继承的优势于一身,是实现基于类型继承的最有效方法。
下面咱们来看下extend的另外一种更为有效的扩展.
function extend(subClass, superClass) { var F = function() {}; F.prototype = superClass.prototype; subClass.prototype = new F(); subClass.prototype.constructor = subClass; subClass.superclass = superClass.prototype; if(superClass.prototype.constructor == Object.prototype.constructor) { superClass.prototype.constructor = superClass; } }
我一直不太明白的是为何要 "new F()", 既然extend的目的是将子类型的 prototype 指向超类型的 prototype,为何不直接作以下操做呢?
subClass.prototype = superClass.prototype;//直接指向超类型prototype
显然, 基于如上操做, 子类型原型将与超类型原型共用, 根本就没有继承关系.
8、new操做符
为了追本溯源, 我顺便研究了new运算符具体干了什么?发现其实很简单,就干了三件事情.
var obj = {}; obj.__proto__ = F.prototype; F.call(obj);
第一行,咱们建立了一个空对象obj;
第二行,咱们将这个空对象的__proto__成员指向了F构造函数的prototype对象;
第三行,咱们将F函数对象的this指针替换成obj,而后再调用F函数.
new 操做符调用构造函数的时候,函数内部实际上发生如下变化:
一、建立一个空对象,而且 this 变量引用该对象,同时还继承了该函数的原型。
二、属性和方法被加入到 this 引用的对象中。
三、新建立的对象由 this 所引用,而且最后隐式的返回 this.
9、属性查找
使用了原型链后, 当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止,到查找到达原型链的顶部 - 也就是 Object.prototype - 可是仍然没有找到指定的属性,就会返回 undefined。
此时若想避免原型链查找, 建议使用hasOwnProperty方法. 由于hasOwnProperty是 JavaScript 中惟一一个处理属性可是不查找原型链的函数。
console.log(instance1.hasOwnProperty('age'));//true
对比: isPrototypeOf 则是用来判断该方法实例对象是否是参数的原型对象,是则返回true,不然返回false。如
console.log(Father.prototype.isPrototypeOf(instance1));//true
10、instanceof && typeof
instanceof 运算符是用来在运行时指出对象是不是构造器的一个实例。
instanceof
能够正确的判断对象的类型,由于内部机制是经过判断对象的原型链中是否是能找到类型的 prototype
。
例如漏写了new运算符去调用某个构造器, 此时构造器内部能够经过 instanceof 来判断.(java中功能相似)
function f(){ if(this instanceof arguments.callee) console.log('此处做为构造函数被调用'); else console.log('此处做为普通函数被调用'); } f();//此处做为普通函数被调用 new f();//此处做为构造函数被调用
对比: typeof 则用以获取一个变量或者表达式的类型, 通常只能返回以下几个结果:
一、typeof
对于基本类型,除了 null
均可以显示正确的类型
二、typeof
对于对象,除了函数都会显示 object
三、对于 null
来讲,虽然它是基本类型,可是会显示 object
总的来讲:number,boolean,string,function(函数),object(NULL,数组,对象),undefined。
11、参考
一、《JavaScript高级程序设计》