js的继承

本文深入解析JavaScript中的六种继承模式:原型链、借用构造函数、组合继承、原型式继承、寄生式继承及寄生组合式继承。每种模式的特点、实现方式及潜在问题均被详尽阐述。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原型链实现继承

由于函数没有签名,在ECMAScript中无法实现接口继承。ECMAScript只支持实现继承,而且其实现继承主要是依靠原型链来实现的。

1.原型链

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

构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。


    function SuperType(){
        this.property = true;
    }
    SuperType.prototype.getSuperValue = function(){
        return this.property;
    }
    function SubType(){
        this.subproperty = false;
    }
    // 继承了SuperType
    SubType.prototype = new SuperType();
    SubType.prototype.getSubValue = function(){
        return this.subproperty;
    }
    var instance = new SubType();
    console.log(instance.getSuperValue()); // true
    console.log(instance.getSubValue()); // false

别忘记默认的原型

所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。所有的函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因。

注意1:SubType重新定义了SuperType里面的getSuperValue方法,通过SubType的实例调用getSuperValue时使用的是新定义的方法,但是通过SuperType的实例调用getSuperValue还是原来的方法;

    function SuperType(){
        this.property = true;
    }
    SuperType.prototype.getSuperValue = function(){
        return this.property
    }
    function SubType(){
        this.subproperty = false;
    }
    // 继承了SuperType
    SubType.prototype = new SuperType();
    // 添加新方法
    SubType.prototype.getSubValue = function(){
        return this.subproperty;
    }
    // 重写超类型中的方法
    SubType.prototype.getSuperValue = function(){
        return false;
    }
    var instance1 = new SubType();
    console.log(instance1.getSuperValue()); //false
    var instance2 = new SuperType()
    console.log(instance2.getSuperValue()); //true

注意2:在通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样做会重写原型链;

function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property
}
function SubType(){
    this.subproperty = false;
}
// 继承了SuperType
SubType.prototype = new SuperType();
SubType.prototype = {
    getSubValue:function(){
        return this.subproperty;
    },
    someOtherMethod:function(){
        return false;
    }
}
var instance = new SubType()
console.log(instance.getSubValue()) // false
console.log(instance.getSuperValue()) // error

原型链的问题:

虽然原型链很强大,可以用它来实现继承,但也存在一些问题.其中,最主要的问题来自包含引用类型值的原型。引用类型值的原型属性会被所有实例共享;而这也是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章的变成了现在的原型属性了.如下代码:


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

    function SubType(){

    }
    // 继承了SuperType
    SubType.prototype = new SuperType();
    var instance1 = new SubType();
    instance1.colors.push('black');
    console.log(instance1.colors); // ["red", "blue", "green", "black"]

    var instance2 = new SubType()
    console.log(instance2.colors); // ["red", "blue", "green", "black"]

    var instance3 = new SuperType()
    console.log(instance3.colors); // ["red", "blue", "green"]

这个例子中的SuperType构造函数定义了一个colors属性包含一个数组(引用类型值)。SuperType的每一个实例都会有各自包含自己数组的colors属性。当SubType通过原型链继承了SuperType之后,SubType.prototype就变成了SuperType的一个实例,因此他也拥有了一个她自己的colors属性—就跟专门创建了一个SubType.prototype.colors属性一样。于是SubType的所有实例都会共享这一个colors属性。而我们对instance1.colors进行修改时instance2.colors也被修改了。
第二个问题是:在创建子类型的实例时,不能想超类型的构造函数中传递参数。实际上,应该说没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

对于第二个问题的理解

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

    function SubType(test){
        this.colors.push(test)
    }
    // 继承了SuperType
    SubType.prototype = new SuperType();
    var instance1 = new SubType('111');
    instance1.colors.push('black');
    console.log(instance1.colors); // ["red", "blue", "green", "111", "black"]

    var instance2 = new SubType('222')
    console.log(instance2.colors); //["red", "blue", "green", "111", "black", "222"]

    var instance3 = new SuperType()
    console.log(instance3.colors); // ["red", "blue", "green"]

2.借用构造函数 (伪造对象或经典继承)

基本思想:在子类型构造函数的内部调用超类型构造函数。

函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在(将来)新创建的对象上执行构造函数。


    function SuperType(){
        this.colors = ["red","blue","green"]
    }
    function SubType(){
        //继承了SuperType
        SuperType.call(this);
    }
    
    var instance1 = new SubType();
    instance1.colors.push("black");
    console.log(instance1.colors); // ["red", "blue", "green", "black"]
    
    var instance2 = new SubType();
    console.log(instance2.colors); // ["red", "blue", "green"]

从本质上来说,借用构造函数,就是在’子类’的构造函数内除去了冗余代码,其实上面的写法等价于

    function SubType(){
        //继承了SuperType
        this.colors = ["red","blue","green"]
    }
    
    var instance1 = new SubType();
    instance1.colors.push("black");
    console.log(instance1.colors); // ["red", "blue", "green", "black"]
    
    var instance2 = new SubType();
    console.log(instance2.colors); // ["red", "blue", "green"]

传递参数

可以在子类型构造函数中向超类型构造函数传递参数。


    function SuperType(name){
        this.name = name;
    }
    function SubType(){
        //继承了SuperType,,同时还传递了参数
        SuperType.call(this,"Nicholas");
        
        //实例属性
        this.age = 29;
    }
    
    var instance = new SubType();
    console.log(instance.name); //"Nicholas"
    console.log(instance.age); //29

为了确保SuprType构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性

借用构造函数的问题

如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题————方法都在构造函数中定义,因此函数复用也就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。


    function SuperType(name){
        this.name = name;
    }
    SuperType.prototype.getName = function(){
        console.log(this.name);
    }
    function SubType(){
        //继承了SuperType,,同时还传递了参数
        SuperType.call(this,"Nicholas");
        
        //实例属性
        this.age = 29;
    }
    
    var instance = new SubType();
    console.log(instance.name); //"Nicholas"
    console.log(instance.age); //29
    console.log(instance.getName());//erros

3.组合继承(伪经典继承)

组合继承(combination inheritance),指的是将原型链和借用构造函数组合到一块。具体是使用原型链实现对原型属性和方法的继承,使用借用构造函数来实现对实例属性的继承。

    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.sayAge = function(){
        console.log(this.age);
    };

    var instance1 = new SubType("Nicholas", 29);
    console.log(instance1.constructor);// function SuperType(){...}
    console.log(SuperType.prototype.isPrototypeOf(instance1));// true
    instance1.colors.push("black");
    console.log(instance1.colors);  // ["red", "blue", "green", "black"]
    instance1.sayName();      // "Nicholas";
    instance1.sayAge();       // 29

    var instance2 = new SubType("Greg", 27);
    console.log(instance2.colors);  // ["red", "blue", "green"]
    instance2.sayName();      // "Greg";
    instance2.sayAge();       // 27

4.原型式继承

这种继承就像是一种浅拷贝。做法是借助已有的对象创建新对象,同时也不必因此创建自定义类型。

    // 在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,
    // 最后返回了这个临时类型的一个新实例。
    // 从本质上将,object()对传入其中的对象执行了一次浅复制。
    function object(o){
        function F(){}
        F.prototype = o;
        return new F();
    }
    function Person() {
        this.name = "Nicholas";
        this.friends = ["Shelby", "Court", "Van"];
    }
    var person = new Person();
    var anotherPerson = object(person);
    anotherPerson.name = "Greg";
    anotherPerson.friends.push("Rob");

    var yetAnotherPerson = object(person);
    yetAnotherPerson.name = "Linda";
    yetAnotherPerson.friends.push("Barbie");

    // 引用类型会共享 因为修改的不是引用类型的值 而是引用类型指向的值。这也说明了这种操作类型类型浅拷贝。
    console.log(person.friends);    // ["Shelby", "Court", "Van", "Rob", "Barbie"]
    console.log(person.name);    // Nicholas
    console.log(anotherPerson.name);    // Greg
    console.log(yetAnotherPerson.name);    // Linda
    console.log(anotherPerson.constructor); //  function Person(){...}
    console.log(anotherPerson instanceof Person); // true 这里体现了继承性

ECMAScript5通过新增Object.create()方法规范化了原型式继承。这种方法接收2个参数,一个用做新对象原型的对象和一个作为新对象定义额外属性的描述符对象(可选)。在传入一个参数的情况下,Object.create()的行为和上面的object()一致。

    var person = {
        name: "Nicholas",
        friends: ["Shelby", "Court", "Van"]
    };
    var anotherPerson = Object.create(person);
    anotherPerson.name = "Greg";
    anotherPerson.friends.push("Rob");
    var yetAnotherPerson = Object.create(person, {
        age: {
            //writable: true,
            value: 23
        }
    });
    yetAnotherPerson.name = "Linda";
    yetAnotherPerson.friends.push("Barbie");
    console.log(person.friends);   // ["Shelby", "Court", "Van", "Rob", "Barbie"]
    console.log(person.age);   // undefined
    console.log(anotherPerson.age);   // undefined
    console.log(yetAnotherPerson.age);   // 23
    yetAnotherPerson.age = 24;
    console.log(yetAnotherPerson.age);   // 23   描述符对象不传writable 默认为false 其他几个属性同理

5.寄生式继承

寄生式继承是与原型式继承紧密相关的一种思路,通过创建一个仅用于封装继承过程的函数,再以某种方式增强对象,最后再返回这个对象。

    function createAnother(obj) {
        var clone = Object.create(obj);//复制一个对象,该对象的原型对象是传进来的对象。
        clone.sayName = function () {//增强该对象
            console.log(this.name);
        }
        return clone;//返回该对象
    }
    function Person() {
        this.name = "hange";
        this.friends = ["miaoch"];
    }
    var person = new Person();
    var another1 = createAnother(person);
    var another2 = createAnother(person);
    another1.sayName() // hange
    another1.friends.push("123");
    another2.sayName() // hange
    console.log(another2.friends) // ["miaoch", "123"]

其实寄生式继承和原型链是类似的,因为生成的another1对象,他们的原型对象也就是父类Person对象的一个实例而已。所以依然存在共享父类引用类型的问题。并且子类的sayName()函数也无法得到复用。

6.寄生组合式继承

前面提到的组合继承是最常用的继承模式,但组合继承也有不足,最大的问题就是无论什么情况下,都会调用两次超类型构造函数

    function SuperType(name) {
        this.name = name;
        this.colors = ["red", "blue"];
    }
    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.constructor会是SuperType
    SubType.prototype.sayAge = function() {
        console.log(this.age);
    }
    var sub = new SubType(); // 第二次调用超类的构造函数
    console.log(sub.colors); // ["red", "blue"]
    delete sub.colors;
    console.log(sub.colors); // ["red", "blue"]
    delete SubType.prototype.colors;
    console.log(sub.colors); // undefined

上面这种做法在第一次调用超类的构造函数时,显然会使子类的原型对象中包含undefined的name属性和[“red”, “blue”]的colors属性。虽然对子类搜索这两个属性时不会有什么问题,但是原型对象中包含这个引用属性显然是不对的。因为delete了子类的colors后,再次搜索colors会搜索到原型对象中的colors,这显然不是我们想要的结果。我们之所以会让子类的原型对象成为超类的实例,只不过是为了获得[[prototype]]指针形成原型链而已,我们并不关心除了[[prototype]]其余的属性,所以说此处的第二次调用超类的构造函数是毫无必要的。在这里,我们就可以用Object.create()去创建一个没有任何其余属性的对象,并使它的[[prototype]]指向超类的原型对象,再使子类的[[prototype]]指针指向该对象。这里再来过一下之前的object()(也就是Object.create()的简单版本)

    function object(originl) {
        function F(){}; // 1
        F.prototype = originl; // 2
        return new F(); // 3
    }

在这里,我们通过object()创建一个对象。当传入超类的原型对象时,执行第一步,创建一个F构造函数,此时F.prototype会被创建,constructor会指向F构造函数。执行第二步,将F.prototype替换(指向)超类的原型对象。返回F的一个实例。可想而知,该实例没有任何属性,只有一个[[prototype]]指向了超类的原型对象。此处因为没有调用超类的构造方法,所以才有这么个效果。通过这一步,我们可以将组合继承代码修改如下

    function SuperType(name) {
        this.name = name;
        this.colors = ["red", "blue"];
    }
    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 = Object.create(SuperType.prototype); // 修改后
    SubType.prototype.constructor = SubType; // 增强对象 否则SubType.prototype.constructor会是SuperType
    SubType.prototype.sayAge = function() {
        console.log(this.age);
    }
    var sub = new SubType(); // 第二次调用超类的构造函数
    console.log(sub.colors); // "red", "blue"
    delete sub.colors;
    console.log(sub.colors); // 修改前"red", "blue"  修改后: undefined
    delete SubType.prototype.colors;
    console.log(sub.colors); // undefined
    console.log(sub instanceof SuperType); // true

上面这种做法就叫寄生组合式继承,最大的优点就是只调用了一次SuperType构造函数,避免了在SubType.prototype上创建不必要的、多余的属性。并且还能保持原型链不变。只要原型链不变,就能正常使用instanceof和isPrototypeOf()。以下例子,说明原型链是决定instanceof执行结果的因素。

    function f1() {}
    function f2() {}
    var test = new f1();
    console.log(test instanceof f1); // true
    console.log(test instanceof f2); // false
    f2.prototype = f1.prototype;
    console.log(test instanceof f1); // true
    console.log(test instanceof f2); // true
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值