不一样于基于类的编程语言,如 C++ 和 Java,JavaScript 中的继承方式是基于原型的。同时因为 JavaScript 是一门很是灵活的语言,其实现继承的方式也很是多。javascript
首要的基本概念是关于构造函数和原型链的,父对象的构造函数称为Parent
,子对象的构造函数称为Child
,对应的父对象和子对象分别为parent
和child
。java
对象中有一个隐藏属性[[prototype]]
(注意不是prototype
),在 Chrome 中是__proto__
,而在某些环境下则不可访问,它指向的是这个对象的原型。在访问任何一个对象的属性或方法时,首先会搜索本对象的全部属性,若是找不到的话则会根据[[prototype]]
沿着原型链逐步搜索其原型对象上的属性,直到找到为止,不然返回undefined
。编程
原型链是 JavaScript 中实现继承的默认方式,若是要让子对象继承父对象的话,最简单的方式是将子对象构造函数的prototype
属性指向父对象的一个实例:数组
function Parent() {} function Child() {} Child.prototype = new Parent()
这个时候,Child
的prototype
属性被重写了,指向了一个新对象,可是这个新对象的constructor
属性却没有正确指向Child
,JS 引擎并不会自动为咱们完成这件工做,这须要咱们手动去将Child
的原型对象的constructor
属性从新指向Child
:app
Child.prototype.constructor = Child
以上就是 JavaScript 中的默认继承机制,将须要重用的属性和方法迁移至原型对象中,而将不可重用的部分设置为对象的自身属性,但这种继承方式须要新建一个实例做为原型对象,效率上会低一些。框架
为了不上一个方法须要重复建立原型对象实例的问题,能够直接将子对象构造函数的prototype
指向父对象构造函数的prototype
,这样,全部Parent.prototype
中的属性和方法也能被重用,同时不须要重复建立原型对象实例:编程语言
Child.prototype = Parent.prototype Child.prototype.constructor = Child
可是咱们知道,在 JavaScript 中,对象是做为引用类型存在的,这种方法其实是将Child.prototype
和Parent.prototype
中保存的指针指向了同一个对象,所以,当咱们想要在子对象原型中扩展一些属性以便以后继续继承的话,父对象的原型也会被改写,由于这里的原型对象实例始终只有一个,这也是这种继承方式的缺点。函数
为了解决上面的问题,能够借用一个临时构造器起到一个中间层的做用,全部子对象原型的操做都是在临时构造器的实例上完成,不会影响到父对象原型:ui
var F = function() {} F.prototype = Parent.prototype Child.prototype = new F() Child.prototype.constructor = Child
同时,为了能够在子对象中访问父类原型中的属性,能够在子对象构造器上加入一个指向父对象原型的属性,如uber
,这样,能够在子对象上直接经过child.constructor.uber
访问到父级原型对象。this
咱们能够将上面的这些工做封装成一个函数,之后调用这个函数就能够方便实现这种继承方式了:
function extend(Child, Parent) { var F = function() {} F.prototype = Parent.prototype Child.prototype = new F() Child.prototype.constructor = Child Child.uber = Parent.prototype }
而后就能够这样调用:
extend(Dog, Animal)
这种继承方式基本没有改变原型链的关系,而是直接将父级原型对象中的属性所有复制到子对象原型中,固然,这里的复制仅仅适用于基本数据类型,对象类型只支持引用传递。
function extend2(Child, Parent) { var p = Parent.prototype var c = Child.prototype for (var i in p) { c[i] = p[i] } c.uber = p }
这种方式对部分原型属性进行了重建,构建对象的时候效率会低一些,可是可以减小原型链的查找。不过我我的以为这种方式的优势并不明显。
除了基于构造器间的继承方法,还能够抛开构造器直接进行对象间的继承。即直接进行对象属性的拷贝,其中包括浅拷贝和深拷贝。
接受要继承的对象,同时建立一个新的空对象,将要继承对象的属性拷贝至新对象中并返回这个新对象:
function extendCopy(p) { var c = {} for (var i in p) { c[i] = p[i] } c.uber = p return c }
拷贝完成以后对于新对象中须要改写的属性能够进行手动改写。
浅拷贝的问题也显而易见,它不能拷贝对象类型的属性而只能传递引用,要解决这个问题就要使用深拷贝。深拷贝的重点在于拷贝的递归调用,检测到对象类型的属性时就建立对应的对象或数组,并逐一复制其中的基本类型值。
function deepCopy(p, c) { c = c || {} for (var i in p) { if (p.hasOwnProperty(i)) { if (typeof p[i] === 'object') { c[i] = Array.isArray(p[i]) ? [] : {} deepCopy(p[i], c[i]) } else { c[i] = p[i] } } } return c }
其中用到了一个 ES5 的Array.isArray()
方法用于判断参数是否为数组,没有实现此方法的环境须要本身手动封装一个 shim。
Array.isArray = function(p) { return p instanceof Array }
可是使用instanceof
操做符没法判断来自不一样框架的数组变量,但这种状况比较少。
借助父级对象,经过构造函数建立一个以父级对象为原型的新对象:
function object(o) { var n function F() {} F.prototype = o n = new F() n.uber = o return n }
这里,直接将父对象设置为子对象的原型,ES5 中的 Object.create()
方法就是这种实现方式。
原型继承方法中以传入的父对象为原型构建子对象,同时还能够在父对象提供的属性以外额外传入须要拷贝属性的对象:
function ojbectPlus(o, stuff) { var n function F() {} F.prototype = o n = new F() n.uber = o for (var i in stuff) { n[i] = stuff[i] } return n }
这种方式不涉及原型链的操做,传入多个须要拷贝属性的对象,依次进行属性的全拷贝:
function multi() { var n = {}, stuff, i = 0, len = arguments.length for (i = 0; i < len; i++) { stuff = arguments[i] for (var key in stuff) { n[i] = stuff[i] } } return n }
根据对象传入的顺序依次进行拷贝,也就是说,若是后传入的对象包含和前面对象相同的属性,后者将会覆盖前者。
JavaScript中的call()
和apply()
方法很是好用,其改变方法执行上下文的功能在继承的实现中也能发挥做用。所谓构造器借用是指在子对象构造器中借用父对象的构造函数对this
进行操做:
function Parent() {} Parent.prototype.name = 'parent' function Child() { Parent.apply(this, arguments) } var child = new Child() console.log(child.name)
这种方式的最大优点就是,在子对象的构造器中,是对子对象的自身属性进行彻底的重建,引用类型的变量也会生成一个新值而不是一个引用,因此对子对象的任何操做都不会影响父对象。
而这种方法的缺点在于,在子对象的构建过程当中没有使用过new
操做符,所以子对象不会继承父级原型对象上的任何属性,在上面的代码中,child
的name
属性将会是undefined
。
要解决这个问题,能够再次手动将子对象构造器原型设为父对象的实例:
Child.prototype = new Parent()
但这样又会带来另外一个问题,即父对象的构造器会被调用两次,一次是在父对象构造器借用过程当中,另外一次是在继承原型过程当中。
要解决这个问题,就要去掉一次父对象构造器的调用,构造器借用不能省略,那么只能去掉后一次调用,实现继承原型的另外一方法就是迭代复制:
extend2(Child, Parent)
使用以前实现的extend2()
方法便可。