javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JS this指向、bind、call、apply

JavaScript中this指向、bind、call、apply、知识点与相关面试题汇总

作者:Hello--_--World

Javascript是一门基于对象的动态语言,也就是说所有东西都是对象,一个很典型的例子就是函数也被视为普通的对象,这篇文章主要介绍了JavaScript中this指向、bind、call、apply、知识点与相关面试题的相关资料,需要的朋友可以参考下

一、JS中 this 究竟指向谁?

在 JavaScript 中,this 的指向问题常被称为“面试第一道坎”。其实,掌握它的秘诀只有一句话:

1. this 的值在何时确定?

this 是在执行上下文(Execution Context)创建时确定的。
这意味着:

2. 四大核心绑定规则

判断 this 指向时,可以按照以下四个规则的优先级进行对比:

① 默认绑定 (Default Binding)

当函数作为独立调用(不带任何修饰的函数调用)时,this 指向全局对象。

② 隐式绑定 (Implicit Binding)

当函数作为某个对象的方法被调用时,this 指向该对象。

const obj = {
  name: 'Gemini',
  sayHi() { console.log(this.name); }
};
obj.sayHi(); // this 指向 obj,输出 "Gemini"

③ 显式绑定 (Explicit Binding)

通过 call()、apply() 或 bind() 直接指定 this 的值。

④ new 绑定 (new Binding)

当使用 new 关键字调用构造函数时,JavaScript 内部会创建一个新对象,并将 this 指向这个新创建的实例对象。

3. 箭头函数的this为何不同?

箭头函数是 this 规则中的“特例”,因为它没有自己的 this

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. 默认绑定与隐式绑定(基础陷阱)

这是最常见的题型,考察你是否清楚“谁调用指向谁”。

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

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 同时出现时,考察的是优先级。

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. 定时器 + 闭包

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 指向判断准则

你可以按照这个优先级顺序来快速判断:

三、 call、apply、bind 核心知识点汇总

1. 核心异同对比表

特性callapplybind
立即执行(返回一个新函数)
参数形式逐个列举:fn.call(obj, 1, 2)数组/类数组:fn.apply(obj, [1, 2])逐个列举(支持偏函数/柯里化传参)
主要用途借用方法、精准修改 this处理数组数据(如 Math.max锁定回调函数的 this、预设参数
返回值函数执行的结果函数执行的结果绑定了 this 后的新函数

2. 为什么设计它们?(优点与场景)

3. 手写模拟实现(面试高频)

① 模拟实现call

核心思路:将函数设为目标对象的临时属性,利用隐式绑定规则执行,执行完后删除。

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 基本一致,区别在于对第二个参数(数组)的处理。

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 实例化调用。

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 的结果

核心结论:包装盒效应
bind 的本质是闭包。每调用一次 bind,就在原函数外面套了一个“壳子”。

关键点:内核的 fn 已经被第一个盒子用 call 或 apply 锁死了,外层的盒子再怎么折腾,也改不动最深层那个 call 的参数。

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 后的 this?
: 只有一种办法——使用 new 关键字。因为 new 绑定的优先级高于 bind 绑定(如果是普通函数而非箭头函数的话)。

2. new 的优先级最高:

通过 bind 绑定的函数,如果被当做构造函数使用 new 调用,原本绑定的 this 会失效。

总结

到此这篇关于JavaScript中this指向、bind、call、apply、知识点与相关面试题汇总的文章就介绍到这了,更多相关JS this指向、bind、call、apply内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文