别再混淆 call/apply/bind 了!一篇讲透用法、场景与手写逻辑(二)

this 关键字是JavaScript 中最复杂的机制之一。

上一篇文章:this基础

而在 JavaScript 中,apply、call和bind函数 都是用来改变this指向的函数,理解这三个函数有助于读者更好的理解this,本文将带读者来从基础用法 ,到理解函数 的原理再到实现自己的apply、call和bind函数

一、call\apply\bind的用法

1.1 call函数

call 函数是挂载在 Function 对象上的原型上的一个函数,也就是 Function.prototype.call(),传入一个对象和参数。

Function.prototype.call() - JavaScript | MDN

作用 :立即调用函数,并指定 this 指向,同时传递参数(参数需逐个传入)。

用法function.call(thisArg,arg1,arg2,...)

  • thisArg:要指向的对象
  • arg1,arg2,...: 传入的参数(需要依次传入)

用法示例:简单写一个测试用例帮助读者理解

js 复制代码
 function test(a, b) {
  console.log(this.name, a + b);
}
const obj = { name: 'test' };
test.call(obj, 1, 2); // 输出 "test 3"

适用场景:当参数数量不确定,或已存在一个数组 / 类数组时(如动态获取的参数列表)。

1.2 apply函数

apply 函数是挂载在 Function 对象上的原型上的一个函数,也就是 Function.prototype.apply(),传入对象和参数数组。

Function.prototype.apply() - JavaScript | MDN

作用 :立即调用函数,并指定 this 指向,同时传递参数(参数以数组或类数组对象传入)。

用法function.apply(thisArg, [argsArray])

  • thisArg:要指向的对象
  • [argsArray]: 参数数组(或类数组,如 arguments 对象))

用法示例:简单写一个测试用例帮助读者理解

js 复制代码
function test(a, b) {
  console.log(this.name, a + b);
}
const obj = { name: 'test' };
test.apply(obj, [1, 2]); // 输出 "test 3"

// 测试 context 为 null
test.apply(null, [3, 4]); // 非严格模式下输出 "window 7"(浏览器环境)

// 测试 argsArray 为类数组
const args = { 0: 5, 1: 6, length: 2 };
test.apply(obj, args); // 输出 "test 11"

适用场景:当参数数量不确定,或已存在一个数组 / 类数组时(如动态获取的参数列表)。

1.3 bind函数

bind 函数是挂载在 Function 对象上的原型上的一个函数,也就是 Function.prototype.bind(),传入对象和参数,创建一个新函数,返回的函数可以再继续传参。

Function.prototype.bind() - JavaScript | MDN

作用 :创建一个新函数 (绑定函数),指定其 this 指向和预设参数,但不会立即执行

用法function.bind(thisArg, arg1, arg2, ...)

  • thisArg:要指向的对象
  • arg1, arg2...:预设的参数(调用新函数时可补充剩余参数)

用法示例:简单写一个测试用例帮助读者理解

js 复制代码
//1.基础测试用例
function test(a, b) {
  console.log(this.name, a + b);
}
const obj = { name: 'test' };
test.bind(obj, 1, 2)(); // 输出 "test 3"

// 2. context 为 null 测试(非严格模式)
test.bind(null, 3, 4)(); // 输出 "window 7" 浏览器环境

// 3. 类数组参数处理(需手动转为数组)
const args = { 0: 5, 1: 6, length: 2 };
test.bind(obj, ...Array.from(args))(); // 输出 "test 11"(需解构类数组)

// 4. 作为构造函数测试
function Person(name) {
  this.name = name;
}
const BoundPerson = Person.bind({}); // 绑定到空对象
const p = new BoundPerson("Bob");
console.log(p.name); // "Bob"(this 指向实例)
console.log(p instanceof Person); // true(继承原型)

适用场景:

  • 需要延迟执行函数(如事件回调、定时器)。
  • 固定函数的 this 指向(避免回调中 this 丢失)。
  • 预设部分参数(函数柯里化)。

不知道柯里化是什么?笔者带你理解函数式编程------柯里化 写多参数函数总重复传值?用柯里化3步搞定参数复用与延迟执行(一) - 掘金

二、实现call\apply\bind函数

每个从 Symbol() 返回的 symbol 值都是唯一的 Symbol - JavaScript | MDN

2.1 手搓 myCall()函数

改变this指向的核心逻辑就是,将函数绑定到需要改变指向的对象上(thisArg),也就是第一个参数thisArg ,再执行函数,此时this就指向的就是该对象。

这句话没看懂没关系,可以看看笔者实现的一个简约版的 myCall() 函数

js 复制代码
Function.prototype.myCall = function (context, ...args) {
  context.key = this;//这个 this 指向的是 function
  //如果这里看不懂:可以看看读者上一篇关于 this 的文章中的 new 绑定规则 
  const result = context.key(...args);// context.key(...args) === function(...args)
  //这里context.key(...args) 中的 this 指向的是 context 对象(注意这里的this不是上方的this)
  // 如果这里看不懂:可以看看读者上一篇关于 this 的文章中的 隐式 绑定规则 
  return result; // 返回结果
};

如果上方的myCall函数看懂了,恭喜读者理解了本文核心想分享的思想逻辑临时挂载函数到目标上下文,笔者添加一些细节完全实现 myCall函数,如下:

js 复制代码
Function.prototype.myCall = function (context, ...args) {
  context = context || window;//为空则默认绑定为 window(非严格 浏览器环境下)
  if (typeof context !== 'object' && typeof context !== 'function') {
    context = Object(context);//错误传参数或者不传入对象,则手动创建一个对象
  }
  const key = Symbol('myCall');//确保唯一键
  context[key] = this;//将函数挂载到对象 context 中
  const result = context[key](...args);// 执行函数
  delete context[key];//卸载该函数 确保执行完后 不修改 context 对象
  return result;
};
// 测试
function test(a, b) {
  console.log(this.name, a + b);
}
const obj = { name: 'test' };
test.myCall(obj, 1, 2); // 输出 "test 3"(正确)

2.2 手搓 myApply()函数

myApply函数和myCall函数的实现方法大致一致,只有在参数处理上有一点区别,笔者就直接将函数给出,并附带测试用例,就不赘述 myApply函数了。

js 复制代码
Function.prototype.myApply = function (context, argsArray) {
  context = context || {};//为空则默认绑定为 {}(这里用 node 环境|严格环境)
  const key = Symbol("myApply");
  if (typeof context !== 'object' && typeof context !== 'function') {
    context = Object(context);
  }
  context[key] = this
  const args = argsArray ? Array.from(argsArray) : []; //类数组的对象转换成数组
  const result = args ? context[key](...args) : context[key]();
  //是否存在参数 存在则带参调用函数,否则直接调用函数
  delete context[key]
  return result
};

// 测试基本功能
function test(a, b) {
  console.log(this.name, a + b);
}
const obj = { name: 'test' };
test.myApply(obj, [1, 2]); // 输出 "test 3"

// 测试 context 为 null
test.myApply(null, [3, 4]); // 非严格模式下输出 "window 7"(浏览器环境)| 严格模式下输出 "undefined 7"

// 测试 argsArray 为类数组对象(需手动转为数组)
const args = { 0: 5, 1: 6, length: 2 };
test.myApply(obj, args); // 输出 "test 11"

2.3 手搓 myBind函数

myBind函数核心逻辑和另外两个函数一致,区别于会收集旧参数创建一个新函数 ,并在闭包中保存旧参数和原函数 ,最后调用新函数 时调用旧函数再将旧参数和新参数给到旧函数执行

有前面的基础,myBind函数读者应该很容易理解了,这里笔者就直接给出该函数的注释、代码和测试代码,提供读者理解myBind

js 复制代码
Function.prototype.mybind = function(context, ...args) {
  const self = this; // 保存原函数
  if (context === null || context === undefined) {
    context = typeof window !== 'undefined' ? window : global;
    //这里将 ( context = context || {} )和 (context = context||window)进行改进
    //防止写在 myApply 和 myCall 增加代码阅读难度
  }
  if (typeof context !== 'object' && typeof context !== 'function') {
    context = Object(context);
  }
  function boundFn(...newArgs) {
  //闭包:...args  self
    const ctx = this instanceof boundFn ? this : context;
    // 如果是 new 调用,this 指向实例,否则指向 context
    return self.apply(ctx, [...args, ...newArgs]);
  }
  boundFn.prototype = Object.create(self.prototype);
  //将原函数的原型挂载到新函数上 确保新函数和原函数的原型一致 
  boundFn.prototype.constructor = boundFn;
  // 修复新函数原型上的的构造器为 新函数boundFn 
  return boundFn;//返回函数体
};

// 1. 基本绑定测试
function test(a, b) {
  console.log(this.name, a + b);
}
const obj = { name: 'test' };
test.mybind(obj, 1, 2)(); // 输出 "test 3"(正确)

// 2. context 为 null 测试(非严格模式)
test.mybind(null, 3, 4)(); // 输出 "window 7"(浏览器环境,正确)

// 3. 类数组参数处理(需手动转为数组)
const args = { 0: 5, 1: 6, length: 2 };
test.mybind(obj, ...Array.from(args))(); // 输出 "test 11"(正确,需解构类数组)

// 4. 作为构造函数测试
function Person(name) {
  this.name = name;
}
const BoundPerson = Person.mybind({}); // 绑定到空对象
const p = new BoundPerson("Bob");
console.log(p.name); // "Bob"(this 指向实例,正确)
console.log(p instanceof Person); // true(继承原型,正确)

三、总结

callapplybind核心差异在于调用时机与参数传递

  • callapply立即执行,前者逐个传参、后者以数组 / 类数组传参。
  • bind返回新绑定函数不立即执行,可预设参数。

三者适用场景各有侧重

  • call/apply适配立即执行场景,apply更适合数组参数。
  • bind适配延迟执行、固定this或函数柯里化。
相关推荐
潜心编码5 小时前
基于vue的停车场管理系统
前端·javascript·vue.js
神奇的小猴程序员5 小时前
Mutantcat Web Pdf Reader —— 开源、轻量、安全的网页 PDF 阅读器
前端·pdf
三小河5 小时前
React Vite 中动态批量导入路由
前端·vue.js
Qinana5 小时前
📚 论如何用代码谈一场不露脸的恋爱
前端·前端框架·html
Forfun_tt5 小时前
xss-labs pass-10
java·前端·xss
T___T5 小时前
从 "送花被拒" 到 "修成正果":用 JS 揭秘恋爱全流程中的对象与代理魔法
前端·javascript
三小河5 小时前
从私服版本冲突到依赖治理:揭秘 resolutions 配置
前端·javascript·架构
Mapmost5 小时前
你的3DGS数据为何难以用在项目里?Web端开发实战指南
前端
举个栗子dhy5 小时前
第一章、React + TypeScript + Webpack项目构建
前端·javascript·react.js