JavaScript Core -- 继承机制


JavaScript 继承机制历史回顾

    javascript最开始被设计为一款运行在浏览器上的简易语言,当时面向对象语言C++/JAVA大行其道。javascript之父 Brendan Eich无疑受到了影响,而且更进一步将javascript中所有的数据类型都设计为了对象(Object)。如果真的作为一种简单易用的语言,其实是不需要拥有“继承”机制的,但是既然JavaScript中都是对象,那怎么将所有的对象联系到一起呢?咳咳,最后Brendan Eich还是设计了“继承”。

    但他还是不想引入“类”的概念,因为这样会将JavaScript语言变得很正式,提高了入门的难度,他的初衷是一门简单易用的脚本语言而已,最终决定使用new 操作符从原型对象生成一个实例对象。使用new后面跟着的构造函数作为原型对象,如下所示生成一个person的实例person1

    function person(name){
    this.name = name;//this对象代表新创建的实例对象
    }
    var person1 = new Person('feng')
    但是,使用构造函数生成一个实例,有一个缺点那就是无法共享属性和方法,如下所示

   function person(name){
<span style="white-space:pre">	</span>this.name = name;//this对象代表新创建的实例对象
<span style="white-space:pre">	</span>this.species = '哺乳类';
    }   
   var person1 = new person('feng');//this.name = 'feng'
   var person2 = new Person('bling');//this.name = 'bling' 
   person1.species = '爬行类'; 
   alert(person2.species);//哺乳类,不会受person1实例属性的改变而影响

 
       考虑到这一点,Brendan Eich决定引入一个全新的名词“prototype”作为构造器的属性,这个属性指向一个对象prototype(原型对象),这个原型对象包含着对象所有需要共享的属性和方法,而不需要共享的属性和方法则放到了构造器中。 
   实例对象一旦创建就自动引入原型对象的属性和方法,属性和方法沿着原型链进行查找 
   。还是上面那个例子修改一下,如下所示 
   

    function person(name){
<span style="white-space:pre">	</span>this.name = name;//this对象代表新创建的实例对象
    }
    person.prototype = {
        species :'哺乳类'
    }
    var person1 = new person('feng');//this.name = 'feng'
    var person2 = new person('bling');//this.name = 'bling'
    alert(person1.species);//哺乳类
    alert(person2.species);//哺乳类
	
    这样所有的实例对象就“继承了”原型对象中的属性和方法,通过实例对象的原型。


JavaScript 继承实现原理

初入江湖:原型继承

    构造函数(类)的原型带有一个 constructor 属性,指回构造函数(如果重新分配了原型指针,需要手动添加 constructor 属性,重新建立构造函数和原型对象之间的联系)。构造函数的实例上会自动生成一个属性指向该类原型(在Chrome上可以通过“__proto__”访问到该对象,而IE上该属性则是不可见的), 而形成了一条原型链(Prototype-chain),如下图

    JavaScript将原型链作为实现继承的主要方法。代码读取某个对象的某个属性的时候,都会按照上图中存在的实现执行一遍搜索,目标是具有给定名字的属性。搜索首先从对象实例开始,如果在实例中找到该属性则返回,如果没有则查找prototype,如果还是没有找到则继续递归prototype的prototype对象,直到找到为止,如果递归到object仍然没有则返回错误。同样道理如果在实例中定义如prototype同名的属性或函数,则会覆盖prototype的属性或函数。搜索时的这个路线抽象成为了所谓的原型链。

 注:写一个函数来判断定属性属于原型中还是存在于对象中

function hasPrototypeProperty(object,name){  
    return !object.hasOwnPrototype(name) &&( name in object);   
}  

    name in object->只要name能被访问,就返回true,无论远行中还是对象中;object.hasOwnPrototype(name)->对象本身拥有name属性返回true。所以,当name属性存在于原型中时,函数返回true

    修改原型对象的属性时,那么所有的实例通过原型链都会访问到更改后的属性;但是如果将原型对象设置为其他值Person.prototype = {},那么原来的实例person1和原来的原型对象的关系没有变化,person1还拥有原来原型对象的属性 ,直接修改prototype属性不会影响已经创建的属性,影响的将是新创建的实例

    下面演示代码的基本原理是利用原型让一个subType引用superType的属性和方法。将subType的prototype属性设为superType的实例,这时候subType的原型链中就包含了superType的属性和方法,大致是这样的

    function SuperType(){
        this.property=true;
    }

    SuperType.prototype.getSuperValue=function(){
        return this.property;
    };

    function SubType(){
        this.subProperty=false;
    }

    SubType.prototype=new SuperType();//让subType的原型指向SuperType的原型

    SubType.prototype.getSubValue=function(){
        return this.subProperty;
    }

    这样SubType就实现了对SuperType的继承,看个图


(原型中的construnctor属性指回那个构造函数)

    验证一下,是不是这个样子

    var instance=new SubType();
    console.log(instance.getSuperValue()); //true
    console.log(instance.getSubValue()); //false
    console.log(instance instanceof SuperType); //true
    console.log(instance instanceof SubType); //true

    果不其然,SubType的实例同时也是SuperType的实例,可以访问到SuperType的属性和方法,实现了SubType对SuperType的继承。在上面的代码中SubType没有使用其默认的prototype对象,而是使用了SuperType的实例,这样SubType的实例拥有

        1.SuperType实例的属性和方法

        2.SuperType的prototype的属性和方法(如上图,在原型链中SuperType的实例有指向SuperType的prototype的指针)

        3.SubType实例属性和方法(subProperty)

        4.SubType新的prototype对象追加的属性和方法(getSubValue)

    熟悉JavaScript的同学肯定清楚:和很多强类型面向对象语言一样,JavaScript的所有引用类型都默认继承自Object对象,这个继承也是通过原型链实现的。

        JavaScript Core -- 继承机制_第1张图片

    所有函数都是Object对象的实例,这样是不是明白了为什么所有自定义类型都会用于toString, valueOf 等默认方法了吧,看起来很不错,但是这样的继承方式有两个需要注意的地方

    1.给子类型的prototype对象内追加方法和属性必须在继承了父类型实例之后,否则追加的方法、属性会被重写,也就是说上面的例子

    function SubType(){
        this.subProperty=false;
    }

    SubType.prototype=new SuperType();

    SubType.prototype.getSubValue=function(){
        return this.subProperty;
    }

不能写成这样

    function SubType(){
        this.subProperty=false;
    }

    SubType.prototype.getSubValue=function(){
        return this.subProperty;
    }
    
    SubType.prototype=new SuperType();

2.子类型的prototype不能通过字面量赋值,这样也会重写原型链而不是追加,也就是说不能写成这样

    function SubType(){
        this.subProperty=false;
        }

    SubType.prototype=new SuperType();

    SubType.prototype={
        getSubValue:function(){
            return this.subProperty;
        }
    }


名动一方:组合继承

    上面的经典继承方式是不完美的,当父类型中存在引用类型的属性的时候,子类型的所有实例会共享这些属性,一处修改,处处更新,这样是不安全的

   function SuperType(){
        this.property=true;
        this.colors=['red','blue','green'];
    }

    SuperType.prototype.getSuperValue=function(){
        return this.property;
    };

    function SubType(){
        this.subProperty=false;
    }

    SubType.prototype=new SuperType();

    SubType.prototype.getSubValue=function(){
        return this.subProperty;
    }

    var sub1=new SubType();
    sub1.colors.push('yellow');
    var sub2=new SubType();
    console.log(sub2.colors);//["red", "blue", "green", "yellow"]

      确实是这样的,但是这个错误很熟悉的赶脚有没有,在javascript创建对象的过程中也会有这个问题。解决方法:子类型构造函数中引入父类型的构造函数,使实例拥有自己独有的属性

    function SuperType(){
        this.property=true;
        this.colors=['red','blue','green'];
    }

    SuperType.prototype.getSuperValue=function(){
        return this.property;
    };

    function SubType(){
        this.subProperty=false;
       <span style="color:#ff0000;"> SuperType.call(this);</span>
    }
    SubType.prototype=new SuperType(); 
    SubType.prototype.getSubValue=function(){
        return this.subProperty;
    }

    var sub1=new SubType();
    sub1.colors.push('yellow');
    var sub2=new SubType();
    console.log(sub2.colors);//["red", "blue", "green"]

    这样在子类的构造函数中调用父类的构造函数,这样的写法实际上是在新创建的SubType的实例环境下调用SuperType的构造函数,实例中执行SuperType函数中对象初始化代码,Subtype的每个实例就会有自己的colors副本了。这样的继承方式在坊间被称为组合式继承。(额,你是否联想到了java中的super()...,一样的道理)

    这样做还多了个好处,可以传递参数了

    function SuperType(name){
        this.property=name;
        this.colors=['red','blue','green'];
    }

    SuperType.prototype.getSuperValue=function(){
        return this.property;
    };

    function SubType(name){
        this.subProperty=false;
        SuperType.call(this,name);
    }
    SubType.prototype=new SuperType(); 
      //或者将这行代码替换为  
      //SubType.prototype = Object.create(SuperType.prototype);//Object.create创建一个空对象,并且对象的原型指向参数SuperType.prototype
      //SubType.prototype.constructor = SubType//若不设置的话,SubType.prototype会指向SuperType

    SubType.prototype.getSubValue=function(){
        return this.subProperty;
    }

    var sub1=new SubType('Byron');
    console.log(sub1.property);//Byron


 
   

一代宗师:寄生组合式继承

    看起来使用组合式继承很完美了,但是...总是有但是,使用组合继承总会调用两次父类的构造函数,这样父类的实例属性和方法在子类的实例中会有两份。父类初始化两次,这有时会导致一些问题,举个例子,父类构造函数中有个alert,那么创建子类实例时,会发现有两次弹框。

    function SubType(name){
        this.subProperty=false;
        SuperType.call(this,name); //第一次
    }

    SubType.prototype=new SuperType(); //第二次

    SubType.prototype.getSubValue=function(){
        return this.subProperty;
    }

    JavaScript Core -- 继承机制_第2张图片

    可以看到子类的原型中也包含了父类的属性,只不过因为解释器的属性查找机制,被子类的属性所覆盖,只要子类的特权成员被删除,原型中相应的成员就会暴露出来:

    问题的根源就是调用两次SuperType的构造函数,其实在第一次调用的时候SubType就已经获得了SuperType的实例属性和方法,第二次调用的时候仅需要SuperType的prototype属性就可以了,因此可以这样写

    function extend(subType,superType){
        var _prototype=new Object(superType.prototype); //得到父类prototype对象副本
        _prototype.constructor=subType; //constructor属性改为子类自己的
        subType.prototype=_prototype; //重写子类prototype
    }

    function SuperType(name){
        this.property=name;
        this.colors=['red','blue','green'];
    }

    SuperType.prototype.getSuperValue=function(){
        return this.property;
    };

    function SubType(name){
        this.subProperty=false;
        SuperType.call(this,name); 
    }

    extend(SubType,SuperType);

    SubType.prototype.getSubValue=function(){
        return this.subProperty;
    }

    JavaScript Core -- 继承机制_第3张图片

    未完待续。。。。。。

你可能感兴趣的:(JavaScript Core -- 继承机制)