1、标准继承
关于继承的文章,前面已经写过不少了(详见:http://qianduanblog.com/search/%E7%BB%A7%E6%89%BF/)。前面的文章介绍了如何实现继承以及各种继承的方法、特点。
1.1、引子:共享原型的原型继承
javascript中通过原型链来实现共享属性和方法做法来实现共享继承。如:
- // People
- function People(){}
- People.prototype.constructor = People;
- People.prototype.type = 'people';
- People.prototype.isPeople = true;
-
-
- // Father
- function Father(){}
- Father.prototype = People.prototype;
- Father.prototype.constructor = Father;
- Father.prototype.type = 'father';
- Father.prototype.isFather = true;
-
-
- // Child
- function Child(){}
- Child.prototype = Father.prototype;
- Child.prototype.constructor = Child;
- Child.prototype.type = 'child';
- Child.prototype.isChild = true
如上代码实现了 Child -> Father -> People
的继承关系,在chrome控制台打印出 Child.prototype
即可明显看出:
Child
的原型中同时拥有了People
、Father
和Child
自身的所有原型属性,而原型是个被所有实例所共享的对象,因此Child
的所有实例都具有了People
和Father
所有的原型属性、方法。接下来看看 Father.prototype
和 People.prototype
:
由图可知,通过共享原型链是达到了继承的目的,但同时也带来了负面的影响,Father
的type
本来是为father
,结果为child
,People
的type
本来是people
,结果为child
。因为People
、Father
和Child
共享了同一个原型对象,因此也额外的实现了Father
继承了Child
、People
继承了Father
的结果,可想而知不是我们想要的。这种结果相当于丰富并改写了People
的原型链,只有多了几个同等性质的构造函数而已,即:
- // People
- function People(name){
- this.name = name || 'unknow';
- }
- People.prototype.constructor = People;
- People.prototype.type = 'people';
- People.prototype.isPeople = true;
-
-
- // Father
- People.prototype.type = 'father';
- People.prototype.isFather = true;
- var Father = People;
-
-
-
- // Child
- People.prototype.type = 'child';
- People.prototype.isChild = true;
- var Child = Father;
在控制台打印它们之间的原型关系:
可见,此时的结果与上述结果一致。
1.2、标准:引用实例化的原型继承
我们知道,虽然构造函数的原型链是共享的,但构造函数的实例是与构造函数和其他实例之间是相互独立的,并且构造函数的实例具备了构造函数的所有原型属性和方法。如:
- function People(name){
- this.name = name || 'unknow';
- }
-
- People.prototype.say = function(){
- console.log('我叫' + this.name + '今年' + (this.age || '20') + '岁');
- };
-
-
- // 实例化2个对象
- var zhang = new People('zhang');
- var song = new People('song');
-
-
- zhang.say();
- // => 我叫zhang今年20岁
-
- song.say();
- // => 我叫song今年20岁
-
-
- // 改写 zhang 的内置属性
- zhang.name = 'zhangsan';
- // 改写 zhang 的原型属性
- zhang.age = 99;
- zhang.say();
- // => 我叫zhangsan今年99岁
-
-
- // 看看 song 的原型属性
- song.say();
- // => 我叫song今年20岁
-
-
- // 看看 People 的内置属性
- console.log(People.prototype.name);
- // => undefined
- // 看看 People 的原型属性
- console.log(People.prototype.age);
- // => undefined
可见,实例化对象与构造函数是相对独立的,并且实例对象上有构造函数的所有内置属性、方法和原型属性、方法。因此可以将继承构造函数的原型对象赋值为被继承构造函数的实例化对象,又因为原型对象是一个对象,我们先来看看实例化对象的类型是否为一个对象。
由上可知,实例化对象确实一个对象,因此可以作为构造函数的原型,但又因此这个对象有初始值,因此需要实例化一个空参数的对象:
由上图可知,empty
对象与普通的字面量对象是无异的,只是由不同的构造函数实例化出来而已,empty
是由People
实例化出来的,而object
是由Object
实例化出来的。总结后的产物:
- function People(name){
- this.name = name || 'unknow';
- }
-
- People.prototype.say = function(){
- console.log('我叫' + this.name + '今年' + (this.age || '20') + '岁');
- };
-
-
- function Father(){}
- Father.prototype = new People();
- Father.prototype.constructor = Father;
- Father.prototype.sex = 'man';
查看他们之间的原型关系:
Father.prototype
与People.prototype
互不影响,并且Father
继承到了People
的原型属性和方法,达到目的。并且:
Father.prototype
继承了People.prototype
动态添加的属性和方法,而People.protype
并没有继承Father.prototype
动态添加的属性和方法,究其原因是Father.prototype
指向的是People
的实例,而实例具备构造函数的所有原型属性和方法,并且是独立存在的,这个过程是单向的,也算是一种另类的继承。
是不是到此,我们的标准独立继承就已经研究完了?
1.3、标准:内置属性和方法的继承
先看例子:
- function People(name){
- this.name = name || 'unknow';
- }
-
- function Father(){}
- Father.prototype = new People();
- Father.prototype.constructor = Father;
-
- var father = new Father('张三');
- console.log(father.name);
- // => unknow
Father.prototype
继承了People.prototype
,但People
的内置属性怎么办?我们可以在Father
实例化的时候走一遍People
。即:
- function People(name){
- this.name = name || 'unknow';
- }
-
- function Father(){
- // 走一遍 People 构造函数
- People.apply(this, arguments);
- }
- Father.prototype = new People();
- Father.prototype.constructor = Father;
-
- var father = new Father('张三');
- console.log(father.name);
- // => 张三
1.4、总结
以上1.2和1.3主要做了两件事:
-
实例化被继承构造函数的实例作为继承构造函数的原型;
-
继承构造函数先运行一次被继承构造函数。
2、标准继承方法
2.1、方法
- /**
- * 标准原型继承,参考自 nodejs 的 util.inherits
- * @param {Function} constructor 继承函数
- * @param {Function} superConstructor 被继承函数
- */
- function inherits(constructor, superConstructor){
- constructor.prototype = new superConstructor();
- constructor.prototype.constructor = constructor;
- // 备用操作:自身的静态属性 super_ 指向被继承函数
- constructor.super_ = superConstructor;
- }
-
- function People(name){
- this.name = name || 'unknow';
- this.type = 'people';
- this.isPeople = true;
- }
-
- function Father(){
- // 1. 执行一次被继承构造函数
- People.apply(this, arguments);
- this.type = 'father';
- this.isFather = true;
- }
-
- // 2. 进行标准原型继承
- inherits(Father, People);
-
- // 3. 添加自己的原型
- // 不能直接赋值: Father.prototype = {};
- Father.prototype.say = function(){
- console.log('我叫' + this.name + '今年' + (this.age || '20') + '岁');
- };
-
-
- // 实例化 People
- var people = new People('张三');
- people.age = 99;
-
- console.log(people.type);
- // => father
-
- console.log(people.isPeople);
- // => true
-
- console.log(people.isFather);
- // => undefined
-
- try{
- people.say();
- }catch(err){
- console.log('%s: %s', err.name, err.message);
- // => TypeError: Object # has no method 'say'
- }
-
-
- // 实例化 Father
- var father = new Father('李四');
- father.age = 99;
-
- console.log(father.type);
- // => father
-
- console.log(father.isPeople);
- // => true
-
- console.log(father.isFather);
- // => true
-
- father.say();
- // => 我叫李四今年99岁
2.2、如何为何继承关系链
先看一段比较长的继承关系:
- function People(){}
-
- function Father(){
- People.apply(this, arguments);
- }
- inherits(Father, People);
-
- function Child(){
- Father.apply(this, arguments);
- }
- inherits(Child, Father);
-
- function Man(){
- Child.apply(this, arguments);
- }
- inherits(Man, Child);
-
- function Body(){
- Man.apply(this, arguments);
- }
- inherits(Body, Man);
如上,Body -> Man -> Child -> Father -> People
,这是一条关系,是谁在维护这条关系呢?见下图:
通过原型上的__proto__
指向被继承的构造函数原型来维持这段原型链条关系,__proto__
是个隐藏的属性,它是非标准的属性。平时我们不需要用到,链接的终点是Object
的原型,因此Object.prototype.__proto__===null
。
在2.1里的inherits
方法里,添加了继承构造函数的super_
静态属性,可以清楚的知道它的被继承构造函数。
通常判断一个构造函数是否继承自另外一个构造函数,如果继承构造函数的原型是被继承构造函数的实例,那么就可以这样判断:
而如果是通过复制拷贝被继承构造函数原型的话,那么就没法如上检测到被继承构造函数了,因此为什么说这样是标准的。
3、特殊用例
3.1、尝试继承Error
如上正常的写法:
- function CustomError(){
- Error.apply(this, Error);
- this.name = 'CustomError';
- }
- inherits(CustomError, Error);
-
- try{
- throw new CustomError('呃……');
- }catch(err){
- console.log('%s: %s', err.name, err.message);
- console.log(err.stack);
- }
在控制台打印出:
这里有以下3个特殊的地方:
-
报错的行号是224,但这里的错误并没有在控制台抛出,而是被捕获住了。
-
抛出错误的行号是227,而捕获到错误堆栈最后错误行号是224。
-
错误堆栈中额外多出了一行,即继承方法里的121行。
那我们来看看原生的Error
是怎样的:
- try{
- throw new Error('呃……');
- }catch(err){
- console.log('%s: %s', err.name, err.message);
- console.log(err.stack);
- }
错误信息和错误行号是完全正确的。
先来看看Error
对象有哪些静态属性和方法、原型属性和方法。
- console.log(Object.getOwnPropertyNames(Error));
- // => ["length", "name", "arguments", "caller", "prototype",
- // "captureStackTrace", "stackTraceLimit"]
- // Error是个Function的实例,因此"length", "name", "arguments",
- // "caller", "prototype" 是 Function 的原型属性;
- // 只有 "captureStackTrace", "stackTraceLimit" 是自身的静态属性。
-
-
- console.log(Object.getOwnPropertyNames(Error.prototype));
- // => ["constructor", "name", "message", "toString"]
- // Error.prototype是Object的实例,因此"constructor", "toString"
- // 是Object的原型属性;
- // 只有 "name", "message" 是自身的原型属性。
我们尝试删除掉Error
对象原型上的name
和message
两个属性,看看实例化之后是否还有两个属性出现。
- console.log(Object.getOwnPropertyNames(Error.prototype));
- delete(Error.prototype.name);
- delete(Error.prototype.message);
- try{
- throw new Error('呃……');
- }catch(err){
- console.log('err.name: %s', err.name);
- console.log('err.message: %s', err.message);
- console.log('err.stack: %s', err.stack);
- }
- console.log(Object.getOwnPropertyNames(Error.prototype));
控制台:
最后的err
对象里,没有name
属性,但有message
和stack
两个属性,说明name
属性读取的是原型上的,而message
和stack
是运行到错误错动态添加上的。
并且从3.1开头的图示里可以看到,错误的源头指向的是实例化Error
的那一行,因此我们不能以实例化被继承构造函数的方式来实现继承。
并且,不同的浏览器内核实现的Error
也不尽相同,因此继承一个Error
需要另辟蹊径:
3.2、继承Error方法——直接伪继承
- function CustomError(name, message){
- if(arguments.length < 2){
- message = name;
- }
-
- var err = new Error();
-
- if(err.stack){
- this.stack = err.stack;
- }
-
- this.name = name || 'CustomError';
- this.message = message || 'CustomError message';
- }
-
- var err = new CustomError('(⊙o⊙)…');
- console.log(err.stack);
- // 正确stack
- console.log(err instanceof CustomError);
- // => true
- console.log(err instanceof Error);
- // => false
以上是为了照顾需要Error的stack属性而做的,如果不计较这些,可以直接用inherits
方法。
3.3、native构造函数的继承
内置的native构造函数与日常的书写方式都不一样,因此直接使用inherits方法继承的话,都会出错,这里就不一一举例了,仅表Error的继承作抛钻引玉之用。而在我们的业务逻辑中继承native构造函数情况最多的就是Error,其他如Array
、Date
、Math
、Object
这些东西都不必去继承它,也没有必要。
本文的重心部分在介绍标准的继承方式,用于业务、框架逻辑上。
4、参考资料
-
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
-
http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx
-
http://stackoverflow.com/questions/783818/how-do-i-create-a-custom-error-in-javascript