ECMAScript 继承

博客分类: 原创

ECMAScript 继承

我是非常不喜欢用继承的,所以对此的概念也不强,或者说自己抽象的能力不强。希望自己对此不仅有些了解,而是自己能真正的用好这个东西。

继承方式分文两种:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。ECMAScript只支持实现继承,并且ECMAScript没有类继承模型,所有实现继承的方式主要通过原型链。

原型链继承

既然说ECMAScript实现继承的方式是基于原型链的,所以我们先看一下什么是原型链,每个构造函数都有一个原型对象,原型对象都包括一个指向构造函数的指针,而实例都包含一个指向原型对象内部的指针。如果这个时候我们将这个实例赋值给另一个原型对象,这个时候新的原型对象将包含一个指向之前那个原型对象内部的指针,且根据链式的关系,新的原型对象也就包含一个指向构造函数的指针,相应的,构造函数又有一个原型对象。这样就构成了实例与原型的链条,这个就是原型链(我是这样理解的)。下面看个代码例子:

function Father(name){
    this.property = true;
    this.colors = ['red', 'green', 'blue', 'black'];
    this.name = name;
}
Father.prototype.getSuperValue = function(){
    return this.property;
}
function Son(){
    this.subproperty = false;
}
Son.prototype = new Father();
Son.prototype.getSubValue = function(){
    return this.subproperty;
}
var instance = new Son();
instance.colors.push('yellow');
var changeColors = new Son();

以上就是一个继承的简单实现。定义了一个构造函数FatherFather有个原型对象Father.prototypeFather.prototype.xxxFather.prototype = {xxx:yyy}会导致不同的instance.__proto__会不同),原型对象上有一个方法getSuperValue,又定义了一个构造函数Son,将Father的实例赋值给Son的原型对象,再给Son的原型对象上添加一个方法getSubValue,在把Son的实例赋值给instance,这时instance就有Sonsubproperty的属性和getSubValue方法,及Fatherproperty的属性和getSuperValue方法。这样就完成了一个继承。

但是这个这些方法都分别在哪里呢?instancesubproperty属性是直接挂在instance上,instancegetSubValue方法和property属性是挂在__proto__上的,instancegetSuperValue方法是挂在instance__proto____proto__上的。这个也可以体现原型链。

上面这中挂载结构也可以体现搜索过程。首先会搜索实例上,然后会搜索Son.prototype,最后会搜索Father.prototype。这样的寻找方式,来找到对应的方法。

问题1:这样的继承方式会带来一个问题,instance.constructor变成了Father;为什么会造成这样的问题也是很显然的。instance.constructor本来应该是Son,但是我们来看如何查找的,instance.__proto__Son.prototype,但是Son.prototype是被重新赋值过的,赋的值是Father的示例,这个赋值就已经修改了Son.prototype.constructor的指向,Father示例的__proto__Father.prototypeFather.prototype.constructor是`Father

问题2:上面的colors的定义是一个引用值时,当我们对这个引用值进行修改时,我们可以看到,changeColorscolors属性也变了。如果这里是一个赋值而不是调用push方法是不会有问题的,因为实例已经是构造器返回的一个新的对象了,但是在js的内存中,colors是一个引用值,此时的colors只是指向['red', 'green', 'blue', 'black']这块堆内存的空间,重新赋值会将这个colors指向新的存储空间,但是push操作还是操作的原来那个堆内存里的值,而所有的示例的colors被实例化出来时都是指向的同一块内存。并非相互影响,只是大家都指向同一块内存,内存里的值发生了改变。

问题3:很明显,在new Son的时候是没办法向Father中传参的,也就是说name这个属性子类是没办法传参赋值的。

借用构造函数

主要的实现思想是在子类构造器中调用父类的构造器,执行父类构造函数,将父类构造函数中的属性及方法继承过来。但是无法继承父类原型对象上的方法。

function Father(name){
    this.property = true;
    this.colors = ['red', 'green', 'blue', 'black'];
    this.name = name;
}
Father.prototype.getSuperValue = function(){
    return this.property;
}
function Son(name){
    Father.call(this, name);
    this.subproperty = false;
}
Son.prototype.getSubValue = function(){
    return this.subproperty;
}
var instance = new Son();
instance.colors.push('yellow');
var changeColors = new Son();
changeColors.getSuperValue === undefined

这种继承方式很明显解决了原型链继承的三个问题,首先instanceconstructorSon了。然后引用类型的值colors在被修改后,之后生成的实例不会改变,第三个问题也能解决,传参可以手动解决,在调用构造函数时,手动将这些参数传过去。

问题1:这样的继承方式也带来了一个问题。原型对象上的方法都是不可见的,无法调用到。这也就导致所有的方法属性都需要定义在构造器中了。

组合继承

思想很简单,就是将以上两种继承方法结合。用原型继承来继承原型对象;用借用构造器继承来继承构造器。

function Father(name){
    this.property = true;
    this.colors = ['red', 'green', 'blue', 'black'];
    this.name = name;
}
Father.prototype.getSuperValue = function(){
    return this.property;
}
function Son(name){
    Father.call(this, name);
    this.subproperty = false;
}
Son.prototype = new Father();
// 手动的将 constructor 指向自己
Son.prototype.constructor = Son
Son.prototype.getSubValue = function(){
    return this.subproperty;
}
var instance = new Son();
instance.colors.push('yellow');
var changeColors = new Son();

这种继承方式的优点在于能解决上面所说的两种继承方式所产生的问题。吸取两种继承的优势。但是还是不能解决constructor的问题,所以需要手动的将constructor指向自己。这样就能解决所有问题了。

问题1:如此优秀的继承方式也会引起一个问题,就是Father的构造函数被调用了两次,我们怎么解决这个问题呢。

原型式继承

这个继承方式没有严格意义上的构造函数,仅仅是最简单的继承原型对象上的属性。借用APIObject.create;实现的实质应该是一次浅复制。浅复制决定了他会有相应的问题。

var person = {
    name: 'karyn',
    friends: ['qi', 'song'],
    getName: function(){
        return this.name;
    }
}
person.prototype = {
    getFriends: function(){
        return this.friends;
    }
}

var gongPerson = Object.create(person);
gongPerson.name = 'gong';
gongPerson.friends.push('wen');

var yanPerson = Object.create(person);
yanPerson.name = 'yan';
yanPerson.friends.push('xxx');

Object.create只是将实例对象gongPerson的原型对象指向person,也就是gongPerson.__proto__person,个人感觉应该是增长原型链。因为person这个对象仅仅是一个对象,不是构造方法,所以person是没有原型对象person.prototype,即使添加了原型对象,也无法将原型对象上的方法和属性让实例通过原型链查找到

首先API支持有限,IE9+。上面的例子也反映出相应的那些问题,如:引用值friends属性的值被修改了。原型对象上的属性是没有办法继承的。

寄生式继承

利用构造函数来封装一次继承。

function createAnother(original){
    var clone = Object.create(original);
    clone.sayHi = function(){
        return 'hi';
    }
    return clone;
}
var person = {
    name: 'karyn',
    friends: ['qi', 'song'],
    getName: function(){
        return this.name;
    }
}
person.prototype = {
    getFriends: function(){
        return this.friends;
    }
}
var gongPerson = createAnother(person);
gongPerson.name = 'gong';
gongPerson.friends.push('wen');

var yanPerson = createAnother(person);
yanPerson.name = 'yan';
yanPerson.friends.push('xxx');

上面这个例子可以看出,与上面的继承不同的是,可以增加构造方法了,同时原型对象上的方法被拷贝了。被挂载到继承函数的属性上了。问题依旧存在。

寄生组合式继承

这种继承能弥补组合式继承的缺点,父对象被调用了两次。

function inheritPrototype(son, father){
    var prototype = Object.create(father.prototype);
    prototype.constructor = son;
    son.prototype = prototype;
}
function Father(name){
    this.property = true;
    this.colors = ['red', 'green', 'blue', 'black'];
    this.name = name;
}
Father.prototype.getSuperValue = function(){
    return this.property;
}
function Son(name){
    Father.call(this, name)
    this.subproperty = false;
}
inheritPrototype(Son, Father)
Son.prototype.getSubValue = function(){
    return this.subproperty;
}
var instance = new Son('karyn');
instance.colors.push('yellow');
var changeColors = new Son('gong');

所有的问题都解决。这也是目前最理想的继承方式。

Klass

基本上是封装了一个继承,也可以用这个声明一个类。完成继承,构造器其实就是__construct,新增的方法都可以放在props里。如果遇到同名方法会覆盖父类上的同名方法,但是可以在子类中调用父类同名方法。props中除了__construct方法,其他的会被到原型对象上。

var Klass = function(Parent, props){
    var Child, F, i;
    Child = function(){
        if (Child.uber && Child.uber.hasOwnProperty('__construct')) {
            Child.uber.__construct.apply(this, arguments);
        }
        if (Child.prototype.hasOwnProperty('__construct')) {
            Child.prototype.__construct.apply(this, arguments);
        }
    }

    Parent = Parent || Object;
    F = function() {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.uber = Parent.prototype;
    Child.prototype.constructor = Child;

    Child.prototype.parent = function(){
        var caller = this.parent.caller,
            func = caller.$sameName;
        if (!func)
            throw new Error('can not call parent');
        else {
            return func.apply(this, arguments);
        }
    };

    for(i in props){
        if (props.hasOwnProperty(i)){
            Child.prototype[i] = props[i];
            if(Parent.prototype[i]){
                Child.prototype[i].$sameName = Parent.prototype[i];
            }
        };
    }

    return Child;
}

var Father = Klass(null, {
    __construct: function(name){
        console.log('Father`s constructor');
        this.name = name;
    },
    getName: function(){
        console.log('Father`s name:' + this.name)
        return this.name;
    }
})
var father = new Father();

var Son = Klass(Father, {
    __construct: function(age){
        console.log('Son`s constructor');
        this.age = age;
    },
    getAge: function(){
        return this.age;
    },
    getName: function(){
        this.parent.apply(this , arguments );
        console.log('Son`s name:' + this.name)
    }
})
var son = new Son('song', 18);