手写和应用 call/apply/bind 的文章参差不齐,总结分享个全面的

本文你能学到什么

欢迎关注公众号《前端界》,文章会同步更新,也可快速加入前端交流群!

前言

最近在复习 JavaScript 手写代码。想搜一下 call/apply/bind实现,发现搜的结果参差不齐,有的是不对的,有的长篇大论不够精简缺应用场景,于是自己手写总结分享下,希望对看到的同学有帮助,同时也是为了方便以后自己复习吧!

call 实现

call 函数内部做了什么

  1. 函数先通__proto__原型链找到 Function.prototype 上的 call 函数
  2. 确定 this 为执行的函数
  3. 接下来要执行函数,但是执行函数的上下文需要是传递进来的第一个参数,所以想办法修改函数执行上下文
  4. 正式执行函数,并返回执行结果

基础版代码实现

根据前面的分析开始编码

js 复制代码
Function.prototype.call = (context,...arguments){
  // 第一步:this 就是对应执行的函数,也就是调用函数 fun
  const self = this; 
  // 第二步:想要执行函数时需要函数的上下文为传入的上下文,想要改变调用上下文最好的办法是直接用上下文对象调用函数,这时候函数内部的 this 就指向了上下文 
  // 一个小结论 xx1.xx2 xx2的 this 就指向了 xx1
  // 使用 symbol 类型可以保证属性名的唯一性,而且不会被遍历枚举出来
  const symbolKey = Symbol('fun');  
  context[symbolKey] = self; 
  // 第三步:执行函数
  const result = context[symbolKey](...arguments); 
  // 删除 symbol 类型的 key
  delete context[symbolKey];
  // 返回结果
  return result; 
}

const fun = function(param){
  let a = this.a;
  console.log(a,param);
}

fun.call({a:234},'哈哈哈');

升级版代码实现

上面实现的代码有些问题

  1. 如果上下文传递的为 null ,会报错 funA.call();
  2. 如果上下文传递的是整数类型,会报错 funA.call(123);

针对以上两种情况,我们升级一下扩展代码

js 复制代码
Function.prototype.callUpgrade =  function(context,...arguments){
  // 校验context是否为空,如果为空,默认值给 window
  context === null ? context = window : null; 
  // 校验上下文是否为对象,对象基础类型情况需要创建一个对象
  !/^(object|function)$/i.test(typeof context) ? context = Object(context) : null; 
  const self = this;
  const symbolKey = Symbol('fun');
  context[symbolKey] = self;
  const result = context[symbolKey](...arguments);
  delete context[symbolKey];
  return result;
}

const fun = function(param){
  const a = this.a;
  console.log(a);
}
fun.call();// 空上下文情况不报错
fun.call(123);// 非对象情况上下文不报错

apply 实现

apply 不过多说明,因为他与 call 唯一的区别是传递参数不同。call 可以传递任意数量的参数,这些参数会作为函数的参数按顺序传递进去。apply 第二个参数只接受一个数组或者类数组对象。

代码实现

js 复制代码
Function.prototype.bind = function(context,arguments){
  context === null ? context = window : null;
  !/^(object|function)$/i.test(typeof context) ? context = Object(context) : null;
  const self = this;
   const symbolKey = Symbol('fun');
  context[symbolKey] = self;
  const result = context[symbolKey](...arguments);
  delete context[symbolKey];
  return result;
}

手写时候使用箭头函数,箭头函数中的 this 指向上一级别

bind 实现

bind 内部做了什么

  1. 先通过__proto__原型连找到 Function.prototype 上的 bind 函数
  2. bind 函数内部没有把函数立即执行,是将传来的信息通过必包方式存储起来
  3. bind 实际返回了一个新的函数。
  4. 这个新函数内容不完成了函数执行,并把 this 上下文和参数改变为之前存储的内容

基础代码实现

js 复制代码
// 使用 call 实现 bind 函数
Function.prototype.bind = function(context,...args){
  let self = this;
  return function Fun(...params){
     self.call(context,...args,...params)
  }
}
// 使用 apply 实现 bind 函数
Function.prototype.bind = function(context,...args){
  let self = this;
  return function Fun(...params){
    self.apply(context,args.concat(params))
  }
}

call/apply/bind 的几个有趣应用场景

apply 数组展开能力

开发过程中我想合并一个数组到目标数组(数据添加),但是不想生成新的数组。

js 复制代码
let target = [1,2,3];
let source = [4,5,6];
  • 扩展运算符 [...target,...source] 是做不到的,会生成新数组
  • target.concat(source) 也是做不到的,也是生成新数组
  • 难道我要 for 循环,然后每一个值 push 进去吗?有没有优雅点的方式

可以使用 apply 函数

js 复制代码
Array.prototype.push.apply(target,source)

apply 函数的第一个参数是调用的函数上下文(即 this 值),第二个参数是一个数组或类数组,会被展开并作为参数传递。在 push 函数内部会遍历将传递进来的参数放到数组末尾。其实和我们 for 循环遍历添加是相同的

因为 Array.prototype.push 本身支持多个参数,你也可以直接使用扩展运算符,target.push(...source)

bind 实现函数柯里化

首先理解一下什么是柯里化,是一种将接受多个参数的函数转化为一系列接受一个参数的函数的过程。这种转化使得函数更加灵活,可以通过部分应用来创建新的函数,从而在需要的时候传递剩余的参数。

函数柯里化有助于在函数式编程中实现复杂的函数组合和延迟求值,提高代码的可维护性和复用性。

举个例子来理解一下柯里化的概念

js 复制代码
/**
 * 原始的普通函数,接受两个参数并返回它们的乘积
 * */
function multiply(x, y) {
  return x * y;
}
/*
 * 使用函数柯里化,将上述函数转化为接受一个参数的函数
 * */
function curriedMultiply(x) {
  return function(y) {
    return x * y;
  };
}

// 创建一个新函数,只需要传递一个参数
const double = curriedMultiply(2);

// 使用新函数来获取传递的参数的两倍
const result = double(5); // 2 * 5 = 10

函数柯里化的好处在于,您可以在需要的时候提供部分参数,然后在稍后的调用中提供剩余的参数。这对于实现通用的函数,避免重复代码以及构建复杂的函数组合非常有用。

介绍完基础概念,我们回到本文主题 bind。更简洁的实现函数柯里化,上面的例子我们可以用 bind 来创建一个新函数

js 复制代码
const multiply = (x,y)=>x*y;
const double = multiply.bind(null,2);
const result = double(5);// 2*5 = 10

有趣的小知识,为什么这种实现叫柯里化,柯里化的概念得名于数学家 Haskell Curry,他是函数式编程的先驱之一。柯里化的概念最早可以追溯到 Lambda 演算,这是一种数学形式化的方法,用于描述函数的运算和变换。Haskell Curry 提出了一种将多参数函数转化为一系列单参数函数的思想,这样就能更好地进行函数组合和变换。

柯里化事件绑定应用

柯里化在前端开发中有许多应用场景,特别是函数式编程和高阶函数的概念中。它可以帮助简化代码,提高可复用性,并促进更模块化和函数式的编程风格。 再举一个事件处理函数的例子;

在前端开发中 ,处理事件常常需要传递附加的数据和上下文信息。柯里化可以用于创建高阶事件处理函数,从而将特定的事件处理逻辑通用的事件逻辑绑定分离

js 复制代码
// 使用柯里化创建高阶事件处理函数
const curriedEventHandler = eventData => event => {
  console.log(`Event: ${event.type},Data: ${JSON.stringify(eventData)}`)
}

const button = document.getElementById('myButton');
const handleClick = curriedEventHandler({action:"click"});

button.addEventListener("click",handleClick);

鸭子类型实现类数组转换

鸭子类型(Duck Typing) 是一种在动态语言中常见的类型检查方式。它关注的不是对象的具体类型,而是关注对象是否有特定的方法,属性或行为。如果一个对象的方法,属性或行为与某个类型的期望一致,那么就可以将该对象视为该类型。

类数组是一种具有索引和length属性,但本身本身不可以调用 Array.prototype 上面的函数的,比如forEach reduce 等原型上的函数。这时候可以使用 bind/call/apply等函数将数组方法应用于类数组函数。这样就拥有了数组原型上面的函数。

可以这么使用

js 复制代码
function sum() {
  return Array.prototype.reduce.call(arguments, (acc, val) => acc + val, 0);
}
const total = sum(1, 2, 3, 4, 5); // 15

也可以这么转换一下哦

js 复制代码
const duckArr = Array.prototype.slice.call(arguments,0);
console.log(Array.isArray(duckArr));// true

🦆鸭子类型 在工作中应用场景和思想还有很多,再举个例子。 当我们谈到迭代起模式时,鸭子类型 的应用是指对象只要有迭代器的行为(如 next()方法),那么这个对象就可以被视为迭代器,无论其实际类型是什么(符合前面提到的鸭子类型的概念)。这意味着你可以在不同类型的对象上使用统一的迭代方式,从而实现更加通用和灵活的代码。

鸭子类迭代器应用

下面举一个简单的例子,展示如何使用鸭子类型来实现迭代器模式:

js 复制代码
/**
 * 定义一个通用的迭代函数
 * */
function iterate(iterator) {
  let result = iterator.next();
  while (!result.done) {
    console.log(result.value);
    result = iterator.next();
  }
}

/**
 * 定义一个模拟的迭代器对象
 **/ 
const mockIterator = {
  index: 0,
  values: [1, 2, 3],
  next() {
    if (this.index < this.values.length) {
      return { done: false, value: this.values[this.index++] };
    } else {
      return { done: true };
    }
  }
};

// 使用迭代函数遍历不同类型的对象
iterate(mockIterator); // 输出: 1, 2, 3

在上述示例中,我们首先定义了一个通用的 iterate 函数,该函数接受一个迭代器对象并使用其 next() 方法来遍历对象的值。然后,我们定义了一个模拟的迭代器对象 mockIterator,它具有 next() 方法和要遍历的值。最后,我们使用 iterate 函数来遍历 mockIterator,实现了迭代器模式。

值得注意的是,iterate 函数不关心传入的迭代器对象的具体类型,只要对象具有期望的 next() 方法,就可以进行遍历。这就是鸭子类型的应用,通过关注对象的行为而不是类型,实现了更灵活的代码。

哈哈哈,发现一个自己的问题,其实总是想要写一篇小文章,但是越写展开的越多,收一下。

欢迎关注公众号《前端界》,文章会同步更新,也可快速加入前端交流群!

本文就到这里如果有收获,欢迎大家点赞,转发支持下。

相关推荐
HSunR3 分钟前
vue3 elementplus tabs切换实现
javascript·vue.js·elementui
搏博7 分钟前
WPS中代码段的识别方法及JS宏实现
开发语言·javascript·wps
三天不学习23 分钟前
VueUse/Core:提升Vue开发效率的实用工具库
前端·javascript·vue.js·vueuse
好青崧29 分钟前
等于和绝对等于的区别
javascript
余道各努力,千里自同风41 分钟前
CSS实现文本自动平衡text-wrap: balance
前端·css
小柳不言败1 小时前
面试题总结一
面试
半青年1 小时前
Qt图表库推荐指南与分析
c语言·开发语言·javascript·c++·qt·信息可视化
Yvonne爱编码1 小时前
CSS- 4.3 绝对定位(position: absolute)&学校官网导航栏实例
前端·css·html·html5·hbuilder
ShineSpark2 小时前
C++面试2——C与C++的关系
c语言·c++·面试
繁依Fanyi2 小时前
ImgShrink:摄影暗房里的在线图片压缩工具开发记
开发语言·前端·codebuddy首席试玩官