JavaScript的9种继承实现方式归纳
不同于基于类的编程语言,如C++和Java,JavaScript中的继承方式是基于原型的。同时由于JavaScript是一门非常灵活的语言,其实现继承的方式也非常多。
首要的基本概念是关于构造函数和原型链的,父对象的构造函数称为Parent,子对象的构造函数称为Child,对应的父对象和子对象分别为parent和child。
对象中有一个隐藏属性[[prototype]](注意不是prototype),在Chrome中是__proto__,而在某些环境下则不可访问,它指向的是这个对象的原型。在访问任何一个对象的属性或方法时,首先会搜索本对象的所有属性,如果找不到的话则会根据[[prototype]]沿着原型链逐步搜索其原型对象上的属性,直到找到为止,否则返回undefined。
1.原型链继承:
原型链是JavaScript中实现继承的默认方式,如果要让子对象继承父对象的话,最简单的方式是将子对象构造函数的prototype属性指向父对象的一个实例:
functionParent(){} functionChild(){} Child.prototype=newParent()
这个时候,Child的prototype属性被重写了,指向了一个新对象,但是这个新对象的constructor属性却没有正确指向Child,JS引擎并不会自动为我们完成这件工作,这需要我们手动去将Child的原型对象的constructor属性重新指向Child:
Child.prototype.constructor=Child
以上就是JavaScript中的默认继承机制,将需要重用的属性和方法迁移至原型对象中,而将不可重用的部分设置为对象的自身属性,但这种继承方式需要新建一个实例作为原型对象,效率上会低一些。
2.原型继承(非原型链):
为了避免上一个方法需要重复创建原型对象实例的问题,可以直接将子对象构造函数的prototype指向父对象构造函数的prototype,这样,所有Parent.prototype中的属性和方法也能被重用,同时不需要重复创建原型对象实例:
Child.prototype=Parent.prototype Child.prototype.constructor=Child
但是我们知道,在JavaScript中,对象是作为引用类型存在的,这种方法实际上是将Child.prototype和Parent.prototype中保存的指针指向了同一个对象,因此,当我们想要在子对象原型中扩展一些属性以便之后继续继承的话,父对象的原型也会被改写,因为这里的原型对象实例始终只有一个,这也是这种继承方式的缺点。
3.临时构造器继承:
为了解决上面的问题,可以借用一个临时构造器起到一个中间层的作用,所有子对象原型的操作都是在临时构造器的实例上完成,不会影响到父对象原型:
varF=function(){} F.prototype=Parent.prototype Child.prototype=newF() Child.prototype.constructor=Child
同时,为了可以在子对象中访问父类原型中的属性,可以在子对象构造器上加入一个指向父对象原型的属性,如uber,这样,可以在子对象上直接通过child.constructor.uber访问到父级原型对象。
我们可以将上面的这些工作封装成一个函数,以后调用这个函数就可以方便实现这种继承方式了:
functionextend(Child,Parent){ varF=function(){} F.prototype=Parent.prototype Child.prototype=newF() Child.prototype.constructor=Child Child.uber=Parent.prototype }
然后就可以这样调用:
extend(Dog,Animal)
4.属性拷贝:
这种继承方式基本没有改变原型链的关系,而是直接将父级原型对象中的属性全部复制到子对象原型中,当然,这里的复制仅仅适用于基本数据类型,对象类型只支持引用传递。
functionextend2(Child,Parent){ varp=Parent.prototype varc=Child.prototype for(variinp){ c[i]=p[i] } c.uber=p }
这种方式对部分原型属性进行了重建,构建对象的时候效率会低一些,但是能够减少原型链的查找。不过我个人觉得这种方式的优点并不明显。
5.对象间继承:
除了基于构造器间的继承方法,还可以抛开构造器直接进行对象间的继承。即直接进行对象属性的拷贝,其中包括浅拷贝和深拷贝。
浅拷贝:
接受要继承的对象,同时创建一个新的空对象,将要继承对象的属性拷贝至新对象中并返回这个新对象:
functionextendCopy(p){ varc={} for(variinp){ c[i]=p[i] } c.uber=p returnc }
拷贝完成之后对于新对象中需要改写的属性可以进行手动改写。
深拷贝:
浅拷贝的问题也显而易见,它不能拷贝对象类型的属性而只能传递引用,要解决这个问题就要使用深拷贝。深拷贝的重点在于拷贝的递归调用,检测到对象类型的属性时就创建对应的对象或数组,并逐一复制其中的基本类型值。
functiondeepCopy(p,c){ c=c||{} for(variinp){ if(p.hasOwnProperty(i)){ if(typeofp[i]==='object'){ c[i]=Array.isArray(p[i])?[]:{} deepCopy(p[i],c[i]) }else{ c[i]=p[i] } } } returnc }
其中用到了一个ES5的Array.isArray()方法用于判断参数是否为数组,没有实现此方法的环境需要自己手动封装一个shim。
Array.isArray=function(p){ returnpinstanceofArray }
但是使用instanceof操作符无法判断来自不同框架的数组变量,但这种情况比较少。
6.原型继承:
借助父级对象,通过构造函数创建一个以父级对象为原型的新对象:
functionobject(o){ varn functionF(){} F.prototype=o n=newF() n.uber=o returnn }
这里,直接将父对象设置为子对象的原型,ES5中的Object.create()方法就是这种实现方式。
7.原型继承和属性拷贝混用:
原型继承方法中以传入的父对象为原型构建子对象,同时还可以在父对象提供的属性之外额外传入需要拷贝属性的对象:
functionojbectPlus(o,stuff){ varn functionF(){} F.prototype=o n=newF() n.uber=o
for(variinstuff){ n[i]=stuff[i] } returnn }