每天搞透一道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

相关推荐
高山我梦口香糖26 分钟前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_7482352429 分钟前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240251 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar1 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人2 小时前
前端知识补充—CSS
前端·css
GISer_Jing2 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245522 小时前
吉利前端、AI面试
前端·面试·职场和发展
理想不理想v2 小时前
webpack最基础的配置
前端·webpack·node.js
pubuzhixing3 小时前
开源白板新方案:Plait 同时支持 Angular 和 React 啦!
前端·开源·github
2401_857600953 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js