题记
关于继承已经是老生常谈的事情了,不管是css的继承还是js的继承,都已经出了不少文章,本次将继续和大家一起探讨js中面向对象的继承。由于js
不像java
那样是完全面向对象的语言,js
是基于对象的,它没有类的概念。所以,要想实现继承,一般都是基于原型链的方式;
一、继承初探 大多数JavaScript的实现用 __proto__
属性来表示一个对象的原型链。
我们可以简单的把prototype
看做是一个模版,新创建的自定义对象都是这个模版(prototype
)的一个拷贝 (实际上不是拷贝而是链接,只不过这种链接是不可见,新实例化的对象内部有一个看不见的__proto__
指针,指向原型对象)
当查找一个对象的属性时,JavaScript 会向上遍历原型链,直到找到给定名称的属性为止。查找方式可以这样表示:
1 2 3 4 5 6 7 8 9 10 11 function getProperty (obj, prop ) {if (obj.hasOwnProperty (prop)) {return obj[prop];} else if (obj.__proto__ !== null ) {return getProperty (obj.__proto__ , prop);} else {return undefined ;} }
我们在js中使用面向对象很多时候是这样子的:
1 2 3 4 5 6 7 8 9 10 11 12 function Person (name,age ){this .name = name;this .age = age;} Person .prototype .printInfo = function ( ){console .log (this .name + ': ' + this .age );}; var person = new Person ('Jack' ,17 );person.printInfo ();
而变一下,可以发现,这种方式也是可以的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var Person = {name : 'name' , age : 'age' ,printInfo : function ( ){console .log (this .name + ': ' + this .age );} }; var person = {name : 'Jack' ,age : 17 ,__proto__ : Person }; person.printInfo ();
其实这里就是通过将 proto 指向了Person从而达到了原型继承的目的(这也许也是后续某种继承方式的来源)
以上两种方式是等价的,但我们看到的更多还是new方式来产生实例对象,其实new方式也是通过继承方式实现的,那一个new 究竟做了什么操作呢?
有两个版本,哪个比较中肯就用哪个吧
1) 1、创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型(即把__proto__属性设置为该对象的prototype。2、属性和方法被加入到 this 引用的对象中(使用apply传参调用)。3、新创建的对象由 this 所引用,并且最后隐式的返回实例。 用代码实现应该就是这样的
1 2 3 4 5 6 7 8 9 function new (f) {var n = { '__proto__' : f.prototype }; return function ( ) {f.apply (n, arguments ); return n; }; }
2)
1 2 3 var obj = {};obj.__proto__ = Base .prototype ; Base .call (obj);
不过我用代码实现的时候,两种情况都出现了无线调用堆栈溢出的情况,也许new的操作内部没那么简单
二、继承方式概览 说了那么多new 也乱了,不如直接切入正题,谈谈js流行的几种继承方式
1)对象冒充 对象冒充也分为几类 – 添加临时属性、apply/call等方式
添加临时属性
缺点是只能继承显示指明的属性,原型上的属性没办法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function Parent (name ){this .name = name;this .words = 'words' ;this .say = function ( ){console .log (this .name + ': ' + this .words );}; } Parent .prototype .say1 = function ( ){console .log (this .name + ': ' + this .words );}; function Child (name ){this .temp = Parent ;this .temp (name);delete this .temp ;} var child = new Child ('child' );child.say (); child.say1 ();
call/apply
实际上是改变了Parent中this的指向,原理跟上个方法一样,但也不能拿到原型的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function Parent (name ){this .name = name;this .words = 'words' ;this .say = function ( ){console .log (this .name + ': ' + this .words );}; } Parent .prototype .say1 = function ( ){console .log (this .name + ': ' + this .words );}; function Child (name ){Parent .apply (this ,[name]);} var child = new Child ('child' );child.say (); child.say1 ();
对象冒充还有一个缺点就是易造成内存的浪费
因为每次冒充的过程都需要实例化一次父对象,而每次实例化的过程,this显示指明的属性将在每个实例中独立存在,不会共用。
比如say()这种方法,每次调用Child都会新产生并。而原型上的say1()方法就可以共用。
2)原型链继承 这种继承方式也许是最常见的了:将父类的新实例赋值给构造函数的原型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function Parent (name ){this .name = name;this .words = 'words' ;this .say = function ( ){console .log (this .name + ': ' + this .words );}; } Parent .prototype .say1 = function ( ){console .log (this .name + ': ' + this .words );}; function Child (name ){this .name = name;} Child .prototype = new Parent ();Child .prototype .constructor = Child ;var child = new Child ('child' );child.say (); child.say1 ();
可以看到,child不仅可以继承到parent的say()也能拿到say1() ,关键点在于 new Parent()这个new操作
根据最开始我们谈到的new操作,可以知道它具体干了什么
下面来一个变体,这种方式也行,虽然不必在Child中再次定义this.name ,但再次new Child()时,就不能更新我们需要的值。
所以这应该也算是原型链继承的一个不足吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function Parent (name ){this .name = name;this .words = 'words' ;} Parent .prototype .say1 = function ( ){console .log (this .name + ': ' + this .words );}; function Child ( ){}Child .prototype = new Parent ('child' );Child .prototype .constructor = Child ;var child = new Child ('newChild' );child.say1 (); var p = new Parent ();p.say1 ();
3) 原型链+对象冒充(借用构造函数) 原型链方式和对象冒充方式都各有缺陷,两者的缺陷正是对方的优势。两者一结合,自然又是一个好方法,就叫它组合继承吧。
它背后的思路是 使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又保证每个实例都有它自己的属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function Parent (name ){this .name = name;this .words = 'words' ;} Parent .prototype .say1 = function ( ){console .log (this .name + ': ' + this .words );}; function Child (name ){Parent .call (this ,name);} Child .prototype = new Parent ();Child .prototype .constructor = Child ;var child = new Child ('child' );child.say1 ();
4)直接继承父类的 prototype 我们知道了原型链的继承是 Child.prototype = new Parent(); 那可不可以跳过实例化父类,直接拿Parent的原型呢?
Child.prototype = Parent.prototype; 其实这也是可以的,来看个例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function Parent (name ){this .name = name;this .words = 'words' ;} Parent .prototype .age = 30 ;Parent .prototype .sayAge = function ( ){console .log (this .age );}; Parent .prototype .say = function ( ){console .log (this .name + ': ' + this .words );}; function Child (name ){} Child .prototype = Parent .prototype ;Child .prototype .constructor = Child ;var child = new Child ('child' );child.say (); child.sayAge ();
可以看到,这种方式仅仅只能拿到父类的原型属性,实例上的name和words属性就拿不到了。
如果想拿,那就使用Parent.call(this.name)就可以啦。
由此看来,直接用prototype应该会更快,因为不需要像上一个方法那样实例化一个对象耗时。但也是有缺点的。
缺点是 Child.prototype和Parent.prototype现在指向了同一个对象,那么任何对Child.prototype的修改,都会反映到Parent.prototype。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function Parent (name ){this .name = name;this .words = 'words' ;} Parent .prototype .age = 30 ;Parent .prototype .sayAge = function ( ){console .log (this .age );}; Parent .prototype .say = function ( ){console .log (this .name + ': ' + this .words );}; function Child (name ){Parent .call (this ,name);} Child .prototype = Parent .prototype ;Child .prototype .constructor = Child ;var child = new Child ('child' );child.say (); child.sayAge (); Child .prototype .age = 40 ;console .log (Parent .prototype .age );
可以看到父级的原型也被更改了,而原型链继承的方式则不会。
但聪明的人类想出了一个好办法:用一个空对象作为中介,再利用操作prototype,
既避免了实例化对象产生太多的耗时,又避免的父子prototype混用的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function Parent (name ){this .name = name;this .words = 'words' ;} Parent .prototype .age = 30 ;Parent .prototype .sayAge = function ( ){console .log (this .age );}; Parent .prototype .say = function ( ){console .log (this .name + ': ' + this .words );}; function Child (name ){Parent .call (this ,name);} function extend (Child,Parent ){function F ( ){}F.prototype = Parent .prototype ; Child .prototype = new F ();Child .prototype .constructor = Child ;} extend (Child ,Parent );var child = new Child ('child' );child.say (); child.sayAge (); Child .prototype .age = 40 ;console .log (Parent .prototype .age );
5)原型式继承 这种继承借助原型并基于已有的对象创建新对象,同时还不用创建自定义类型的方式称为原型式继承。
可以封装成一个方法,这方法其实只做一件事,就是把子对象的prototype属性,指向父对象,从而使得子对象与父对象连在一起。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 var Parent = {name : 'parent' , words : ['word1' ,'word2' ],say : function ( ){console .log (this .name + ': ' + this .words );} }; Object .create = function (Parent ){function F ( ){}F.prototype = Parent ; return new F ();}; var Child = Object .create (Parent );Child .say ();Child .name = 'child' ;Child .words .push ('word3' );Child .say ();Parent .say ();
Child继承了父类的属性方法后就可以自行更新属性值或再定义了,不过这里存在一个属性共享问题。
如果是引用类型的数据,比如Object ,就比如Child往words里添加了一项,父类也会被更新,造成某种程度上的问题。
而解决引用类型数据共享问题的方法,一般就是不继承该属性,或者
6)把父对象的属性,全部拷贝给子对象 除了使用”prototype链”以外,还有另一种思路:把父对象的属性,全部拷贝给子对象,也能实现继承。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 var Parent = {name : 'parent' , words : ['word1' ,'word2' ],say : function ( ){console .log (this .name + ': ' + this .words );} }; function extendCopy (obj ){var newObj = {};for (var item in obj){newObj[item] = obj[item]; } return newObj;} var Child = extendCopy (Parent );Child .say (); Child .name = 'child' ;Child .words .push ('word3' );Child .say (); Parent .say ();
这样的拷贝有一个问题。
那就是,如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。
所以上方Child修改之后也会反应到Parent上去。
所以需要进行深度拷贝 ,一直到拿到真正的值为止
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 var Parent = {name : 'parent' , words : ['word1' ,'word2' ],say : function ( ){console .log (this .name + ': ' + this .words );} }; function deepCopy (obj,newObj ){newObj = newObj || {}; for (var item in obj){if (typeof obj[item] === 'object' ){newObj[item] = (Object .prototype .toString .call (obj[item]) === '[object Array]' ) ? [] : {}; deepCopy (obj[item],newObj[item]);}else { newObj[item] = obj[item] } } return newObj;} var Child = deepCopy (Parent );Child .say (); Child .name = 'child' ;Child .words .push ('word3' );Child .say (); Parent .say ();
最后附图两张: