JavaScript的原型继承详解


Posted in Javascript onFebruary 15, 2015

JavaScript是一门面向对象的语言。在JavaScript中有一句很经典的话,万物皆对象。既然是面向对象的,那就有面向对象的三大特征:封装、继承、多态。这里讲的是JavaScript的继承,其他两个容后再讲。

JavaScript的继承和C++的继承不大一样,C++的继承是基于类的,而JavaScript的继承是基于原型的。

现在问题来了。

原型是什么?原型我们可以参照C++里的类,同样的保存了对象的属性和方法。例如我们写一个简单的对象

function Animal(name) {

    this.name = name;

}

Animal.prototype.setName = function(name) {

    this.name = name;

}

var animal = new Animal("wangwang");

我们可以看到,这就是一个对象Animal,该对象有个属性name,有个方法setName。要注意,一旦修改prototype,比如增加某个方法,则该对象所有实例将同享这个方法。例如

function Animal(name) {

    this.name = name;

}

var animal = new Animal("wangwang");

这时animal只有name属性。如果我们加上一句,

Animal.prototype.setName = function(name) {

    this.name = name;

}

这时animal也会有setName方法。

继承本复制——从空的对象开始我们知道,JS的基本类型中,有一种叫做object,而它的最基本实例就是空的对象,即直接调用new Object()生成的实例,或者是用字面量{ }来声明。空的对象是“干净的对象”,只有预定义的属性和方法,而其他所有对象都是继承自空对象,因此所有的对象都拥有这些预定义的 属性与方法。原型其实也是一个对象实例。原型的含义是指:如果构造器有一个原型对象A,则由该构造器创建的实例都必然复制自A。由于实例复制自对象A,所以实例必然继承了A的所有属性、方法和其他性质。那么,复制又是怎么实现的呢?方法一:构造复制每构造一个实例,都从原型中复制出一个实例来,新的实例与原型占用了相同的内存空间。这虽然使得obj1、obj2与它们的原型“完全一致”,但也非常不经济——内存空间的消耗会急速增加。如图:

JavaScript的原型继承详解

方法二:写时复制这种策略来自于一致欺骗系统的技术:写时复制。这种欺骗的典型示例就是操作系统中的动态链接库(DDL),它的内存区总是写时复制的。如图:

JavaScript的原型继承详解

我们只要在系统中指明obj1和obj2等同于它们的原型,这样在读取的时候,只需要顺着指示去读原型即可。当需要写对象(例如obj2)的属性时,我们就复制一个原型的映像出来,并使以后的操作指向该映像即可。如图:

JavaScript的原型继承详解

这种方式的优点是我们在创建实例和读属性的时候不需要大量内存开销,只在第一次写的时候会用一些代码来分配内存,并带来一些代码和内存上的开销。但此后就不再有这种开销了,因为访问映像和访问原型的效率是一致的。不过,对于经常进行写操作的系统来说,这种方法并不比上一种方法经济。方法三:读遍历这种方法把复制的粒度从原型变成了成员。这种方法的特点是:仅当写某个实例的成员,将成员的信息复制到实例映像中。当写对象属性时,例如(obj2.value=10)时,会产生一个名为value的属性值,放在obj2对象的成员列表中。看图:JavaScript的原型继承详解

可以发现,obj2仍然是一个指向原型的引用,在操作过程中也没有与原型相同大小的对象实例创建出来。这样,写操作并不导致大量的内存分配,因此内存的使用上就显得经济了。不同的是,obj2(以及所有的对象实例)需要维护一张成员列表。这个成员列表遵循两条规则:保证在读取时首先被访问到如果在对象中没有指定属性,则尝试遍历对象的整个原型链,直到原型为空或或找到该属性。原型链后面会讲。显然,三种方法中,读遍历是性能最优的。所以,JavaScript的原型继承是读遍历的。constructor熟悉C++的人看完最上面的对象的代码,肯定会疑惑。没有class关键字还好理解,毕竟有function关键字,关键字不一样而已。但是,构造函数呢?实际上,JavaScript也是有类似的构造函数的,只不过叫做构造器。在使用new运算符的时候,其实已经调用了构造器,并将this绑定为对象。例如,我们用以下的代码

var animal = Animal("wangwang");

animal将是undefined。有人会说,没有返回值当然是undefined。那如果将Animal的对象定义改一下:

function Animal(name) {

    this.name = name;

    return this;

}

猜猜现在animal是什么?
此时的animal变成window了,不同之处在于扩展了window,使得window有了name属性。这是因为this在没有指定的情况下,默认指向window,也即最顶层变量。只有调用new关键字,才能正确调用构造器。那么,如何避免用的人漏掉new关键字呢?我们可以做点小修改:

function Animal(name) {

    if(!(this instanceof Animal)) {

        return new Animal(name); 

    }

    this.name = name;

}

这样就万无一失了。构造器还有一个用处,标明实例是属于哪个对象的。我们可以用instanceof来判断,但instanceof在继承的时候对祖先对象跟真正对象都会返回true,所以不太适合。constructor在new调用时,默认指向当前对象。

console.log(Animal.prototype.constructor === Animal); // true

我们可以换种思维:prototype在函数初始时根本是无值的,实现上可能是下面的逻辑

// 设定__proto__是函数内置的成员,get_prototyoe()是它的方法

var __proto__ = null;

function get_prototype() {

    if(!__proto__) {

        __proto__ = new Object();

        __proto__.constructor = this;

    }

    return __proto__;

}

这样的好处是避免了每声明一个函数都创建一个对象实例,节省了开销。constructor是可以修改的,后面会讲到。基于原型的继承继承是什么相信大家都差不多知道,就不秀智商下限了。

JS的继承有好几种,这里讲两种

1. 方法一这种方法最常用,安全性也比较好。我们先定义两个对象

function Animal(name) {

    this.name = name;

}

function Dog(age) {

    this.age = age;

}

var dog = new Dog(2);

要构造继承很简单,将子对象的原型指向父对象的实例(注意是实例,不是对象)

Dog.prototype = new Animal("wangwang");

这时,dog就将有两个属性,name和age。而如果对dog使用instanceof操作符

console.log(dog instanceof Animal); // true

console.log(dog instanceof Dog); // false

这样就实现了继承,但是有个小问题

console.log(Dog.prototype.constructor === Animal); // true

console.log(Dog.prototype.constructor === Dog); // false

可以看到构造器指向的对象更改了,这样就不符合我们的目的了,我们无法判断我们new出来的实例属于谁。因此,我们可以加一句话:

Dog.prototype.constructor = Dog;

再来看一下:

console.log(dog instanceof Animal); // false

console.log(dog instanceof Dog); // true

done。这种方法是属于原型链的维护中的一环,下文将详细阐述。2. 方法二这种方法有它的好处,也有它的弊端,但弊大于利。先看代码

<pre name="code" class="javascript">function Animal(name) {

    this.name = name;

}

Animal.prototype.setName = function(name) {

    this.name = name;

}

function Dog(age) {

    this.age = age;

}

Dog.prototype = Animal.prototype;

这样就实现了prototype的拷贝。

这种方法的好处就是不需要实例化对象(和方法一相比),节省了资源。弊端也是明显,除了和上文一样的问题,即constructor指向了父对象,还只能复制父对象用prototype声明的属性和方法。也即是说,上述代码中,Animal对象的name属性得不到复制,但能复制setName方法。最最致命的是,对子对象的prototype的任何修改,都会影响父对象的prototype,也就是两个对象声明出来的实例都会受到影响。所以,不推荐这种方法。

原型链

写过继承的人都知道,继承可以多层继承。而在JS中,这种就构成了原型链。上文也多次提到了原型链,那么,原型链是什么?一个实例,至少应该拥有指向原型的proto属性,这是JavaScript中的对象系统的基础。不过这个属性是不可见的,我们称之为“内部原型链”,以便和构造器的prototype所组成的“构造器原型链”(亦即我们通常所说的“原型链”)区分开。我们先按上述代码构造一个简单的继承关系:

function Animal(name) {

    this.name = name;

}

function Dog(age) {

    this.age = age;

}

var animal = new Animal("wangwang");

Dog.prototype = animal;

var dog = new Dog(2);

提醒一下,前文说过,所有对象都是继承空的对象的。所以,我们就构造了一个原型链:

JavaScript的原型继承详解

我们可以看到,子对象的prototype指向父对象的实例,构成了构造器原型链。子实例的内部proto对象也是指向父对象的实例,构成了内部原型链。当我们需要寻找某个属性的时候,代码类似于

function getAttrFromObj(attr, obj) {

    if(typeof(obj) === "object") {

        var proto = obj;

        while(proto) {

            if(proto.hasOwnProperty(attr)) {

                return proto[attr];

            }

            proto = proto.__proto__;

        }

    }

    return undefined;

}

在这个例子中,我们如果在dog中查找name属性,它将在dog中的成员列表中寻找,当然,会找不到,因为现在dog的成员列表只有age这一项。接着它会顺着原型链,即.proto指向的实例继续寻找,即animal中,找到了name属性,并将之返回。假如寻找的是一个不存在的属性,在animal中寻找不到时,它会继续顺着.proto寻找,找到了空的对象,找不到之后继续顺着.proto寻找,而空的对象的.proto指向null,寻找退出。

原型链的维护我们在刚才讲原型继承的时候提出了一个问题,使用方法一构造继承时,子对象实例的constructor指向的是父对象。这样的好处是我们可以通过constructor属性来访问原型链,坏处也是显而易见的。一个对象,它产生的实例应该指向它本身,也即是

(new obj()).prototype.constructor === obj;

然后,当我们重写了原型属性之后,子对象产生的实例的constructor不是指向本身!这样就和构造器的初衷背道而驰了。我们在上面提到了一个解决方案:

Dog.prototype = new Animal("wangwang");

Dog.prototype.constructor = Dog;

看起来没有什么问题了。但实际上,这又带来了一个新的问题,因为我们会发现,我们没法回溯原型链了,因为我们没法寻找到父对象,而内部原型链的.proto属性是无法访问的。于是,SpiderMonkey提供了一个改良方案:在任何创建的对象上添加了一个名为__proto__的属性,该属性总是指向构造器所用的原型。这样,对任何constructor的修改,都不会影响__proto__的值,就方便维护constructor了。

但是,这样又两个问题:

__proto__是可以重写的,这意味着使用它时仍然有风险

__proto__是spiderMonkey的特殊处理,在别的引擎(例如JScript)中是无法使用的。

我们还有一种办法,那就是保持原型的构造器属性,而在子类构造器函数内初始化实例的构造器属性。

代码如下:改写子对象

function Dog(age) {

    this.constructor = arguments.callee;

    this.age = age;

}

Dog.prototype = new Animal("wangwang");

这样,所有子对象的实例的constructor都正确的指向该对象,而原型的constructor则指向父对象。虽然这种方法的效率比较低,因为每次构造实例都要重写constructor属性,但毫无疑问这种方法能有效解决之前的矛盾。ES5考虑到了这种情况,彻底的解决了这个问题:可以在任意时候使用Object.getPrototypeOf() 来获得一个对象的真实原型,而无须访问构造器或维护外部的原型链。因此,像上一节所说的寻找对象属性,我们可以如下改写:

function getAttrFromObj(attr, obj) {

    if(typeof(obj) === "object") {

        do {

            var proto = Object.getPrototypeOf(dog);

            if(proto[attr]) {

                return proto[attr];

            }

        }

        while(proto);

    }

    return undefined;

}

当然,这种方法只能在支持ES5的浏览器中使用。为了向后兼容,我们还是需要考虑上一种方法的。更合适的方法是将这两种方法整合封装起来,这个相信读者们都非常擅长,这里就不献丑了。

Javascript 相关文章推荐
jQuery Validation实例代码 让验证变得如此容易
Oct 18 Javascript
js自定义方法通过隐藏iframe实现文件下载
Feb 21 Javascript
将Datatable转化成json发送前台实现思路
Sep 06 Javascript
文本域中换行符的替换示例
Mar 04 Javascript
javascript中scrollTop详解
Apr 13 Javascript
JS中处理时间之setUTCMinutes()方法的使用
Jun 12 Javascript
Jquery 分页插件之Jquery Pagination
Aug 25 Javascript
jQuery 获取多选框的值及多选框中文的函数
May 16 Javascript
js中 计算两个日期间的工作日的简单实例
Aug 08 Javascript
javascript 网页进度条简单实例
Feb 22 Javascript
判断颜色是否合法的正则表达式(详解)
May 03 Javascript
JS使用百度地图API自动获取地址和经纬度操作示例
Apr 16 Javascript
javascript 闭包详解
Feb 15 #Javascript
PhotoShop给图片自动添加边框及EXIF信息的JS脚本
Feb 15 #Javascript
浅析javascript中函数声明和函数表达式的区别
Feb 15 #Javascript
不同编码的页面表单数据乱码问题解决方法
Feb 15 #Javascript
浅谈javascript中this在事件中的应用
Feb 15 #Javascript
javascript使用正则表达式实现去掉空格之后的字符
Feb 15 #Javascript
分享一个自己写的简单的javascript分页组件
Feb 15 #Javascript
You might like
ThinkPHP3.1.3版本新特性概述
2014/06/19 PHP
php自定义函数实现二维数组按指定key排序的方法
2016/09/29 PHP
微信公众平台开发教程⑥ 微信开发集成类的使用图文详解
2019/04/10 PHP
Code: write(s,d) 输出连续字符串
2007/08/19 Javascript
用JavaScript计算在UTF-8下存储字符串占用字节数
2013/08/08 Javascript
jquery设置按钮停顿3秒不可用
2014/03/07 Javascript
jQuery头像裁剪工具jcrop用法实例(附演示与demo源码下载)
2016/01/22 Javascript
indexedDB bootstrap angularjs之 MVC DOMO (应用示例)
2016/06/20 Javascript
仿Angular Bootstrap TimePicker创建分钟数-秒数的输入控件
2016/07/01 Javascript
关于List.ToArray()方法的效率测试
2016/09/30 Javascript
微信小程序 小程序制作及动画(animation样式)详解
2017/01/06 Javascript
简单实现js悬浮导航效果
2017/02/05 Javascript
jquery实现数字输入框
2017/02/22 Javascript
vue-router 学习快速入门
2017/03/01 Javascript
详解vue中computed 和 watch的异同
2017/06/30 Javascript
JS实现键值对遍历json数组功能示例
2018/05/30 Javascript
JQuery实现ajax请求的示例和注意事项
2018/12/10 jQuery
vue动态注册组件实例代码详解
2019/05/30 Javascript
理解Python中的类与实例
2015/04/27 Python
Python基于Tkinter实现的记事本实例
2015/06/17 Python
Python设计实现的计算器功能完整实例
2017/08/18 Python
利用python求解物理学中的双弹簧质能系统详解
2017/09/29 Python
Flask框架路由和视图用法实例分析
2019/11/07 Python
Django中的session用法详解
2020/03/09 Python
Pytorch通过保存为ONNX模型转TensorRT5的实现
2020/05/25 Python
软件测试有哪些?什么是配置项?
2012/02/12 面试题
学校办公室主任职责
2013/12/27 职场文书
怎样客观的做好自我评价
2013/12/28 职场文书
致长跑运动员广播稿
2014/01/31 职场文书
征用土地赔偿协议书
2014/09/26 职场文书
黄石寨导游词
2015/02/05 职场文书
生产现场禁烟通知
2015/04/23 职场文书
5分钟教你docker安装启动redis全教程(全新方式)
2021/05/29 Redis
SpringMVC 整合SSM框架详解
2021/08/30 Java/Android
十大最帅动漫男主 碓冰拓海上榜,第一是《灌篮高手》男主角
2022/03/18 日漫
python文件与路径操作神器 pathlib
2022/04/01 Python