在 JavaScript 开发中,你是不是也遇到过这些困惑:
点击按钮时,this 明明该指向按钮,却变成了 window?
setTimeout 里的回调函数,this 怎么突然找不到对象了?
明明写了 obj.fn(),函数里的 this 却不是 obj?
其实这些问题的根源,都在于没搞懂 this 的动态绑定规则——this 既不是 “谁定义就是谁”,也不是 “谁调用就是谁” 这么简单,它的指向完全取决于函数执行时的场景。今天我们就从实际开发场景出发,拆解 this 绑定的核心逻辑,帮你避开 90% 的坑。
一、先搞懂:this 的本质是什么?this 是函数执行时的上下文对象,它的作用是让函数能 “找到” 自己所属的环境。比如:
当你写 user.getName() 时,this 指向 user,函数才能拿到 user 里的 name;当你用 new Person() 时,this 指向新创建的实例,才能给实例添加属性。关键误区:this 不是在函数定义时确定的,而是在函数执行时确定的。哪怕同一个函数,执行方式不同,this 也会变。
二、日常开发中,5 种常见的 this 绑定场景我们从最常用到最冷门的场景,逐个拆解,每个场景都配 “代码例子 + 实际问题”。
场景 1:对象方法调用(隐式绑定)—— 最常用也最容易踩坑当函数作为对象的属性被调用时(比如 obj.fn()),this 会隐式绑定到这个对象上。这是开发中最常见的场景,比如操作 DOM 元素、调用实例方法。
例子:DOM 按钮点击事件javascript
运行
代码语言:javascript复制// HTML:
const btn = document.getElementById('btn');
btn.textContent = '点击获取按钮文本';
// 给按钮绑定点击事件,函数作为 btn 的方法执行
btn.onclick = function() {
console.log(this.textContent); // 输出:点击获取按钮文本
// 这里 this 指向 btn,因为函数是通过 btn.onclick 调用的
};关键细节:只有 “最后一层对象” 影响 this如果是多层对象嵌套(比如 obj1.obj2.fn()),this 只会绑定到最后一个调用函数的对象(也就是 obj2):
javascript
运行
代码语言:javascript复制const obj1 = {
name: 'obj1',
obj2: {
name: 'obj2',
sayName: function() {
console.log(this.name); // 输出:obj2
}
}
};
obj1.obj2.sayName(); // 最后调用者是 obj2,this 指向 obj2场景 2:独立函数调用(默认绑定)—— 最容易忽略的坑当函数没有通过任何对象调用时(比如直接写 fn()),就会触发默认绑定。
非严格模式下:this 绑定到全局对象(浏览器里是 window,Node.js 里是 global);严格模式下:this 是 undefined(避免全局变量污染,但容易报错)。例子:函数内部调用另一个函数javascript
运行
代码语言:javascript复制// 非严格模式
const name = '全局name';
function outer() {
const name = 'outername';
inner(); // 独立调用 inner,触发默认绑定
}
function inner() {
console.log(this.name); // 输出:全局name(this 指向 window)
}
outer();实际坑点:回调函数里的默认绑定很多人以为 setTimeout、forEach 里的回调函数会 “继承” 外层 this,其实不然 —— 回调函数是独立调用的,默认绑定全局对象:
javascript
运行
代码语言:javascript复制const user = {
name: '张三',
getInfo: function() {
// 错误写法:setTimeout 回调是独立调用,this 指向 window
setTimeout(function() {
console.log(this.name); // 输出:undefined(window 没有 name)
}, 1000);
}
};
user.getInfo();场景 3:显式绑定(call/apply/bind)—— 主动控制 this如果想手动指定 this 的指向,就用显式绑定。JavaScript 给函数提供了三个方法:call、apply、bind,它们的核心作用都是 “强制绑定 this”。
三者的区别很简单:
方法
语法
特点
call
fn.call(thisObj, a, b)
直接执行函数,参数逐个传入
apply
fn.apply(thisObj, [a,b])
直接执行函数,参数用数组传入
bind
const newFn = fn.bind(thisObj, a)
不执行函数,返回新函数(硬绑定)
例子:修复 setTimeout 的 this 问题用 bind 给回调函数显式绑定 user,就能解决默认绑定的坑:
javascript
运行
代码语言:javascript复制const user = {
name: '张三',
getInfo: function() {
// 正确写法:用 bind 绑定 this 为 user
setTimeout(function() {
console.log(this.name); // 输出:张三
}.bind(this), 1000); // this 此时是 user(因为 getInfo 是 user 的方法)
}
};
user.getInfo();实际用途:数组 forEach 的上下文参数forEach 等数组方法自带 “上下文参数”,本质就是显式绑定 this:
javascript
运行
代码语言:javascript复制const obj = { prefix: '结果:' };
const numbers = [1, 2, 3];
// forEach 第二个参数是上下文,会绑定到回调函数的 this
numbers.forEach(function(num) {
console.log(this.prefix + num); // 输出:结果:1、结果:2、结果:3
}, obj);场景 4:构造函数调用(new 绑定)—— 创建实例时的 this当用 new 关键字调用函数时(比如 new Person()),这个函数就变成了 “构造函数”,this 会绑定到新创建的实例对象上。
例子:创建用户实例javascript
运行
代码语言:javascript复制function Person(name) {
// new 调用时,this 指向新实例(比如下面的 zhangsan)
this.name = name;
this.sayHi = function() {
console.log('你好,我是' + this.name);
};
}
// new 绑定:this 指向 zhangsan 实例
const zhangsan = new Person('张三');
zhangsan.sayHi(); // 输出:你好,我是张三关键细节:new 做了什么?new 关键字会隐式执行 4 步操作,这也是 this 绑定到实例的原因:
创建一个全新的空对象;让这个空对象继承构造函数的原型(__proto__ 指向 Person.prototype);把构造函数的 this 绑定到这个空对象上;如果构造函数没有返回其他对象,就返回这个新对象。场景 5:间接调用(逗号 / 赋值表达式)—— 冷门但容易踩的坑当函数通过 “赋值表达式” 或 “逗号操作符” 调用时,会触发间接调用,本质是 “函数引用被剥离对象”,最终变成独立调用,this 指向全局。
比如这两种写法,你可能在老项目里见过:
javascript
运行
代码语言:javascript复制const name = '全局name';
const obj = {
name: 'objname',
sayName: function() {
console.log(this.name);
}
};
// 1. 赋值表达式:obj.sayName 赋值给 fn,然后调用 fn()
const fn = obj.sayName;
fn(); // 输出:全局name(独立调用)
// 2. 逗号操作符:(0, obj.sayName) 返回函数本身,然后调用
(0, obj.sayName)(); // 输出:全局name(独立调用)为什么会这样?因为 “赋值表达式” 和 “逗号操作符” 会返回 “函数本身”,而不是 “对象的方法引用”。比如 obj.sayName 本身是一个函数,赋值给 fn 后,fn 就和 obj 没关系了,调用时自然是独立调用。
三、this 丢失的 3 个高频坑,以及避坑方案前面的场景里其实已经提到了 this 丢失的问题,这里集中总结 3 个最常见的坑,以及对应的解决方案。
坑 1:回调函数里的 this 丢失(setTimeout/forEach)问题:回调函数独立调用,this 指向全局或 undefined。
解决方案:
方案 1:用 bind 显式绑定 this;方案 2:用箭头函数(继承外层 this,后面讲);方案 3:保存 this 到变量(老写法,比如 const that = this)。javascript
运行
代码语言:javascript复制const user = {
name: '张三',
getInfo: function() {
// 方案3:老写法,保存 this 到 that
const that = this;
setTimeout(function() {
console.log(that.name); // 输出:张三
}, 1000);
}
};坑 2:函数作为参数传递时丢失 this问题:把对象方法作为参数传给其他函数,调用时会剥离对象,变成独立调用。
解决方案:传递时用 bind 绑定 this。
javascript
运行
代码语言:javascript复制const obj = {
value: 10,
double: function() {
return this.value * 2;
}
};
// 问题:obj.double 作为参数传递,调用时 this 丢失
function calculate(fn) {
return fn(); // 独立调用,this 指向全局
}
calculate(obj.double); // 输出:NaN(全局没有 value)
// 解决方案:传递时用 bind 绑定 this
calculate(obj.double.bind(obj)); // 输出:20坑 3:间接引用导致的 this 丢失问题:通过赋值、逗号操作符等间接引用函数,调用时变成独立调用。
解决方案:直接通过对象调用,或用 bind 绑定。
javascript
运行
代码语言:javascript复制const obj = {
name: 'objname',
sayName: function() {
console.log(this.name);
}
};
// 问题:间接引用
const indirectFn = obj.sayName;
indirectFn(); // 输出:全局name
// 解决方案1:直接通过对象调用
obj.sayName(); // 输出:objname
// 解决方案2:bind 绑定
const boundFn = obj.sayName.bind(obj);
boundFn(); // 输出:objname四、绑定优先级:谁的权力更大?如果一个函数同时满足多种绑定场景(比如 new + bind),this 会听谁的?
这里给大家一个明确的优先级顺序(从高到低):
new 绑定 > 显式绑定(bind)> 隐式绑定 > 默认绑定
我们用两个对比例子验证:
对比 1:new 绑定 vs 显式绑定(bind)new 的优先级更高,哪怕用 bind 硬绑定了 this,new 依然会把 this 指向新实例:
javascript
运行
代码语言:javascript复制function Person(name) {
this.name = name;
}
const obj = { name: 'objname' };
// 用 bind 绑定 this 为 obj
const BoundPerson = Person.bind(obj);
// new 调用 BoundPerson:this 指向新实例,不是 obj
const person = new BoundPerson('张三');
console.log(person.name); // 输出:张三(new 优先级更高)
console.log(obj.name); // 输出:objname(obj 没被修改)对比 2:显式绑定 vs 隐式绑定bind 等显式绑定的优先级高于隐式绑定:
javascript
运行
代码语言:javascript复制const obj1 = { name: 'obj1', sayName: function() { console.log(this.name); } };
const obj2 = { name: 'obj2' };
// 隐式绑定:this 指向 obj1
obj1.sayName(); // 输出:obj1
// 显式绑定:用 call 把 this 改成 obj2,优先级更高
obj1.sayName.call(obj2); // 输出:obj2五、特殊情况:箭头函数的 thisES6 新增的箭头函数,完全不遵循上面的所有规则 —— 它的 this 是静态的,在函数定义时就确定了,永远继承自 “外层词法上下文” 的 this(简单说就是 “外层函数或全局的 this”)。
箭头函数的核心特点:没有自己的 this,继承外层 this;不能用 new 调用(会报错,因为没有 this 可以绑定到新实例);不能用 call/apply/bind 修改 this(修改无效)。例子:用箭头函数修复回调函数 this 问题javascript
运行
代码语言:javascript复制const user = {
name: '张三',
getInfo: function() {
// 箭头函数继承外层 this(getInfo 的 this,即 user)
setTimeout(() => {
console.log(this.name); // 输出:张三
}, 1000);
}
};
user.getInfo();注意:箭头函数不要滥用比如对象的方法不能用箭头函数,否则 this 会继承全局对象,导致错误:
javascript
运行
代码语言:javascript复制// 错误写法:对象方法用箭头函数,this 继承全局
const obj = {
name: 'objname',
sayName: () => {
console.log(this.name); // 输出:全局name(非严格模式)
}
};
obj.sayName();六、严格模式下的 this 差异之前提到过,严格模式('use strict')会改变默认绑定的 this:
非严格模式:独立函数调用,this 指向全局对象;严格模式:独立函数调用,this 是 undefined。例子:严格模式下的默认绑定javascript
运行
代码语言:javascript复制'use strict'; // 开启严格模式
function fn() {
console.log(this); // 输出:undefined(不是 window)
}
fn(); // 独立调用,this 是 undefined
// 坑点:如果访问 this.name,会报错(Cannot read property 'name' of undefined)七、判断 this 的 3 步口诀最后给大家一个简单的口诀,遇到 this 问题时,按这个顺序判断,保证不会错:
看函数是不是箭头函数?
是 → this 继承外层词法上下文的 this;
否 → 走下一步。看函数是不是用 new 调用?
是 → this 指向新创建的实例;
否 → 走下一步。看函数是不是显式绑定(call/apply/bind)?
是 → this 指向显式指定的对象;
否 → 走下一步。看函数是不是对象方法调用(隐式绑定)?
是 → this 指向调用函数的对象;
否 → 走下一步。默认绑定:
非严格模式 → this 指向全局对象(window/global);
严格模式 → this 是 undefined。总结this 绑定的核心不是 “谁调用就是谁”,而是 “执行场景决定指向”。日常开发中,最容易踩坑的是 “回调函数 this 丢失” 和 “间接调用”,记住用 bind 或箭头函数可以解决大部分问题。
如果记不住所有规则,就用最后的 “3 步口诀” 判断 —— 先看箭头函数,再看 new,再看显式绑定,最后看隐式绑定,剩下的就是默认绑定。多写几个例子测试,很快就能熟练掌握!
参考书籍:《你不知道的 JavaScript(上卷)》