JS:this指向、bind、call、apply、知识点与相关面试题

文章目录

  • [一、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、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)。

两个特殊检查点:

  1. 先看是否为箭头函数:

    如果是,无视以上所有规则,直接看它定义时 外层的普通函数 this 是谁。

  2. 回调函数的陷阱:

    例如 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 会失效。

相关推荐
jserTang2 小时前
手撕 Claude Code-4: TodoWrite 与任务系统
前端·javascript·后端
腹黑天蝎座2 小时前
大屏开发必读:Scale/VW/Rem/流式/断点/混合方案全解析(附完整demo)
前端·javascript
jserTang2 小时前
手撕 Claude Code-5:Subagent 与 Agent Teams
前端·javascript·后端
沐知全栈开发2 小时前
CSS Text(文本)
开发语言
前进吧-程序员2 小时前
现代 C++ 异步编程:从零实现一个高性能 ThreadPool (C++20 深度实践)
开发语言·c++·c++20
Rsun045512 小时前
10、Java 桥接模式从入门到实战
java·开发语言·桥接模式
于慨2 小时前
mac安装flutter
javascript·flutter·macos
jieyucx2 小时前
Golang 完整安装与 VSCode 开发环境搭建教程
开发语言·vscode·golang
pearlthriving2 小时前
c++当中的泛型思想以及c++11部分新特性
java·开发语言·c++