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

相关推荐
神仙别闹13 分钟前
基于tensorflow和flask的本地图片库web图片搜索引擎
前端·flask·tensorflow
aPurpleBerry37 分钟前
JS常用数组方法 reduce filter find forEach
javascript
sszmvb123441 分钟前
测试开发 | 电商业务性能测试: Jmeter 参数化功能实现注册登录的数据驱动
jmeter·面试·职场和发展
测试杂货铺1 小时前
外包干了2年,快要废了。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
王佑辉1 小时前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
真忒修斯之船1 小时前
大模型分布式训练并行技术(三)流水线并行
面试·llm·aigc
GIS程序媛—椰子1 小时前
【Vue 全家桶】7、Vue UI组件库(更新中)
前端·vue.js
DogEgg_0011 小时前
前端八股文(一)HTML 持续更新中。。。
前端·html
ZL不懂前端1 小时前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x1 小时前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint