继承和原型链

@fwon 2014-07-04 09:04:40发表于 fwon/blog

继承和原型链


javascript中的每个对象都有一个内部私有的链接指向另一个对象 ,这个对象就是原对象的原型. 这个原型对象也有自己的原型, 直到对象的原型为null为止(也就是没有原型)最上一层为Object. 这种一级一级的链结构就称为原型链.

我们最常见的实现原型继承的方式如下:

function Animal() {
    this.name = 'Animal';
}

Animal.prototype.sleep = function() {
    console.log('i can sleep');
}

function Dog(name) {
    this.name = name;
}

Dog.prototype = new Animal(); //原型链指向Animal.prototype
Dog.prototype.constructor = Dog; //将constructor指向自己
var dog = new Dog('wangwang');
dog.name; //wangwang
dog.sleep(); //i can sleep

有些人不理解这句话的意义:

Dog.prototype.constructor = Dog;

Dog将构造函数指向了自己,这是因为在执行了上面一句之后,会有:

Dog.prototype.constructor == Animal; //true
Dog.prototype.constructor == Dog; //false

这显然是不符合逻辑的,那么是否意味这这是一种hack的继承方式,有木有更好更自然的继承方法呢?别着急,慢慢来。
我开始好奇new Animal()的时候js内部做了什么操作,既然Dog.prototype继承了Animal.prototype中的方法,那么我们来做个尝试,修改代码:

function Animal() {
    this.name = 'Animal';
}

Animal.prototype.sleep = function() {
    console.log('i can sleep');
}

function Dog(name) {
    this.name = name;
}

Dog.prototype = Animal.prototype; //将原型Animal.prototype直接赋值给Dog
Dog.prototype.constructor = Dog; //将constructor指向自己
var dog = new Dog('wangwang');
dog.name; //wangwang
dog.sleep(); //i can sleep

输出正常,好,我们再给狗多加一个技能:

Dog.prototype.eatShit() {
    console.log('i can eat shit');
}
dog.eatShit(); //i can eat shit
Animal.prototype.eatShit(); //i can eat shit 

但是我们发现Animal的原型中也多了eatShit这个技能,其他动物如果也继承了Animal,那么他也会eatShit了,这明显不太科学。

这个问题是怎么导致的呢?
由于我们将原型直接赋值,所以Animal和Dog共用了同一个原型,你可以理解为指向同一个内存地址,那么只要其中一个更改了,就会影响到另外一个的值。

那神秘的new Animal()到底悄悄地做了什么呢?

var animal = new Animal();
animal.__proto__ === Animal.prototype; //true

1.创建一个通用对象 var o = new Object();
2.将o作为关键字this的值传递给Animal的构造函数,var returnObject = Animal.constructor(o, arguments); this = o; 这个过程中构造函数显式地设置了name的值为“Animal”(执行Animal(),返回给o),隐式地将其内部的__proto__属性设置为Animal.prototype的值。即o.proto = Animal.protorype;
3.返回新创建的对象returnObject并将animal的值指向该对象。

这个过程中,__proto__提供了一个钩子,当请求prototype上的属性someProp时,JavaScript首先检查对象自身中是否存在属性的值,如果有,则返回该值。如果不存在,则检查Object.getPrototypeOf(o).someProp是否存在,如果仍然不存在,就继续检查Object.getPrototypeOf(Object.getPrototypeOf(0)).someProp,依次类推。

所以存在这样的关系:animal.proto = Animal.prototype;
有没有发现,我们实现继承的最简单方法就可以简化为:

Dog.prototype.__proto__ = Animal.prototype;

ok,能理解么?既然new是将父类的prototype赋值给子类的__proto__,那么我们只要对__proto__赋值就能够继承原型链了。
but,遗憾的是这种方法并不能够继承到父类的私有属性。

var animal = {};
animal.__proto__ = Animal.prototype;
animal.name; //undefined

并且这只适用于可扩展对象,一个不可扩展对象的__proto__属性是不可变的。

var obj = {};
Object.preventExtensions(obj);
obj.__proto__ = {}; //抛出异常TypeError

ECMAScript5中引入一个新方法:Object.create.可以调用这个方法来创建新对象,新对象的原型就是调用create方法时传入的第一个参数:

Object.create(proto [, propertiesObject ])

proto: 一个对象,作为新创建对象的原型。
propertiesObject: 一个对象值,可以包含若干个属性。
对于第一个参数的实现原理如下:

if (!Object.create) {
    Object.create = function (o) {
        if (arguments.length > 1) {
            throw new Error('Object.create implementation only accepts the first parameter.');
        }
        function F() {}
        F.prototype = o;
        return new F();
    };
}

其本质也是在内部定义了一个中间量F,并进行原型的赋值和new操作,实现原型的继承。
下面这个例子采用Object.create()完整实现继承:

function Animal(name) {
    this.name = name;
}

Animal.prototype = {
    name: null,
    doSomething: function() {
        //...
    }
}

function Dog(name, age) {
    Animal.call(this, name); //私有属性继承
    this.age = age;
}

Dog.prototype = Object.create(Animal.prototype, {
    age: {
        value: null,
        enumerable: true,
        configurable: true,
        writable: true
    },
    doSomething: {
        value: function() {
            Animal.prototype.doSomething.apply(this, arguments); //call super
        },
        enumerable: true,
        configurable: true,
        writable: true
    }
});

var dog = new Dog();
dog.doSomething();

性能

在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。

遍历对象的属性时,原型链上的每个属性都是可枚举的。

检测对象的属性是定义在自身上还是在原型链上,有必要使用hasOwnProperty方法,该方法由所有对象继承自Object.proptotype。

hasOwnProperty是JavaScript中唯一一个只涉及对象自身属性而不会遍历原型链的方法。