深入理解JavaScript的原型、原型链与继承

在介绍原型是什么之前,首先需要知道原型是做什么用的,在JS高设书中,明显可以看到介绍有关原型的知识是在介绍创建对象的方式时提出来的,即使用原型模式来创建对象,显而易见,原型这个概念是与创建对象联系在一起的。当然,创建对象的方式有很多种,如工厂模式,构造函数模式,以及与原型模式有关的其他模式等。

原型模式

理解原型模式

我们创建的每一个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。简单的说,prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,就是不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中,如下面例子所示:

    function Person () {

    }
    Person.prototype.name="Jack";
    Person.prototype.age=20;
    Person.prototype.sayName = function () {
        alert(this.name);
    };
    var person1=new Person();
    person1.sayName();    //"Jack"
    var person2=new Person();
    person2.sayName();    //"Jack"
    alert(person1.sayName==person2.sayName);//true

在这个例子中,我们将sayName( )方法和属性直接添加到了Person的prototype属性中,构造函数变成了空函数。即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。但与构造函数不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1和person2访问的都是同一组属性和一个sayName( )函数,要理解原型模式的工作原理,必须先理解ECSMScript中原型对象的性质。

理解原型对象

无论什么时候,只要创建了一个新的函数,就会根据一种特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认的情况下,所有的原型对象都会自动获得一个叫做constructor的属性,也是一个指针,指向拥有prototype属性的函数,即构造函数Person。在上面的例子中,即Person.prototype.constructor指向构造函数Person。

创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性,至于其他方法,则都是从Object上继承而来的。当调用构造函数创建一个新实例之后,该实例的内部将包含一个指针_proto_,这个指针指向构造函数的原型对象。

以前面代码为例,下图展示了它们之间的关系深入理解JavaScript的原型、原型链与继承_第1张图片

深入理解JavaScript的原型、原型链与继承_第2张图片

可以看出,虽然Person的每个实例——person1和person2都不包含属性和方法,但是仍然可以调用person1.sayName( ),这是通过查找对象属性的过程来实现的。

注意:当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中的同名属性。换句话说,为实例添加一个同名属性,只会阻止我们访问原型中的那个属性,但不会修改那个属性,即使将这个属性设为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过,使用delete操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性。

实例共享原型对象的基本原理

每当代码读取到某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性,搜索首先从对象实例的本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性,如果找到了就返回这个值。也就是说,在上例中,我们在调用person1.sayName( )时会先后执行两次搜索。

原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也是照样如此。

    function Person ( ){
        
    }
    var friend = new Person();
    person.prototype.sayHi= function () {
        alert("hi");
    };
    friend.sayHi(); //"hi"

上例中即使friend实例是在是在添加新方法之前创建的,但它仍然可以访问这个新方法。其原因可以归结为实例与原型之间的松散连接关系。当我们调用friend.sayHi( )时,首先会在实例中搜索名为sayHi的属性,在没找到的情况下,会继续搜索原型。因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的sayHi属性并返回保存在哪里的函数。

重写原型对象

尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的指针_proto_,而把原型修改给另外一个对象就等于切断了构造函数与最初原型之间的联系。

    function Person(){

    }
    var person=new Person();
    Person.prototype={
        constructor:Person,
        name:"Jack",
        age:20,
        sayName: function () {
            console.log(this.name)
        }
    };
    console.log(person.name);//undefined
    person.sayName();//报错

由此可知,重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系,它们引用的仍然是最初的原型。

原型对象的问题

原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不便,但这不是原型最大的问题。

原型中的所有属性是被很多实例共享的,这种共享对于函数非常适合。对于那些包含基本类型的属性高倒也说得过去,毕竟我们可以通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于引用类型值的属性来说,问题就非常突出。

因为引用类型在引用时候并不是创建一个副本,而是通过指针直接在原值上操作,如果通过在某个实例上修改了原型对象的某个属性,就相当于这个属性本身被修改了,在其他原型访问这个属性时,已经是修改后的属性了,显然这带来了很多麻烦。

因此,实际上很少有人单独使用原型模式来创建对象。一般都会采取组合使用原型模式和构造函数模式,寄生构造函数模式,稳妥都在函数模式等来创建对象。

与原型有关的一些方法

  • isPrototypeOf( )    这个方法是原型对象的方法,用来确定某个实例是否有_proto_属性指向这个原型对象,返回布尔值。
    Person.prototype.isPrototypeOf(person); //true
  • Object.getPrototypeOf( )    括号内参数为一个实例,返回的对象就是这个实例的原型。
    console.log(Object.getPrototypeOf(person1).name); //"Jack"
     
  • hasOwnProperty( )   这个方法是实例的方法,可以检测一个属性是存在于实例中(true)还是存在于原型中(false)。
    person1.hasOwnProperty("name");

       原型与 in 操作符 :in操作在单独使用时,如果对象能够访问给定的属性,就会返回true,无论该属性存在于实例中还是原型中。  

      console.log("name" in person1);

       组合使用in操作符和hasOwnProperty( ),就可以确定该属性到底存在于对象中还是存在于原型中。

    function hasPrototypeProperty(object,name){
        return !Object.hasOwnProperty(name) && (name in object)
    }

原型链与继承

理解原型链

其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

由构造函数,原型,实例的关系可知,每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,调用构造函数实例化的对象实例,都一个可以指向原型对象的内部指针。那么假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象都将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一种类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。而互相关联的原型组成的链状结构就是所谓的原型链。

实现原型链继承

    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;
    };
    var instance=new subType();
    alert(instance.getSuperValue());  //true

在这个例子中,创建了两个构造函数superType( )和subType( );给每个构造函数添加了属性,利用原型对象添加属性的方法为其添加了两个不同的方法。然后subType.prototype = new superType(),这行代码实现了继承,其原理是subType.prototype作为构造函数superType()的一个实例,所以subType.prototype就可以访问到superType.prototype的属性和方法,然后再将一个新的变量instance作为构造函数subType( )的实例对象,所以instance可以访问到其构造函数原型subType.prototype的属性和方法。如果要在instance上访问名为getSuperValue()的方法,会执行三次搜索,如下:

  • 第一次先在instance实例上搜索名为getSuperValue()的方法,没有找到;
  • 所以继续搜索指针指向的原型对象subType.prototype,在这里还是没有找到给定名字的方法;
  • 则继续搜索subType.prototype作为实例时指针指向的原型对象,即superType.prototype,在这里找到了getSuperValue()方法,所以返回方法内的值,搜索停止。

深入理解JavaScript的原型、原型链与继承_第3张图片

图中红色的即为原型链。

默认的原型

前面例子中所展示的原型链还少一环。我们知道,所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。需要注意,所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。

确定原型和实例的关系

  • isPrototypeOf( )   上面已介绍过
  • instanceof操作符   (实例 instance 构造函数),如果实例是原型的实例,返回true。

总结

  • 给原型添加方法的代码要放在替换原型的语句之后;
  • 不能使用对象字面量创建原型方法,因为这样做会导致重写原型链;
  • 原型链实现继承的问题是包含引用类型值的原型,另一个问题是不能向超类型的构造函数中传递参数
  • 实践中很少单独使用原型链,一般会借用构造函数实现伪造对象或经典继承,或者使用组合继承。
  • 除原型链继承外,还存在原型式继承,寄生式继承,寄生组合式继承。

 

你可能感兴趣的:(js/jquery,前端)