JavaScript 继承与 this 指向操作详解

JavaScript 继承与 this 指向操作详解

本文从 ES5 原型机制到 ES6 class 语法,系统拆解 JavaScript 继承的实现逻辑,结合 call/apply/bind 方法深入剖析 this 指向规则,补充实战踩坑点与优化方案,助力开发者彻底掌握继承与 this 核心知识点。

一、原型继承(ES5 基于原型链的核心继承方式)

关键注意事项

  1. 原型链查找规则 :子实例访问属性/方法时,遵循「自身 → 子原型 → 父实例 → 父原型 → ... →Object.prototypenull」的查找链路,找到目标后立即返回,直至终点仍未找到则返回 undefined

  2. constructor ** 修复必要性**:重写子类原型(如 Son.prototype = new Father())后,子类原型的 constructor 会丢失并默认指向 Object。手动赋值 Son.prototype.constructor = Son 可恢复正确指向,确保 instanceof 检测、实例构造函数判断(如 son.constructor === Son)的准确性。

  3. 原型继承的缺点

    • 父类的实例属性会被所有子类实例共享,修改一个子类实例的引用类型属性(如数组、对象)会影响其他子类实例,值类型属性不受此影响。

    • 子类构造函数无法向父类构造函数动态传参,参数只能在赋值子类原型时固定,灵活性极差。

二、call / apply / bind(修改 this 指向,辅助 ES5 实现继承)

三者核心功能均为修改函数执行时的 this 指向,核心差异体现在「参数传递方式」和「是否立即执行」,其中仅 call/apply 可辅助实现构造函数继承。

1. call 方法:立即执行,参数逐个传递

核心用途

  • 临时改变函数 this 指向,且立即执行该函数。

  • 实现构造函数继承 :在子类构造函数中调用父类构造函数,将 this 绑定为子类实例,实现父类属性的独立复用(避免共享)。

完整代码示例

javascript 复制代码
// 1. 父构造函数:封装公共属性与方法
function Father(name, type) {
    this.name = name; // 实例属性(值类型)
    this.type = type;
    this.hobbies = []; // 实例属性(引用类型,用于验证共享问题)
    this.say = function (a, b, c) {
        console.log(`我是${a},今年${b}岁,是最聪明的${c}`);
    };
}

// 2. 子构造函数:通过 call 实现属性继承(独立不共享)
function Son() {
    // 绑定 this 为子类实例,向父类传递参数
    Father.call(this, "动物", "哺乳类");
}

// 3. 验证继承效果与属性独立性
const son1 = new Son();
const son2 = new Son();
son1.hobbies.push("跑步"); // 修改 son1 的引用类型属性
console.log("son1 爱好:", son1.hobbies); // 输出:[ '跑步' ]
console.log("son2 爱好:", son2.hobbies); // 输出:[](无共享,独立存在)
console.log("子类继承的属性:", son1.name, son1.type); // 输出:动物 哺乳类

// 4. 临时借用父实例方法(改变 this 指向)
const dad = new Father("野兽", "肉食类");
// call 逐个传递参数,立即执行函数
dad.say.call(son1, "一只狗", 18, "狗"); // 输出:我是一只狗,今年18岁,是最聪明的狗

2. apply 方法:立即执行,参数数组传递

核心区别

call 功能完全一致,唯一差异是参数需以「数组/类数组」形式传递,适合参数数量不固定、动态生成参数列表的场景(如接收函数参数集合 arguments)。

完整代码示例

javascript 复制代码
// 1. 父构造函数(与 call 示例一致)
function Father(name, type) {
    this.name = name;
    this.type = type;
    this.say = function (a, b, c) {
        console.log(`我是${a},今年${b}岁,是最聪明的${c}`);
    };
}

// 2. 子构造函数:通过 apply 实现属性继承
function Son() {
    // 数组形式传递参数,适配动态参数场景
    Father.apply(this, ["动物", "哺乳类"]);
}

// 3. 验证继承效果
const son = new Son();
console.log("子类继承的属性:", son.name, son.type); // 输出:动物 哺乳类

// 4. 动态参数场景示例(接收不确定数量的参数)
function handleSay(target, ...args) {
    // apply 接收数组参数,适配 args 集合
    dad.say.apply(target, args);
}
const dad = new Father("野兽", "肉食类");
handleSay(son, ["一只猫", 18, "猫"]); // 输出:我是一只猫,今年18岁,是最聪明的猫

3. bind 方法:不立即执行,返回绑定后的新函数

核心区别

  • 绑定 this 指向后,不立即执行函数,而是返回一个全新的函数实例,需手动调用才会执行。

  • 支持「预传参」:绑定时常量参数可提前传入,调用新函数时可补充剩余参数。

  • 无法实现构造函数继承:仅适用于重复调用绑定函数的场景(如事件回调、定时器函数)。

完整代码示例

javascript 复制代码
// 1. 父构造函数(与前两个示例一致)
function Father(name, type) {
    this.name = name;
    this.type = type;
    this.say = function (a, b, c) {
        console.log(`我是${a},今年${b}岁,是最聪明的${c}`);
    };
}

// 2. 验证 bind 方法的绑定与预传参功能
const dad = new Father("野兽", "肉食类");
const son = {};

// 3. bind 绑定 this,预传第一个参数,返回新函数(不立即执行)
const bindSay = dad.say.bind(son, "一只猫");

// 4. 手动调用新函数,补充剩余参数
bindSay(18, "猫"); // 输出:我是一只猫,今年18岁,是最聪明的猫

// 5. 关键验证:bind 无法实现构造函数继承
Father.bind(son, "动物", "哺乳类"); // 仅绑定 this,不执行父构造函数
console.log("bind 无法继承属性:", son.name); // 输出:undefined

// 6. 实战场景:事件回调中固定 this
const obj = {
    name: "测试对象",
    logName: function () {
        console.log(this.name);
    }
};
// 定时器回调中,this 默认指向 window,用 bind 固定为 obj
setTimeout(obj.logName.bind(obj), 1000); // 1秒后输出:测试对象

三者核心区别对比表

方法 参数传递方式 是否立即执行 核心适用场景 是否支持预传参
call 逗号分隔,逐个传递 构造函数继承、参数数量固定的临时函数调用
apply 数组/类数组传递,自动解构 构造函数继承、参数数量不固定的临时函数调用
bind 逗号分隔(绑定可预传参) 事件回调、定时器函数、需重复调用的绑定函数

三、组合继承 + 寄生组合继承(ES5 完美继承方案)

1. 组合继承(ES5 基础最优解)

核心原理

结合「原型继承」(复用父类方法,减少内存占用)和「构造函数继承」(避免属性共享,支持动态传参)的优点,是 ES6 之前最常用的基础继承方案,但存在轻微缺陷。

完整代码示例

javascript 复制代码
// 1. 父构造函数:封装公共属性(值类型/引用类型均独立)
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.hobbies = []; // 引用类型属性,通过构造函数继承实现独立
}

// 2. 父原型:添加公共方法(所有实例共享,减少内存占用)
Person.prototype.say = function () {
    console.log(`我是${this.name},今年${this.age}岁`);
};

// 3. 子构造函数:通过 call 实现属性继承
function Student(name, age, grade) {
    Person.call(this, name, age); // 构造函数继承:属性独立,支持动态传参
    this.grade = grade; // 子类独有属性
}

// 4. 原型继承:继承父类方法
Student.prototype = new Person(); // 此处调用父构造函数(缺陷根源)
Student.prototype.constructor = Student; // 修复 constructor 指向

// 5. 子类原型:添加独有方法
Student.prototype.study = function () {
    console.log(`${this.name} 在${this.grade}年级好好学习`);
};

// 6. 验证效果
const student1 = new Student("张三", 12, 6);
const student2 = new Student("李四", 13, 7);
student1.hobbies.push("读书");
console.log(student1.hobbies); // 输出:[ '读书' ]
console.log(student2.hobbies); // 输出:[](属性独立)
student1.say(); // 输出:我是张三,今年12岁(复用父类方法)
student2.study(); // 输出:李四 在7年级好好学习(子类独有方法)

// 7. 缺陷:父构造函数被调用两次
// 第一次:Student.prototype = new Person();
// 第二次:Person.call(this, name, age);
// 导致子类原型上冗余父类实例属性(如 name、age,不影响使用但浪费内存)
console.log(Student.prototype.name); // 输出:undefined(因无传参,值为 undefined,仍属冗余)

2. 寄生组合继承(ES5 完美继承方案)

核心优化点

解决组合继承中「父构造函数被调用两次」的缺陷,通过「空对象中介」或 Object.create() 继承父类原型,避免子类原型冗余父类实例属性,实现内存最优、功能完善的继承。

完整代码示例(Object.create 简化版)

javascript 复制代码
// 1. 父构造函数(与组合继承一致)
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.hobbies = [];
}
Person.prototype.say = function () {
    console.log(`我是${this.name},今年${this.age}岁`);
};

// 2. 子构造函数(与组合继承一致)
function Student(name, age, grade) {
    Person.call(this, name, age); // 仅调用一次父构造函数(无冗余)
    this.grade = grade;
}

// 3. 寄生组合继承核心:继承父类原型,不调用父构造函数
// Object.create(prototype):创建新对象,原型指向传入的 prototype
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student; // 修复 constructor 指向

// 4. 子类独有方法(与组合继承一致)
Student.prototype.study = function () {
    console.log(`${this.name} 在${this.grade}年级好好学习`);
};

// 5. 验证效果(与组合继承一致,且无冗余属性)
const student = new Student("张三", 12, 6);
student.say(); // 输出:我是张三,今年12岁
student.study(); // 输出:张三 在6年级好好学习
console.log(Student.prototype.name); // 输出:undefined(无冗余属性)
console.log(student instanceof Student); // 输出:true(类型判断正确)

四、ES6 class 继承(语法糖与高级特性)

ES6 class 本质是「构造函数 + 原型」的语法糖,简化了继承写法,同时提供 extendssuper 等关键字,支持静态成员、方法重写等高级特性,兼容性覆盖所有现代浏览器。

1. class 基础继承语法

核心语法说明

  • class:定义类(替代 ES5 构造函数,内部自动关联原型)。

  • constructor:类的构造函数,实例化时自动执行,用于初始化属性。

  • extends:子类继承父类,自动关联原型链,实现属性和方法复用。

  • super():在子类构造函数中调用父类构造函数,必须优先调用(否则无法访问 this)。

完整代码示例

javascript 复制代码
// 1. 定义父类(基类)
class Person {
    // 父类构造函数:初始化公共属性
    constructor(name, age) {
        this.name = name; // 实例属性(挂载在 this 上)
        this.age = age;
    }

    // 父类实例方法(自动挂载在 Person.prototype 上,所有实例共享)
    say() {
        console.log(`我是${this.name},今年${this.age}岁`);
    }
}

// 2. 定义子类(派生类),继承父类
class Student extends Person {
    // 子类构造函数
    constructor(name, age, grade) {
        // 必须先调用 super(),初始化父类属性,否则 this 报错
        super(name, age); 
        this.grade = grade; // 子类独有属性
    }

    // 子类实例方法(挂载在 Student.prototype 上)
    study() {
        console.log(`${this.name} 在${this.grade}年级认真学习`);
    }
}

// 3. 实例化子类并验证
const student = new Student("李四", 13, 7);
student.say(); // 输出:我是李四,今年13岁(继承父类方法)
student.study(); // 输出:李四 在7年级认真学习(子类独有方法)
console.log(student.grade); // 输出:7(子类独有属性)
console.log(student instanceof Person); // 输出:true(原型链关联正确)

2. 静态成员(静态属性/方法)

核心规则

  • 静态成员挂载在「类本身」,而非原型上,仅能通过类名访问,无法通过实例访问。

  • 子类通过 extends 可继承父类静态成员,支持子类重写静态成员。

  • 静态方法中的 this 指向调用该方法的类(父类/子类)。

完整代码示例

javascript 复制代码
class Person {
    // 类内定义静态属性(ES6 标准语法)
    static className = "Person";
    // 类外定义静态属性(兼容低版本环境备选方案)
    static maxAge = 120;

    // 静态方法
    static getClassInfo() {
        // this 指向调用者(Person 或其子类)
        console.log(`类名:${this.className},最大年龄限制:${this.maxAge}`);
    }

    // 实例方法
    say() {
        console.log(`所属类:${Person.className}`);
    }
}

// 子类继承父类静态成员
class Student extends Person {
    // 重写父类静态属性
    static className = "Student";
    // 子类新增静态属性
    static minAge = 6;
}

// 验证静态成员访问
console.log(Person.className); // 输出:Person(父类静态属性)
console.log(Student.maxAge); // 输出:120(继承父类静态属性)
console.log(Student.minAge); // 输出:6(子类静态属性)
Person.getClassInfo(); // 输出:类名:Person,最大年龄限制:120
Student.getClassInfo(); // 输出:类名:Student,最大年龄限制:120(this 指向 Student)

// 验证:实例无法访问静态成员
const student = new Student();
console.log(student.className); // 输出:undefined
student.getClassInfo(); // 报错:student.getClassInfo is not a function

3. super 关键字的完整使用细节

super 是 ES6 继承核心,有两种使用场景:「作为函数调用」和「作为对象使用」,功能与限制完全不同,需严格区分。

场景 1:super 作为函数调用(调用父类构造函数)

  • 仅能在「子类构造函数中」使用,用于调用父类构造函数,初始化父类属性。

  • 调用时 this 自动绑定为子类实例,无需手动绑定。

  • 子类构造函数中,必须先调用 super(),再访问 this,否则报错。

javascript 复制代码
class Person {
    constructor(name) {
        this.name = name;
        console.log(`父类构造函数执行:初始化 name 为 ${name}`);
    }
}

class Student extends Person {
    constructor(name, age) {
        // super 作为函数:调用父类构造函数
        super(name); 
        this.age = age; // 先 super(),再使用 this,无报错
    }
}

const student = new Student("王五", 14);
// 输出:父类构造函数执行:初始化 name 为 王五
console.log(student.name, student.age); // 输出:王五 14

场景 2:super 作为对象使用(访问父类属性/方法)

  • 实例方法中 :super 指向父类原型对象(如 Person.prototype),仅能访问父类原型上的方法/属性,无法访问父类实例属性(实例属性挂载在 this 上)。

  • 静态方法中 :super 指向父类本身(如 Person),可访问父类的静态属性/方法。

  • 通过 super 调用父类方法时,this 仍绑定为子类实例(实例方法)或子类(静态方法)。

javascript 复制代码
class Person {
    constructor(name) {
        this.name = name; // 实例属性,挂载在 this 上,super 无法直接访问
    }

    // 父类原型方法
    say() {
        return `我是${this.name}(父类方法)`;
    }

    // 父类静态属性
    static staticProp = "父类静态属性";

    // 父类静态方法
    static staticMethod() {
        return `父类静态方法执行`;
    }
}

class Student extends Person {
    constructor(name, age) {
        super(name);
        this.age = age;
    }

    // 实例方法中使用 super
    study() {
        const parentSay = super.say(); // 调用父类原型方法,this 绑定子类实例
        console.log(`${parentSay},今年${this.age}岁,正在学习`);
        console.log("super 访问父类实例属性:", super.name); // 输出:undefined(无法访问)
    }

    // 静态方法中使用 super
    static studentStaticMethod() {
        const parentProp = super.staticProp; // 访问父类静态属性
        const parentMethod = super.staticMethod(); // 调用父类静态方法
        console.log(parentProp, parentMethod);
    }
}

// 验证实例方法中的 super
const student = new Student("赵六", 15);
student.study(); 
// 输出:我是赵六(父类方法),今年15岁,正在学习
// 输出:super 访问父类实例属性:undefined

// 验证静态方法中的 super
Student.studentStaticMethod(); // 输出:父类静态属性 父类静态方法执行

4. 子类重写父类方法

核心规则

子类方法与父类方法同名时,会覆盖父类方法(原型链查找优先取子类方法);通过 super.方法名() 可在子类方法中调用父类原方法,实现「扩展逻辑」而非完全替换。

完整代码示例

javascript 复制代码
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    say() {
        return `我是${this.name},今年${this.age}岁`;
    }
}

class Student extends Person {
    constructor(name, age, grade) {
        super(name, age);
        this.grade = grade;
    }

    // 重写父类 say 方法
    say() {
        // 调用父类原方法,扩展子类逻辑(实战常用技巧)
        const parentContent = super.say();
        return `${parentContent},就读于${this.grade}年级`;
    }

    // 重写父类不存在的方法(本质是新增子类方法)
    study() {
        console.log(`${this.name} 正在认真学习`);
    }
}

const student = new Student("孙七", 16, 8);
console.log(student.say()); // 输出:我是孙七,今年16岁,就读于8年级(扩展父类逻辑)
student.study(); // 输出:孙七 正在认真学习(子类新增方法)

// 父类方法不受影响
const person = new Person("周八", 30);
console.log(person.say()); // 输出:我是周八,今年30岁

五、常见 this 指向坑点与避坑指南

1. 箭头函数与普通函数的 this 差异

箭头函数无自身 this,其 this 继承自「定义时的外层作用域」,而非调用时;且无法通过 call/apply/bind 修改 this 指向,适合固定 this 场景(如回调函数)。

javascript 复制代码
const obj = {
    name: "测试对象",
    // 普通函数:this 指向调用者(obj)
    normalFn: function () {
        console.log("普通函数 this:", this.name);
    },
    // 箭头函数:this 继承外层作用域(全局 window)
    arrowFn: () => {
        console.log("箭头函数 this:", this.name);
    },
    // 嵌套场景:箭头函数继承外层普通函数的 this
    nestedFn: function () {
        const innerArrow = () => {
            console.log("嵌套箭头函数 this:", this.name);
        };
        innerArrow();
    }
};

obj.normalFn(); // 输出:普通函数 this:测试对象
obj.arrowFn(); // 输出:箭头函数 this:undefined(浏览器全局无 name 属性)
obj.nestedFn(); // 输出:嵌套箭头函数 this:测试对象

// 验证:箭头函数 this 无法被修改
const bindArrow = obj.arrowFn.bind(obj);
bindArrow(); // 输出:箭头函数 this:undefined

2. DOM 事件回调中的 this 指向

DOM 事件回调中,普通函数的 this 指向「触发事件的元素」,箭头函数的 this 继承外层作用域,需根据需求选择。

javascript 复制代码
// 浏览器环境示例(需页面存在 button 元素)
const btn = document.querySelector("button");
btn.textContent = "点击测试";

// 普通函数回调:this 指向 btn 元素
btn.addEventListener("click", function () {
    this.style.color = "red"; // 点击后按钮文字变红
    console.log(this); // 输出:<button>点击测试</button>
});

// 箭头函数回调:this 指向全局 window
btn.addEventListener("click", () => {
    console.log(this); // 输出:window
    // this.style.color = "red"; // 报错:无法设置 window 的 style 属性
});

3. 原型链与 this 指向的关联

通过原型链调用方法时,this 始终指向「当前实例对象」,而非方法所在的原型对象,确保属性访问的正确性。

javascript 复制代码
function Person(name) {
    this.name = name;
}

Person.prototype.say = function () {
    console.log(`this 指向:${this.name}`); // this 指向调用方法的实例
};

const person = new Person("张三");
person.say(); // 输出:this 指向:张三

// 手动修改原型链调用
const obj = { name: "李四" };
obj.__proto__ = Person.prototype; // 让 obj 继承 Person 原型
obj.say(); // 输出:this 指向:李四(this 指向 obj,非 Person.prototype)

六、继承与 this 指向核心总结

  1. ES5 继承 :核心是原型链,寄生组合继承(Object.create + call)是完美方案,解决属性共享、动态传参、内存冗余问题。

  2. this 指向核心 :普通函数 this 指向「调用者」,箭头函数 this 继承「定义时外层作用域」;call/apply/bind 可修改普通函数 this,无法影响箭头函数。

  3. ES6 class 继承 :语法糖简化原型继承,super 可作为函数(调用父构造)或对象(访问父类成员),静态成员挂载在类本身,支持方法重写扩展。

  4. 实战避坑 :避免箭头函数用于需要动态 this 的场景(如对象方法、事件回调);子类重写父类方法时,用 super 保留父类逻辑;ES5 继承优先使用寄生组合继承。

相关推荐
雨季6662 小时前
Flutter for OpenHarmony 入门实践:从 Scaffold 到 Container 的三段式布局构建
开发语言·javascript·flutter
副露のmagic2 小时前
更弱智的算法学习 day53
开发语言·python
HellowAmy2 小时前
我的C++规范 - 回调的设想
开发语言·c++·代码规范
Java程序员威哥2 小时前
SpringBoot多环境配置实战:从基础用法到源码解析与生产避坑
java·开发语言·网络·spring boot·后端·python·spring
mudtools2 小时前
C#中基于Word COM组件的数学公式排版实践
开发语言·c#·word
Q741_1472 小时前
C++ 优先级队列 大小堆 模拟 力扣 1046. 最后一块石头的重量 每日一题
开发语言·c++·算法·leetcode·优先级队列·
HIT_Weston2 小时前
109、【Ubuntu】【Hugo】搭建私人博客:搜索功能(五)
linux·javascript·ubuntu
一个处女座的程序猿O(∩_∩)O2 小时前
Next.js 与 React 深度解析:为什么选择 Next.js?
开发语言·javascript·react.js
KiefaC2 小时前
【C++】特殊类设计
开发语言·c++