引言

  在JavaScript中,实现继承的主要方式是通过原型链技术。这一篇文章我们就通过介绍JavaScript中实现继承的几种方式来慢慢领会JavaScript中继承实现的点点滴滴。

原型链介绍

  原型链作为JS实现继承的主要方式,其基本思想是:利用原型让一个引用类型继承另一个引用类型的属性和方法。我们可以简单回顾下构造函数、原型对象和实例对象之间的关系。每一个构造函数都有一个指向原型对象的指针,当然原型对象的构造器属性也指向构造函数对象,而实例对象内部有prototype属性指向原型对象。如果我们让原型对象等于另一个引用类型的实例。那么这时候的原型对象将包含一个指向另一个原型对象的指针。当然,另一个原型对象也包含着指向另一个构造函数的指针。如果另一个原型又是另一个引用类型的实例,那么上述关系依然成立,就这样层层的推进,就构成了实例与原型的链条。这就是原型链的基本原理。

  下面我们来看个实现继承的基本例子:

 /**
* 实现继承的基本方式(原型链基本用法)
**/
function SuperType() {
this.property = true;
} SuperType.prototype.getSuperValue = function () {
return this.property;
} function SubType() {
this.subProperty = false;
} //设置子类型的原型对象是超类的实例
SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function () {
return this.subProperty;
} var instance = new SubType();
alert(instance.getSuperValue()); //true

  在这段代码中,我们看到了最基本的使用原型链来实现继承的方式。在这段代码中最核心的部分就是地17行代码。我们SubType的原型对象设置为SuperType的一个实例。这样存在于SuperType中的所有属性和方法现在也都存在于SubType的原型对象中。我们给SubType的原型中添加了getSubValue方法,实际就是在继承SuperType的属性和方法的基础上又添加了一个方法。

  下面请看详细的关系图

  通过这张图我们看到instance指向的是SubType的原型对象。而SubType原型又指向SuperType原型。通过实现原型链,本质上扩展了前面介绍的原型搜索机制。当以读取模式访问一个属性的时候,首先会在实例中进行搜索,如果搜索不到还会到实例的原型对象中进行搜索。在通过原型链继承的情况下,搜索过程就会沿着原型链继续往上搜索。例如:调用getSuperValue。1、第一步搜索instance实例对象。2、搜索SubType原型对象。3、搜索SuperType原型对象。最后在第三步才找到方法进行调用。

  讲到这里,我们千万别忘记了。所有的引用类型都继承自Object对象。而这个继承也是通过原型链实现的。所有函数的默认原型都是Object的实例。因此默认原型会包含一个指针指向Object.prototype。这也正是所有引用类型都会继承toString()、toValueOf()方法的根本原因。下面这张图很好的表达了这一点。

  

  我们调用instance.toString()方法实际上调用的是Object原型对象中的toString()方法。

  原型链存在的问题

  原型链虽然强大,可以用它来实现继承,但是也存在一些致命的问题。其中,最主要的问题是包含引用类型值的原型。想必大家在上一篇文章中也肯定还记得在原型对象中定义引用类型值的属性会被所有的实例共享。现在使用原型链,原型实际上会变成另一个引用类型的实例。如果我们在这个实例中定义了引用类型的属性(比如:Array)。那么子类型的所有实例都会共享这个实例中的这个属性值。这实际上就回到了我们上一篇遇到的问题。所以,我们首先想到的还是借用构造函数。让实例属性和原型属性分开的方式来进行解决。

  借用构造函数

  在解决原型中包含引用类型值带来的问题的过程中,人们开始使用一种叫借用构造函数的方式来解决此类问题。这种方法很简单。就是在子类构造函数中调用父类构造函数。记住:函数只是在特定环境中环境中执行代码的对象,因此可以使用call()或者apply()函数来在新创建的对象上调用构造函数。请看下面的例子:

 /**
* 借用构造函数
*/
function SuperType() {
this.color = ["red", "blue", "green"];
} function SubType() {
SuperType.call(this);
} var instance1 = new SubType();
instance1.color.push("black");
alert(instance1.color); //"red", "blue", "green","black" var instance2 = new SubType();
alert(instance1.color); //"red", "blue", "green"

  在这段代码中,我们看第9行。SubType构造函数调用了SuperType的构造函数。通过使用call()或者apply()函数,我们在新创建的对象上调用SuperType的构造函数。这样我们会在新创建的对象上执行超类构造函数的初始化代码(this指向的是新创建的对象)。所以每一个新创建的实例都有color的副本了。

  借用构造函数的问题

  如果我们仅仅借用构造函数的方式,那么我们将无法避免构造函数模式带来的问题。1、方法都在构造函数中定义,根本没有办法谈函数复用的问题。2、在超类中定义的方法在子类中根本不可见,结果所有类型都只能使用构造函数模式。为了解决这个问题,我们下面介绍一种方式:组合继承。

  组合继承

  组合继承技术是将原型链和借用构造函数技术组合在一起使用。其背后的思想是:通过使用原型链来实现对原型属性和方法的继承,通过借用构造函数模式实现对实例属性的继承。这样,既通过在原型上定义的方法实现了函数的复用,又能保证每一个实例都有自己的属性。请看下面的例子:

 /**
* 组合继承
**/
function SuperType(name) {
this.name = name;
this.color = ["red", "blue", "black"];
} SuperType.prototype.sayName = function () {
return this.name;
} function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
} SubType.prototype = new SuperType(); SubType.prototype.sayAge = function () {
return this.age;
} var instance1 = new SubType("Nicolas", 29);
instance1.color.push("green");
alert(instance1.color); //"red", "blue", "black","green"
alert(instance1.sayName()); //Nicolas
alert(instance1.sayAge()); // var instance2 = new SubType("Grey", 26);
alert(instance2.color); //"red", "blue", "black"
alert(instance2.sayName()); //Grey
alert(instance2.sayAge()); //

  通过以上的例子我们看到,我们在创建SubType实例的时候,调用SubType构造函数,在构造函数中通过call方法,使每一个实例上都有name、color属性的一份副本。同时设置SubType的原型为SuperType的实例,有可以进一步获取SuperType定义的共享属性和方法。实现了我们前面提到的目标。此处有一点需要注意:color属性其实对于SubType实例来说应该有两份,一份在实例中,还有一份在原型对象中,只是原型对象中的那一份被实例中的那一份覆盖了。还记得对象中属性值的搜索机制吗?

  应该说,组合继承融合了原型链和构造函数方式的优点,成为JavaScript中最常用的一种继承模式。但是对于它的改进还是存在的。请看下面把。

  寄生组合式继承

  虽然组合继承是JavaScript中最常用的一直继承模式,但是它也存在着明显的缺点。例如:1、无论在什么情况下都需要调用两次超类的构造函数。2、子类的实例都会包含超类实例的全部实例属性,但是我们只能在调用超类构造函数的时候重写这些属性,让它覆盖原型对象中的属性值。请看下图:

  

  其实,我们实现继承的关键无非就是一个超类原型对象的副本而已。在组合继承中我们使用超类实例作为原型对象,这样间接的关联上了超类的原型对象。但是我们无法避免子类新的原型对象中那些冗余的实例属性。如果我们定义一个自定义类型里面不存在任何实例属性,将它的原型对象设置为我们需要关联的超类的原型对象。这样不就不必存储那些不必要的实例属性了吗?看下面的代码

 /**
* 组合继承(改进版)
**/
function SuperType(name) {
this.name = name;
this.color = ["red", "blue", "black"];
} SuperType.prototype.sayName = function () {
return this.name;
} function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
} //SubType.prototype = new SuperType();
SubType.prototype = object(SuperType.prototype); SubType.prototype.sayAge = function () {
return this.age;
} var instance1 = new SubType("Nicolas", 29);
instance1.color.push("green");
alert(instance1.color); //"red", "blue", "black","green"
alert(instance1.sayName()); //Nicolas
alert(instance1.sayAge()); // var instance2 = new SubType("Grey", 26);
alert(instance2.color); //"red", "blue", "black"
alert(instance2.sayName()); //Grey
alert(instance2.sayAge()); // function object(obj) {
function F() { }
F.prototype = obj;
return new F();
}

  在代码中我们我们看到,我们定义的object方法的本质就是使用一个自定义的引用类型来复制超类的原型对象。并且这个对象里面很干净,不存在任何实例属性。这初步解决了我们上面提到的属性重复存储的问题。下面咱们看看还有没有进一步的优化空间。

  下面介绍一种更加科学的方法,寄生组合继承的基本模式。并且配合详细的图片给大家分析下。请看代码:

/**
* 寄生组合继承基本模式
**/
function initPrototype(SubType, SuperType) {
var prototype = object(SuperType.prototype);
prototype.constructor = SubType;
SubType.prototype = prototype;
}

  下面请看完整的代码例子:

 /**
* 组合继承(最终版)
**/
function SuperType(name) {
this.name = name;
this.color = ["red", "blue", "black"];
} SuperType.prototype.sayName = function () {
return this.name;
} function SubType(name, age) {
SuperType.call(this, name);
this.age = age;
} //SubType.prototype = new SuperType();
//SubType.prototype = object(SuperType.prototype);
initPrototype(SubType, SuperType); SubType.prototype.sayAge = function () {
return this.age;
} var instance1 = new SubType("Nicolas", 29);
instance1.color.push("green");
alert(instance1.color); //"red", "blue", "black","green"
alert(instance1.sayName()); //Nicolas
alert(instance1.sayAge()); // var instance2 = new SubType("Grey", 26);
alert(instance2.color); //"red", "blue", "black"
alert(instance2.sayName()); //Grey
alert(instance2.sayAge()); // function object(obj) {
function F() { }
F.prototype = obj;
return new F();
} /**
* 寄生组合继承基本模式
**/
function initPrototype(SubType, SuperType) {
var prototype = object(SuperType.prototype);
prototype.constructor = SubType;
SubType.prototype = prototype;
}

浅谈JavaScript中的继承的更多相关文章

  1. 浅谈 JavaScript 中的继承模式

    最近在读一本设计模式的书,书中的开头部分就讲了一下 JavaScript 中的继承,阅读之后写下了这篇博客作为笔记.毕竟好记性不如烂笔头. JavaScript 是一门面向对象的语言,但是 ES6 之 ...

  2. 浅谈javaScript中的继承关系<一>

    // JavaScript Document //创建三个构造函数 function Shape(){ this.name='ahape'; this.toString=function(){retu ...

  3. 浅谈JavaScript中的闭包

    浅谈JavaScript中的闭包 在JavaScript中,闭包是指这样一个函数:它有权访问另一个函数作用域中的变量. 创建一个闭包的常用的方式:在一个函数内部创建另一个函数. 比如: functio ...

  4. 浅谈JavaScript中的null和undefined

    浅谈JavaScript中的null和undefined null null是JavaScript中的关键字,表示一个特殊值,常用来描述"空值". 对null进行typeof类型运 ...

  5. 浅谈JavaScript中的正则表达式(适用初学者观看)

    浅谈JavaScript中的正则表达式 1.什么是正则表达式(RegExp)? 官方定义: 正则表达式是一种特殊的字符串模式,用于匹配一组字符串,就好比用模具做产品,而正则就是这个模具,定义一种规则去 ...

  6. 浅谈Javascript中的原型、原型链、继承

    构造函数,原型,实例三者的关系 构造函数: 构造函数是创建对象的一种常用方式, 其他创建对象的方式还包括工厂模式, 原型模式, 对象字面量等.我们来看一个简单的构造函数: function Produ ...

  7. 浅谈JavaScript中继承的实现

    谈到js中的面向对象编程,都有一个共同点,选择原型属性还是构造函数,两者各有利弊,而就片面的从js的对象创建以及继承的实现两个方面来说,官方所推荐的是两个相结合,各尽其责,各取其长,在前面的例子中,我 ...

  8. 浅谈JavaScript中的内存管理

    一门语言的内存存储方式是我们学习他必须要了解的,接下来让我浅谈一下自己对他的认识. 首先说,JavaScript中的变量包含两种两种类型: 1)值类型或基本类型:undefined.null.numb ...

  9. 【总结】浅谈JavaScript中的接口

    一.什么是接口 接口是面向对象JavaScript程序员的工具箱中最有用的工具之一.在设计模式中提出的可重用的面向对象设计的原则之一就是“针对接口编程而不是实现编程”,即我们所说的面向接口编程,这个概 ...

随机推荐

  1. linux定时器(crontab)实例

    linux实验示例----实现每2分钟将“/etc”下面的文件打包存储到“/usr/lobal”目录下 ·Step1:编辑当前用户的crontab并保存终端输入:>crontab -u root ...

  2. css-使用line-height实现垂直居中的一些问题

    网上都是这么说的,把line-height值设置为height一样大小的值可以实现单行文字的垂直居中.这句话确实是正确的,但其实也是有问题的.问题在于height,看我的表述:"把line- ...

  3. Leetcode 77, Combinations

    Given two integers n and k, return all possible combinations of k numbers out of 1 ... n. For exampl ...

  4. JSR303注解

    Annotation 属于Bean Validation 规范 应用位置 作用 对Hibernate Core中的元数据的影响 @AssertFalse yes field/property 检查被标 ...

  5. 【bzoj3110】 Zjoi2013—K大数查询

    http://www.lydsy.com/JudgeOnline/problem.php?id=3110 (题目链接) 题意 有N个位置,M个操作.操作有两种,每次操作如果是1 a b c的形式表示在 ...

  6. 洛谷P1460 健康的荷斯坦奶牛 Healthy Holsteins

    题目描述 农民JOHN以拥有世界上最健康的奶牛为傲.他知道每种饲料中所包含的牛所需的最低的维他命量是多少.请你帮助农夫喂养他的牛,以保持它们的健康,使喂给牛的饲料的种数最少. 给出牛所需的最低的维他命 ...

  7. dedecms /member/buy_action.php Weak Password Vulnerability Algorithm Vul

    catalog . 漏洞描述 . 漏洞触发条件 . 漏洞影响范围 . 漏洞代码分析 . 防御方法 . 攻防思考 1. 漏洞描述 . 漏洞由mchStrCode函数弱算法(异或算法: 得其中2知余下1) ...

  8. 数据结构作业——sights(最短路/最近公共祖先)

    sights Description 美丽的小风姑娘打算去旅游散心,她走进了一座山,发现这座山有 n 个景点,由于山路难修,所以施工队只修了最少条的路,来保证 n 个景点联通,娇弱的小风姑娘不想走那么 ...

  9. 树状数组求第k小的元素

    int find_kth(int k) { int ans = 0,cnt = 0; for (int i = 20;i >= 0;i--) //这里的20适当的取值,与MAX_VAL有关,一般 ...

  10. HD2767Proving Equivalences(有向图强连通分量+缩点)

    题目链接 题意:有n个节点的图,现在给出了m个边,问最小加多少边是的图是强连通的 分析:首先找到强连通分量,然后把每一个强连通分量缩成一个点,然后就得到了一个DAG.接下来,设有a个节点(每个节点对应 ...