JS this取值深度解读
前言:被 this 折磨的前端日常
"为什么函数里的 this 一会儿是 window,一会儿是 undefined?" "对象方法里的 this,赋值给变量后调用怎么就指向全局了?" "箭头函数的 this 为什么跟外层函数一模一样,改都改不了?" "用 new 创建实例时,this 明明指向实例,怎么返回个对象就变了?"
this 是 JavaScript 中最容易让人困惑的概念之一 ------ 它既不是 "定义时绑定",也不是 "谁调用就指向谁" 这么简单。很多开发者靠 "经验猜 this",遇到复杂场景就陷入调试困境。本文将从 "执行上下文本质" 出发,通过 "场景复现→原理拆解→代码验证→避坑指南" 的逻辑,帮你彻底搞懂 this 的取值规则,从此不再靠 "猜" 写代码。
一、先破后立:this 的本质不是 "谁调用指向谁"
在拆解具体场景前,必须先纠正一个流传甚广的误区:this 不是 "谁调用指向谁",而是 "函数调用时,执行上下文绑定的一个变量"。
1.1 this 的核心特性:执行时绑定
this 的指向在函数定义时完全不确定 ,只有在函数被调用的那一刻,才会根据 "调用方式" 绑定到具体对象,也就是无论这个函数声明在哪里、被赋值过多少次。这是理解 this 的第一个关键:
javascript
// 函数定义时,this毫无意义
function sayHi() {
console.log(this.name);
}
// 调用方式1:普通函数调用 → this指向window(浏览器)/global(Node)
const name = "全局";
sayHi(); // 输出"全局"
// 调用方式2:对象方法调用 → this指向对象
const obj = { name: "张三", sayHi };
obj.sayHi(); // 输出"张三"
// 调用方式3:new调用 → this指向新实例
function Person(name) {
this.name = name;
}
const p = new Person("李四");
console.log(p.name); // 输出"李四"
同样的函数sayHi,只因调用方式不同,this 指向完全不同 ------ 这说明 "调用方式" 才是 this 绑定的核心依据。
1.2 this 的底层逻辑:执行上下文
JavaScript 执行函数时,会创建一个 "执行上下文(Execution Context)",其中包含三个核心变量:
-
this:当前函数的调用者关联对象
-
AO(Activation Object):函数的活动对象(存储局部变量、参数等)
-
作用域链:决定变量的查找范围
this 是执行上下文的固有属性,其值由 "函数调用时的调用点(Call Site)" 决定,而非函数定义的位置。
1.3 this 绑定规则的优先级
当多个规则同时生效时,优先级决定最终 this 指向,优先级从高到低:new 绑定 > 显式绑定(bind) > 隐式绑定 > 默认绑定 验证优先级:
- 显式绑定 > 隐式绑定:
javascript
const obj1 = { a: 1, foo: function() { console.log(this.a); } };
const obj2 = { a: 2 };
obj1.foo.call(obj2); // 输出 2 → 显式绑定覆盖隐式
- new 绑定 > bind 显式绑定:
javascript
function foo(a) { this.a = a; }
const boundFoo = foo.bind({ a: 10 }); // 显式绑定到 {a:10}
const instance = new boundFoo(20); // new 绑定
console.log(instance.a); // 输出 20 → new 覆盖 bind
附上bind的实现代码(可知:new 绑定 > bind 显式绑定):
javascript
Function.prototype.bind = function (context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
// new创建的时候,this指向new出来的对象实例,这个实例通过new创建的,那么实例的constructor指向fBound
return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
}
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
fBound.prototype = this.prototype;
return fBound;
}
二、场景拆解:6 种核心调用方式的 this 绑定规则
实际开发中,this 的绑定场景可归纳为 6 类,覆盖 99% 的业务需求。每类场景都有明确的绑定规则,掌握这些规则就能精准判断 this 指向。
2.1 场景 1:普通函数调用(无任何绑定)
调用形式 :直接通过函数名()调用,不挂载在任何对象上。 绑定规则:
-
非严格模式:this 绑定到全局对象(浏览器是
window,Node 是global); -
严格模式(
use strict):this 绑定到undefined(禁止自动绑定全局对象)。
代码验证:
javascript
// 非严格模式
function normalCall() {
console.log("非严格模式:", this); // window(浏览器)
}
normalCall();
// 严格模式
function strictCall() {
"use strict";
console.log("严格模式:", this); // undefined
}
strictCall();
// 坑点:函数内嵌套函数,仍按普通调用处理
function outer() {
console.log("outer this:", this); // window(非严格模式)
function inner() {
console.log("inner this:", this); // window(非严格模式)
}
inner(); // 普通调用,与outer的this无关
}
outer();
避坑点:
-
❌ 不要在普通函数中依赖
this获取全局对象(严格模式下会报错); -
✅ 如需访问全局对象,浏览器用
window,Node 用globalThis(通用)。
2.2 场景 2:对象方法调用(挂载在对象上调用)
调用形式 :通过对象.函数名()调用,函数作为对象的属性存在。 绑定规则 :this 绑定到 "调用该方法的对象"(即.前面的对象)。
代码验证:
javascript
const user = {
name: "张三",
sayHi() {
console.log("this指向:", this); // 指向user对象
console.log("用户名:", this.name); // 输出"张三"
},
address: {
city: "北京",
getCity() {
console.log("城市:", this.city); // 指向address对象,输出"北京"
}
}
};
// 直接调用对象方法 → this指向对象
user.sayHi();
user.address.getCity();
// 坑点1:方法赋值给变量后,变成普通调用
const sayHi = user.sayHi;
sayHi(); // 普通调用 → this指向window,name为undefined
// 坑点2:嵌套对象中,this指向直接调用的对象(非外层)
user.address.getCity.call(user); // 强制改变this为user,输出undefined(user无city属性)
关键原理:
this 绑定的是 "直接调用者",而非 "函数定义时所在的对象"。即使函数定义在其他地方,只要通过obj.fn()调用,this 就指向obj:
javascript
// 函数定义在外部
function getUserName() {
console.log(this.name);
}
// 挂载到不同对象调用
const obj1 = { name: "李四", getUserName };
const obj2 = { name: "王五", getUserName };
obj1.getUserName(); // 输出"李四"(this指向obj1)
obj2.getUserName(); // 输出"王五"(this指向obj2)
2.3 场景 3:构造函数调用(new 关键字)
调用形式 :通过new 函数名()创建实例。 绑定规则:this 绑定到 "新创建的实例对象"。
代码验证:
javascript
function Person(name, age) {
// new调用时,this指向新创建的Person实例
this.name = name;
this.age = age;
console.log("构造函数this:", this); // Person { name: "...", age: ... }
}
// new调用 → this绑定到实例
const p1 = new Person("赵六", 25);
console.log(p1.name); // 输出"赵六"
// 坑点:构造函数返回对象会覆盖this
function PersonWithReturn(name) {
this.name = name;
// 返回对象时,new创建的实例会被这个对象替代
return { name: "钱七" };
}
const p2 = new PersonWithReturn("孙八");
console.log(p2.name); // 输出"钱七"(this被覆盖)
// 注意:返回基本类型(如number、string)不会覆盖this
function PersonWithPrimitive(name) {
this.name = name;
return 123; // 基本类型,不影响this
}
const p3 = new PersonWithPrimitive("周九");
console.log(p3.name); // 输出"周九"
new 调用的底层流程:
-
创建一个新的空对象(
const obj = {}); -
将新对象的
__proto__指向构造函数的prototype(实现继承); -
调用构造函数,将 this 绑定到新对象;
-
若构造函数返回对象,则返回该对象;否则返回新对象。
2.4 场景 4:apply/call/bind 绑定(强制改变 this)
调用形式 :通过fn.apply(obj)、fn.call(obj)、fn.bind(obj)调用。 绑定规则 :this 强制绑定到传入的obj(obj为null/undefined时,非严格模式绑定全局,严格模式绑定obj)。
三者区别:
| 方法 | 调用形式 | 是否立即执行 | 参数传递方式 |
|---|---|---|---|
| apply | fn.apply(obj, [arg1, arg2]) |
是 | 数组传递参数 |
| call | fn.call(obj, arg1, arg2) |
是 | 逗号分隔传递参数 |
| bind | const newFn = fn.bind(obj) |
否 | 返回新函数,延迟执行 |
代码验证:
javascript
function sayInfo(age, city) {
console.log(`姓名:${this.name},年龄:${age},城市:${city}`);
}
const user = { name: "吴十" };
// apply调用 → 数组传参,立即执行
sayInfo.apply(user, [28, "上海"]); // 输出"姓名:吴十,年龄:28,城市:上海"
// call调用 → 逗号传参,立即执行
sayInfo.call(user, 28, "上海"); // 输出同上
// bind调用 → 返回新函数,延迟执行
const boundSayInfo = sayInfo.bind(user, 28);
boundSayInfo("上海"); // 输出同上
// 坑点:bind是硬绑定,后续无法被apply/call修改
const newObj = { name: "郑十一" };
boundSayInfo.call(newObj, "北京"); // 仍输出"吴十"(this无法改变)
特殊情况:obj 为 null/undefined
javascript
// 非严格模式:obj为null/undefined时,this绑定全局
sayInfo.call(null, 28, "广州"); // 姓名:undefined(window.name为空)
// 严格模式:obj为null/undefined时,this绑定obj本身
function strictSayInfo() {
"use strict";
console.log(this);
}
strictSayInfo.call(null); // 输出null
strictSayInfo.call(undefined); // 输出undefined
2.5 场景 5:箭头函数(无独立 this)
调用形式 :const fn = () => { ... }(无function关键字)。 绑定规则:箭头函数没有独立的 this,其 this 继承自 "外层执行上下文的 this"(定义时的外层,非调用时)。
核心特性:
-
无法通过
apply/call/bind改变 this(绑定后仍为外层 this); -
不能作为构造函数(用 new 调用会报错);
-
没有
arguments对象(需用剩余参数...args替代)。
代码验证:
javascript
// 场景1:箭头函数作为对象方法 → this继承外层(window)
const obj = {
name: "王十二",
sayHi: () => {
console.log(this.name); // undefined(this指向window)
}
};
obj.sayHi();
// 场景2:箭头函数嵌套在对象方法中 → 继承方法的this
const obj2 = {
name: "李十三",
outer() {
const inner = () => {
console.log(this.name); // 继承outer的this,指向obj2,输出"李十三"
};
inner();
}
};
obj2.outer();
// 场景3:箭头函数无法被apply/call改变this
const arrowFn = () => {
console.log(this);
};
arrowFn.call({ name: "张十四" }); // 输出window(非严格模式),无法改变
// 场景4:箭头函数不能作为构造函数
const ArrowPerson = () => {};
new ArrowPerson(); // 报错:ArrowPerson is not a constructor
适用场景:
-
嵌套函数中需要继承外层 this(如定时器、回调函数);
-
避免普通函数中 this 绑定全局的问题(如 React 类组件的事件回调)。
2.6 场景 6:DOM 事件回调与 class 中的 this
(1)DOM 事件回调
调用形式 :dom.addEventListener('click', fn)。 绑定规则:this 绑定到 "触发事件的 DOM 元素"(即事件源)。
javascript
const btn = document.createElement("button");
btn.textContent = "点击我";
document.body.appendChild(btn);
// 普通函数 → this指向btn
btn.addEventListener("click", function() {
console.log(this); // <button>点击我</button>
});
// 坑点:箭头函数 → this继承外层(window)
btn.addEventListener("click", () => {
console.log(this); // window(无法获取btn)
});
(2)class 中的 this
绑定规则 :class 内部默认启用严格模式,方法中的 this 默认绑定到实例,但脱离实例调用时为undefined。
javascript
class User {
constructor(name) {
this.name = name;
}
sayHi() {
console.log(this.name);
}
}
const user = new User("刘十五");
user.sayHi(); // 输出"刘十五"(this指向实例)
// 坑点:方法赋值后调用 → 严格模式下this为undefined
const sayHi = user.sayHi;
sayHi(); // 报错:Cannot read properties of undefined (reading 'name')
// 解决方案:在constructor中绑定this
class UserWithBind {
constructor(name) {
this.name = name;
// 绑定this到实例
this.sayHi = this.sayHi.bind(this);
}
sayHi() {
console.log(this.name);
}
}
const user2 = new UserWithBind("陈十六");
const sayHi2 = user2.sayHi;
sayHi2(); // 输出"陈十六"(this已绑定)
三、避坑指南:8 个高频 this 指向错误及解决方案
掌握规则后,还要能识别实际开发中的 "隐形陷阱",以下是 8 个最容易踩的坑及解决方法。
3.1 坑 1:对象方法赋值给变量后调用,this 指向错误
错误代码:
javascript
const obj = {
name: "赵十七",
sayHi() {
console.log(this.name);
}
};
const hi = obj.sayHi;
hi(); // undefined(this指向window)
解决方案:
-
直接通过对象调用:
obj.sayHi(); -
用 bind 绑定 this:
const hi = obj.sayHi.bind(obj); hi(); -
用箭头函数包裹:
const hi = () => obj.sayHi(); hi()。
3.2 坑 2:定时器回调中 this 指向全局
错误代码:
javascript
const obj = {
name: "孙十八",
delaySayHi() {
setTimeout(function() {
console.log(this.name); // undefined(this指向window)
}, 1000);
}
};
obj.delaySayHi();
解决方案:
-
用箭头函数(继承外层 this):
javascriptsetTimeout(() => { console.log(this.name); // 输出"孙十八" }, 1000); -
保存 this 到变量:
javascriptconst self = this; setTimeout(function() { console.log(self.name); // 输出"孙十八" }, 1000);
3.3 坑 3:数组 forEach/map 中的 this 指向错误
错误代码:
javascript
const obj = {
prefix: "编号:",
processArr(arr) {
return arr.map(function(item) {
return this.prefix + item; // undefined(this指向window)
});
}
};
obj.processArr([1, 2, 3]); // ["undefined1", "undefined2", "undefined3"]
解决方案:
-
用箭头函数:
javascriptarr.map(item => this.prefix + item); -
传 this 作为 forEach/map 的第二个参数:
javascriptarr.map(function(item) { return this.prefix + item; }, this); // 第二个参数绑定this
3.4 坑 4:class 方法作为事件回调,this 为 undefined
错误代码:
javascript
class Button {
constructor() {
this.text = "点击";
this.btn = document.createElement("button");
this.btn.textContent = this.text;
this.btn.addEventListener("click", this.handleClick);
}
handleClick() {
console.log(this.text); // undefined(this为undefined,严格模式)
}
}
new Button();
解决方案:
-
constructor 中 bind 绑定:
javascriptconstructor() { // ... this.handleClick = this.handleClick.bind(this); } -
用箭头函数作为回调:
javascriptthis.btn.addEventListener("click", (e) => this.handleClick(e));
3.5 坑 5:箭头函数作为对象方法,this 指向错误
错误代码:
javascript
const obj = {
name: "周十九",
sayHi: () => {
console.log(this.name); // undefined(this指向window)
}
};
obj.sayHi();
解决方案:
-
放弃箭头函数,用普通函数作为对象方法:
javascriptsayHi() { console.log(this.name); // 输出"周十九" }
3.6 坑 6:构造函数返回对象,this 被覆盖
错误代码:
javascript
function Product(name) {
this.name = name;
// 错误:返回对象覆盖this
return { name: "默认商品" };
}
const phone = new Product("手机");
console.log(phone.name); // 输出"默认商品"
解决方案:
-
不返回对象,或返回 this:
javascriptfunction Product(name) { this.name = name; return this; // 或不写return } -
若需返回额外数据,挂载到 this 上:
javascriptfunction Product(name) { this.name = name; this.extra = { price: 999 }; // 额外数据挂载到this }
3.7 坑 7:bind 多次绑定,只有第一次生效
错误代码:
javascript
function fn() {
console.log(this.name);
}
const obj1 = { name: "吴二十" };
const obj2 = { name: "郑二十一" };
// 多次bind,只有第一次生效
const bound1 = fn.bind(obj1);
const bound2 = bound1.bind(obj2);
bound2(); // 输出"吴二十"(obj2绑定无效)
解决方案:
- 避免多次 bind,如需动态改变 this,用 apply/call(而非 bind)。
3.8 坑 8:严格模式与非严格模式混用,this 指向混乱
错误代码:
javascript
// 外层非严格模式
function outer() {
"use strict"; // 内层严格模式
function inner() {
console.log(this); // undefined(严格模式)
}
inner();
}
outer();
解决方案:
-
项目中统一严格模式(推荐),在入口文件或模块顶部添加
"use strict"; -
避免函数内部局部启用严格模式,导致 this 行为不一致。
四、总结:this 指向的判断流程(万能公式)
遇到任何 this 指向问题,都可以按以下步骤判断,准确率 100%:
-
函数是否为箭头函数? → 是:this 继承外层执行上下文的 this(直接找外层 this); → 否:进入下一步。
-
函数是否用 new 调用? → 是:this 指向新创建的实例; → 否:进入下一步。
-
函数是否用 apply/call/bind 绑定? → 是:this 指向绑定的对象(obj 为 null/undefined 时,非严格模式绑全局,严格模式绑 obj); → 否:进入下一步。
-
函数是否作为对象方法调用(obj.fn ())? → 是:this 指向调用方法的对象(obj); → 否:进入下一步。
-
是否为严格模式(普通调用)? → 是:this 绑定到 undefined; → 否:this 绑定到全局对象(window/global)。
五、最后:this 的设计意义
为什么 JavaScript 要设计 this?本质是为了 "代码复用"------ 让函数可以在不同对象上调用,无需为每个对象重复定义相同逻辑。
比如一个sayHi函数,通过 this 可以在不同用户对象上复用,输出不同用户名:
javascript
function sayHi() {
console.log(`Hi, ${this.name}`);
}
const userA = { name: "用户A", sayHi };
const userB = { name: "用户B", sayHi };
userA.sayHi(); // Hi, 用户A
userB.sayHi(); // Hi, 用户B
理解 this 的设计初衷,才能更好地运用它。希望本文能帮你告别 "this 困惑",写出逻辑清晰、无隐藏 bug 的 JavaScript 代码。总而言之,一键点赞、评论、喜欢 加收藏吧!这对我很重要!