JavaScript原型链与继承

QQ群招募中646258285(招募中,没几个人说话),
需要交流的朋友可以直接加我微信( DntBeliv )或QQ( 1121864253 )


理解JavaScript函数与原型

在这里最重要的是理解一点,JavaScript里面一切都是一个值,换句话说,object是一个值,function也是一个值。这些值与基本类型的区别就在于,他们只是一个变量标签,存储的是一个指向其内容的指针。

函数相对来说就稍微复杂一点,因为默认情况下函数没有确定的运行环境,需要调用者提供运行环境(this)。但实际上函数本身就是一个变量,也是一个值,一个变量标签,这就是函数对象。通过函数对象可以构造函数对象的实例,本质上是通过一定规则对函数对象的一个拷贝(可以先不理解这句话)。

先不管函数对象,使用函数分两种情况,作为普通函数而言,它只是一系列过程的封装,我们使用的也只是这一系列的过程,也因此没有体现出函数的面向对象特性,此时函数的运行环境是调用者,在全局环境下是全局对象。作为构造函数而言,即new function(),其中function()里面一般没有返回值。它除了封装了一系列方法,还将自身实例化为一个对象实例,自己为自己提供了运行环境。我们看下面这个例子

在这里插入图片描述

如果函数内部有返回值,使用函数时前面加个new,会怎么样呢?事实上,当我们自定义返回值时,则我们返回的值取代了函数默认生成的对象实例。因此返回的将是return后面的值。

现在来说原型,说到原型时,我们就要考虑到函数对象了。函数是一个对象,是一个值,除了自身的“构造方法”之外,实际上还会有一个默认的属性:对于JavaScript而言,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性是一个引用,其指向函数的原型对象。此外,这个原型对象又会有一个属性constructor,也是一个引用,指向函数对象。从而形成一个圆环。此外,函数对象的实例实际上也有一个隐藏的属性__proto__指向函数的原型对象。

总结一下,一个函数代表着两个对象,应该是函数对象,一个是函数的原型对象。此外可以通过函数对象调用构造函数生成函数对象的实例,其属性__proto__指向函数的原型对象。

function test(){
    console.log("test "+this);
    return this;
}

var b = new test();
console.log(b == test);//false
console.log(b instanceof test);//true
console.log(test);//[Function: test]
console.log(test.prototype);//test {}
console.log(test.prototype.constructor);//[Function: test]

默认的原型对象只自己生成了constructor,其余的属性实际上都是从Object继承而来的。

对象属性搜索机制与原型链

事实上,任何一个对象实例都有__proto__属性,其指向其所在的原型,我们使用instanceof检查对象类型时,实际上也是通过这个检查的。进一步讲,原型也是一个对象实例,那么原型对象也有自己的__proto__属性,从而指向原型的对象类型。事实上,所有的对象最终都指向Object类型。

我们看到,通过__proto__属性,任何一个对象通过原型都形成了一个通往Object的链条,这就是原型链。因此使用instanceof检查对象类型时,所有对象都是Object对象,只要是在原型链上的类型,instanceof都会返回true。

那么当我们访问一个实例的属性时,会发生什么呢?举个例子A的原型是B,B的原型是Object,现在用A生成一个实例a,代码如下:

在这里插入图片描述

这里我直接给__proto__属性赋值,但实际不要这样做,逻辑上这个属性是不应该被修改的,如果用在实际中,鬼知道会出现什么问题。但是从这个代码中我们就看到了属性搜索的过程:首先搜索当前对象的所有属性,如果找到了(例如A.name)那就返回,如果没有找到,就在它的原型对象中搜索(例如A.age),如果找到了就返回,如果还是没有找到,继续沿着原型链搜索。如果始终没有找到就返回undefined。

继承

有了原型链的机制,再谈继承的时候,我们是不是只需要给将父类对象加给子类对象的原型就可以了,这样一方面子类可以访问父类的属性和方法,另一方面父类自己也有原型,这样就形成了继承链。

但是有一个问题,如果是这样的话,所有子类都会共享父类的属性,而没有自己的属性。当然这也好办,子类重写这些属性就可以了,这样一来,子类的属性就可以屏蔽掉父类的属性。但如何优雅地实现这一切呢?

组合继承

//组合继承
function SuperType(name){
    this.name = name;
    this.colors = ["red","blue","green"];
}

SuperType.prototype.sayName = function(){
    console.log(this.name);
};

function SubType(name,age){
    //继承属性
    SuperType.call(this,name);

    this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
//指定子类对象类型
SubType.prototype.constructor = SubType;
//添加子类方法
SubType.prototype.sayAge = function(){
    console.log(this.age);
};

var instance1 = new SubType("jack",22);
instance1.colors.push("black");
console.log(instance1.colors);
instance1.sayName();
instance1.sayAge();

var instance2 = new SubType("greg",12);
console.log(instance2.colors);
instance2.sayName();
instance2.sayAge();

console.log(instance1 instanceof SubType);//true
console.log(instance1 instanceof SuperType);//true

// [Running] node "c:\Users\Administrator\Desktop\JavaScript\继承.js"
// [ 'red', 'blue', 'green', 'black' ]
// jack
// 22
// [ 'red', 'blue', 'green' ]
// greg
// 12

可以看到,子类的构造函数通过call(用apply也可以),调用父类的构造函数,相当于是使用父类对象作为函数的用法,将子类对象作为环境变量输入到父类对象的构造函数当中,从而借用父类构造函数为子类对象做了一份属性的拷贝。之后将子类的原型赋值为父类的实例,从而让子类拥有了父类的所有方法。由于子类中用父类构造函数重写了属性,因此父类中对应的属性会被屏蔽掉。

如果直接使用父类实例作为原型,那么使用instanceof只能检测到子类对象类型为父类,所以要将SubType.prototype.constructor设置为SubType,同时因为子类原型为父类实例,父类实例的原型为父类对象,因此子类也属于父类。

注意:该方法不可在子类的构造函数内为子类的prototype重新复制,这样会导致父类无效。

寄生式继承

寄生式继承式根据已有的一个对象创建一个新的对象类型,并增强这个对象的属性或方法。

/**
 * 该方法实际上是创建了一个空的函数对象,
 * 并把这个函数对象的原型设置为已有的对象,
 * 从而拥有该对象的特性
 */
function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}

var person = {
    name:"Jack",
    friends:["job","van"]
}
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("rob");

var anotherPerson2 = object(person);
anotherPerson2.name = "linda";
anotherPerson2.friends.push("barbie");

console.log(anotherPerson.name);
console.log(anotherPerson2.friends);
console.log(person.friends);

// [Running] node "c:\Users\Administrator\Desktop\JavaScript\tempCodeRunnerFile.js"
// Greg
// [ 'job', 'van', 'rob', 'barbie' ]
// [ 'job', 'van', 'rob', 'barbie' ]

/**
 * 这个是通过上述方法创建已有对象的副本,
 * 然后添加属性增强的一个示例
 */
function createAnother(original) {
    var clone = object(original);
    clone.sayHi = function () {
        console.log("hi");
    };
    return clone;
}

一般来说这种方式用处不大,但借用这种方式,我们可以对组合式继承进一步升级,得到寄生组合式继承的方式。

寄生组合式继承

组合模式有一个局限性,那就是要调用两次父类构造函数,并且专门为子类创建了一个父类实例。但实际上,所有需要共享的函数(或属性)实际上都会放在父类的原型之中,而我们要继承的父类的属性都不需要他们共享。通过组合式继承,我们多创建了一份父类的实例,相当于就多创建了一份父类属性的副本,但实际上并不会用到。

寄生组合式继承就解决了这一问题,下面看代码:

//寄生组合式继承
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function inheritPrototype(subType, superType) {
    //创建空函数对象,其原型为父类原型
    //(注意这里并没有调用父类构造函数,而是将父类原型直接赋值)
    var prototype = object(superType.prototype);
    //增强对象
    prototype.constructor = subType;
    //指定对象:将子类原型设置为该对象,相当于之拷贝父类原型
    subType.prototype = prototype;
}

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

SuperType.prototype.sayName = function () {
    console.log(this.name);
};

function SubType(name, age) {
    //借用构造函数(唯一的一次调用父类构造函数)
    SuperType.call(this, name);
    this.age = age;
}

inheritPrototype(SubType, SuperType);

//添加子类方法
SubType.prototype.sayAge = function () {
    console.log(this.age);
};

这里object()函数通过直接将父类原型赋值过来,从而避免了创建无用的父类实例,而子类调用SuperType.call(this, name);复制了所有的父类属性。所以十分巧妙。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章