JavaScript的原型继承

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

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

如今问题来了。浏览器

原型是什么?

原型咱们能够参照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的全部属性、方法和其余性质。
那么,复制又是怎么实现的呢?

方法一:构造复制

每构造一个实例,都从原型中复制出一个实例来,新的实例与原型占用了相同的内存空间。这虽然使得obj一、obj2与它们的原型“彻底一致”,但也很是不经济——内存空间的消耗会急速增长。如图:

方法二:写时复制

这种策略来自于一致欺骗系统的技术:写时复制。这种欺骗的典型示例就是操做系统中的动态连接库(DDL),它的内存区老是写时复制的。如图:
咱们只要在系统中指明obj1和obj2等同于它们的原型,这样在读取的时候,只须要顺着指示去读原型便可。当须要写对象(例如obj2)的属性时,咱们就复制一个原型的映像出来,并使之后的操做指向该映像便可。如图:
这种方式的优势是咱们在建立实例和读属性的时候不须要大量内存开销,只在第一次写的时候会用一些代码来分配内存,并带来一些代码和内存上的开销。但此后就再也不有这种开销了,由于访问映像和访问原型的效率是一致的。不过,对于常常进行写操做的系统来讲,这种方法并不比上一种方法经济。

方法三:读遍历

这种方法把复制的粒度从原型变成了成员。这种方法的特色是:仅当写某个实例的成员,将成员的信息复制到实例映像中。当写对象属性时,例如(obj2.value=10)时,会产生一个名为value的属性值,放在obj2对象的成员列表中。看图:
能够发现,obj2仍然是一个指向原型的引用,在操做过程当中也没有与原型相同大小的对象实例建立出来。这样,写操做并不致使大量的内存分配,所以内存的使用上就显得经济了。不一样的是,obj2(以及全部的对象实例)须要维护一张成员列表。这个成员列表遵循两条规则:
  1. 保证在读取时首先被访问到
  2. 若是在对象中没有指定属性,则尝试遍历对象的整个原型链,直到原型为空或或找到该属性。
原型链后面会讲。
 
显然,三种方法中,读遍历是性能最优的。因此,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);
提醒一下,前文说过,全部对象都是继承空的对象的。
因此,咱们就构造了一个原型链:

咱们能够看到,子对象的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,寻找退出。ide

原型链的维护

咱们在刚才讲原型继承的时候提出了一个问题,使用方法一构造继承时,子对象实例的constructor指向的是父对象。这样的好处是咱们能够经过constructor属性来访问原型链,坏处也是显而易见的。一个对象,它产生的实例应该指向它自己,也便是
(new obj()).prototype.constructor === obj; 
而后,当咱们重写了原型属性以后,子对象产生的实例的constructor不是指向自己!这样就和构造器的初衷背道而驰了。
咱们在上面提到了一个解决方案:
Dog.prototype = new Animal("wangwang");
Dog.prototype.constructor = Dog;
看起来没有什么问题了。但实际上,这又带来了一个新的问题,由于咱们会发现,咱们无法回溯原型链了,由于咱们无法寻找到父对象,而内部原型链的.proto属性是没法访问的。
因而,SpiderMonkey提供了一个改良方案:在任何建立的对象上添加了一个名为__proto__的属性,该属性老是指向构造器所用的原型。这样,对任何constructor的修改,都不会影响__proto__的值,就方便维护constructor了。可是,这样又两个问题:
  1. __proto__是能够重写的,这意味着使用它时仍然有风险
  2. __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的浏览器中使用。为了向后兼容,咱们仍是须要考虑上一种方法的。更合适的方法是将这两种方法整合封装起来,这个相信读者们都很是擅长,这里就不献丑了。
相关文章
相关标签/搜索