首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >搞懂 JavaScript 的 this 绑定:从踩坑到举一反三

搞懂 JavaScript 的 this 绑定:从踩坑到举一反三

原创
作者头像
用户11757497
发布2025-09-08 14:47:29
发布2025-09-08 14:47:29
18900
代码可运行
举报
运行总次数:0
代码可运行

在 JavaScript 开发中,你是不是也遇到过这些困惑: 点击按钮时,this 明明该指向按钮,却变成了 windowsetTimeout 里的回调函数,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
代码运行次数:0
运行
复制
// HTML:<button id="btn">点击</button>
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
代码运行次数:0
运行
复制
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);
  • 严格模式下:thisundefined(避免全局变量污染,但容易报错)。
例子:函数内部调用另一个函数

javascript

运行

代码语言:javascript
代码运行次数:0
运行
复制
// 非严格模式
const name = '全局name';
function outer() {
  const name = 'outername';
  inner(); // 独立调用 inner,触发默认绑定
}

function inner() {
  console.log(this.name); // 输出:全局name(this 指向 window)
}
outer();
实际坑点:回调函数里的默认绑定

很多人以为 setTimeoutforEach 里的回调函数会 “继承” 外层 this,其实不然 —— 回调函数是独立调用的,默认绑定全局对象:

javascript

运行

代码语言:javascript
代码运行次数:0
运行
复制
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 给函数提供了三个方法:callapplybind,它们的核心作用都是 “强制绑定 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
代码运行次数:0
运行
复制
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
代码运行次数:0
运行
复制
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
代码运行次数:0
运行
复制
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 绑定到实例的原因:

  1. 创建一个全新的空对象;
  2. 让这个空对象继承构造函数的原型(__proto__ 指向 Person.prototype);
  3. 把构造函数的 this 绑定到这个空对象上;
  4. 如果构造函数没有返回其他对象,就返回这个新对象。

场景 5:间接调用(逗号 / 赋值表达式)—— 冷门但容易踩的坑

当函数通过 “赋值表达式” 或 “逗号操作符” 调用时,会触发间接调用,本质是 “函数引用被剥离对象”,最终变成独立调用,this 指向全局。

比如这两种写法,你可能在老项目里见过:

javascript

运行

代码语言:javascript
代码运行次数:0
运行
复制
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
代码运行次数:0
运行
复制
const user = {
  name: '张三',
  getInfo: function() {
    // 方案3:老写法,保存 this 到 that
    const that = this;
    setTimeout(function() {
      console.log(that.name); // 输出:张三
    }, 1000);
  }
};

坑 2:函数作为参数传递时丢失 this

问题:把对象方法作为参数传给其他函数,调用时会剥离对象,变成独立调用。 解决方案:传递时用 bind 绑定 this

javascript

运行

代码语言:javascript
代码运行次数:0
运行
复制
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
代码运行次数:0
运行
复制
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 硬绑定了 thisnew 依然会把 this 指向新实例:

javascript

运行

代码语言:javascript
代码运行次数:0
运行
复制
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
代码运行次数:0
运行
复制
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

五、特殊情况:箭头函数的 this

ES6 新增的箭头函数,完全不遵循上面的所有规则 —— 它的 this静态的,在函数定义时就确定了,永远继承自 “外层词法上下文” 的 this(简单说就是 “外层函数或全局的 this”)。

箭头函数的核心特点:

  1. 没有自己的 this,继承外层 this
  2. 不能用 new 调用(会报错,因为没有 this 可以绑定到新实例);
  3. 不能用 call/apply/bind 修改 this(修改无效)。
例子:用箭头函数修复回调函数 this 问题

javascript

运行

代码语言:javascript
代码运行次数:0
运行
复制
const user = {
  name: '张三',
  getInfo: function() {
    // 箭头函数继承外层 this(getInfo 的 this,即 user)
    setTimeout(() => {
      console.log(this.name); // 输出:张三
    }, 1000);
  }
};
user.getInfo();
注意:箭头函数不要滥用

比如对象的方法不能用箭头函数,否则 this 会继承全局对象,导致错误:

javascript

运行

代码语言:javascript
代码运行次数:0
运行
复制
// 错误写法:对象方法用箭头函数,this 继承全局
const obj = {
  name: 'objname',
  sayName: () => {
    console.log(this.name); // 输出:全局name(非严格模式)
  }
};
obj.sayName();

六、严格模式下的 this 差异

之前提到过,严格模式('use strict')会改变默认绑定的 this

  • 非严格模式:独立函数调用,this 指向全局对象;
  • 严格模式:独立函数调用,thisundefined
例子:严格模式下的默认绑定

javascript

运行

代码语言:javascript
代码运行次数:0
运行
复制
'use strict'; // 开启严格模式

function fn() {
  console.log(this); // 输出:undefined(不是 window)
}

fn(); // 独立调用,this 是 undefined

// 坑点:如果访问 this.name,会报错(Cannot read property 'name' of undefined)

七、判断 this 的 3 步口诀

最后给大家一个简单的口诀,遇到 this 问题时,按这个顺序判断,保证不会错:

  1. 看函数是不是箭头函数? 是 → this 继承外层词法上下文的 this; 否 → 走下一步。
  2. 看函数是不是用 new 调用? 是 → this 指向新创建的实例; 否 → 走下一步。
  3. 看函数是不是显式绑定(call/apply/bind)? 是 → this 指向显式指定的对象; 否 → 走下一步。
  4. 看函数是不是对象方法调用(隐式绑定)? 是 → this 指向调用函数的对象; 否 → 走下一步。
  5. 默认绑定: 非严格模式 → this 指向全局对象(window/global); 严格模式 → thisundefined

总结

this 绑定的核心不是 “谁调用就是谁”,而是 “执行场景决定指向”。日常开发中,最容易踩坑的是 “回调函数 this 丢失” 和 “间接调用”,记住用 bind 或箭头函数可以解决大部分问题。

如果记不住所有规则,就用最后的 “3 步口诀” 判断 —— 先看箭头函数,再看 new,再看显式绑定,最后看隐式绑定,剩下的就是默认绑定。多写几个例子测试,很快就能熟练掌握!

参考书籍:《你不知道的 JavaScript(上卷)》

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、先搞懂:this 的本质是什么?
  • 二、日常开发中,5 种常见的 this 绑定场景
    • 场景 1:对象方法调用(隐式绑定)—— 最常用也最容易踩坑
      • 例子:DOM 按钮点击事件
      • 关键细节:只有 “最后一层对象” 影响 this
    • 场景 2:独立函数调用(默认绑定)—— 最容易忽略的坑
      • 例子:函数内部调用另一个函数
      • 实际坑点:回调函数里的默认绑定
    • 场景 3:显式绑定(call/apply/bind)—— 主动控制 this
      • 例子:修复 setTimeout 的 this 问题
      • 实际用途:数组 forEach 的上下文参数
    • 场景 4:构造函数调用(new 绑定)—— 创建实例时的 this
      • 例子:创建用户实例
      • 关键细节:new 做了什么?
    • 场景 5:间接调用(逗号 / 赋值表达式)—— 冷门但容易踩的坑
      • 为什么会这样?
  • 三、this 丢失的 3 个高频坑,以及避坑方案
    • 坑 1:回调函数里的 this 丢失(setTimeout/forEach)
    • 坑 2:函数作为参数传递时丢失 this
    • 坑 3:间接引用导致的 this 丢失
  • 四、绑定优先级:谁的权力更大?
    • 对比 1:new 绑定 vs 显式绑定(bind)
    • 对比 2:显式绑定 vs 隐式绑定
  • 五、特殊情况:箭头函数的 this
    • 箭头函数的核心特点:
      • 例子:用箭头函数修复回调函数 this 问题
      • 注意:箭头函数不要滥用
  • 六、严格模式下的 this 差异
    • 例子:严格模式下的默认绑定
  • 七、判断 this 的 3 步口诀
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档