图解JS继承方式

核心概念

在开始之前,理解几个关键概念很重要:

  1. 构造函数 (Constructor):new 关键字调用的函数,用于创建对象实例。
  2. 原型对象 (Prototype Object): 每个构造函数都有一个 prototype 属性,指向一个对象。这个对象包含了由该构造函数创建的所有实例共享的属性和方法。
  3. 实例 (Instance): 通过 new 构造函数创建的对象。
  4. __proto__ / [[Prototype]]: 每个对象(实例)都有一个内部链接(在旧浏览器中是 __proto__,标准中是 [[Prototype]],可通过 Object.getPrototypeOf() 访问),指向其构造函数的原型对象。这就是原型链的基础。
  5. 原型链 (Prototype Chain): 当试图访问一个对象的属性时,如果在该对象本身找不到,JavaScript 会沿着 __proto__ 链向上查找,直到找到该属性或到达链的末端 (null)。

1. 原型链继承

  • 核心思想: 让子类的原型对象等于父类的一个实例。

  • 实现: Child.prototype = new Parent();

  • 代码示例:

    javascript 复制代码
    function Parent() {
        this.parentProp = 'Parent Property';
        this.colors = ['red', 'blue']; // 引用类型属性
    }
    Parent.prototype.getParentProp = function() {
        console.log(this.parentProp);
    }
    
    function Child() {
        this.childProp = 'Child Property';
    }
    
    // 关键步骤:子类的原型指向父类的实例
    Child.prototype = new Parent();
    // 可选:修复构造函数指向(否则 Child 实例的 constructor 会指向 Parent)
    // Child.prototype.constructor = Child; // 如果需要的话
    
    let child1 = new Child();
    let child2 = new Child();
    
    console.log(child1.parentProp); // "Parent Property" (来自父类实例)
    child1.getParentProp();         // "Parent Property" (来自父类原型)
    
    child1.colors.push('green');
    console.log(child2.colors);    // ["red", "blue", "green"] - 问题:共享了引用类型
  • 图解:

  • 优点:

    • 简单直观,易于实现。
    • 父类原型上的方法能被子类实例共享。
    • 实例既是 instanceof Child 也是 instanceof Parent
  • 缺点:

    • 共享引用类型: 父类实例上的引用类型属性(如 colors 数组)会被所有子类实例共享,一个实例修改会影响其他实例。
    • 无法传递参数: 创建子类实例时,无法向父类构造函数传递参数。
    • 创建父类实例有多余属性:Child.prototype 上包含了 parentPropcolors 这些本应属于实例的属性。

2. 构造函数继承

  • 核心思想: 在子类构造函数内部,使用 call()apply() 调用父类构造函数,将父类的实例属性复制到子类实例上。

  • 代码示例:

    javascript 复制代码
    function Parent(name) {
        this.name = name;
        this.colors = ['red', 'blue'];
        this.sayName = function() { // 方法在构造函数中,非原型
            console.log(this.name);
        }
    }
    Parent.prototype.parentProtoProp = 'Parent Proto Prop';
    
    function Child(name, age) {
        // 关键步骤:借用父类构造函数,并传递参数
        Parent.call(this, name); // this 指向 Child 实例
        this.age = age;
    }
    
    let child1 = new Child('Alice', 10);
    let child2 = new Child('Bob', 12);
    
    
    child1.colors.push('green');
    
    console.log(child1.name);       // "Alice"
    console.log(child1.age);        // 10
    console.log(child1.colors);     // ["red", "blue", "green"]
    console.log(child2.name);       // "Bob"
    console.log(child2.colors);     // ["red", "blue"] - 解决了共享引用类型问题
    child1.sayName();               // "Alice"
    
    // console.log(child1.parentProtoProp); // undefined - 问题:无法继承父类原型属性/方法
    // console.log(child1 instanceof Parent); // false - 问题:实例并非 Parent 的实例
  • 优点:
    • 解决引用类型共享问题: 每个子类实例都有自己独立的父类实例属性副本。
    • 可以传递参数: 可以在子类构造函数中向父类构造函数传递参数。
  • 缺点:
    • 无法继承原型: 只能继承父类构造函数中的属性/方法,无法继承父类原型上的属性/方法。
    • 方法冗余: 父类构造函数中的方法(如 sayName)会被复制到每个子类实例上,无法复用,浪费内存。
    • instanceof 问题:子类实例不是父类构造函数的实例 (child1 instanceof Parentfalse)。

3. 组合继承 (Combination Inheritance)

  • 核心思想: 结合原型链继承和构造函数继承。使用构造函数继承来继承实例属性(解决引用共享和传参问题),使用原型链继承来继承原型方法(实现方法复用)。

  • 代码示例:

    javascript 复制代码
    function Parent(name) {
        this.name = name;
        this.colors = ['red', 'blue'];
    }
    Parent.prototype.sayHello = function() {
        console.log('Hello from Parent Proto');
    }
    
    function Child(name, age) {
        // 步骤 1: 借用构造函数继承实例属性
        Parent.call(this, name);
        this.age = age;
    }
    
    // 步骤 2: 原型链继承原型方法
    Child.prototype = new Parent(); // 调用了一次 Parent 构造函数
    Child.prototype.constructor = Child; // 修复 constructor 指向
    
    Child.prototype.sayAge = function() {
        console.log(this.age);
    }
    
    let child1 = new Child('Alice', 10);
    let child2 = new Child('Bob', 12);
    
    child1.colors.push('green');
    
    console.log(child1.name);       // "Alice"
    console.log(child1.colors);     // ["red", "blue", "green"]
    console.log(child2.name);       // "Bob"
    console.log(child2.colors);     // ["red", "blue"]
    child1.sayHello();              // "Hello from Parent Proto"
    child1.sayAge();                // 10
    console.log(child1 instanceof Child);  // true
    console.log(child1 instanceof Parent); // true
  • 图解:

  • 优点:
    • 结合了前两种方式的优点:既能继承实例属性(独立副本、可传参),又能继承原型方法(共享)。
    • instanceof 正常工作。
  • 缺点:
    • 调用两次父类构造函数:
      1. 在子类构造函数中 Parent.call(this) 调用一次。
      2. 在设置子类原型时 Child.prototype = new Parent() 又调用一次。
    • 这导致子类原型对象上有一份多余的、从未被使用的父类实例属性(如图中 Child.prototype 也就是那个 Parent Instance 也会有 name, colors 属性,虽然 child1 实例上的同名属性会覆盖它们)。

4. 原型式继承

  • 核心思想: 基于一个现有对象创建一个新对象,新对象的 [[Prototype]] 指向现有对象。ES5 提供了 Object.create() 方法来实现。

  • 代码示例:

    javascript 复制代码
    let person = {
        name: "Base Person",
        friends: ["Shelby", "Court"]
    };
    
    let anotherPerson = Object.create(person);
    anotherPerson.name = "Greg"; // 覆盖属性
    anotherPerson.friends.push("Rob");
    
    let yetAnotherPerson = Object.create(person);
    yetAnotherPerson.name = "Linda";
    yetAnotherPerson.friends.push("Barbie");
    
    console.log(anotherPerson.name); // "Greg"
    console.log(yetAnotherPerson.name); // "Linda"
    console.log(person.friends);     // ["Shelby", "Court", "Rob", "Barbie"] - 问题:共享引用类型
  • 图解:

  • 优点:
    • 不需要显式创建构造函数即可实现继承。
    • Object.create() 可以传入第二个参数来定义新对象的自有属性。
  • 缺点:
    • 共享引用类型: 与原型链继承类似,原型对象上的引用类型属性会被所有实例共享。
    • 无法直接实现构造函数模式(如传递初始化参数)。

5. 寄生式继承

  • 核心思想: 创建一个仅用于封装继承过程的函数。该函数在内部以某种方式(如原型式继承)创建对象,然后增强该对象(添加属性/方法),最后返回这个对象。

  • 代码示例:

    javascript 复制代码
    function createAnother(original) {
        // 步骤 1: 创建一个继承自 original 的新对象
        let clone = Object.create(original); // 或者使用 object(original)
    
        // 步骤 2: 增强这个新对象
        clone.sayHi = function() {
            console.log("hi, I am " + this.name);
        };
    
        // 步骤 3: 返回增强后的对象
        return clone;
    }
    
    let person = {
        name: "Base Person",
        friends: ["Shelby"]
    };
    
    let anotherPerson = createAnother(person);
    anotherPerson.name = "Greg";
    anotherPerson.sayHi(); // "hi, I am Greg"
    
    let yetAnotherPerson = createAnother(person);
    yetAnotherPerson.name = "Linda";
    yetAnotherPerson.sayHi(); // "hi, I am Linda"
    
    // 问题:sayHi 方法在每个实例上都重新创建了
    console.log(anotherPerson.sayHi === yetAnotherPerson.sayHi); // false
  • 图解: (类似原型式继承,但 createAnother 函数是关键)

  • 优点:
    • 可以为对象添加方法而无需修改原始对象或其原型。
    • 代码封装性较好。
  • 缺点:
    • 方法冗余: 与构造函数继承类似,增强的方法(如 sayHi)是在每个新对象上单独创建的,没有实现函数复用。
    • 本质上还是基于原型式或其他继承方式,并带有其缺点(如引用类型共享)。

6. 寄生组合式继承 - 推荐 (ES6 Class 之前)

  • 核心思想: 结合构造函数继承(获取实例属性)和寄生式继承的思路来继承原型。避免了组合继承中调用两次父类构造函数的问题。

  • 步骤:

    1. 使用 Parent.call(this, ...) 继承父类实例属性。
    2. 使用 Object.create(Parent.prototype) 创建一个空对象,其 [[Prototype]] 指向父类原型。
    3. 将这个空对象赋值给 Child.prototype
    4. 修复 Child.prototype.constructor 指向 Child
  • 代码示例:

    javascript 复制代码
    function inheritPrototype(childConstructor, parentConstructor) {
        // 步骤 2 & 3: 创建父类原型的副本,并赋值给子类原型
        let prototype = Object.create(parentConstructor.prototype);
        // 步骤 4: 修复 constructor 指向
        prototype.constructor = childConstructor;
        childConstructor.prototype = prototype;
    }
    
    function Parent(name) {
        this.name = name;
        this.colors = ['red', 'blue'];
    }
    Parent.prototype.sayHello = function() {
        console.log('Hello from Parent Proto');
    }
    
    function Child(name, age) {
        // 步骤 1: 借用构造函数
        Parent.call(this, name);
        this.age = age;
    }
    
    // 关键步骤:调用辅助函数实现原型继承
    inheritPrototype(Child, Parent);
    
    Child.prototype.sayAge = function() {
        console.log(this.age);
    }
    
    let child1 = new Child('Alice', 10);
    let child2 = new Child('Bob', 12);
    
    child1.colors.push('green');
    
    console.log(child1.name);       // "Alice"
    console.log(child1.colors);     // ["red", "blue", "green"]
    console.log(child2.name);       // "Bob"
    console.log(child2.colors);     // ["red", "blue"]
    child1.sayHello();              // "Hello from Parent Proto"
    child1.sayAge();                // 10
    console.log(child1 instanceof Child);  // true
    console.log(child1 instanceof Parent); // true
  • 图解:

  • 优点:
    • 只调用一次父类构造函数: 避免了组合继承的性能问题。
    • 完美继承: 同时继承实例属性(独立、可传参)和原型方法(共享)。
    • instanceof 关系正确。
    • 被认为是 ES6 class extends 出现之前最理想的继承范式。
  • 缺点:
    • 实现相对复杂一些,需要封装辅助函数。

7. ES6 Class 继承

  • 核心思想: 使用 classextends 关键字实现继承,是寄生组合式继承的语法糖super 关键字用于调用父类的构造函数和方法。

  • 代码示例:

    javascript 复制代码
    class Parent {
        constructor(name) {
            this.name = name;
            this.colors = ['red', 'blue'];
        }
    
        sayHello() {
            console.log('Hello from Parent class');
        }
    }
    
    class Child extends Parent {
        constructor(name, age) {
            // 关键:调用父类构造函数,相当于 Parent.call(this, name)
            super(name);
            this.age = age;
        }
    
        sayAge() {
            console.log(this.age);
        }
    }
    
    let child1 = new Child('Alice', 10);
    let child2 = new Child('Bob', 12);
    
    child1.colors.push('green');
    
    console.log(child1.name);       // "Alice"
    console.log(child1.colors);     // ["red", "blue", "green"]
    console.log(child2.name);       // "Bob"
    console.log(child2.colors);     // ["red", "blue"]
    child1.sayHello();              // "Hello from Parent class"
    child1.sayAge();                // 10
    console.log(child1 instanceof Child);  // true
    console.log(child1 instanceof Parent); // true
    
    // class 内部的方法定义在原型上
    console.log(child1.sayAge === Child.prototype.sayAge); // true
  • 优点:

    • 语法简洁清晰: 使用 class, extends, super 关键字,更符合传统面向对象语言的习惯。
    • 内置实现: 本质上是寄生组合继承的最佳实践,解决了之前各种方式的缺点。
    • 是现代 JavaScript 中推荐的继承方式。
  • 缺点:

    • 需要 ES6 环境或构建工具(如 Babel)转译。
    • class 语法可能掩盖其基于原型的本质,对于初学者可能需要额外理解其工作原理。

总结:

JavaScript 的继承经历了从简单但有缺陷(原型链、构造函数)到组合(解决部分问题但有冗余)再到优化(原型式、寄生式、寄生组合式)最终到标准化、语法糖化(ES6 Class)的过程。目前,ES6 Class 继承 是最推荐和最常用的方式。

相关推荐
星空寻流年3 小时前
css3伸缩盒模型第二章(侧轴相关)
javascript·css·css3
GalenWu5 小时前
对象转换为 JSON 字符串(或反向解析)
前端·javascript·微信小程序·json
zwjapple5 小时前
“ES7+ React/Redux/React-Native snippets“常用快捷前缀
javascript·react native·react.js
数据潜水员5 小时前
插槽、生命周期
前端·javascript·vue.js
优雅永不过时·5 小时前
实现一个漂亮的Three.js 扫光地面 圆形贴图扫光
前端·javascript·智慧城市·three.js·贴图·shader
春天姐姐7 小时前
vue知识点总结 依赖注入 动态组件 异步加载
前端·javascript·vue.js
Pop–9 小时前
Vue3 el-tree:全选时只返回父节点,半选只返回勾选中的节点(省-市区-县-镇-乡-村-街道)
开发语言·javascript·vue.js
滿9 小时前
Vue3 + Element Plus 动态表单实现
javascript·vue.js·elementui
阿金要当大魔王~~9 小时前
面试问题(连载。。。。)
前端·javascript·vue.js
yuanyxh9 小时前
commonmark.js 源码阅读(一) - Block Parser
开发语言·前端·javascript