JavaScript中this指向怎么判断?箭头函数与call/apply/bind面试题

作者:半夏

时间:2026-10-30

浏览:550

深入解析JavaScript中this指向的核心判断法则、四大绑定优先级、箭头函数的词法作用域特性,并拆解call/apply/bind的高频面试题与底层原理。

文章目录

判断 JavaScript 中 this 指向的核心准则只有一个:除箭头函数外,this 的指向并不是在函数声明时确定的,而完全取决于函数是在哪里、以何种形式被调用的(即调用位置与调用方式)。如果能够理清“默认绑定、隐式绑定、显式绑定、new 绑定”四大规则的优先级阶梯,再结合箭头函数的“词法作用域查找”机制,绝大多数复杂的面试题与日常代码陷阱都能在数秒内推导出确切答案。

一、四步法则:this 指向的优先级阶梯

在普通函数中,JavaScript 引擎在执行函数调用时,会根据以下顺序从高到低依次匹配 this 绑定规则:

  1. 是否由 new 调用?如果是,this 绑定到新创建的实例对象。
  2. 是否由 call、apply 或 bind 显式调用?如果是,this 绑定到指定的上下文对象。
  3. 是否由上下文对象发起调用(如 obj.fn())?如果是,this 绑定到该上下文对象(隐式绑定)。
  4. 以上都不是(默认独立调用,如 fn())?严格模式('use strict')下绑定到 undefined,非严格模式下绑定到全局对象(浏览器环境为 window,Node.js 为 global)。

如果遇到箭头函数,则直接跳出上述四种规则——箭头函数内部根本没有自身的 this,它会直接沿词法作用域向外层查找 this,且一旦外层词法环境确定,call/apply/bind 也无法改变其指向。

JavaScript中this指向怎么判断?箭头函数与call/apply/bind面试题
图 1:普通函数与箭头函数 this 指向判断优先级决策树

二、四类经典绑定的代码场景与细节考查

1. 默认绑定(独立函数调用)

当一个函数不带有任何修饰,直接被调用(如 foo())时,适用的就是默认绑定规则。在非严格模式下,this 默认指向全局对象;而在严格模式下,this 绑定到 undefined。

function show() {
  console.log(this);
}

show(); // 浏览器非严格模式: window;严格模式: undefined

// 注意:即使嵌套在深层函数中,只要是独立调用,也是默认绑定
function outer() {
  function inner() {
    console.log(this); // window (非严格模式)
  }
  inner();
}
outer();

2. 隐式绑定及其易丢失陷阱

当函数作为某个对象的方法调用时,this 会指向最后调用该函数的对象(即紧邻调用点左侧的对象引用)。

const user = {
  name: 'Alice',
  greet() {
    console.log(this.name);
  }
};

user.greet(); // 输出: 'Alice' (this 指向 user)

// 多层链式调用:只看直接发起者
const company = {
  name: 'TechCorp',
  dept: {
    name: 'R&D',
    getName() {
      console.log(this.name);
    }
  }
};
company.dept.getName(); // 输出: 'R&D' (this 指向 dept,而非 company)
高频面试考点:隐式绑定的丢失(Implicit Binding Lost)

当把对象方法赋值给另一个变量,或者作为回调函数传递(例如传给 setTimeout、forEach 或自定义回调)时,传递的只是函数本身的内存引用地址。在真正执行时,函数依然是以无宿主的普通形式调用的,导致隐式绑定丢失,退化为默认绑定。

const obj = {
  count: 10,
  inc() {
    console.log(this.count);
  }
};

// 场景 1:赋值给变量后再调用
const fn = obj.inc;
fn(); // undefined (等价于直接调用 fn(),this 指向全局)

// 场景 2:作为回调函数传入定时器
setTimeout(obj.inc, 100); // undefined (setTimeout 内部执行回调时是独立调用)

3. 显式绑定:call、apply 与 bind

为了精准控制函数的执行上下文,Function 原型上提供了 call、apply 和 bind 三个原生方法。面试中常常考查它们在执行时机、参数传递形式以及返回值上的关键差异。

JavaScript中this指向怎么判断?箭头函数与call/apply/bind面试题
图 2:call、apply 与 bind 的参数传递模式与执行时机对比
function multiply(a, b) {
  return this.factor * a * b;
}

const context = { factor: 2 };

// 1. call: 逐个传参,立即执行
console.log(multiply.call(context, 3, 4)); // 输出 24

// 2. apply: 数组传参,立即执行
console.log(multiply.apply(context, [3, 4])); // 输出 24

// 3. bind: 锁定上下文,分步传参(柯里化特性)
const doubleMultiply = multiply.bind(context, 3);
console.log(doubleMultiply(4)); // 输出 24

4. new 绑定与对象构造

当通过 new Foo(...) 实例化一个函数时,JavaScript 引擎会在内部完成四件事:

  • 在内存中创建一个全新的普通空对象。
  • 将这个空对象的内部原型(__proto__)链接到构造函数的 prototype 对象。
  • 将构造函数内部的 this 绑定到刚刚创建的新对象上,并执行构造函数的代码。
  • 如果构造函数显式返回了一个非 null 的对象,则返回该对象;否则返回该新创建的实例对象。
function Person(name) {
  this.name = name;
}
const p = new Person('Bob');
console.log(p.name); // 'Bob' (this 成功绑定到了实例 p 上)

三、箭头函数的 this:词法作用域的不可变性

ES6 引入的箭头函数在处理 this 时采取了完全不同的策略:它不使用普通函数的动态绑定规则,而是遵循词法作用域(Lexical Scope)链向上层外层函数或全局作用域捕获 this。

  • 箭头函数没有属于自己的 this,它只是把 this 当成一个普通的词法变量,沿作用域链向外层查找。
  • 一旦捕获外层 this,该指向便被永久固定,无论后续怎样调用,使用 call、apply 或 bind 均无法篡改。
  • 致命误区:对象字面量({ a: 1 })的大括号不会创建独立的作用域块(它不是函数执行上下文)。在对象字面量直接定义属性时使用箭头函数,其外层作用域通常是全局对象(window)。
JavaScript中this指向怎么判断?箭头函数与call/apply/bind面试题
图 3:运行时动态绑定(普通函数)与词法静态绑定(箭头函数)机制对比

四、经典面试题真题拆解与陷阱剖析

面试题 1:连续 bind 与硬绑定不可变性

function say() {
  console.log(this.name);
}

const obj1 = { name: 'First' };
const obj2 = { name: 'Second' };
const obj3 = { name: 'Third' };

say.bind(obj1).bind(obj2).bind(obj3)();

输出结果:First

底层原理解析:bind 的本质是在原函数外包装了一层返回函数闭包。第一次执行 say.bind(obj1) 时,生成了函数 boundFn1,其内部保存了 say.apply(obj1, args);第二次执行 boundFn1.bind(obj2) 时,生成了 boundFn2,其内部调用的是 boundFn1.apply(obj2)。当最终执行时,boundFn2 把 this 绑定给 boundFn1,而 boundFn1 内部执行时,根本不在乎外部传入的 this 是什么,而是坚决使用当初闭包捕获的 obj1 去调用最底层的 say。因此:不论对一个函数执行多少次 bind,其 this 永远只受第一次 bind 决定的控制。

面试题 2:箭头函数与对象方法嵌套混合考查

var name = 'window';

const person = {
  name: 'person',
  foo1: function() {
    console.log(this.name);
  },
  foo2: () => console.log(this.name),
  foo3: function() {
    return function() {
      console.log(this.name);
    };
  },
  foo4: function() {
    return () => {
      console.log(this.name);
    };
  }
};

person.foo1();          // 答案 1
person.foo2();          // 答案 2
person.foo3()();        // 答案 3
person.foo4()();        // 答案 4

深度推导过程:

  1. person.foo1():典型的隐式绑定,直接由 person 发起调用,this 指向 person,输出 'person'。
  2. person.foo2():foo2 是箭头函数。注意 person = { ... } 这个对象字面量不是作用域。在定义 foo2 时,所处的外层作用域就是全局作用域,因此捕获全局的 this,在浏览器非严格模式下输出 'window'。
  3. person.foo3()():执行 person.foo3() 返回了一个普通匿名函数,随后紧接着进行独立调用 (...)()。此时该匿名函数的调用方式没有任何前缀修饰,属于默认绑定,指向全局,输出 'window'。
  4. person.foo4()():执行 person.foo4() 时是作为 person 的方法调用的,此时 foo4 执行上下文中的 this 是 person。在执行过程中,它返回了一个箭头函数,该箭头函数在被创建时捕获了外层(即当前 foo4 执行时)的 this(person)。随后的独立调用不会更改词法捕获的 this,因此依然输出 'person'。

面试题 3:new 遇到 bind 的优先级大碰撞

function Widget(val) {
  this.val = val;
}

const obj = {};
const BoundWidget = Widget.bind(obj);

const inst = new BoundWidget(42);
console.log(inst.val); // 42
console.log(obj.val);  // undefined (没有被写入 obj!)

原理解析:在这段代码中,new 与 bind 产生了直接冲突。JavaScript 规范明确规定:当用 new 操作符调用一个经过 bind 绑定的包装函数时,new 的优先级高于 bind。new 会忽略之前绑定的目标对象(obj),而是直接将新创建的实例作为 this 传给原函数执行。这也是为什么我们手写 bind 时,必须用 this instanceof bound 判断是否作为构造函数被调用的核心原因。

五、知识全景与避坑总结

JavaScript中this指向怎么判断?箭头函数与call/apply/bind面试题
图 4:JavaScript this 规则全景速查与高频易错点总结

日常编码与面试答题中,只需牢记以下避坑要诀:

  • 找准调用者:不要去看函数是在哪一行代码写下的,必须紧盯真正发起执行的那一刻,调用括号 () 左侧究竟挂着什么。
  • 回调必留心:凡是把对象方法作为参数塞给定时器、事件监听器或数组高阶函数,绝大多数情况下内部都会发生隐式丢失;如果需要保留当前实例上下文,优先使用箭头函数或 .bind(this)。
  • 区分词法与执行:普通函数的 this 是动态的、可变的;箭头函数的 this 是词法的、固定的,且不能作为构造函数(使用 new 会直接抛出类型错误)。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章