文章目录一、JS中 this 究竟指向谁1. this 的值在何时确定2. 四大核心绑定规则① 默认绑定 (Default Binding)② 隐式绑定 (Implicit Binding)③ 显式绑定 (Explicit Binding)④ new 绑定 (new Binding)3. 箭头函数的 this 为何不同4. 如何判断复杂场景下的指向两个特殊检查点总结公式二、js中 this 常见面试题1. 默认绑定与隐式绑定基础陷阱2. 箭头函数核心差异3. 构造函数与显式绑定优先级4. 定时器 闭包 总结this 指向判断准则三、 call、apply、bind 核心知识点汇总1. 核心异同对比表2. 为什么设计它们优点与场景3. 手写模拟实现面试高频① 模拟实现 call② 模拟实现 apply四、call、apply、bind 面试坑点1. 连续 bind 的结果2. new 的优先级最高一、JS中 this 究竟指向谁在 JavaScript 中this 的指向问题常被称为“面试第一道坎”。其实掌握它的秘诀只有一句话this 的指向并不取决于函数定义的位置而是取决于函数“如何被调用”。箭头函数的 this 捕获自定义时所处的外部父级词法环境。1. this 的值在何时确定this 是在执行上下文Execution Context创建时确定的。这意味着定义时无效 仅仅声明一个普通函数this 是没有实际意义的占位符。运行时确定 只有当函数被真正触发调用时JavaScript 引擎才会根据调用方式把特定的对象“绑定”给 this。2. 四大核心绑定规则判断 this 指向时可以按照以下四个规则的优先级进行对比① 默认绑定 (Default Binding)当函数作为独立调用不带任何修饰的函数调用时this 指向全局对象。非严格模式 指向 window (浏览器) 或 global (Node.js)。严格模式(‘use strict’) this 为 undefined。② 隐式绑定 (Implicit Binding)当函数作为某个对象的方法被调用时this 指向该对象。constobj{name:Gemini,sayHi(){console.log(this.name);}};obj.sayHi();// this 指向 obj输出 Gemini注意隐式丢失隐式绑定容易出现“丢失”现象。例如将 obj.sayHi 赋值给一个变量再调用就会退化为默认绑定。③ 显式绑定 (Explicit Binding)通过 call()、apply() 或 bind() 直接指定 this 的值。call/apply立即执行函数并改变 this。bind返回一个硬绑定了 this 的新函数之后无论怎么调用this 都不会再变。④ new 绑定 (new Binding)当使用 new 关键字调用构造函数时JavaScript 内部会创建一个新对象并将 this 指向这个新创建的实例对象。3. 箭头函数的this为何不同箭头函数是this规则中的“特例”因为它没有自己的this。有何不同箭头函数的this捕获自定义时所处的外部父级词法环境。为什么在箭头函数出现前回调函数如setTimeout中的this经常意外指向全局开发者不得不使用var self this这种写法。箭头函数通过词法作用域Lexical Scoping让this保持逻辑上的连贯性即“外层是谁我就是谁”。不可改变由于箭头函数本身没有绑定this所以call()、apply()或bind()对它无效。4. 如何判断复杂场景下的指向面对嵌套、回调等复杂代码可以遵循以下“优先级排除法”由高到低优先级判定条件结论1 (最高)函数是new调用的吗是指向新创建的实例对象。2函数通过call/apply/bind调用吗是指向指定的第一个参数。3函数在某个上下文对象中调用吗终点指向该所属对象如obj.foo()。4 (最低)以上都不是独立调用默认指向全局对象严格模式下为undefined。两个特殊检查点先看是否为箭头函数如果是无视以上所有规则直接看它定义时外层的普通函数this是谁。回调函数的陷阱例如setTimeout(obj.foo, 100)这里虽然传入了obj.foo但函数实际上是在定时器到期后由全局环境调用的属于**“隐式丢失”**this通常指向全局。总结公式谁调用指谁没调用指全局new 了指实例箭头函数看“亲爹”。二、js中 this 常见面试题1. 默认绑定与隐式绑定基础陷阱这是最常见的题型考察你是否清楚“谁调用指向谁”。varnameWindow;constobj{name:Object,getName:function(){console.log(this.name);}};constbareobj.getName;obj.getName();// 打印什么bare();// 打印什么obj.getName()属于隐式绑定调用者是 obj所以 this 指向 obj。输出“Object”。bare()虽然它拿到了函数引用但调用时是直接运行的默认绑定。在非严格模式下this 指向 window。输出“Window”。2. 箭头函数核心差异箭头函数是面试官最喜欢用来对比 this 的工具因为它没有自己的 this。constobj{name:ArrowObj,sayHi:(){console.log(this.name);},sayHello:function(){constinner()console.log(this.name);inner();}};obj.sayHi();// 打印什么obj.sayHello();// 打印什么obj.sayHi()箭头函数的 this取决于它定义时所在的外层作用域。这里外层是全局环境不是 obj 的花括号所以指向 window。输出“Window”。obj.sayHello()inner 是箭头函数它会找外层非箭头函数的 this。它的外层是 sayHello而 sayHello 是被 obj 调用的其 this 是 obj。输出“ArrowObj”。3. 构造函数与显式绑定优先级当 new、bind、call/apply 同时出现时考察的是优先级。functionFoo(name){this.namename;}constobj1{};constbarFoo.bind(obj1);bar(Jack);console.log(obj1.name);// 打印什么constbazznewbar(Rose);console.log(obj1.name);// 打印什么console.log(bazz.name);// 打印什么bar(“Jack”)使用 bind 将 this 永久绑定到了 obj1。输出“Jack”。new bar(“Rose”)重点new 绑定的优先级高于 bind。即使函数被绑定到了 obj1使用 new 时依然会创建一个新对象并将 this 指向这个新对象。obj1.name 依然是 “Jack”而 bazz.name 是 “Rose”。4. 定时器 闭包varlength10;functionfn(){console.log(this.length);}varobj{length:5,method:function(fn){fn();arguments[0]();}};obj.method(fn,1);fn()作为参数传递后直接调用属于默认绑定。输出10。arguments0这是一个极具迷惑性的点。arguments[0] 实际上是调用 arguments 对象上的第 0 个属性。这属于隐式绑定this 指向 arguments 对象。因为 arguments 传入了两个参数它的 length 是 2。输出2。 总结this 指向判断准则你可以按照这个优先级顺序来快速判断new 绑定函数是否在 new 中调用如果是this 指向新创建的对象。显式绑定函数是否通过 call、apply 或 bind 调用如果是this 指向指定的对象。隐式绑定函数是否在某个上下文对象中调用如 obj.func()如果是this 指向那个对象。默认绑定如果以上都不是在非严格模式下指向 window严格模式下是 undefined。箭头函数以上规则统统无效请看它外层的函数/全局作用域的 this 是谁。三、 call、apply、bind 核心知识点汇总1. 核心异同对比表特性callapplybind立即执行是是否返回一个新函数参数形式逐个列举fn.call(obj, 1, 2)数组/类数组fn.apply(obj, [1, 2])逐个列举支持偏函数/柯里化传参主要用途借用方法、精准修改this处理数组数据如Math.max锁定回调函数的this、预设参数返回值函数执行的结果函数执行的结果绑定了this后的新函数2. 为什么设计它们优点与场景代码复用无需通过继承即可让一个对象直接“借用”另一个对象的方法。解耦将对象与其方法执行的上下文分离增加逻辑灵活性。解决 this 丢失在异步回调如setTimeout或事件处理中防止this意外指向全局对象。3. 手写模拟实现面试高频① 模拟实现call核心思路将函数设为目标对象的临时属性利用隐式绑定规则执行执行完后删除。Function.prototype.myCallfunction(context,...args){// 【1】确定要绑定的目标对象// 如果没传目标对象就默认是 windowcontextcontext||window;// 【2】把当前函数“变”成目标对象的一个方法// 这里的 this 指向的就是那个“正在求助”的函数constfnKeySymbol(temporaryFunction);context[fnKey]this;// 【3】通过对象来调用这个函数// 关键一旦用 context.xxx() 调用函数内部的 this 就会指向 contextconstresultcontext[fnKey](...args);// 【4】任务完成把刚才临时加进去的方法删掉保持对象原貌deletecontext[fnKey];// 【5】返回函数执行的结果returnresult;};② 模拟实现 apply逻辑与 call 基本一致区别在于对第二个参数数组的处理。Function.prototype.myApplyfunction(context,args){contextcontext||window;constkeySymbol(key);context[key]this;// 判断 args 是否为数组或类数组并进行解构传参constresultArray.isArray(args)?context[key](...args):context[key]();deletecontext[key];returnresult;};③ 模拟实现 bind (进阶版)需要考虑返回新函数、参数合并柯里化、支持 new 实例化调用。Function.prototype.myBindfunction(context,...args){constselfthis;// 保存原函数constboundfunction(...nextArgs){// 如果是通过 new 调用的this 应指向实例此时忽略 bind 传入的 contextconstisNewthisinstanceofbound;returnself.apply(isNew?this:context,args.concat(nextArgs));};// 维护原型链让实例能继承原函数 prototype 上的属性bound.prototypeObject.create(self.prototype);returnbound;};四、call、apply、bind 面试坑点1. 连续 bind 的结果fn.bind(obj1).bind(obj2)() - this 依然指向 obj1。原因bind 返回的是一个新的包装函数第二次 bind 绑定的是这个包装函数而最内层的 fn 始终只认第一次绑定的 obj1。核心结论包装盒效应bind 的本质是闭包。每调用一次 bind就在原函数外面套了一个“壳子”。第一次 bind把原函数 fn 塞进一个包装盒并在盒子里写死 this 指向 obj1。第二次 bind是给这个包装盒又套了一个新盒子试图改变包装盒的 this。最终执行最外层的盒子被调用它去调用里面的盒子里面的盒子最终调用最内核的 fn。关键点内核的 fn 已经被第一个盒子用 call 或 apply 锁死了外层的盒子再怎么折腾也改不动最深层那个 call 的参数。varnameGlobal Window;constobj1{name:Object_1};constobj2{name:Object_2};constobj3{name:Object_3};functionfn(){console.log(this.name);}// // 核心实验连续三次绑定// constbind1fn.bind(obj1);constbind2bind1.bind(obj2);constbind3bind2.bind(obj3);bind3();/* * 【 打印结果 】Object_1 */// // 逻辑伪代码还原为什么改不动// // 1. bind1 相当于constbind1_logicfunction(...args){returnfn.apply(obj1,args);// --- 这里已经把 obj1 写死了};// 2. bind2 相当于constbind2_logicfunction(...args){// 这里的 this 指向 obj2但那又怎样// 它内部调用的是 bind1_logicreturnbind1_logic.apply(obj2,args);};// 3. 执行 bind2() 时// 调用 bind2_logic - 执行 bind1_logic.apply(obj2)// 内部执行 bind1_logic() - 执行 fn.apply(obj1)// 最终 fn 看到的 this 永远是 obj1问多次 bind 之后this 指向谁答 始终指向第一次 bind 绑定的对象。原因 * bind 方法返回的是一个新函数闭包。当我们多次 bind 时实际上是多层函数嵌套。只有最靠近原函数的那个 bind即第一次是通过 apply/call 真正绑定了原函数的 this。后续的 bind 只是在修改“包装函数”的 this而包装函数内部执行原函数时依然使用的是第一次绑定好的 obj1。追问那怎么才能改掉 bind 后的 this答 只有一种办法——使用 new 关键字。因为 new 绑定的优先级高于 bind 绑定如果是普通函数而非箭头函数的话。2. new 的优先级最高通过 bind 绑定的函数如果被当做构造函数使用 new 调用原本绑定的 this 会失效。