JS学习44:一个标准继承

1、标准继承

关于继承的文章,前面已经写过不少了(详见:http://qianduanblog.com/search/%E7%BB%A7%E6%89%BF/)。前面的文章介绍了如何实现继承以及各种继承的方法、特点。

1.1、引子:共享原型的原型继承

javascript中通过原型链来实现共享属性和方法做法来实现共享继承。如:

 
 
  1. // People
  2. function People(){}
  3. People.prototype.constructor = People;
  4. People.prototype.type = 'people';
  5. People.prototype.isPeople = true;
  6. // Father
  7. function Father(){}
  8. Father.prototype = People.prototype;
  9. Father.prototype.constructor = Father;
  10. Father.prototype.type = 'father';
  11. Father.prototype.isFather = true;
  12. // Child
  13. function Child(){}
  14. Child.prototype = Father.prototype;
  15. Child.prototype.constructor = Child;
  16. Child.prototype.type = 'child';
  17. Child.prototype.isChild = true

如上代码实现了 Child -> Father -> People 的继承关系,在chrome控制台打印出 Child.prototype 即可明显看出:

20140816134235607005933604.png

Child的原型中同时拥有了PeopleFatherChild自身的所有原型属性,而原型是个被所有实例所共享的对象,因此Child的所有实例都具有了PeopleFather所有的原型属性、方法。接下来看看 Father.prototype 和 People.prototype

20140816134817447844726327.png

由图可知,通过共享原型链是达到了继承的目的,但同时也带来了负面的影响,Fathertype本来是为father,结果为childPeopletype本来是people,结果为child。因为PeopleFatherChild共享了同一个原型对象,因此也额外的实现了Father继承了ChildPeople继承了Father的结果,可想而知不是我们想要的。这种结果相当于丰富并改写了People的原型链,只有多了几个同等性质的构造函数而已,即:

 
 
  1. // People
  2. function People(name){
  3.     this.name = name || 'unknow';
  4. }
  5. People.prototype.constructor = People;
  6. People.prototype.type = 'people';
  7. People.prototype.isPeople = true;
  8. // Father
  9. People.prototype.type = 'father';
  10. People.prototype.isFather = true;
  11. var Father = People;
  12. // Child 
  13. People.prototype.type = 'child';
  14. People.prototype.isChild = true;
  15. var Child = Father;

在控制台打印它们之间的原型关系:

20140816135731623020154680.png

可见,此时的结果与上述结果一致。

1.2、标准:引用实例化的原型继承

我们知道,虽然构造函数的原型链是共享的,但构造函数的实例是与构造函数和其他实例之间是相互独立的,并且构造函数的实例具备了构造函数的所有原型属性和方法。如:

 
 
  1. function People(name){
  2.     this.name = name || 'unknow';
  3. }
  4. People.prototype.say = function(){
  5.     console.log('我叫' + this.name + '今年' + (this.age || '20') + '岁');
  6. };
  7. // 实例化2个对象
  8. var zhang = new People('zhang');
  9. var song = new People('song');
  10. zhang.say();
  11. // => 我叫zhang今年20岁 
  12. song.say();
  13. // => 我叫song今年20岁 
  14. // 改写 zhang 的内置属性
  15. zhang.name = 'zhangsan';
  16. // 改写 zhang 的原型属性
  17. zhang.age = 99;
  18. zhang.say();
  19. // => 我叫zhangsan今年99岁 
  20. // 看看 song 的原型属性
  21. song.say();
  22. // => 我叫song今年20岁
  23. // 看看 People 的内置属性
  24. console.log(People.prototype.name);
  25. // => undefined
  26. // 看看 People 的原型属性
  27. console.log(People.prototype.age);
  28. // => undefined

可见,实例化对象与构造函数是相对独立的,并且实例对象上有构造函数的所有内置属性、方法和原型属性、方法。因此可以将继承构造函数的原型对象赋值为被继承构造函数的实例化对象,又因为原型对象是一个对象,我们先来看看实例化对象的类型是否为一个对象。

20140816142438843241872202.png

由上可知,实例化对象确实一个对象,因此可以作为构造函数的原型,但又因此这个对象有初始值,因此需要实例化一个空参数的对象:

20140816142757439836528994.png

由上图可知,empty对象与普通的字面量对象是无异的,只是由不同的构造函数实例化出来而已,empty是由People实例化出来的,而object是由Object实例化出来的。总结后的产物:

 
 
  1. function People(name){
  2.     this.name = name || 'unknow';
  3. }
  4. People.prototype.say = function(){
  5.     console.log('我叫' + this.name + '今年' + (this.age || '20') + '岁');
  6. };
  7. function Father(){}
  8. Father.prototype = new People();
  9. Father.prototype.constructor = Father;
  10. Father.prototype.sex = 'man';

查看他们之间的原型关系:

20140816143350282680924629.png

Father.prototypePeople.prototype互不影响,并且Father继承到了People的原型属性和方法,达到目的。并且:

20140816143605088486051735.png

Father.prototype继承了People.prototype动态添加的属性和方法,而People.protype并没有继承Father.prototype动态添加的属性和方法,究其原因是Father.prototype指向的是People的实例,而实例具备构造函数的所有原型属性和方法,并且是独立存在的,这个过程是单向的,也算是一种另类的继承。

是不是到此,我们的标准独立继承就已经研究完了?

1.3、标准:内置属性和方法的继承

先看例子:

 
 
  1. function People(name){
  2.     this.name = name || 'unknow';
  3. }
  4. function Father(){}
  5. Father.prototype = new People();
  6. Father.prototype.constructor = Father;
  7. var father = new Father('张三');
  8. console.log(father.name);
  9. // => unknow

Father.prototype继承了People.prototype,但People的内置属性怎么办?我们可以在Father实例化的时候走一遍People。即:

 
 
  1. function People(name){
  2.     this.name = name || 'unknow';
  3. }
  4. function Father(){
  5.     // 走一遍 People 构造函数
  6.     People.apply(thisarguments);
  7. }
  8. Father.prototype = new People();
  9. Father.prototype.constructor = Father;
  10. var father = new Father('张三');
  11. console.log(father.name);
  12. // => 张三

1.4、总结

以上1.2和1.3主要做了两件事:

  1. 实例化被继承构造函数的实例作为继承构造函数的原型;

  2. 继承构造函数先运行一次被继承构造函数。

2、标准继承方法

2.1、方法

 
 
  1. /**
  2.  * 标准原型继承,参考自 nodejs 的 util.inherits
  3.  * @param  {Function} constructor      继承函数
  4.  * @param  {Function} superConstructor 被继承函数
  5.  */
  6. function inherits(constructorsuperConstructor){
  7.     constructor.prototype = new superConstructor();
  8.     constructor.prototype.constructor = constructor;
  9.     // 备用操作:自身的静态属性 super_ 指向被继承函数
  10.     constructor.super_ = superConstructor;
  11. }
  12. function People(name){
  13.     this.name = name || 'unknow';
  14.     this.type = 'people';
  15.     this.isPeople = true;
  16. }
  17. function Father(){
  18.     // 1. 执行一次被继承构造函数
  19.     People.apply(thisarguments);
  20.     this.type = 'father';
  21.     this.isFather = true;
  22. }
  23. // 2. 进行标准原型继承
  24. inherits(FatherPeople);
  25. // 3. 添加自己的原型
  26. // 不能直接赋值: Father.prototype = {};
  27. Father.prototype.say = function(){
  28.     console.log('我叫' + this.name + '今年' + (this.age || '20') + '岁');
  29. };
  30. // 实例化 People
  31. var people = new People('张三');
  32. people.age = 99;
  33. console.log(people.type);
  34. // => father
  35. console.log(people.isPeople);
  36. // => true
  37. console.log(people.isFather);
  38. // => undefined
  39. try{
  40.     people.say();
  41. }catch(err){
  42.     console.log('%s: %s'err.nameerr.message);
  43.     // => TypeError: Object #<People> has no method 'say'
  44. }
  45. // 实例化 Father
  46. var father = new Father('李四');
  47. father.age = 99;
  48. console.log(father.type);
  49. // => father
  50. console.log(father.isPeople);
  51. // => true
  52. console.log(father.isFather);
  53. // => true
  54. father.say();
  55. // => 我叫李四今年99岁

2.2、如何为何继承关系链

先看一段比较长的继承关系:

 
 
  1. function People(){}
  2. function Father(){
  3.     People.apply(thisarguments);
  4. }
  5. inherits(FatherPeople);
  6. function Child(){
  7.     Father.apply(thisarguments);
  8. }
  9. inherits(ChildFather);
  10. function Man(){
  11.     Child.apply(thisarguments);
  12. }
  13. inherits(ManChild);
  14. function Body(){
  15.     Man.apply(thisarguments);
  16. }
  17. inherits(BodyMan);

如上,Body -> Man -> Child -> Father -> People,这是一条关系,是谁在维护这条关系呢?见下图:

20140816151400285682428163.png

通过原型上的__proto__指向被继承的构造函数原型来维持这段原型链条关系,__proto__是个隐藏的属性,它是非标准的属性。平时我们不需要用到,链接的终点是Object的原型,因此Object.prototype.__proto__===null

在2.1里的inherits方法里,添加了继承构造函数的super_静态属性,可以清楚的知道它的被继承构造函数。

20140816153104863260895739.png

通常判断一个构造函数是否继承自另外一个构造函数,如果继承构造函数的原型是被继承构造函数的实例,那么就可以这样判断:

20140816153511189586574117.png

而如果是通过复制拷贝被继承构造函数原型的话,那么就没法如上检测到被继承构造函数了,因此为什么说这样是标准的。

3、特殊用例

3.1、尝试继承Error

如上正常的写法:

 
 
  1. function CustomError(){
  2.     Error.apply(thisError);
  3.     this.name = 'CustomError';
  4. }
  5. inherits(CustomErrorError);
  6. try{
  7.     throw new CustomError('呃……');
  8. }catch(err){
  9.     console.log('%s: %s'err.nameerr.message);
  10.     console.log(err.stack);
  11. }

在控制台打印出:

20140816163500986383434757.png


这里有以下3个特殊的地方:

  1. 报错的行号是224,但这里的错误并没有在控制台抛出,而是被捕获住了。

  2. 抛出错误的行号是227,而捕获到错误堆栈最后错误行号是224。

  3. 错误堆栈中额外多出了一行,即继承方法里的121行。

那我们来看看原生的Error是怎样的:

 
 
  1. try{
  2.     throw new Error('呃……');
  3. }catch(err){
  4.     console.log('%s: %s'err.nameerr.message);
  5.     console.log(err.stack);
  6. }

20140816163014346744373296.png

错误信息和错误行号是完全正确的。

先来看看Error对象有哪些静态属性和方法、原型属性和方法。

 
 
  1. console.log(Object.getOwnPropertyNames(Error));
  2. // => ["length", "name", "arguments", "caller", "prototype", 
  3. // "captureStackTrace", "stackTraceLimit"]
  4. // Error是个Function的实例,因此"length", "name", "arguments", 
  5. // "caller", "prototype" 是 Function 的原型属性;
  6. // 只有 "captureStackTrace", "stackTraceLimit" 是自身的静态属性。
  7. console.log(Object.getOwnPropertyNames(Error.prototype));
  8. // => ["constructor", "name", "message", "toString"]
  9. // Error.prototype是Object的实例,因此"constructor", "toString"
  10. // 是Object的原型属性;
  11. // 只有 "name", "message" 是自身的原型属性。

我们尝试删除掉Error对象原型上的namemessage两个属性,看看实例化之后是否还有两个属性出现。

 
 
  1. console.log(Object.getOwnPropertyNames(Error.prototype));
  2. delete(Error.prototype.name);
  3. delete(Error.prototype.message);
  4. try{
  5.     throw new Error('呃……');
  6. }catch(err){
  7.     console.log('err.name: %s'err.name);
  8.     console.log('err.message: %s'err.message);
  9.     console.log('err.stack: %s'err.stack);
  10. }
  11. console.log(Object.getOwnPropertyNames(Error.prototype));

控制台:

20140816180508937334065232.png

最后的err对象里,没有name属性,但有messagestack两个属性,说明name属性读取的是原型上的,而messagestack是运行到错误错动态添加上的。

并且从3.1开头的图示里可以看到,错误的源头指向的是实例化Error的那一行,因此我们不能以实例化被继承构造函数的方式来实现继承。

并且,不同的浏览器内核实现的Error也不尽相同,因此继承一个Error需要另辟蹊径:

3.2、继承Error方法——直接伪继承

 
 
  1. function CustomError(namemessage){
  2.     if(arguments.length < 2){
  3.         message = name;
  4.     }
  5.     var err = new Error();
  6.     
  7.     if(err.stack){
  8.         this.stack = err.stack;
  9.     }
  10.     this.name = name || 'CustomError';
  11.     this.message = message || 'CustomError message';
  12. }
  13. var err = new CustomError('(⊙o⊙)…');
  14. console.log(err.stack);
  15. // 正确stack
  16. console.log(err instanceof CustomError);
  17. // => true
  18. console.log(err instanceof Error);
  19. // => false

以上是为了照顾需要Error的stack属性而做的,如果不计较这些,可以直接用inherits方法。

3.3、native构造函数的继承

内置的native构造函数与日常的书写方式都不一样,因此直接使用inherits方法继承的话,都会出错,这里就不一一举例了,仅表Error的继承作抛钻引玉之用。而在我们的业务逻辑中继承native构造函数情况最多的就是Error,其他如ArrayDateMathObject这些东西都不必去继承它,也没有必要。

本文的重心部分在介绍标准的继承方式,用于业务、框架逻辑上。

4、参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值