「✍️JS原子笔记 」一文搞懂 call、apply、bind 特征及手写实现

一文搞懂 JS 中 call、apply、bind 及手写实现

在 JavaScript 中,this的指向一直是核心难点,而callapplybind作为改变函数this指向的三大方法,是前端开发中必须掌握的知识点。大家要围绕传参区别调用时机返回值 三个方面来思考,手写也需要围绕传入什么改变什么返回什么来思考。

一、为什么需要改变 this 指向?

JavaScript 中函数的this指向并非定义时确定,而是执行时确定 ,默认指向调用它的对象。这种特性在某些场景下会导致this指向不符合预期,例如:

javascript 复制代码
const person = {
  name: "小明",
  sayHi: function() {
    console.log(`你好,我是${this.name}`);
  }
};

// 正常调用,this指向person
person.sayHi(); // 输出:你好,我是小明

// 将方法赋值给变量后调用,this指向全局(浏览器为window,Node为global)
const aloneSay = person.sayHi;
aloneSay(); // 输出:你好,我是undefined

此时就需要callapplybind来强制改变this的指向,让函数按照我们的预期执行。

二、call、apply、bind 的核心区别

三者的核心功能都是改变函数执行时的 this 指向 ,主要区别体现在传参方式执行时机上。

2.1 call:立即执行,参数逐个传递

  • 语法函数名.call(thisArg, 参数1, 参数2, ...)

  • 参数说明

    • thisArg:函数执行时要绑定的this对象;
    • 后续参数:逐个传入函数的参数。
  • 特点 :调用后立即执行函数,返回函数执行结果。

示例

javascript 复制代码
const person = { name: "小明" };

function sayHi(age, gender) {
  console.log(`你好,我是${this.name},${age}岁,${gender}`);
}

// 用call改变this指向person,同时逐个传参
sayHi.call(person, 20, "男"); // 输出:你好,我是小明,20岁,男

2.2 apply:立即执行,参数以数组传递

  • 语法函数名.apply(thisArg, [参数1, 参数2, ...])

  • 参数说明

    • thisArg:与call一致,为绑定的this对象;
    • 第二个参数:必须是数组 / 类数组,数组元素会被逐个传入函数。
  • 特点 :调用后立即执行函数,返回函数执行结果,适合参数数量不确定的场景。

示例

javascript 复制代码
// 复用上述sayHi函数和person对象
const args = [20, "男"];
sayHi.apply(person, args); // 输出:你好,我是小明,20岁,男

2.3 bind:不立即执行,返回新函数

  • 语法const 新函数 = 函数名.bind(thisArg, 参数1, 参数2, ...)

  • 参数说明

    • thisArg:绑定的this对象;
    • 后续参数:可选,提前传入函数的参数(预传参)。
  • 特点不立即执行 函数,返回一个绑定了this的新函数;后续调用新函数时,可补传剩余参数。

示例

javascript 复制代码
// 复用上述sayHi函数和person对象
// bind返回新函数,不立即执行
const bindSayHi = sayHi.bind(person, 20); 
// 调用新函数时补传剩余参数
bindSayHi("男"); // 输出:你好,我是小明,20岁,男

2.4 三者核心区别总结表

方法 执行时机 传参方式 返回值
call 立即执行 逐个传递参数 函数执行的结果
apply 立即执行 数组 / 类数组传递参数 函数执行的结果
bind 不立即执行 可预传参数,后续可补传 绑定了 this 的新函数

三、手写实现 call、apply、bind

理解了三者的使用方式后,我们通过手写实现来深入其底层逻辑,从易到难依次实现applybindcallcall的实现与apply类似)。

3.1 手写实现 apply:newApply

apply的核心逻辑是:将函数挂载到目标对象上,通过对象调用函数实现this绑定,再传递数组参数执行函数,最后删除挂载的函数并返回执行结果。

实现代码

javascript 复制代码
function sayHi(name, age) {
  console.log(`我是${this.name},昵称${name},年龄${age}`);
  return `执行结果:${name}-${age}`; // 函数返回结果,方便测试
}

Function.prototype.newApply = function(content) {
  // 边缘检测调用者是否为函数
  if (typeof this !== 'function') {
    throw new TypeError('The caller must be a function');
  }
  // 确定绑定的this对象,默认指向window(浏览器环境)
  content = content || window;
  // 将原函数挂载到目标对象上,此时content.fn就是原函数
  content.fn = this;
  let result; // 存储函数执行"结果" ->记住apply返回的是函数执行结果

  // 处理参数:如果有第二个参数(数组),则解构传递;否则直接执行
  // 此时this挂在了content.fn上面
  result = arguments[1] ? content.fn(...arguments[1]) : content.fn()
  
  
  
  // 删除挂载的函数,避免污染目标对象
  delete content.fn;
  // 返回结果
  return result; // 返回函数执行结果
}

// 测试代码
const person = { name: "张三" };
const finalResult = sayHi.newApply(person, ["小张", 20]);
console.log(finalResult); // 输出:执行结果:小张-20

拓展一个小知识

当遇到单独出现if else的时候,可以换成"三元运算符" ? + :的形式缩短代码,显得高级

核心逻辑解析

  1. 类型校验 :确保newApply的调用者是函数,否则抛出错误;
  2. 绑定 this 对象 :若未传入目标对象,默认绑定window
  3. 挂载原函数 :将原函数(this指向调用newApply的函数)挂载到目标对象上,通过content.fn()调用时,this自然指向content
  4. 参数处理:利用三元运算符简洁处理参数传递,解构数组参数执行函数并保存结果;
  5. 清理与返回 :删除挂载的fn属性,避免污染对象,最后返回函数执行结果。

3.2 手写实现 bind:newBind

bind的核心逻辑是:返回一个新函数,新函数执行时通过apply调用原函数并绑定this

代码实现

javascript 复制代码
Function.prototype.newBind = function () {
  // 保存原函数和传入的参数(类数组转数组)
  const _this = this, args = Array.prototype.slice.call(arguments);
  // 取出第一个参数作为绑定的this对象
  const newThis = args.shift();
  // 返回新函数,延迟执行
  return function () {
    // 通过apply调用原函数,绑定this并传递参数
    return _this.apply(newThis, args);
  }
};

提示

这里的apply也可以用上述手写的newapply代替哦~

3.3 手写实现 call:newCall

call的实现与apply几乎一致,唯一区别是参数传递方式call逐个传参,apply数组传参)。

实现代码

javascript 复制代码
Function.prototype.newCall = function(content) {
  // 类型校验
  if (typeof this !== 'function') {
    throw new TypeError('The caller must be a function');
  }
  content = content || window;
  content.fn = this;
  let result;

  // 处理参数:从第二个参数开始,逐个解构传递
  const args = Array.prototype.slice.call(arguments, 1);
  result = content.fn(...args);

  delete content.fn;
  return result;
};

// 测试代码
const person = { name: "李四" };
sayHi.newCall(person, "小李", 25); // 输出:我是李四,昵称小李,年龄25

四、常见问题解析

在手写实现的过程中,新手常遇到两个核心问题,这里集中解答:

4.1 为什么要用Array.prototype.slice.call(arguments)

arguments是函数内部的类数组对象 ,有下标和length属性,但没有数组的sliceconcat等方法。Array.prototype.slice.call(arguments)的作用是将类数组的 arguments 转为真正的数组,方便后续操作参数。

ES6 后可简化为:

javascript 复制代码
const args = Array.from(arguments); // 或 const args = [...arguments];

4.2 为什么要把this存到_this变量中?

newBind方法中,this原本指向调用newBind的原函数 ,但返回的新函数执行时,其内部的this指向会发生变化(浏览器中默认指向window,严格模式下为undefined)。提前将this保存到_this,是为了保留原函数的引用 ,确保后续能通过_this.apply()调用原函数。

五、总结

  1. callapplybind的核心都是改变函数的this指向,区别在于执行时机和传参方式;
  2. callapply立即执行,前者逐个传参,后者数组传参;bind返回新函数,延迟执行且支持预传参;
  3. 手写实现的核心思路是通过对象挂载函数 实现this绑定,再通过apply/call执行原函数并处理参数。

掌握这三个方法的使用与底层逻辑,能让我们更灵活地处理 JavaScript 中的this指向问题,也是前端面试中的高频考点。希望本文能帮助大家彻底理解并掌握这一知识点!

相关推荐
iccb10132 小时前
客服系统前端主题配色动态切换的一种实现思路(含代码)
前端
karshey2 小时前
【前端】svelte支持scss,包管理器是webpack
前端·webpack·scss
A24207349302 小时前
深入理解JS DOM:从基础操作到性能优化的全面指南
开发语言·javascript·ecmascript
Zyx20072 小时前
手写 `new`:揭开 JavaScript 实例化背后的秘密
javascript
Можно2 小时前
深入理解 HTML 中的 iframe:特性、用法与现代实践
前端·html
布局呆星2 小时前
Vue 3 事件处理与列表渲染---02
前端·javascript·vue.js
漫天黄叶远飞2 小时前
🎄2025年圣诞节,单身的我只能用 Gemini 3 “嘴遁”出了一棵赛博圣诞树
前端·人工智能·gemini
云舟吖2 小时前
Chrome 扩展开发指南:从入门到精通 Manifest V3
前端·chrome·前端框架
syt_10132 小时前
设计模式之-状态模式
javascript·设计模式·状态模式