this 指向详解
this 是 JavaScript 中最容易混淆的概念之一:它的指向在调用时才确定,取决于函数的调用方式,而不是定义位置。本文用四张"绑定规则"梳理 this 的全部可能取值,再讲解箭头函数、call/apply/bind 以及常见陷阱的规避方法。
一、this 是什么
this 是函数内部的一个特殊关键字,代表"当前执行上下文"。普通函数的 this 由调用方式决定,遵循"谁调用,this 指向谁"的直觉,但规则远不止这一条:
function show() {
console.log(this);
}
show(); // 非严格模式:window(浏览器)/ global(Node);严格模式:undefined二、四种绑定规则
2.1 默认绑定
独立函数调用(不带任何调用者)时使用默认绑定:非严格模式下 this 指向全局对象,严格模式下为 undefined:
function f() {
console.log(this);
}
f(); // 浏览器中打印 window
"use strict";
function g() {
console.log(this);
}
g(); // undefined2.2 隐式绑定
函数作为对象的方法被调用时,this 指向该对象:
const user = {
name: "张三",
say() {
console.log(this.name);
},
};
user.say(); // 张三,this 指向 user
const dog = {
name: "旺财",
bark: user.say, // 方法被赋值给另一个对象
};
dog.bark(); // 旺财,this 指向调用者 dog关键点:this 与函数的"宿主"无关,只与调用时"."前面的对象有关。
2.3 显式绑定
通过 call、apply、bind 可以指定函数执行时的 this:
function introduce(city) {
console.log(`我是${this.name},来自${city}`);
}
const p1 = { name: "李四" };
const p2 = { name: "王五" };
introduce.call(p1, "北京"); // 我是李四,来自北京
introduce.apply(p2, ["上海"]); // 我是王五,来自上海
const bound = introduce.bind(p1);
bound("广州"); // 我是李四,来自广州2.4 new 绑定
用 new 调用构造函数时,this 指向新建的对象:
function Person(name) {
this.name = name; // this 指向新对象
}
const p = new Person("赵六");
console.log(p.name); // 赵六
console.log(p instanceof Person); // true三、绑定优先级
当多种规则同时适用时,按如下优先级从高到低判定:
| 优先级 | 规则 | 示例 |
|---|---|---|
| 最高 | new 绑定 | new F() |
| 次高 | 显式绑定 | f.call(obj)、f.bind(obj) |
| 中等 | 隐式绑定 | obj.f() |
| 最低 | 默认绑定 | f() |
const obj = { name: "对象" };
function f() {
console.log(this.name);
}
// 显式绑定 > 隐式绑定
const o2 = { name: "外层对象", fn: f };
o2.fn.call(obj); // 对象,显式绑定胜出
// new 绑定 > 显式绑定
function Person(name) {
this.name = name;
}
const b = Person.bind({ name: "被忽略" });
const np = new b("new 创建");
console.log(np.name); // new 创建,new 绑定胜出,bind 的 this 被忽略四、箭头函数的 this
箭头函数没有自己的 this,它的 this 是定义时外层作用域的 this(词法捕获),且不能被 call/apply/bind 改变,也不能通过 new 调用:
const name = "全局名";
const obj = {
name: "对象名",
normal: function () {
console.log(this.name); // 对象名,调用时确定
},
arrow: () => {
console.log(this.name); // 全局名,定义时外层 this 是全局
},
};
obj.normal(); // 对象名
obj.arrow(); // 全局名
// call 无法改变箭头函数的 this
obj.arrow.call({ name: "无效" }); // 全局名,显式绑定对箭头函数无效箭头函数最常见的价值是穿透一层函数边界,保留外层 this:
const timer = {
seconds: 0,
start() {
// 普通函数回调:this 变成 undefined/window
setInterval(function () {
this.seconds++; // 错误!this 不是 timer
}, 1000);
// 箭头函数回调:捕获 start() 的 this(即 timer)
setInterval(() => {
this.seconds++;
}, 1000);
},
};| 对比项 | 普通函数 | 箭头函数 |
|---|---|---|
| this 来源 | 调用时确定 | 定义时外层作用域 |
| 能否被 call/apply/bind 改变 | 能 | 不能 |
| 能否作为构造函数 | 能 | 不能 |
| 适用场景 | 方法、构造函数 | 回调、需要保留外层 this |
五、call / apply / bind 的区别
三者都用于显式指定 this,区别在于传参方式与是否立即执行:
const person = { name: "孙七" };
function greet(prefix, suffix) {
console.log(`${prefix} ${this.name} ${suffix}`);
}
greet.call(person, "你好,", "!"); // 你好, 孙七 !
greet.apply(person, ["你好,", "!"]); // 你好, 孙七 !(参数放数组)
const g = greet.bind(person, "你好,", "!");
g(); // 你好, 孙七 !(bind 返回新函数,不立即执行)
g("多余参数会被忽略"); // bind 的参数固定,再传参数仅作为追加| 方法 | this 绑定 | 传参方式 | 执行时机 | 返回值 |
|---|---|---|---|---|
call | 指定对象 | 逐个列出 | 立即执行 | 原函数返回值 |
apply | 指定对象 | 数组/类数组 | 立即执行 | 原函数返回值 |
bind | 指定对象(固定) | 可预置部分参数 | 不执行,返回新函数 | 新函数 |
5.1 经典应用场景
// 借用数组方法处理 arguments
function sum() {
const arr = Array.prototype.slice.call(arguments);
return arr.reduce((acc, n) => acc + n, 0);
}
console.log(sum(1, 2, 3)); // 6
// apply 展开数组求最值
console.log(Math.max.apply(null, [3, 9, 5])); // 9
// bind 预置参数:偏函数
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2);
console.log(double(5)); // 10六、严格模式下的 this
严格模式("use strict")下,默认绑定的 this 是 undefined,避免误触全局对象:
"use strict";
function f() {
return this;
}
console.log(f()); // undefined(非严格模式下为 window)
const obj = { a: 1 };
console.log(f.call(obj)); // { a: 1 },显式绑定不受影响非严格模式还有一个"自动装箱"行为:this 为原始值时会包装成对应对象,严格模式下保持原始值不变。
七、常见陷阱
7.1 方法赋值后丢失 this(引用丢失)
把方法从对象上取出来单独调用,隐式绑定失效,退化为默认绑定:
const user = {
name: "张三",
say() {
console.log(this.name);
},
};
const fn = user.say;
fn(); // undefined(浏览器非严格模式下是 window.name),this 丢失
// 修复:显式绑定
const fn2 = user.say.bind(user);
fn2(); // 张三7.2 回调中的 this
回调由外部在任意上下文中调用,this 很可能不是预期对象:
const user = {
name: "张三",
greet() {
setTimeout(function () {
console.log(this.name); // undefined,setTimeout 以默认绑定调用回调
}, 0);
},
};
user.greet();
// 修复方案见第八节7.3 事件处理中的 this
DOM 事件监听中,普通函数回调的 this 指向绑定事件的元素,而不是定义它的对象:
const handler = {
count: 0,
// document.addEventListener("click", this.onClick); // 直接传方法,this 变成按钮元素
onClick() {
// 在浏览器事件中,这里 this 指向触发事件的 DOM 元素
console.log(this);
},
};
// 修复:箭头函数包裹,捕获 handler
// document.addEventListener("click", () => handler.onClick());7.4 解构赋值与方法引用
const user = { name: "李四", say() { console.log(this.name); } };
const { say } = user;
say(); // undefined,解构取出的方法 this 丢失| 陷阱 | 现象 | 修复 |
|---|---|---|
| 方法赋值 | this 变全局/undefined | bind 绑定 |
| 定时器/异步回调 | this 丢失 | 箭头函数或保存 this |
| 事件监听 | this 指向元素 | 箭头函数包裹 |
| 解构方法 | this 丢失 | 用对象名调用或 bind |
八、绑定技巧
8.1 箭头函数包裹
用箭头函数把方法调用包一层,箭头函数捕获外层 this:
const user = {
name: "张三",
greet() {
// setTimeout(() => console.log(this.name), 0); // 推荐:箭头函数捕获 this
},
};8.2 bind 显式绑定
在传入回调之前就把 this 固定:
const user = {
name: "张三",
greet() {
setTimeout(function () {
console.log(this.name);
}.bind(this), 0); // bind(this) 固定为 user
},
};
user.greet(); // 张三8.3 保存 this 引用(经典写法)
用局部变量 self/that 保存外层 this,普通函数回调中使用:
const user = {
name: "张三",
greet() {
const self = this; // 保存当前 this
setTimeout(function () {
console.log(self.name); // 张三
}, 0);
},
};
user.greet();九、显式绑定与硬绑定
硬绑定(hard binding)是基于 bind 的一种固定手法:无论如何调用,this 都无法被改变:
function f() {
console.log(this.name);
}
const obj = { name: "固定对象" };
const hard = f.bind(obj); // bind 返回的新函数 this 永久固定
hard(); // 固定对象
hard.call({ name: "无效" }); // 固定对象,显式绑定也改不了
const obj2 = { fn: hard };
obj2.fn(); // 固定对象,隐式绑定也改不了原理:bind 内部用闭包保存了目标对象,调用时通过 apply 强制指定 this。可以用 apply 模拟最简单的硬绑定:
function bindSimple(fn, ctx) {
return function (...args) {
return fn.apply(ctx, args);
};
}
const s = bindSimple(f, obj);
s(); // 固定对象一句话总结 this 的判断顺序:先看是不是 new 调用,再看有没有 call/apply/bind,然后看是不是对象方法调用,最后才是独立调用(默认绑定);箭头函数则跳过以上所有规则,直接用定义时外层作用域的 this。