☰
前端必会:this绑定规则与call/apply显式绑定详解
2026/10/14 18:14:11 网站建设 项目流程

如果你在前端岗位待过超过半年,一定经历过那些和this有关的"至暗时刻":一个函数单独调用时工作得好好的,塞进对象方法里就翻车;事件回调里 console.log(this) 发现它指向的不是你以为的那个对象;面试官轻描淡写地甩过来一段代码问你"输出什么",你盯着屏幕,大脑一片空白。

这个人人都会说"谁调用指向谁"的this,真正用起来却像极了玄学。我见过不少工作两三年的人,能熟练写业务,却答不上来一个简单的obj.method()与const fn = obj.method的区别。原因很简单——this的知识点之间牵扯太多,靠死记硬背真的容易被绕晕。所以今天我打算用"思维脑图 + 代码验证"的方式,好好地把this、call、apply这件事彻底梳理清楚。这篇是系列第一篇,先把最核心的绑定规则和显式绑定的两个基础方法讲透;后续再聊bind、箭头函数,以及框架源码里的各种this处理技巧。

这篇文章适合谁?零基础想搞懂this原理的新手、准备面试前端岗位的求职者、写代码遇到this漂移问题想系统排查的开发者。我会把结论、代码、排查技巧全部放在同一篇里,你甚至可以当工具文收藏。

1. 先搞清楚this到底是个啥

1.1 它的本质是"执行上下文",不是定义上下文

很多教科书会把this解释成"当前对象",这个说法没错,但不完整。在我看来,最直观的理解方式是:this是在函数被调用时动态确定的一个绑定值,它指向什么,取决于函数是怎么被调用的,而不是函数在哪里被定义。

我常用一个生活化的类比来解释:this就像口语里的"我"字。张三说"我饿了","我"指张三;李四说"我饿了","我"指李四。同一个"我"字在不同人嘴里含义不同。同理,同一段函数代码,在不同调用方式下,this指向会变。

从语言实现角度看,函数在执行时会创建一个执行上下文,这个上下文里包含this绑定。调用方式变了,执行上下文里的this绑定就变了。这就是为什么你在函数内部打印this,看到的结果和你拍脑袋想的完全不同的根源。

1.2 四条绑定规则:所有this问题的总纲

this的确定有一套通用规则,业内习惯归纳成下面四条,按使用场景划分:

  • 默认绑定:函数以普通独立调用的方式执行,非严格模式下this指向全局对象,严格模式下指向undefined。
  • 隐式绑定:函数作为某个对象的方法被调用,即obj.fn()这种形式,this指向该对象。
  • 显式绑定:通过call、apply、bind人为指定函数执行时的this指向。
  • new 绑定:函数用new关键字以构造方式调用,this指向新创建出来的实例对象。

这四条规则是你遇到任何this问题时先要对照的方向。我见过很多人在分析代码时一上来就看"定义在哪",这是误区。先看"怎么被调用",再去对照规则,基本不会跑偏。

1.3 优先级顺序:真正让this变难的地方

四条规则同时存在,就意味着它们之间可能出现冲突。比如一个用new调用的函数又用了call,到底听谁的?

答案是:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定。

直接背优先级可能有点抽象,我实际测给你看:

function sayName() { console.log(this.name); } const objA = { name: "对象A" }; const objB = { name: "对象B" }; objA.say = sayName; objA.say(); // 隐式绑定 -> 对象A objA.say.call(objB); // 隐式 + 显式 -> 显式优先 -> 对象B

看到没?本来用objA.say()调用时this应该指向objA,但叠加了call(objB)之后,输出变成了"对象B"。这就是"显式绑定优先于隐式绑定"的体现。

同样的逻辑,new的优先级比显式绑定还高,所以就算你对一个构造函数做了fn.call(某个对象),用new调用时this依然指向新创建的实例。

提示:分析this的时候,先看是不是new调用的,再看有没有call/apply/bind,接着看是不是obj.method()的形式,最后才落到默认绑定。这个排查顺序基本能覆盖所有情况。

2. 思维脑图:一张图编织this知识框架

2.1 为什么思维脑图特别适合this这个知识点

this的知识结构带有明显的层级关系和分支特例,比如"默认绑定"下面有"严格模式"和"非严格模式"两个分支;"隐式绑定"下面有"对象方法调用"和"方法取出后再调用"两个结果完全不同的场景。如果把这些知识点平铺在一页笔记里,很容易遗漏细节。

思维脑图的优势在于,它能用层级结构天然地展示"主规则 → 子场景 → 例外情况"这样的关系。你一眼就能看到某一类问题的完整知识树,而不是零散的一堆结论。

我通常建议学习者把一条主线画出来:根节点是this,一级分支是四条绑定规则加两个特例(箭头函数、全局/严格模式),二级分支是每个规则下的具体场景、代码模式、典型坑位。这样画完之后,知识不再是散点,而是一张经纬分明的网。

2.2 我建的脑图主干结构参考

如果你不想从零开始梳理,可以直接参考我常用的结构,先照抄再自己扩展:

this 指向(根节点) ├─ 1. 默认绑定 │ ├─ 非严格模式 -> 全局对象(浏览器中是 window) │ ├─ 严格模式 -> undefined │ └─ 典型场景:独立函数调用、回调函数 ├─ 2. 隐式绑定 │ ├─ 对象方法调用 -> 指向对象本身 │ ├─ 方法取出单独调用 -> this 丢失 │ ├─ 定时器回调 -> this 丢失 │ └─ 事件回调 -> this 指向绑定事件的元素 ├─ 3. 显式绑定 │ ├─ call(thisArg, arg1, arg2) │ ├─ apply(thisArg, [argsArray]) │ └─ bind(thisArg)(下一篇文章展开) ├─ 4. new 绑定 │ └─ 构造调用 -> 新实例对象 ├─ 5. 箭头函数 │ ├─ 不绑定自身 this │ └─ 继承外层词法作用域的 this └─ 6. 优先级 ├─ new > 显式 > 隐式 > 默认 └─ 箭头函数例外:call/apply 无法改变其 this

这里我故意把"箭头函数"和"优先级"也放到脑图里,是因为它们在分析题目时会反复用到。第一篇文章里我先把前四块讲透,箭头函数与bind放到系列后面重点展开,但脑图可以先拉出框架。

2.3 脑图与代码结合的实操方法

脑图不是画完就完了,关键是要让它"活"起来。我的做法是这样的:脑图里出现的每条结论,都在编辑器里跑一遍最小示例。

比如脑图里写"隐式绑定下,方法取出后再调用,this丢失",我就立刻写一段代码验证:

const user = { name: "张三", greet: function () { console.log(`你好,我是${this.name}`); }, }; user.greet(); // 你好,我是张三 const fn = user.greet; fn(); // 你好,我是undefined(this 不再指向 user)

这种验证动作看起来简单,但作用非常大。当你亲眼在控制台看到this的变化,记忆深度远超过看文档。我给这条方法起了个名字:专项验证。脑图提供方向,代码确认细节,两者互相配合,知识网才会真的建立起来。

2.4 关于工具选型的一句话建议

你不需要在脑图工具上纠结太久。手绘白板、系统自带画图、在线思维导图工具都可以,只要能画出层级关系就行。我在整理初期甚至用过文本缩进加符号的方式在编辑器里画,效果完全够用。

重点是"画的过程",而不是"画的工具"。你在往纸上写"默认绑定 → 严格模式 → undefined"这个分支时,大脑已经在做一次主动理解和归类,这比直接看别人总结好的笔记高效得多。

3. call 和 apply:this的显式开关

3.1 语法对比与参数传递方式

先看两个方法的基本语法和唯一区别:

function introduce(age, city) { console.log(`${this.name},${age}岁,来自${city}`); } const person = { name: "李四" }; introduce.call(person, 25, "杭州"); introduce.apply(person, [25, "杭州"]);

call和apply的作用完全一致:立即调用函数,同时把this绑定到第一个参数指定的对象上。区别只在后续参数的传递方式——call是一个个平铺着传,apply则把所有参数放进一个数组。

选择哪个用哪个?我的经验是:参数个数固定、明确时,用call看着更直观;参数本来就是数组或类数组时,用apply省得手动展开。现在 ES6 普及后,很多人会习惯写fn.call(obj, ...args)来替代apply,也完全可行,效果一样。

3.2 高频场景:借用方法

call和apply最经典的使用场景,就是让一个对象去借用另一个对象里的方法。现实里你可以类比成"借工具":你自己没带剪刀,但邻居有,你请求一句"借我用一下",对方同意后你就能用这把剪刀剪东西。

代码层面也是这个逻辑:

const cat = { name: "喵喵", speak: function () { console.log(`${this.name} 在说话`); }, }; const dog = { name: "旺财" }; cat.speak(); // 喵喵 在说话 cat.speak.call(dog); // 旺财 在说话

这里dog自己没有speak方法,但不妨碍通过call借用cat的方法来执行,并且this明确指向dog。这种写法在代码复用、工具函数封装里特别常见。

3.3 伪数组转真数组:现场最常用的一招

日常业务里apply出现频率最高的场景之一,就是把函数的arguments转成真正意义上的数组。老前端看到下面这行代码,应该会心一笑:

function sum() { const args = Array.prototype.slice.call(arguments); return args.reduce((acc, cur) => acc + cur, 0); } console.log(sum(1, 2, 3, 4)); // 10

这行Array.prototype.slice.call(arguments)的思路很巧妙:arguments是类数组对象,本身没有数组的slice方法,于是我们借来数组原型上的slice,让它作用于arguments,从而得到一个新数组。

按现在的标准,你完全可以用Array.from(arguments)或者[...arguments]来实现同样的效果,写起来更简洁。但在老代码维护、面试原理问答里,理解slice.call(arguments)依旧有价值,它背后体现的就是"借用方法 + 显式绑定"两个知识点的组合运用。

3.4 参数为 null 时需要注意的差异

call和apply的第一个参数如果传的是null或undefined,在非严格模式下会被忽略,函数里的this依然指向全局对象;在严格模式下则会是null或undefined本身。

function showThis() { console.log(this); } showThis.call(null); // 非严格模式 -> 全局对象;严格模式 -> null

这个细节在Math.max.apply(null, arr)这类写法中经常被忽略。我们只是借apply来展开数组,并不打算指定this,所以传null只是"占个位"。理解这个机制之后,你看到这类代码就不会觉得奇怪。

3.5 提取方法时this也会跟着"复制"吗

很多人以为const fn = obj.method; fn.call(obj)与obj.method()完全等价,其实核心逻辑是一样的——都靠显式或隐式的方式把this绑定到obj上。不过有一个小坑要注意:如果你先写了const fn = obj.method;再在另一个地方fn(),这时的this已经丢了,因为fn变成了普通独立调用。你得用fn.call(obj)把它拽回来。

4. 代码实测:四种绑定的验证与避坑

4.1 场景1:事件绑定里this的丢失

这是一个我在项目里遇到过的真实问题。页面里有一个按钮,我们希望点击时输出某个配置对象里的文案:

const config = { text: "按钮被点击了", init: function () { const btn = document.getElementById("btn"); btn.addEventListener("click", function () { console.log(this.text); // 这里 this 指向按钮 DOM,不是 config }); }, }; config.init();

点击按钮后,控制台输出的是undefined。原因在于事件监听的回调函数在被触发时,浏览器会把回调里的this绑定到绑定事件的元素上(这是 DOM 事件系统的隐式绑定),而不是我们期待的config对象。

解决方案有两种常用思路。第一种是把回调改写成箭头函数,因为箭头函数不绑定自己的this,会继承外层init方法里的this,也就是config:

btn.addEventListener("click", () => { console.log(this.text); // 按钮被点击了 });

第二种是在回调外面先把this存下来,老代码里常见const self = this的写法,逻辑上也没有问题。

注意:事件回调里的this指代 DOM 元素,这个行为本身是语言设计的一部分,不是 bug。你只有理解了这个绑定的存在,才能在写事件回调时避免踩坑。

4.2 场景2:用Math.max.apply求数组最大值

在 ES6 的展开运算符普及前,Math.max.apply(null, arr)是前端面试里的高频题。它的本质是:Math.max需要接收多个数字参数,而我们的arr是个数组,用apply恰好能把它展开成一个一个的参数传进去。

const numbers = [3, 8, 4, 10, 2]; // 不借助 apply 的写法,要手写遍历: let max = -Infinity; for (let num of numbers) { if (num > max) { max = num; } } // 借助 apply 的写法: const applyMax = Math.max.apply(null, numbers); console.log(applyMax); // 10 // 现代写法: const spreadMax = Math.max(...numbers); console.log(spreadMax); // 10

代码的运行结果都一致,但从理解this的角度来看,apply在这里的作用机制非常直观:我们把this设为null,告诉Math.max我不关心你的this,只需要你把numbers里的元素逐个当成参数传进去就行。这种写法是"借用 apply 的参数展开能力"的典型代表。

4.3 场景3:对象方法借用与参数传递

如果两个对象结构相似,但其中一个缺少方法,可以借用另一个的方法。结合参数传参,可以完成更灵活的行为:

const rectangle = { width: 10, height: 5, area: function (unit) { return `${this.width * this.height}平方${unit}`; }, }; const square = { width: 4, height: 4, }; console.log(rectangle.area("米")); // 50平方米 console.log(rectangle.area.call(square, "英尺")); // 16平方英尺

可以看到,square自己没有area方法,但通过call显式地把this指到square上,就复用了rectangle里的面积计算方法。这种模式在封装布局计算、数据格式化等场景里很常见。

4.4 严格模式与 Node 环境的特殊差异

在浏览器非严格模式下,默认绑定的this指向window,几乎所有人都知道。但有两个容易被忽略的差异值得单独提醒。

第一个是严格模式。只要函数内部开启了"use strict",独立调用的函数中this就是undefined,而不是全局对象:

function normal() { console.log(this); // 非严格模式 -> window } function strictFn() { "use strict"; console.log(this); // 严格模式 -> undefined }

第二个是 Node.js 环境。Node 里默认绑定指向global对象,而不是浏览器的window。如果你把一套浏览器代码直接丢进 Node 里跑,可能会因为this指向不同而出现诡异的报错。开发时先认清运行环境,再分析this。

5. 常见问题与排查技巧

5.1 经典面试套路与对应答案速查

我整理了面试和工作中最高频的this指向问题,以表格形式列出场景和结论:

代码写法this 指向原因
fn()独立调用全局对象 / undefined(严格模式)默认绑定
obj.fn()对象内调用指向 obj隐式绑定
const fn = obj.fn; fn()全局对象 / undefined隐式绑定丢失
fn.call(target)指向 target显式绑定
new Fn()指向新创建的实例new 绑定
箭头函数定义在对象方法内指向外层方法的 this箭头函数不绑定自身
事件监听器回调监听器绑定的 DOM 元素DOM 事件系统隐式绑定

这张表可以当成速查卡用,想不起来时扫一眼,比翻完整篇文档高效得多。

5.2 运行时排查this的三个实用技巧

当你在业务代码里遇到this不符合预期时,别急着一行一行读代码。我一般按下面三个思路排查,速度非常快。

第一,直接把this打进控制台。在可疑函数体第一行加一句console.log(this),运行后立刻能知道它的真实指向。这是最笨但最有效的方法,一次打印解决大部分疑问。

第二,在编辑器里给this打个断点,通过调试面板的 Watch 窗口查看。这个方法的优势是你可以同时看到this以及作用域内的其他变量,方便对照上下文。

第三,判断当前函数是不是被"提取"过。如果它是从对象里取出来赋给某个变量后再调用的,几乎可以断定this已经丢失,排查重点就放在调用位置而不是函数定义位置。

5.3 一些值得记住的边界与细节

有几个容易被忽略的细节,在实际开发中一旦踩中就很磨人。

第一,call和apply无法改变箭头函数里的this指向。箭头函数在定义时就确定了this,这个绑定是词法层面的,后续无论你call、apply还是bind,都撼动不了它。

const obj = { name: "测试" }; const fn = () => { console.log(this); }; fn.call(obj); // 输出依然不是 obj,而是定义箭头函数时所在作用域的 this

第二,回调函数里使用call时要小心别把this改坏了。有些库的内部实现会对回调函数做额外的call调用,导致传入的this和你预期不一致。这时候调试技巧第一条就派上用场了——先打印,再推断。

第三,早期代码里常用var self = this来保住外层指向,这在this相关知识不完善的时代是常规操作。现在有了箭头函数和bind,self写法的优势已经不明显,但你在维护老项目时依然会遇到,理解它的原理能让你快速看懂旧代码。

个人感受

讲真,this并不是一个需要靠天赋才能掌握的知识点,它更像是一个"规则明确但分支超多"的思维迷宫。我最初学习时也走过弯路:只看结论不画结构,结果遇到新题就懵。后来把思维脑图配合代码验证做一轮,很多概念才真正在心里扎下根。

这第一篇文章,相当于把地基打扎实了:四种绑定规则的原理、优先级、以及call和apply这两个显式武器。下一篇我准备聊bind的机制、柯里化应用,以及箭头函数在实战中的注意事项。如果这篇里的脑图结构帮你理清了思路,建议你亲手画一遍,再把我给的示例代码跑一遍。

在this这个问题上,没有"背过就会"这回事,只有"试过才懂"的人是真的会。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询