每天搞透一道JS手写题💪「Day4手写函数的三种原型方法(call、apply、bind)」

还是老规矩,动手写代码前先复习一下:

方法 参数 返回值 作用
call thisArg, arg1, arg2, ... 函数的返回值 调用一个函数,将其 this 值设置为提供的值,并为其提供指定的参数。
apply thisArg, [arg1, arg2, ...] 函数的返回值 调用一个函数,将其 this 值设置为提供的值,并以数组形式为其提供参数。
bind thisArg, arg1, arg2, ... 新函数 创建一个新函数,当调用这个新函数时,它的 this 值被绑定到提供的值,并且它的参数序列前置指定的参数。

call

在手写一个原生的 API 之前,我们要先知道它在使用时的各种表现,才能更贴近地实现其功能。比如在上一期每天搞透一道JS手写题💪「Day3手写数组的六种原型方法中笔者提到的要用 Object.prototype.hasOwnProperty.call(this, i) 在遍历中判断当前数组的当前位置是空还是值为 undefined,就是因为数组原型方法中对这两者的处理是不一样的。

那么以 call 方法为例,它有哪些特点呢:

  • 调用对象应该是一个函数,否则会抛出类型错误。
  • 在非严格模式下,如果绑定的 thisArgnullundefinedthisArg 会被指向 globalThis。
  • 在严格模式下,如果绑定的 thisArgnullundefinedthisArg 会保持原来的值。
  • 如果绑定的 thisArg 不是引用数据类型,会被转换成对应的引用数据类型,如 1 变成 Number(1)。

核心的实现思路是,在需要绑定的 thisArg 指向的对象上创建一个调用 call 方法的函数的副本,在函数调用完毕后删除这个副本属性。接下来我们按照这个核心思路,同时注意原生 call 的特点,来实现我们的 myCall

js 复制代码
/**
 * 实现函数的call方法
 * @param {Object} thisArg - 函数运行时使用的this值
 * @param {...*} args - 传递给函数的参数列表
 * @returns {*} 函数执行的返回值
 */
Function.prototype.myCall = function(thisArg, ...args) {
  // 判断是否在严格模式下运行
  const isStrict = (function() { return !this; })();
  
  // 处理thisArg的值
  if (thisArg === null || thisArg === undefined) {
    thisArg = isStrict ? undefined : globalThis;
  } else {
    thisArg = Object(thisArg);
  }
  
  // 将函数作为thisArg的一个属性
  const fn = Symbol('fn');
  thisArg[fn] = this;
  
  // 调用函数并获取返回值
  const result = thisArg[fn](...args);
  
  // 删除添加的属性
  delete thisArg[fn];
  
  // 返回结果
  return result;
}
  • 在非严格模式下,函数独立调用 this 会指向 window;而在严格模式下,this 会是 undefined
  • 使用 Symbol 来创建一个唯一的属性名,这一步的目的是防止 thisArg 对象有同名的属性导致其被临时的副本属性覆盖。

apply

applycall 的功能基本一致,所以实现起来的核心思路也是一样的。它们的主要区别在于如何传递参数:

  • call 直接传递一个参数列表,而 apply 接受一个参数数组。这也为它带来了一个区别于 call 的特点:如果传给 apply 的第二个参数不是一个数组,会抛出一个错误 'CreateListFromArrayLike called on non-object'
  • 第二个参数如果是 nullundefined,在 call 中会被传递给调用的函数,但在 apply 中会被视为空数组,调用 apply 的函数不会有任何参数。
js 复制代码
/**
 * 实现函数的apply方法
 * @param {Object} thisArg - 函数运行时使用的this值
 * @param {Array} argsArray - 传递给函数的参数数组
 * @returns {*} 函数执行的返回值
 */
Function.prototype.myApply = function(thisArg, argsArray) {
  // 判断是否在严格模式下运行
  const isStrict = (function() { return !this; })();
  
  // 处理thisArg的值
  if (thisArg === null || thisArg === undefined) {
    thisArg = isStrict ? undefined : globalThis;
  } else {
    thisArg = Object(thisArg);
  }
  
  // 检查argsArray是否为数组或类数组对象
  if (!Array.isArray(argsArray) && !(argsArray instanceof Object && 'length' in argsArray) && (argsArray !== null && argsArray !== undefined)) {
    throw new TypeError('CreateListFromArrayLike called on non-object');
  }
  
  // 将函数作为thisArg的一个属性
  const fn = Symbol('fn');
  thisArg[fn] = this;
  
  // 调用函数并获取返回值, 展开运算符对 null 或 undefined 使用会报错,故需判断
  const result = argsArray ? thisArg[fn](...argsArray) : thisArg[fn]();
  
  // 删除添加的属性
  delete thisArg[fn];
  
  // 返回结果
  return result;
}

bind

bind 方法和前面两种方法不同,它是创建了一个新的函数,这个新的函数的 this 值永远指向绑定的 thisArg 对象。它也具备 call 方法的那四个特点,额外还有一个需要注意的特点:

  • 如果绑定后的函数被用作构造函数,那么新创建的对象会作为 thisArg 值传递给原函数。
js 复制代码
/**
 * 实现函数的bind方法
 * @param {Object} thisArg - 函数运行时使用的this值
 * @param {...*} args - 预设的参数列表
 * @returns {Function} 绑定后的新函数
 */
Function.prototype.myBind = function(thisArg, ...args) {
  // 检查调用对象是否为函数
  if (typeof this !== 'function') {
    throw new TypeError('Bind must be called on a function');
  }
  
  // 判断是否在严格模式下运行
  const isStrict = (function() { return !this; })();
  
  // 处理thisArg的值
  if (thisArg === null || thisArg === undefined) {
    thisArg = isStrict ? undefined : globalThis;
  } else {
    thisArg = Object(thisArg);
  }

  // 获取原函数引用
  const self = this;

  // 返回一个新函数,该新函数绑定了this值和预设参数列表
  return function(...newArgs) {
    // 如果作为构造函数调用,则忽略传入的thisArg,使用新创建的对象作为this值
    if (new.target) {
      thisArg = this;
    }

    // 调用原函数并返回结果,将预设参数和新传入参数拼接起来传递给原函数
    return self.apply(thisArg, [...args, ...newArgs]);
  }
}

作为构造函数调用的情况可能有一些不好理解,这里举个例子:

js 复制代码
// 定义一个测试函数
function TestFunc(name) {
  this.name = name;
}

// 使用bind方法绑定this值和参数
const BoundFunc = TestFunc.bind(null, 'Alice');

// 使用new运算符调用绑定后的函数
const obj = new BoundFunc();

// 输出: Alice
console.log(obj.name);

可以看到,即使绑定的 thisArgnull,最后的 this 仍然指向了新建的 obj

相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax