每天搞透一道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 方法为例,它有哪些特点呢:

  • 调用对象应该是一个函数,否则会抛出类型错误。
  • 在非严格模式下,如果绑定的 thisArg 为 null 或 undefined,thisArg 会被指向 globalThis。
  • 在严格模式下,如果绑定的 thisArg 为 null 或 undefined,thisArg 会保持原来的值。
  • 如果绑定的 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

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

  • call 直接传递一个参数列表,而 apply 接受一个参数数组。这也为它带来了一个区别于 call 的特点:如果传给 apply 的第二个参数不是一个数组,会抛出一个错误 'CreateListFromArrayLike called on non-object'。
  • 第二个参数如果是 null 或 undefined,在 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);

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

相关推荐
子兮曰4 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰4 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万5 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝5 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋5 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
晨米酱5 天前
AGENTS.md:Agent 的上下文策略层
面试·架构·agent
卡布鲁5 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
李少兄5 天前
JavaScript 隐式全局变量解析
javascript
彧azz5 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
汉堡大王95275 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端