JavaScript 继承与 this 指向操作详解
本文从 ES5 原型机制到 ES6 class 语法,系统拆解 JavaScript 继承的实现逻辑,结合 call/apply/bind 方法深入剖析 this 指向规则,补充实战踩坑点与优化方案,助力开发者彻底掌握继承与 this 核心知识点。
一、原型继承(ES5 基于原型链的核心继承方式)
关键注意事项
-
原型链查找规则 :子实例访问属性/方法时,遵循「自身 → 子原型 → 父实例 → 父原型 → ... →
Object.prototype→null」的查找链路,找到目标后立即返回,直至终点仍未找到则返回undefined。 -
constructor** 修复必要性**:重写子类原型(如Son.prototype = new Father())后,子类原型的constructor会丢失并默认指向Object。手动赋值Son.prototype.constructor = Son可恢复正确指向,确保instanceof检测、实例构造函数判断(如son.constructor === Son)的准确性。 -
原型继承的缺点
-
父类的实例属性会被所有子类实例共享,修改一个子类实例的引用类型属性(如数组、对象)会影响其他子类实例,值类型属性不受此影响。
-
子类构造函数无法向父类构造函数动态传参,参数只能在赋值子类原型时固定,灵活性极差。
-
二、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 本质是「构造函数 + 原型」的语法糖,简化了继承写法,同时提供 extends、super 等关键字,支持静态成员、方法重写等高级特性,兼容性覆盖所有现代浏览器。
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 指向核心总结
-
ES5 继承 :核心是原型链,寄生组合继承(
Object.create+call)是完美方案,解决属性共享、动态传参、内存冗余问题。 -
this 指向核心 :普通函数 this 指向「调用者」,箭头函数 this 继承「定义时外层作用域」;
call/apply/bind可修改普通函数 this,无法影响箭头函数。 -
ES6 class 继承 :语法糖简化原型继承,
super可作为函数(调用父构造)或对象(访问父类成员),静态成员挂载在类本身,支持方法重写扩展。 -
实战避坑 :避免箭头函数用于需要动态 this 的场景(如对象方法、事件回调);子类重写父类方法时,用
super保留父类逻辑;ES5 继承优先使用寄生组合继承。