欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

javascript继承

程序员文章站 2022-06-15 13:40:43
...

首先,介绍一种成熟的JavaScript继承实现方式。然后,一步步梳理对于javascript原型和继承的理解。
  这种继承方式叫做寄生组合式继承,它使用构造函数实现属性的继承,保证每个实例拥有一份独立的属性,属性都添加在生成的实例对象上,然后使用原型链实现方法的继承,保证方法只有一份代码,节省内存。

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

// 原型上添加方法,相当于java中的静态方法,为所有实例所共享。
// 可以访问原型属性、原型方法、实例属性、实例方法。不能访问私有属性和方法。
SuperType.prototype.sayName = function() {
    console.log(this.name);
}

// 原型上添加属性,相当于java中的静态属性,为所有实例所共享。
// 可以被原型方法,实例方法,私有方法所访问。尽量不要在原型方法,实例方法,私有方法之外定义方法。
SuperType.prototype.country = 'CN';

function SubType(name, age) {
    // 在添加实例属性之前,先调用超类的构造函数,避免覆盖。
    SuperType.call(this, name);
    this.age = age;

    // 私有属性,只能被当前构造函数私有方法和实例方法所访问。实例方法也叫特权方法,因为它有访问私有成员的特权。
    var sex = 'male';

    this.getSex = function() {
        return sex;
    };

    this.setSex = function(value) {
        sex = value;
    };

    // 私有方法,只能被当前构造函数内的私有方法或实例方法所访问。
    var that = this;
    function talk() {
        console.log('I like talk');
        // 私有方法可以访问原型方法、特权方法、私有方法
        that.sayAge();
    }

    // 特权方法可以访问私有方法、特权方法、原型方法
    this.init = function() {
        talk();
    }

}

function inheritPrototype(subType, superType) {
    // 浅复制超类的原型对象
    var prototype = Object.create(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}

// 实现原型继承
inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function() {
    console.log(this.age);
}

这种继承方式,只会在使用new生成子类型实例时,调用一次超类型的构造函数。子类型构造函数和各级超类型构造函数中的属性都追加在新生成的子类型实例上,每个子类型实例拥有一份独立的属性。各级构造函数的私有属性每次生成新实例时,都会生成一份,但是没有追加在新生成的实例上,它们的作用域只局限于它们所处的构造函数内部。
  在inheritPrototype函数中,通过寄生方式,实现了子类型原型继承超类型的原型。静态属性和静态方法,可以添加在原型上,为所有实例所共享。静态方法可以访问静态成员和实例成员,但不能访问私有成员。静态属性和方法可以被静态方法、实例方法、私有方法访问。
  私有成员在构造函数中定义,作用域在构造函数内部,可以被私有方法和特权方法访问。
  每个实例拥有单独的一份私有成员和实例成员,原型成员为所有实例所共享,类似于Java中的静态成员。

然后,一步步分析javascript原型与继承
  1. 对象。javascript中对象定义的最方便方法是使用花括号。如 var david = {name: 'david', age: 27}; 这种方式生成对象适用于生成单独一个对象,如果要生成很多个相似的对象,把这条语句复制多份,然后做修改,可以实现需求,但是这样的弊端是,如果要修改结构,需要修改很多个地方,而且代码臃肿,没有复用。为此,可以使用工厂方法生成对象。
  2. 工厂方法。可以使用如下工厂方法方式生成对象,这样,生成对象的代码被集中了,但是缺点是,生成的对象之间没有内存关联,也无法共享属性和方法。

function Person(name, age) {
    return {
        name: name,
        age: age
    };
}

var per1 = Person('david', 21);
var per2 = Person('jack', 23);

3. 构造函数。为解决生成的对象之间没有内在联系的问题,js提供了构造函数,使用new关键字生成实例,这样生成的对象可以判断自身是否是某个构造函数的实例。在构造函数中,this表示新生成的对象,在构造函数开始时,它是个空对象,然后使用this.向this上面添加属性和方法,最后返回这个构造好的this对象。生成的实例内部默认包含一个constructor对象,指向生成它的构造函数,使用它可以判断对象与构造函数之间的对应关系。另外,Js提供了instanceof运算符,用于判断某个构造函数是否在对象的父或祖先构造函数在。使用构造函数的缺点是,所有对象都拥有一套单独的属性和方法,不能共享属性和方法,一方面造成内存的浪费,另一方面修改一个实例的共有属性,不会影响另一个实例的共有属性,与需求不符。为此,js引用了原型的概念。

function Animal() {
    this.property = 'Animal';
}

function Cat(name, age) {
    Animal.call(this);
    this.name = name;
    this.age = age;
}

function inheritPrototype(subType, superType) {
    var prototype = Object.create(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}

inheritPrototype(Cat, Animal);

var c1 = new Cat('jack', 25);
console.log(c1.constructor == Animal);    // false
console.log(c1.constructor == Cat);       // true
console.log(c1 instanceof Animal);        // true
console.log(c1 instanceof Cat);           // true

4. 原型。为了在同一构造函数的不同实例间共享属性和方法,js中为构造函数设计了一个prototype属性,这个属性就是原型,属性值是一个对象,为所有实例所共享,使用一个实例修改原型中的属性,其它实例再引用的就是修改后的值。而且这样,共享的方法在内存中只有一份,也节省内存,提高运行效率。需要共享的属性和方法,放在prototype中,为所有实例所共享,不需要共享的属性和方法,放在构造函数中,每个实例都各有一份。这样,实例的属性和方法,分为两种,一种是本地的,通过构造函数生成,直接添加在实例上,另一种是引用的,通过原型属性存取。prototype对象中内置一个属性constructor,默认指向它对应的构造函数,实例可以引用该属性,用于判断实例与构造函数之间的对应关系。由于所有实例共享prototye对象,看起来,就像prototype对象是实例的原型,而实例是在原型上作扩展一样。js提供了isPrototypeOf方法,用于确定一个原型与对象之间的对应关系。每个实例对象都有一个hasOwnProperty方法,用于判断一个属性是本地属性,还是继承自原型对象的属性。in运算符,可以用来判断对象是否包括某个属性,不管属性是本地属性还是继承自原型对象的属性,这个运算符还可以用于遍历对象的所有属性。但是到目前为止,还只是生成对象,没有继承,为了解决对象之间的继承,第一种思路是使用原型实现。
  5. 原型链继承。子构造函数的prototype指向父构造函数实例,父构造函数的prototype指向更高一级构造函数的实现,使用这种方法,可以实现原型链继承。问题1:不能向父构造函数传递参数。问题2:所有实例共享父构造函数的实例,共享其中的属性,不能在父构造函数为子实例添加与其它实例独立的属性,如果想要子实例的属性与其它实例的属性值无关,需要将这种属性的添加放在子构造函数中,而不能放在父构造函数中,属于父构造函数的属性,*放在各个子构造函数中实现,事实上就没有实现继承。为了父构造函数中添加的属性,在所有子类实例中都拥有独立的属性值,可以使用构造函数继承。

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.constructor = SubType;
SubType.prototype.getSubValue = function() {
    return this.subproperty;
}

var instance1 = new SubType();
instance1.__proto__.property = 'new value';
instance1.colors.push('black');
console.log(instance1.colors, instance1.property);    // [ 'red', 'blue', 'green', 'black' ] 'new value'
var instance2 = new SubType();
console.log(instance2.colors, instance2.property);    // [ 'red', 'blue', 'green', 'black' ] 'new value'

6. 构造函数继承。在子构造函数内调用父构造函数,传入子构造函数的实例,可以使父构造函数中的属性和方法在不同实例上均相互独立,这样就实现了继承。而父构造函数中的私有成员作用范围在父构造函数内部。但这种继承实现的缺点是,所有实例都拥有独立的属性和方法,不能实现共享属性,也不能共享方法,浪费内存空间。为此,可以使用组合继承方式,结合原型链继承和构造函数继承的优点。

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

function SubType() {
    // 借调超类型构造函数,使用当前实例的this
    // 同时,传递参数
    SuperType.call(this, 'Nicholas');
    this.age = 29;
}

var instance1 = new SubType();
instance1.colors.push('yellow');
console.log('instance1 colors ' + instance1.colors);    // red,green,blue,yellow

var instance2 = new SubType();
console.log('instance2 colors ' + instance2.colors);    // red,green,blue
console.log(instance2.name, instance2.age);

7. 组合继承。使用原型链实现对原型属性和方法的继承,使用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。这种继承方式的缺点是,父构造函数被调用两次,一次在原型继承生成父构造函数实现时,一次在new子构造函数时,这样,子构造函数实例内拥有两份父构造函数的属性和方法,一份在本地,一份在原型中,这样就有了冗余的操作和内容。

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);
}

8. 原型式继承。在没有必要兴师动众创建构造函数,而只想让一个构造函数与另 一个构造函数相似的情况下,可以使用原型式继承。首先,对基础对象执行一次浅拷贝,生成的复本对象的原型为基础对象,然后在复本对象上添加属性或方法。原型式继承相当于在原型对象基础上,做相应的修改和增强。在ECMAScript中原生添加了Object.create()方法实现浅拷贝。

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

var person = {
    name: 'Nicholas',
    friends: ['Shelby', 'Court', 'Van']
};

var anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');

9. 寄生式继承。创建一个仅用于封装继承过程的函数,函数内部以某种方式增强对象,并返回。这种继承是在原型式继承的基础上,提供了封装,提高了代码复用。

function createAnother(origin) {
    var clone = Object.clone(origin);
    clone.sayHi = function() {
        console.log('hi');
    };
    return clone;
}

var person = {
    name: 'Nicholas',
    friends: ['Shelby', 'Court', 'Van']
};

var anotherPerson = createAnother(person);

10. 寄生组合式继承。由于组合式继承有调用两次父构造函数,生成两份父构造函数成员的问题,可以结合寄生式继承和组合式继承的优点来解决这个问题。首先,使用寄生式继承来实现原型链的继承,实现共享属性和方法在原型链中的继承。然后,使用函数式继承,实现实例属性和方法在构造函数中的继承。这样,即有共享属性和方法,又保证了每个实例的实例属性相互独立。

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

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

SuperType.prototype.country = 'CN';

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

function inheritPrototype(subType, superType) {
    var prototype = Object.create(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function() {
    console.log(this.age);
}