在 JavaScript 开发中,你是不是也遇到过这些困惑:
点击按钮时,this
明明该指向按钮,却变成了 window
?
setTimeout
里的回调函数,this
怎么突然找不到对象了?
明明写了 obj.fn()
,函数里的 this
却不是 obj
?
其实这些问题的根源,都在于没搞懂 this
的动态绑定规则——this
既不是 “谁定义就是谁”,也不是 “谁调用就是谁” 这么简单,它的指向完全取决于函数执行时的场景。今天我们就从实际开发场景出发,拆解 this
绑定的核心逻辑,帮你避开 90% 的坑。
this
是函数执行时的上下文对象,它的作用是让函数能 “找到” 自己所属的环境。比如:
user.getName()
时,this
指向 user
,函数才能拿到 user
里的 name
;new Person()
时,this
指向新创建的实例,才能给实例添加属性。关键误区:this
不是在函数定义时确定的,而是在函数执行时确定的。哪怕同一个函数,执行方式不同,this
也会变。
我们从最常用到最冷门的场景,逐个拆解,每个场景都配 “代码例子 + 实际问题”。
当函数作为对象的属性被调用时(比如 obj.fn()
),this
会隐式绑定到这个对象上。这是开发中最常见的场景,比如操作 DOM 元素、调用实例方法。
javascript
运行
// HTML:<button id="btn">点击</button>
const btn = document.getElementById('btn');
btn.textContent = '点击获取按钮文本';
// 给按钮绑定点击事件,函数作为 btn 的方法执行
btn.onclick = function() {
console.log(this.textContent); // 输出:点击获取按钮文本
// 这里 this 指向 btn,因为函数是通过 btn.onclick 调用的
};
如果是多层对象嵌套(比如 obj1.obj2.fn()
),this
只会绑定到最后一个调用函数的对象(也就是 obj2
):
javascript
运行
const obj1 = {
name: 'obj1',
obj2: {
name: 'obj2',
sayName: function() {
console.log(this.name); // 输出:obj2
}
}
};
obj1.obj2.sayName(); // 最后调用者是 obj2,this 指向 obj2
当函数没有通过任何对象调用时(比如直接写 fn()
),就会触发默认绑定。
this
绑定到全局对象(浏览器里是 window
,Node.js 里是 global
);this
是 undefined
(避免全局变量污染,但容易报错)。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
运行
const user = {
name: '张三',
getInfo: function() {
// 错误写法:setTimeout 回调是独立调用,this 指向 window
setTimeout(function() {
console.log(this.name); // 输出:undefined(window 没有 name)
}, 1000);
}
};
user.getInfo();
如果想手动指定 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) | 不执行函数,返回新函数(硬绑定) |
用 bind
给回调函数显式绑定 user
,就能解决默认绑定的坑:
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
等数组方法自带 “上下文参数”,本质就是显式绑定 this
:
javascript
运行
const obj = { prefix: '结果:' };
const numbers = [1, 2, 3];
// forEach 第二个参数是上下文,会绑定到回调函数的 this
numbers.forEach(function(num) {
console.log(this.prefix + num); // 输出:结果:1、结果:2、结果:3
}, obj);
当用 new
关键字调用函数时(比如 new Person()
),这个函数就变成了 “构造函数”,this
会绑定到新创建的实例对象上。
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
关键字会隐式执行 4 步操作,这也是 this
绑定到实例的原因:
__proto__
指向 Person.prototype
);this
绑定到这个空对象上;当函数通过 “赋值表达式” 或 “逗号操作符” 调用时,会触发间接调用,本质是 “函数引用被剥离对象”,最终变成独立调用,this
指向全局。
比如这两种写法,你可能在老项目里见过:
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
指向全局或 undefined
。
解决方案:
bind
显式绑定 this
;this
,后面讲);this
到变量(老写法,比如 const that = this
)。javascript
运行
const user = {
name: '张三',
getInfo: function() {
// 方案3:老写法,保存 this 到 that
const that = this;
setTimeout(function() {
console.log(that.name); // 输出:张三
}, 1000);
}
};
问题:把对象方法作为参数传给其他函数,调用时会剥离对象,变成独立调用。
解决方案:传递时用 bind
绑定 this
。
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
问题:通过赋值、逗号操作符等间接引用函数,调用时变成独立调用。
解决方案:直接通过对象调用,或用 bind
绑定。
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)> 隐式绑定 > 默认绑定
我们用两个对比例子验证:
new
的优先级更高,哪怕用 bind
硬绑定了 this
,new
依然会把 this
指向新实例:
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 没被修改)
bind
等显式绑定的优先级高于隐式绑定:
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
ES6 新增的箭头函数,完全不遵循上面的所有规则 —— 它的 this
是静态的,在函数定义时就确定了,永远继承自 “外层词法上下文” 的 this
(简单说就是 “外层函数或全局的 this”)。
this
,继承外层 this
;new
调用(会报错,因为没有 this
可以绑定到新实例);call/apply/bind
修改 this
(修改无效)。javascript
运行
const user = {
name: '张三',
getInfo: function() {
// 箭头函数继承外层 this(getInfo 的 this,即 user)
setTimeout(() => {
console.log(this.name); // 输出:张三
}, 1000);
}
};
user.getInfo();
比如对象的方法不能用箭头函数,否则 this
会继承全局对象,导致错误:
javascript
运行
// 错误写法:对象方法用箭头函数,this 继承全局
const obj = {
name: 'objname',
sayName: () => {
console.log(this.name); // 输出:全局name(非严格模式)
}
};
obj.sayName();
之前提到过,严格模式('use strict'
)会改变默认绑定的 this
:
this
指向全局对象;this
是 undefined
。javascript
运行
'use strict'; // 开启严格模式
function fn() {
console.log(this); // 输出:undefined(不是 window)
}
fn(); // 独立调用,this 是 undefined
// 坑点:如果访问 this.name,会报错(Cannot read property 'name' of undefined)
最后给大家一个简单的口诀,遇到 this
问题时,按这个顺序判断,保证不会错:
this
继承外层词法上下文的 this
;
否 → 走下一步。new
调用?
是 → this
指向新创建的实例;
否 → 走下一步。this
指向显式指定的对象;
否 → 走下一步。this
指向调用函数的对象;
否 → 走下一步。this
指向全局对象(window/global);
严格模式 → this
是 undefined
。this
绑定的核心不是 “谁调用就是谁”,而是 “执行场景决定指向”。日常开发中,最容易踩坑的是 “回调函数 this 丢失” 和 “间接调用”,记住用 bind
或箭头函数可以解决大部分问题。
如果记不住所有规则,就用最后的 “3 步口诀” 判断 —— 先看箭头函数,再看 new,再看显式绑定,最后看隐式绑定,剩下的就是默认绑定。多写几个例子测试,很快就能熟练掌握!
参考书籍:《你不知道的 JavaScript(上卷)》
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。