本文并不想探讨JavaScript的面向对象特性(如果有兴趣,可参看我的《领悟面向对象JavaScript》),也不会涉及全部的面向对象概念,只是试图寻找一个还未被任何人发现的“宝藏”,即完美的JavaScript继承的实现方法。

在面向对象语言中,继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。

在JavaScript中,按照实现方式,继承被分为如下五种(也有人说是七种,即组合继承细分为原型组合继承、寄生组合继承,另外再加上一个只能继承属性而无法继承方法的借用构造函数继承):
▪ 拷贝继承
▪ 原型继承
▪ 调用继承
▪ 寄生(parasitic)继承
▪ 组合(combination)继承

拷贝继承
通常来说,拷贝继承并不被大家所接受为“真正的继承”,因为这种继承的原理是拷贝简单对象的属性。jQuery框架中提供的就是拷贝继承,主要用于插件编程中的元数据部分,采用元数据的jQuery插件的重要原因是它可以让你使用定义之外的标签来覆写你的插件属性设置,外部只提供必要的而非全部的属性设置,然后通过继承让外部属性与插件默认属性“结合”,提供最大的设置灵活性。如:

var o = $.meta ? $.extend({}, opts, $this.data()) : opts;

jQuery框架提供的拷贝功能非常强大,支持简单对象的深拷贝与浅拷贝。但是,它还不是类级的继承,因为它的拷贝只作用于简单对象(如{name:'zcj',age:28})。

原型继承
在JavaScript中最容易理解的一个继承实现方式,主要是因为它发挥了原型链的概念,即利用原型让一个引用类型继承另一个应用类型的属性和方法。继承的实现比较简单,如父类为BaseClass,子类为SubClass时,继承实现如下:

SubClass.prototype = new BaseClass();

由示例代码可见,必须要有一个对象的实例以便让它作为另一个对象的基础。原型继承由于原型链的原因,子类的原型指向了父类的原型,所以子类实例的constructor就指向了父类,而子类的实例类型(instanceof)既是父类又是子类。抛开上述的小缺陷,危险的原型继承还会给我们带来更加恐怖的问题,如原型链断裂与引用类型的原型继承。

当原型继承发生后,如果希望使用子类的原型添加新的方法时,会发生原型链断裂。如:

SubClass.prototype = new BaseClass();
//使用原型添加新的方法, 上一行的继承无效, 原型链断裂
SubClass.prototype = {getValue: function() {//to do}
};

由于原型仅仅是以对象为模板构建副本,所以它是一种引用拷贝,包含引用类型值的原型属性会被所有的实例共享,那么一旦改变某个原型上引用类型的属性的值,将会彻底影响到这个类型创建的每一个实例。这种情况我在《领悟面向对象JavaScript》的原型继承章节有过详细的讨论,这里不再累述。

PS:2006年,道格拉斯•克罗克福德在文章《Prototypal Inheritance in JavaScript》中提出了一种不使用严格意义构造函数的原型继承法,虽然是优秀的,但是仍然存在应用类型属性值等缺陷。

调用继承
调用继承也是一种实现继承的简单方案,其实质是在子类函数体中调用父类的构造器方法,并使构造器方法执行于上下文上。也就是说,我们在父类构造器方法上通过this上下文所操作的内容实际上都是在操作子类实例化对象的内容。如:

function SubClass() {BaseClass.call(this); //当父类构造函数带有参数时, 一般使用apply替代call, 以便传参
}

在子类SubClass进行定义的时候,调用父类BaseClass的call方法并将子类的this作为参数传入,使父类的方法在子类的上下文上执行,这样的结果就是,父类中所有的通过this方式定义的公有成员都会被子类继承。

这种继承方式被很多人所接受,主要是因为它不存在原型继承的缺陷与问题,而且满足继承的所有特性,也另继承的终极目的--多态(polymorphisn)体现了出来。从各个角度来看,调用继承似乎就是最完美的方案了,那么这个“宝藏”究竟是不是我们的“圣杯”还是“杯具”呢?

调用继承的“圣杯”场景:

var BaseClass = function() {this.className = "Base";this.showName = function() {alert(this.className);};
};
var SubClass = function() {BaseClass.call(this);this.classDesc = "SubClass";this.showDesc = function() {alert(this.classDesc);};this.showName = function() {alert(this.className);};
};

调用继承的“杯具”场景:

var BaseClass = function() {this.className = "Base";
};
BaseClass.prototype = {showName: function() {alert(this.className);}
};
var SubClass = function() {BaseClass.call(this);this.classDesc = "SubClass";
};
SubClass.prototype = {showDesc: function() {alert(this.classDesc);},showName: function() {alert(this.className);}
};

其实,这两段代码希望完成的功能是一模一样的,只不过写法不同。很不巧的是,具有JavaScript经验的开发者都会选择“杯具”场景中的方式定义类,究其原因就是性能。

在高级面向对象语言中,类的存储是很优美的,对类的每个实例来说,其不同的部分就是用于描述其状态的属性,而它们的行为(即方法)都是相同的,所以在运行期只会为每个类实例的属性分配存储空间,方法则是每个类实例所共享的,是不占用存储空间的。

在JavaScript中,方法(Function)也是变量的一种,即使它和对象、数组都是按引用传递的,也是要被分配存储空间的。在“圣杯”场景中,所有Manager的实例都会被分配用于存储方法showPerson与showManager的空间,这绝对是一种巨额的内存浪费。而在“杯具”场景中,方法是在原型链上进行扩展的,这样就保证了类的所有实例都在原型链上共享了相同的方法,而不会为每个实例都分配存储方法的空间。

调用继承恰恰在正确的场景中出了问题,还真是个杯具啊。

寄生继承
这是一种非常不常见的实现继承的方案,是与原型继承紧密相关的一种方式,其本质与寄生构造函数和工厂模式很类似,即创建一个仅用于封装继承过程的函数,并在这个函数的内部以某种方式增强对象,最后返回这个函数。

由于不常见,所以我姑且把实例代码也省略了。因为其本质与原型继承紧密相关,所以也存在了对象方法无法复用的性能问题。

组合继承
所谓组合继承,其实就是综合使用了上述继承方式,最终达到性能、继承、多态均满意的结果的实现方案。目前已知的组合继承为原型调用继承和寄生调用继承两种,其实这两种方案的本质还是综合了原型继承与调用继承。

示例如下:

var BaseClass = function() {this.className = "Base";
};
BaseClass.prototype = {showName: function() {alert(this.className);}
};
var SubClass = function() {BaseClass.call(this); //第二次调用BaseClass()this.classDesc = "SubClass";
};
SubClass.prototype = {showDesc: function() {alert(this.classDesc);}
};
SubClass.prototype = new BaseClass(); //第一次调用BaseClass()

组合继承通过两次调用父类的构造函数,最终实现了一种新型的继承。在第一次调用父类的构造函数时,子类的原型得到了父类的实例属性(className)。当调用SubClass的构造函数时,将再次调用父类的构造函数,这样就在新的对象上创建了实例属性,于是这个实例属性就屏蔽了原型中的同名属性。

目前,基本上认为组合继承是JavaScript中实现继承的最完美方案。但是,个人认为这种说法还是片面了一些,因为这里存在不必要的性能消耗。从示例中可以看出,为了实现继承,出现了两次调用父类构造函数的情况。当需要很多子类实例的情况出现时,这种继承并不高效,因为它在子类的原型上创建了不必要且多余的属性。另外,因为重写原型,它还失去了默认的constructor属性。

在雅虎的YUI框架的YAHOO.lang.extend()中,实现了寄生调用继承。如果将上述示例改造为寄生调用继承,则代码如下:

function object(o) {function F(){}F.prototype = o;return new F();
}
function inheritPrototype(subC, superC) {var prototype = object(superC.prototype); //创建对象prototype.constructor = subC; //增强对象subC.prototype = prototype; //指定对象
}
var BaseClass = function() {this.className = "Base";
};
BaseClass.prototype = {showName: function() {alert(this.className);}
};
var SubClass = function() {BaseClass.call(this);this.classDesc = "SubClass";
};
inheritPrototype(SubClass, BaseClass);
SubClass.prototype.showDesc = function() {alert(this.classDesc);
};

寄生组合模式主要靠inheritPrototype函数实现,它首先创建了父类原型的一个副本,然后为这个副本添加constructor属性,弥补了因重写原型而失去的默认constructor,最后将副本赋值给子类的原型。以这样的方式实现了一个高效的且保持原型链不变的理想继承模式。

但是,个人认为将寄生组合继承称为完美的继承还是有一点点的牵强,个人的理由是它不够优雅!从艺术的角度来看,完美的事务无一不是优雅的。在寄生组合继承中,在子类的原型链上添加方法的代码书写方式破坏了优雅,如示例代码中子类SubClass的showDesc方法的写法。可以预见的是,如果实际开发中我们要在子类的原型上添加N(代表很多)个方法,那么“子类.prototype”就要书写N次。可能有的朋友会说:反正代码都一样,复制粘贴就好了。OK,复制粘贴的确可以,因为毕竟它并不复杂,但是这样也无形中使js文件的尺寸增长了,对站点的性能将产生不利的影响。

那么,如果我们仍然使用寄生组合模式,并优化在子类原型上添加方法的书写方式,这样是否行得通呢?请看改造代码:

function object(o) {function F(){}F.prototype = o;return new F();
}
function inheritPrototype(subC, superC) {var prototype = object(superC.prototype); //创建对象prototype.constructor = subC; //增强对象subC.prototype = prototype; //指定对象
}
var BaseClass = function() {this.className = "Base";
};
BaseClass.prototype = {showName: function() {alert(this.className);}
};
var SubClass = function() {BaseClass.call(this);this.classDesc = "SubClass";
};
//位置一
SubClass.prototype = {showDesc: function() {alert(this.classDesc);}
};
inheritPrototype(SubClass, BaseClass); //位置二

可以看到,改写之后我将实现寄生组合继承的核心方法(inheritPrototype)的调用放到了位置二,以保证让继承可以正确实现。但这时会出现一个问题,我们会发现子类自身定义的方法不见了,这是因为在inheritPrototype中父类的原型重写了子类的原型导致的。解决这个问题的、理论上的方法就是将inheritPrototype方法的调用放到位置一上,即在子类原型上添加方法的动作发生之前,父类的原型重写子类原型的动作就已经完成了。但是这样做继承是不会被实现的,原因我们在原型继承章节中提到过,即原型链断裂。我之前在寄生继承章节中提到过,寄生继承是与原型继承紧密相关的,简单的说寄生继承可以理解为原型继承的一种“包装”,本质上并无变化。

完美继承
这里所谓的完美继承,其实也是应用了组合继承这个概念。由上文可知,寄生组合继承已经非常贴近于完美继承了,只是因为inheritPrototype的实现方式导致了一些额外的问题出现。所以,我的主要思路集中在对inheritPrototype的改造上,改造的实质就是抛弃使用父类原型副本重写子类原型的做法。将这个思路延伸,如果要实现在子类原型链上具有父类原型链上的属性(即方法,原因可参考调用继承章节中的“杯具”场景说明),那么一个理想的解决方法就是拷贝。所以,我为这种继承模式起名为“拷贝组合继承(copy combination inherit)”,实现方式如下:

function extend(subC, baseC) {for (var ptototypeName in baseC.prototype) {if (typeof(subC.prototype[ptototypeName]) === 'undefined') {subC.prototype[ptototypeName] = baseC.prototype[ptototypeName]; //原型属性的拷贝}}subC.prototype.constructor = subC; //增强
}
var BaseClass = function() {this.className = "Base";
};
BaseClass.prototype = {showName: function() {alert(this.className);}
};
var SubClass = function() {BaseClass.call(this); //只执行一次父类构造函数this.classDesc = "SubClass";
};
SubClass.prototype = {showDesc: function() {alert(this.classDesc);}
};
extend(SubClass, BaseClass); //不破坏子类原型链的位置二

这种方式不仅兼顾了效率、继承实现、多态,而且不创建父类原型副本,即不发生原型的重写,避免了寄生组合继承存在的缺陷。另外,拷贝组合继承显而易见的对多重继承的实现提供了更优的支持。

转载于:https://www.cnblogs.com/zhaodabao/archive/2011/04/21/2023333.html

探寻完美 之 JavaScript继承相关推荐

  1. JavaScript继承详解(四)

    文章截图 - 更好的排版 在本章中,我们将分析Douglas Crockford关于JavaScript继承的一个实现 - Classical Inheritance in JavaScript. C ...

  2. JavaScript继承详解(四) 转

    在本章中,我们将分析Douglas Crockford关于JavaScript继承的一个实现 - Classical Inheritance in JavaScript. Crockford是Java ...

  3. Javascript继承机制的设计思想

    我一直很难理解Javascript语言的继承机制. 它没有"子类"和"父类"的概念,也没有"类"(class)和"实例" ...

  4. 浅谈JavaScript继承

    前言 关于JavaScript继承相关的定义和方法网上已经有很多解释啦,本菜鸟就不抄抄写写惹人嫌了,本文主要探讨三种基本的继承方式并且给出优化方案. 正文 借助构造函数实现继承 function Pa ...

  5. 浅谈javascript继承【读javascript设计模式第四章节继承有感】

    javascript继承,无任是类式继承,原型式继承还是渗元式继承都是通过不同方法去围绕着prototype转,简单分析下三种不同继承方法是如何围绕prototype转的 一:类似继承,先上关键代码 ...

  6. 理解JavaScript继承(二)

    理解JavaScript继承(二) 5.寄生式继承 function object(o) {function F() {} F.prototype = o; return new F(); }func ...

  7. 【JavaScript】重温Javascript继承机制

    上段时间,团队内部有过好几次给力的分享,这里对西风师傅分享的继承机制稍作整理一下,适当加了些口语化的描述,留作备案. 一.讲个故事吧 澄清在先,Java和Javascript是雷锋和雷峰塔的关系.Ja ...

  8. [转]JavaScript继承详解

    原文地址:http://www.cnblogs.com/sanshi/archive/2009/07/08/1519036.html 面向对象与基于对象 几乎每个开发人员都有面向对象语言(比如C++. ...

  9. 重温Javascript继承机制

    上段时间,团队内部有过好几次给力的分享,这里对西风师傅分享的继承机制稍作整理一下,适当加了些口语化的描述,留作备案. 一.讲个故事吧 澄清在先,Java和Javascript是雷锋和雷峰塔的关系.Ja ...

最新文章

  1. mysql去除字符串首的非字母_如何从SQLServer中的字符串中删除所有非字母字符?...
  2. Android Service Security
  3. wifi共享大师电脑版_手机也能给电脑上网!WiFi热点已经out了,蓝牙USB线也能共享网络...
  4. 精心整理吐血推荐的AUTOSAR科普介绍材料
  5. a.out、coff、elf三种文件格式
  6. 数通手稿留档——Multicast
  7. 音频开发基本知识总结
  8. JMeter配置元件【HTTP请求默认值】
  9. 解决载入自定义视图时出现InflateException #8:Error inflating的错误
  10. atitit.提升研发效率的利器---重型框架与类库的区别与设计原则
  11. win7护眼透明主题 “魅力win7”
  12. Android为页面添加水印
  13. python爬虫爬取豆瓣电影评分排行榜前n名的前n页影评
  14. hihoCoder1378 (最大流最小割)
  15. html单标签和双标签汇总
  16. radeon r600 hpd中断处理
  17. python百位数千位数取整
  18. 10个最佳企业移动支付APP应用和酷站欣赏
  19. 自动化测试学习步骤及路线(超详细)
  20. An unexpected error occurred: “EACCES: permission denied, mkdir

热门文章

  1. java 输出字符串的所有排列_JAVA 输出指定字符串所有排列组合
  2. VMware15设置快照回到指定时间的状态
  3. 仿ios桌面vivo_vivo全新OriginOS细节曝光:系统UI大变样
  4. php m grep event,linux grep (linux查找关键字在php出现的次数)
  5. maven引用外部jar依赖
  6. nginx优化——包括https、keepalive等
  7. win7 蓝屏信息获取和处理
  8. RSA加密算法的简单案例
  9. [Err] 1231 - Variable 'sql_mode' can't be set to the value of 'NULL
  10. LA 4127 - The Sky is the Limit (离散化 扫描线 几何模板)