文章目录
- [一、JS中 this 究竟指向谁?](#一、JS中 this 究竟指向谁?)
-
- [1. this 的值在何时确定?](#1. this 的值在何时确定?)
- [2. 四大核心绑定规则](#2. 四大核心绑定规则)
-
- [① 默认绑定 (Default Binding)](#① 默认绑定 (Default Binding))
- [② 隐式绑定 (Implicit Binding)](#② 隐式绑定 (Implicit Binding))
- [③ 显式绑定 (Explicit Binding)](#③ 显式绑定 (Explicit Binding))
- [④ new 绑定 (new Binding)](#④ new 绑定 (new Binding))
- [3. 箭头函数的 `this` 为何不同?](#3. 箭头函数的
this为何不同?) - [4. 如何判断复杂场景下的指向?](#4. 如何判断复杂场景下的指向?)
- [二、js中 this 常见面试题](#二、js中 this 常见面试题)
-
- [1. 默认绑定与隐式绑定(基础陷阱)](#1. 默认绑定与隐式绑定(基础陷阱))
- [2. 箭头函数(核心差异)](#2. 箭头函数(核心差异))
- [3. 构造函数与显式绑定(优先级)](#3. 构造函数与显式绑定(优先级))
- [4. 定时器 + 闭包](#4. 定时器 + 闭包)
- [💡 总结:this 指向判断准则](#💡 总结:this 指向判断准则)
- [三、 call、apply、bind 核心知识点汇总](#三、 call、apply、bind 核心知识点汇总)
-
- [1. 核心异同对比表](#1. 核心异同对比表)
- [2. 为什么设计它们?(优点与场景)](#2. 为什么设计它们?(优点与场景))
- [3. 手写模拟实现(面试高频)](#3. 手写模拟实现(面试高频))
-
- [① 模拟实现 `call`](#① 模拟实现
call) - [② 模拟实现 apply](#② 模拟实现 apply)
- [① 模拟实现 `call`](#① 模拟实现
- [四、call、apply、bind 面试坑点](#四、call、apply、bind 面试坑点)
-
- [1. 连续 bind 的结果](#1. 连续 bind 的结果)
- [2. new 的优先级最高:](#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 指向该对象。
javascript
const obj = {
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. 默认绑定与隐式绑定(基础陷阱)
这是最常见的题型,考察你是否清楚"谁调用指向谁"。
javascript
var name = "Window";
const obj = {
name: "Object",
getName: function() {
console.log(this.name);
}
};
const bare = obj.getName;
obj.getName(); // 打印什么?
bare(); // 打印什么?
obj.getName():属于隐式绑定,调用者是 obj,所以 this 指向 obj。输出:"Object"。
bare():虽然它拿到了函数引用,但调用时是直接运行的(默认绑定)。在非严格模式下,this 指向 window。输出:"Window"。
2. 箭头函数(核心差异)
箭头函数是面试官最喜欢用来对比 this 的工具,因为它没有自己的 this。
javascript
const obj = {
name: "ArrowObj",
sayHi: () => {
console.log(this.name);
},
sayHello: function() {
const inner = () => 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 同时出现时,考察的是优先级。
javascript
function Foo(name) {
this.name = name;
}
const obj1 = {};
const bar = Foo.bind(obj1);
bar("Jack");
console.log(obj1.name); // 打印什么?
const bazz = new bar("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. 定时器 + 闭包
javascript
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
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. 核心异同对比表
| 特性 | call | apply | bind |
|---|---|---|---|
| 立即执行 | 是 | 是 | 否(返回一个新函数) |
| 参数形式 | 逐个列举:fn.call(obj, 1, 2) |
数组/类数组:fn.apply(obj, [1, 2]) |
逐个列举(支持偏函数/柯里化传参) |
| 主要用途 | 借用方法、精准修改 this |
处理数组数据(如 Math.max) |
锁定回调函数的 this、预设参数 |
| 返回值 | 函数执行的结果 | 函数执行的结果 | 绑定了 this 后的新函数 |
2. 为什么设计它们?(优点与场景)
- 代码复用:无需通过继承,即可让一个对象直接"借用"另一个对象的方法。
- 解耦:将对象与其方法执行的上下文分离,增加逻辑灵活性。
- 解决 this 丢失 :在异步回调(如
setTimeout)或事件处理中,防止this意外指向全局对象。
3. 手写模拟实现(面试高频)
① 模拟实现 call
核心思路:将函数设为目标对象的临时属性,利用隐式绑定规则执行,执行完后删除。
javascript
Function.prototype.myCall = function(context, ...args) {
// 【1】确定要绑定的目标对象
// 如果没传目标对象,就默认是 window
context = context || window;
// 【2】把当前函数"变"成目标对象的一个方法
// 这里的 this 指向的就是那个"正在求助"的函数
const fnKey = Symbol('temporaryFunction');
context[fnKey] = this;
// 【3】通过对象来调用这个函数
// 关键:一旦用 context.xxx() 调用,函数内部的 this 就会指向 context
const result = context[fnKey](...args);
// 【4】任务完成,把刚才临时加进去的方法删掉,保持对象原貌
delete context[fnKey];
// 【5】返回函数执行的结果
return result;
};
② 模拟实现 apply
逻辑与 call 基本一致,区别在于对第二个参数(数组)的处理。
javascript
Function.prototype.myApply = function(context, args) {
context = context || window;
const key = Symbol('key');
context[key] = this;
// 判断 args 是否为数组或类数组,并进行解构传参
const result = Array.isArray(args) ? context[key](...args) : context[key]();
delete context[key];
return result;
};
③ 模拟实现 bind (进阶版)
需要考虑:返回新函数、参数合并(柯里化)、支持 new 实例化调用。
javascript
Function.prototype.myBind = function(context, ...args) {
const self = this; // 保存原函数
const bound = function(...nextArgs) {
// 如果是通过 new 调用的,this 应指向实例(此时忽略 bind 传入的 context)
const isNew = this instanceof bound;
return self.apply(isNew ? this : context, args.concat(nextArgs));
};
// 维护原型链(让实例能继承原函数 prototype 上的属性)
bound.prototype = Object.create(self.prototype);
return bound;
};
四、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 的参数。
javascript
var name = 'Global Window';
const obj1 = { name: 'Object_1' };
const obj2 = { name: 'Object_2' };
const obj3 = { name: 'Object_3' };
function fn() {
console.log(this.name);
}
// ==========================================
// 核心实验:连续三次绑定
// ==========================================
const bind1 = fn.bind(obj1);
const bind2 = bind1.bind(obj2);
const bind3 = bind2.bind(obj3);
bind3();
/* * 【 打印结果 】:Object_1
*/
// ==========================================
// 逻辑伪代码还原:为什么改不动?
// ==========================================
// 1. bind1 相当于:
const bind1_logic = function(...args) {
return fn.apply(obj1, args); // <--- 这里已经把 obj1 写死了
};
// 2. bind2 相当于:
const bind2_logic = function(...args) {
// 这里的 this 指向 obj2,但那又怎样?
// 它内部调用的是 bind1_logic
return bind1_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 会失效。