前端学习(十一)构造函数的继承

本文介绍JavaScript中构造函数的五种继承方式:构造函数绑定、prototype模式、直接继承prototype、利用空对象作为中介及拷贝继承,并提供代码示例。

构造函数间的继承,如:

function Animal(){
   this.atr = "动物";
}

function Pig(color, name){
  this.color = color;
  this.name = name;
}
//想让Pig构造器继承Animal构造器

一、 构造函数绑定
第一种方法也是最简单的方法,使用call或apply方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:

function Pig(color, name){
   Animal.apply(this);
   //Animal.call(this);
   this.color = color;
   this.name = name;
}
var redPig = new Pig("red", "zht");
alert(redPig.atr);   //动物

二、 prototype模式
使用prototype属性,指向要继承的对象的实例。
那么所有Pig的实例都能继承Animal。

Pig.prototype = new Animal();
Pig.prototype.constructor = Pig;
var redPig = new Pig("red", "zht");
alert(redPig.atr); 

任何一个prototype对象都有一个constructor属性,指向它的构造函数。如果没有Pig.prototype = new Animal();这一行,Pig.prototype.constructor是指向Pig的;加了这一行以后,Pig.prototype.constructor指向Animal。
所以,
编程时务必要遵守
如果替换了prototype对象,如

 object.prototype = {};

下一步必然是为新的prototype对象加上constructor属性,并将这个属性指回原来的构造函数。

  object.prototype.constructor = object;

因为,constructor属性,默认调用prototype对象的constructor属性

三、 直接继承prototype
第三种方法是对第二种方法的改进。由于Animal对象中,不变的属性都可以直接写入prototype。所以,我们也可以让Pig()跳过 Animal(),直接继承Animal.prototype。

function Animal(){}
Animal.prototype.atr = "动物";

Pig.prototype = Animal.prototype;
Pig.prototype.constructor = Pig; //错误,会把Animal的prototy.constructor修改为Pig
var redPig = new Pig("red", "zht");
alert(redPig.atr);

与前一种方法相比,这样做的优点是效率比较高(不用执行和建立Animal的实例了),比较省内存。缺点是 Pig.prototype和Animal.prototype现在指向了同一个对象,那么任何对Pig.prototype的修改,都会反映到Animal.prototype。

四、 利用空对象作为中介
利用一个空对象作为中介:

var cons = function(){};
cons.prototype = Animal.prototype;
Pig.prototype = new cons();
Pig.prototype.constructor = Pig;
alert(Animal.prototype.construtor);// Animal
//F是空对象,所以几乎不占内存。这时,修改Pig的prototype对象,就不会影响到Animal的prototype对象

封装成一个函数,便于使用:

function extend(Child, Parent) {
    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    Child.uber = Parent.prototype;//意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。(uber是一个德语词,意思是"向上"、"上一层"。)这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。
  }

这个extend函数,就是YUI库如何实现继承的方法。
最后一行,意思是为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。(uber是一个德语词,意思是”向上”、”上一层”。)这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。

五、 拷贝继承
把父对象的所有属性和方法,拷贝进子对象

function extend(Child, Parent){
   var p = Parent.prototype;
   var c = Child.prototype;
   for(var arg in p){
      c[arg] = p[arg];
   }
   c.uber = p;
}
<think> 在JavaScript中,本身并不直接支持多继承(即一个类继承自多个父类)。但是,我们可以通过一些模式来模拟多继承的效果。其中,构造函数继承(也称为借用构造函数)是实现多继承的一种方式,但需要结合其他技术。 ### 构造函数继承实现多继承的思路 构造函数继承的核心是在子类构造函数中调用多个父类的构造函数,从而将多个父类的属性复制到子类实例上。然而,这种方法只能继承父类构造函数中定义的属性和方法,无法继承父类原型上的方法。 ### 实现步骤 1. **定义多个父类构造函数** 2. **在子类构造函数中依次调用多个父类构造函数** 3. **手动组合多个父类的原型方法(可选)** ### 代码示例 ```javascript // 第一个父类 function Parent1(name) { this.name = name; this.skill1 = function() { console.log("Parent1 skill: " + this.name); }; } // 第二个父类 function Parent2(age) { this.age = age; this.skill2 = function() { console.log("Parent2 skill: " + this.age); }; } // 子类构造函数 function Child(name, age, gender) { // 调用第一个父类的构造函数 Parent1.call(this, name); // 调用第二个父类的构造函数 Parent2.call(this, age); this.gender = gender; } // 测试 var c = new Child("张三", 20, "男"); c.skill1(); // 输出: Parent1 skill: 张三 c.skill2(); // 输出: Parent2 skill: 20 console.log(c.gender); // 输出: 男 ``` ### 问题与局限性 1. **无法继承父类原型上的方法**:上述方法只继承了父类构造函数中定义的属性和方法,父类原型上的方法不会被继承。 ```javascript Parent1.prototype.common = function() { console.log("Common method in Parent1"); }; // c.common(); // 报错:c.common is not a function ``` 2. **方法重复定义**:每个父类构造函数中的方法都会在每个子类实例中创建一份,造成内存浪费。 ### 改进方案:混合模式(组合继承 + 混入) 为了解决原型方法的继承问题,我们可以使用混入(Mixin)模式将多个父类原型的方法合并到子类原型上。 ```javascript // 混入函数:将源对象的属性复制到目标对象 function mixin(target, source) { for (var key in source) { if (source.hasOwnProperty(key)) { target[key] = source[key]; } } return target; } // 子类构造函数 function Child(name, age, gender) { // 继承属性(构造函数继承) Parent1.call(this, name); Parent2.call(this, age); this.gender = gender; } // 继承第一个父类的原型 mixin(Child.prototype, Parent1.prototype); // 继承第二个父类的原型 mixin(Child.prototype, Parent2.prototype); // 注意:如果父类原型上有同名方法,后混入的会覆盖先混入的 // 添加子类自己的方法 Child.prototype.childMethod = function() { console.log("Child method"); }; // 测试 var c = new Child("张三", 20, "男"); c.skill1(); // Parent1中定义的方法 c.skill2(); // Parent2中定义的方法 c.common(); // 如果Parent1或Parent2的原型上有common方法,则这里可以调用 ``` ### 更优雅的ES6实现 ES6中可以使用class和混入模式来实现多继承,但需要注意,class本身只能extends一个父类,我们可以通过混入多个类的方式来实现。 ```javascript class Parent1 { constructor(name) { this.name = name; } skill1() { console.log("Parent1 skill: " + this.name); } } class Parent2 { constructor(age) { this.age = age; } skill2() { console.log("Parent2 skill: " + this.age); } } // 混入函数(用于混入多个类) function mix(...mixins) { class Mix { constructor() { for (let mixin of mixins) { copyProperties(this, new mixin()); // 拷贝实例属性 } } } for (let mixin of mixins) { copyProperties(Mix, mixin); // 拷贝静态属性 copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性 } return Mix; } function copyProperties(target, source) { for (let key of Reflect.ownKeys(source)) { if ( key !== "constructor" && key !== "prototype" && key !== "name") { let desc = Object.getOwnPropertyDescriptor(source, key); Object.defineProperty(target, key, desc); } } } class Child extends mix(Parent1, Parent2) { constructor(name, age, gender) { super(); // 调用混入类的构造函数 this.name = name; this.age = age; this.gender = gender; } } // 测试 let c = new Child("张三", 20, "男"); c.skill1(); // Parent1 skill: 张三 c.skill2(); // Parent2 skill: 20 ``` ### 注意事项 1. **方法冲突**:如果多个父类有同名方法,后面的会覆盖前面的。 2. **构造函数调用顺序**:在子类构造函数中调用多个父类构造函数时,顺序很重要。 3. **原型链污染**:混入模式可能会意外覆盖子类原型上的属性。 ### 总结 JavaScript中实现多继承主要有两种方式: 1. **构造函数继承 + 混入原型**:适用于ES5,注意内存和方法覆盖问题。 2. **ES6类 + 混入函数**:更现代的方式,但需要编写混入函数。 --- ### §§相关问题§§: 1. 混入模式(Mixin)和多重继承有什么区别? 2. 在JavaScript中,如何避免混入过程中的命名冲突? 3. 除了混入模式,还有哪些方式可以实现多继承? 4. 在ES6中,使用`super`关键字时,如何确定调用的是哪个父类的方法? </think> ### JavaScript 中实现多继承的解决方案 JavaScript 本身不支持真正的多继承,但可以通过多种技术模拟多继承的效果。以下是使用构造函数继承实现多继承的几种方法: #### 方法 1: 构造函数链式调用(基础版) ```javascript // 父类1 function Parent1(name) { this.name = name; this.method1 = function() { return "来自Parent1的方法"; }; } // 父类2 function Parent2(age) { this.age = age; this.method2 = function() { return "来自Parent2的方法"; }; } // 子类 function Child(name, age, gender) { // 调用第一个父类的构造函数 Parent1.call(this, name); // 调用第二个父类的构造函数 Parent2.call(this, age); this.gender = gender; } // 创建实例 const child = new Child("张三", 25, "男"); // 验证继承 console.log(child.name); // 输出: 张三 console.log(child.age); // 输出: 25 console.log(child.method1()); // 输出: 来自Parent1的方法 console.log(child.method2()); // 输出: 来自Parent2的方法 ``` **局限性**: - 只能继承父类构造函数中的属性和方法 - 无法继承父类原型上的方法 #### 方法 2: Mixin 模式(推荐方案) ```javascript // Mixin 工具函数 function mixin(target, ...sources) { Object.assign(target.prototype, ...sources.map(src => src.prototype)); } // 父类1 function Engineer() {} Engineer.prototype.code = function() { return "编写代码"; }; // 父类2 function Designer() {} Designer.prototype.design = function() { return "设计界面"; }; // 子类 function FullStackDev(name) { this.name = name; } // 应用 Mixin mixin(FullStackDev, Engineer, Designer); // 添加子类特有方法 FullStackDev.prototype.introduce = function() { return `我是${this.name},既能${this.code()}也能${this.design()}`; }; // 创建实例 const dev = new FullStackDev("李四"); console.log(dev.code()); // 输出: 编写代码 console.log(dev.design()); // 输出: 设计界面 console.log(dev.introduce()); // 输出: 我是李四,既能编写代码也能设计界面 ``` #### 方法 3: 代理实现多继承(ES6 Proxy) ```javascript // 多继承工厂函数 function createMultiInherited(...classes) { return class MultiInherited { constructor(...args) { // 创建所有父类实例 this.parents = classes.map(Cls => new Cls(...args)); } // 代理方法调用 get(target, prop) { // 先在自身查找 if (target[prop]) return target[prop]; // 在父类实例中查找 for (const parent of target.parents) { if (typeof parent[prop] === 'function') { return (...args) => parent[prop].apply(parent, args); } } } }; } // 父类A class A { methodA() { return "方法A"; } } // 父类B class B { methodB() { return "方法B"; } } // 创建多继承类 const MultiClass = createMultiInherited(A, B); // 实例化 const instance = new MultiClass(); console.log(instance.methodA()); // 输出: 方法A console.log(instance.methodB()); // 输出: 方法B ``` ### 多继承的关键挑战与解决方案 | 挑战 | 解决方案 | |------|----------| | **钻石问题**(同名方法冲突) | 明确定义方法解析顺序(MRO) | | **原型链污染** | 使用组合而非继承(优先选择Mixin) | | **构造函数初始化** | 显式调用所有父类构造函数 | | **`instanceof`检测** | 自定义`[Symbol.hasInstance]`方法 | ### 最佳实践建议 1. **优先使用组合而非继承**:大多数情况下,组合模式比多继承更灵活 ```javascript class Developer { constructor(coder, designer) { this.coder = coder; this.designer = designer; } work() { return this.coder.code() + " 和 " + this.designer.design(); } } ``` 2. **使用接口模式**:定义明确的合约而非具体实现 ```javascript // 可编码接口 const Codable = { code() { throw new Error("必须实现code方法"); } }; // 可设计接口 const Designable = { design() { throw new Error("必须实现design方法"); } }; // 实现类 class FullStackDeveloper { code() { /* 实现 */ } design() { /* 实现 */ } } // 应用接口 Object.assign(FullStackDeveloper.prototype, Codable, Designable); ``` 3. **使用TypeScript接口**(如果环境支持): ```typescript interface Coder { code(): string; } interface Designer { design(): string; } class FullStackDev implements Coder, Designer { code() { return "编码"; } design() { return "设计"; } } ``` ### 多继承适用场景 1. 需要组合多个独立功能模块 2. 实现类似装饰器模式的行为扩展 3. 需要复用多个现有类的功能 4. 模拟现实世界中多重角色/身份的场景 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值