call()、apply()、bind() 区别、使用场景、实现方式

news2025/7/10 10:05:33

目录

1. call()、apply()、bind() 三者区别

1.1 作用

1.2 参数

1.3 执行时机

2. call()、apply() 使用场景

2.1 使用 Array.prototype.push.apply(arr1, arr2) 合并两个数组

2.1.1 原理(看了手写方法,或许会更有助于理解)

2.1.2 如何解决参数过多的问题呢?—— 将参数数组切块,循环传入目标方法

2.2 获取数组中的最大值和最小值

2.3 使用 Object.prototype.toString() 验证是否是数组

2.4 类数组对象(Array-like Object)使用数组方法 

2.4.1 什么是类数组对象?为什么要出现类数组对象?

2.4.2 使用 Array.prototype.slice.call 将 类数组对象 转换为 数组 

2.4.3 使用 ES6 提供的 Array.form / 解构赋值 实现 类数组对象 转 数组

2.5 调用父构造函数实现继承

3. bind() 使用场景

3.1 使用 Object.prototype.toString() 验证是否是数(bind 版)

3.2 柯里化(curry)

4. 手写 call()、apply()、bind()

4.1 实现 call()、apply()

4.1.1 实现思路

4.1.2 实现代码

4.1.3 优化版本(具体请阅读下方参考文章木易杨老师的博客,此处仅放结果)

4.2 实现 bind()

4.2.1 实现思路

4.2.2 实现代码

5. 参考链接


1. call()、apply()、bind() 三者区别

1.1 作用

call()、apply()、bind() 都用于 显式绑定 函数的 this 指向

1.2 参数

call()、apply()、bind() 第一个参数相同:都代表 this 要指向的对象(若该参数为 undefined 或 null 或 不传参,this 则默认指向全局 window)

call()、apply()、bind() 除第一个传参外的其他参数不同:

  • call() 是参数列表 arguments
  • apply() 是数组
  • bind () 可以分多次传入,实现参数合并

1.3 执行时机

call()、apply() 是立即执行

bind() 是返回绑定 this 之后的新函数,需要手动调用;如果这个新函数作为 构造函数 被调用,那么 this 不再指向传入 bind() 的第一个参数,而是指向新生成的对象

2. call()、apply() 使用场景

再来回忆一遍这两位的区别:

var func = function(arg1, arg2) {
     ...
};

func.call(this, arg1, arg2); // 使用 call,参数列表
func.apply(this, [arg1, arg2]) // 使用 apply,参数数组

2.1 使用 Array.prototype.push.apply(arr1, arr2) 合并两个数组

2.1.1 原理(看了手写方法,或许会更有助于理解)

  • 使用 apply 将 Array.prototype.push 这个函数方法的 this 指向改成 arr1
  • 也就是说:arr1 现在有一个 push 属性方法
  • 又因为 apply 改变 this 指向后,会直接执行函数
  • 所以 arr1 会直接调用 push 方法,并接收 arr2 传来的参数数组
  • 最终实现数组合并

注意:

  • arr2 数组不能太大,因为一个函数能接受的参数个数有限,JavaScript 核心限制在 65535
  • 不同引擎限制不同,如果参数太多,可能会报错,也可能不会报错但参数丢失

2.1.2 如何解决参数过多的问题呢?—— 将参数数组切块,循环传入目标方法

具体实现步骤:

  • 定义每次连接的数组,最多有 groupNum 个元素
  • 需要连接的数组 arr2 总长度设为 len
  • 使用 for 循环,每循环一次,i 增加一个分组那么多
  • 也就是说,每循环一次,就连接原数组 和 新数组的第 i 个分组
  • 最后一个分组,如果元素不够,则直接截取到最后,也就是 arr2.length
function concatOfArray(arr1, arr2) {
  // 数组分组后,每组元素个数
  var groupNum = 32768;
  var len = arr2.length;
  // 每循环一次,数组都添加一组个数
  for (var i = 0; i < len; i += groupNum) {
    // 当最后一组个数不足 groupNum 时,直接截取到最后即可,也就是 len
    // 一块一块连接数组
    Array.prototype.push.apply(arr1, arr2.slice(i, Math.min(i + groupNum, len)));
  }
  return arr1;
}

// 验证代码
var arr1 = [-3, -2, -1];
var arr2 = [];
for (var i = 0; i < 1000000; i++) {
  arr2.push(i);
}

Array.prototype.push.apply(arr1, arr2);
// Uncaught RangeError: Maximum call stack size exceeded

concatOfArray(arr1, arr2);
// (1000003) [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]

2.2 获取数组中的最大值和最小值

  • 数组没有直接获取最大最小值的方法,但是 Math 有
  • 使用 call 将 Max.max 这个方法的 this 指向绑定到 Math 上
  • 由于 call 会让绑定后的函数立刻执行,因此接收到 数组 后,Math 会立即执行寻找最值
var numbers = [5, 458 , 120 , -215 ]; 

Math.max.apply(Math, numbers); // 458    

Math.max.call(Math, 5, 458 , 120 , -215); // 458

// ES6
Math.max.call(Math, ...numbers); // 458

2.3 使用 Object.prototype.toString() 验证是否是数组

不同对象的 toString() 有不同的实现,可以通过 Object.prototype.toString() 获取每个对象的类型

使用 call()、apply() 实现检测,下面是我在 chrome 中打印的效果

 

因此,可以这么封装:

function isArray(obj){ 
    return Object.prototype.toString.call(obj) === '[object Array]';
}

isArray([1, 2, 3]); // true

2.4 类数组对象(Array-like Object)使用数组方法 

2.4.1 什么是类数组对象?为什么要出现类数组对象?

JavaScript 中有一种对象,结构非常像数组,但其实是个对象:

  • 类数组对象不具有:push、shift、forEach、indexOf 等数组方法
  • 类数组对象具有:指向对象元素的 数字索引下标 length 属性

常见的类数组对象:

  • arguments 参数列表
  • DOM API 返回的 NodeList

类数组对象出现的原因:为了更快的操作复杂数据。

JavaScript 类型化数组是一种类似数组的 对象,并提供了一种用于访问原始二进制数据的机制。Array存储的对象能动态增多和减少,并且可以存储任何 JavaScript 值。JavaScript 引擎会做一些内部优化,以便对数组的操作可以很快。然而,随着 Web 应用程序变得越来越强大,尤其一些新增加的功能例如:音频视频编辑,访问 WebSockets 的原始数据等,很明显有些时候如果使用 JavaScript 代码可以快速方便地通过类型化数组来操作原始的二进制数据,这将会非常有帮助。

2.4.2 使用 Array.prototype.slice.call 将 类数组对象 转换为 数组 

slice 将 Array-like 类数组对象,通过下标操作,放进了新的 Array 里面:

  • 将数组的 slice 方法,通过 call 改变 this 指向,绑定到需要修改的类数组对象;
  • 由于 call 会在修改完绑定后自动执行函数,因此 类数组对象 调用它被绑的 slice 方法,并返回了真的数组
// 类数组对象 不是数组,不能使用数组方法
var domNodes = document.getElementsByTagName("*");
domNodes.unshift("h1");
// TypeError: domNodes.unshift is not a function

// 使用 Array.prototype.slice.call 将 类数组对象 转换成 数组
var domNodeArrays = Array.prototype.slice.call(domNodes);
domNodeArrays.unshift("h1");
// ["h1", html.gr__hujiang_com, head, meta, ...] 

也可以这么写,简单点 —— var arr = [].slice.call(arguments);

注意:此方法存在兼容性问题,在 低版本IE(< 9) 下,不支持 Array.prototype.slice.call(args),因为低版本IE下的 DOM 对象,是以 com 对象的形式实现的,JavaScript 对象与 com 对象不能进行转换

2.4.3 使用 ES6 提供的 Array.form / 解构赋值 实现 类数组对象 转 数组

Array.from() 可以将两种 类对象 转为 真正的数组:

  • 类数组对象(arguments、NodeList)
  • 可遍历(iterable)对象(包括 ES6 新增的数据结构 Set 和 Map)

let arr = Array.from(arguments);
let arr = [...arguments];

2.5 调用父构造函数实现继承

在子构造函数中,通过调用父构造函数的 call()方法,实现继承

SubType 的每个实例都会将SuperType 中的 属性/方法 复制一份

function  SuperType(){
    this.color=["red", "green", "blue"];
}
function  SubType(){
    // 核心代码,继承自SuperType
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.color.push("black");
console.log(instance1.color);
// ["red", "green", "blue", "black"]

var instance2 = new SubType();
console.log(instance2.color);
// ["red", "green", "blue"]

缺点:

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现复用,每个子类都有父类实例函数的副本,影响性能 

3. bind() 使用场景

再来回忆下 bind() 使用方法:

fun.bind(thisArg[, arg1[, arg2[, ...]]])

bind() 方法会创建一个新函数,当这个新函数被调用时,它的 this 值是传递给 bind() 的第一个参数,传入bind方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

bind 返回的绑定函数也能使用 new 操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

bind() 是 ES5 加入的,IE8 以下的浏览器不支持

3.1 使用 Object.prototype.toString() 验证是否是数(bind 版)

var toStr = Function.prototype.call.bind(Object.prototype.toString);

function isArray(obj){ 
    return toStr(obj) === '[object Array]';
}

isArray([1, 2, 3]); // true

// 使用改造后的 toStr
toStr([1, 2, 3]); // "[object Array]"
toStr("123"); // "[object String]"
toStr(123); // "[object Number]"
toStr(Object(123)); // "[object Number]"

注意:如果 toString() 方法被覆盖了,则上述方法无法使用:

Object.prototype.toString = function() {
    return '';
}

isArray([1, 2, 3]); // false

3.2 柯里化(curry)

只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

如下所示:

  • 定义了一个 add 函数,它接受一个参数,并返回一个新的函数。
  • 调用 add 之后,返回的函数通过 闭包 的方式,记住了 add 的第一个参数
  • 所以说 bind 本身也是 闭包 的一种使用场景
var add = function(x) {
  return function(y) {
    return x + y;
  };
};

var increment = add(1);

var addTen = add(10);

increment(2);
// 3

addTen(2);
// 12

add(1)(2);
// 3

 

4. 手写 call()、apply()、bind()

4.1 实现 call()、apply()

4.1.1 实现思路

关键点:

  • call() 改变了 this 指向
  • call() 改变指向后会立刻调用并执行函数

因此得出以下思路:

  • 先判断 即将绑定的对象 context 是否有值,如果是 null、undefined、空,则将 this 改成 window
  • 将调用 call() 的函数(相当于 this,谁调用 call,谁就是 this),作为属性(属性必须独一无二,防止 context 上已经有同名的属性),添加到 即将绑定的对象 context 上
  • 执行 context 上新增加的属性方法,传入参数列表,借助隐式绑定,将属性方法的 this 绑定到 context 上
  • 执行完成后,删除 context 上新增加的属性,并返回执行结果

4.1.2 实现代码

由于 call()、apply() 仅接收值不同,此处仅用 call() 做例子

实现 apply(),只需要将 入参 ....args,替换为 args 即可(多个参数转换为一个数组参数)

/**
 * 实现 call
 * @param context 要将函数显式绑定到哪个对象上
 * @param args 参数列表
 */
Function.prototype.Call = function (context, ...args) {
  // context 为 undefined 或 null 或 不传参 时,则 this 默认指向全局 window
  if (!context || context === null || context === undefined) {
    context = window;
  }
  // 利用 Symbol 创建一个唯一的 key 值,防止新增加的属性与 context 中的属性名重复
  let fn = Symbol();

  // 把调用 Call 的函数,作为属性,赋给即将绑定的对象
  // 比如 foo.Call(context),把 foo 作为属性,赋值给 context
  context[fn] = this;

  // Call 显示绑定后,函数会自动执行
  // 因此此处调用 context 上新增的属性 fn,也就是 foo 方法
  // 方法执行时,谁调用,就隐式绑定到谁身上,此处 foo 方法就被隐式绑定到了 context 上
  let res = context[fn](...args);

  // 执行完成后,删除新增加的 fn 属性
  delete context[fn];
  return res;
};

4.1.3 优化版本(具体请阅读下方参考文章木易杨老师的博客,此处仅放结果)

ES3 call:
Function.prototype.call = function (context) {
    context = context ? Object(context) : window; 
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}

ES6 call:
Function.prototype.call = function (context) {
  context = context ? Object(context) : window; 
  context.fn = this;

  let args = [...arguments].slice(1);
  let result = context.fn(...args);

  delete context.fn
  return result;
}

ES3 apply:
Function.prototype.apply = function (context, arr) {
    context = context ? Object(context) : window; 
    context.fn = this;

    var result;
    // 判断是否存在第二个参数
    if (!arr) {
        result = context.fn();
    } else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')');
    }

    delete context.fn
    return result;
}

// ES6 apply:
Function.prototype.apply = function (context, arr) {
    context = context ? Object(context) : window; 
    context.fn = this;
  
    let result;
    if (!arr) {
        result = context.fn();
    } else {
        result = context.fn(...arr);
    }
      
    delete context.fn
    return result;
}

4.2 实现 bind()

4.2.1 实现思路

关键点:

  • bind() 改变了 this 指向
  • bind() 返回一个函数
  • 可以传入参数
  • 柯里化

4.2.2 实现代码

/**
 * 实现 bind
 * @descripttion bind 要考虑返回的函数,作为 构造函数 被调用的情况
 * @param context 要将函数显式绑定到哪个对象上
 * @param args 参数列表
 */
Function.prototype.Bind = function (context, ...args) {
  // context 为 undefined 或 null 或 不传参 时,则 this 默认指向全局 window
  if (!context || context === null || context === undefined) {
    context = window;
  }
  // 利用 Symbol 创建一个唯一的 key 值,防止新增加的属性与 context 中的属性名重复
  let f = Symbol();

  // 此处的 fn 表示调用 Bind 的函数,或者 函数新创建的对象
  // 比如 foo.Bind(obj),this 就代表 foo
  // 再比如 const a = new foo.Bind(obj)(); Bind 返回的函数作为构造函数使用,则 this 就代表新创建的对象 a
  let fn = this;

  const result = function (...args1) {
    // this instanceof fn —— 用于判断 new 出来的对象是否是 fn 的实例
    if (this instanceof fn) {
      // result 如果作为构造函数被调用,this 指向的是 new 出来的对象
      this[f] = fn;
      let res = this[f](...args, ...args1);
      // 执行完成后,删除新增加的属性
      delete this[f];
      return res;
    } else {
      // result 如果作为普通函数被调用,this 指向的是 context
      context[f] = fn;
      let res = context[f](...args, ...args1);
      // 执行完成后,删除新增加的属性
      delete context[f];
      return res;
    }
  };
  // 如果绑定的是构造函数,那么需要继承构造函数原型属性和方法
  // 使用 Object.create 实现继承
  result.prototype = Object.create(fn.prototype);
  // Bind 函数被调用后,返回一个新的函数,而不是直接执行
  return result;
};

5. 参考链接

深度解析 call 和 apply 原理、使用场景及实现 | 木易杨前端进阶高级前端进阶之路icon-default.png?t=M85Bhttps://muyiy.cn/blog/3/3.3.html#%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF深度解析bind原理、使用场景及模拟实现 | 木易杨前端进阶高级前端进阶之路icon-default.png?t=M85Bhttps://muyiy.cn/blog/3/3.4.html#bind

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/19704.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

微电网两阶段鲁棒优化(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

LeetCode刷题复盘笔记—一文搞懂62. 不同路径 63. 不同路径 II(动态规划系列第三篇)

今日主要总结一下动态规划的两道题目&#xff0c;62. 不同路径 && 63. 不同路径 II 题目一&#xff1a;62. 不同路径 题目描述&#xff1a; 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或…

HTML CSS 网页设计作业「体育小站」(梅西足球 6页 )

⛵ 源码获取 文末联系 ✈ Web前端开发技术 描述 网页设计题材&#xff0c;DIVCSS 布局制作,HTMLCSS网页设计期末课程大作业 | 校园篮球网页设计 | 足球体育运动 | 体育游泳运动 | 兵乓球 | 网球 | 等网站的设计与制作| HTML期末大学生网页设计作业&#xff0c;Web大学生网页 HT…

JavaScript之PC端网页特效(55th)

在前面学习了JS基础、DOM 和 BOM 的基本操作后&#xff0c;这部分主要学习这些知识的拓展应用 1、元素偏移量 offset 系列 1、offset 概述 offset 翻译过来就是偏移量&#xff0c;我们使用 offset 系列相关属性可以动态的得到该元素的位置&#xff08;偏移&#xff09;、大小…

SpringBoot SpringBoot 开发实用篇 4 数据层解决方案 4.7 SpringBoot 操作 Redis 客户端实现技术切换【jedis】

SpringBoot 【黑马程序员SpringBoot2全套视频教程&#xff0c;springboot零基础到项目实战&#xff08;spring boot2完整版&#xff09;】 SpringBoot 开发实用篇 文章目录SpringBootSpringBoot 开发实用篇4 数据层解决方案4.7 SpringBoot 操作 Redis 客户端实现技术切换【je…

数据结构实验教程-第二套

5.一棵左子树为空的二叉树在先序线索化后&#xff0c;其中空的链域的个数是 a&#xff0e;不确定 b.0 c.1 d.2在先序线索化之后&#xff0c;相当于只有开始节点没有前驱&#xff0c;最后的节点没有后继&#xff0c;因此空链域只有2个&#xff0c;分别是开始节点的左孩子和最后节…

【笔试题】【day23】

文章目录第一题&#xff08;二叉树度结点的计算&#xff09;第二题&#xff08;平衡查找二叉树&#xff09;第三题&#xff08;堆的插入&#xff09;第四题&#xff08;哈希表的查找&#xff09;第五题&#xff08;大数排序&#xff09;第一题&#xff08;二叉树度结点的计算&a…

功能测试

功能测试 按照是否覆盖源代码 黑盒测试&#xff08;输入和输出&#xff09; 白盒测试&#xff08;代码内部实现逻辑&#xff09; 灰盒测试&#xff08;输入输出和代码逻辑&#xff09; 介于白盒测试和黑盒测试之间的测试&#xff0c;多用于集成阶段&#xff0c;不仅关注输入输…

智能制造与数字化工厂

智能制造是基于新一代信息技术&#xff0c;贯穿设计、生产、管理、服务等制造活动各个环节&#xff0c;具有信息深度自感知、智慧优化自决策、精准控制自执行等功能的先进制造过程、系统与模式的总称。具有以智能工厂为载体&#xff0c;以关键制造环节智能化为核心&#xff0c;…

通信用多模光纤主要有哪些类型?OM1~OM5有什么区别

1 前言 根据光纤内光信号传输模式的不同&#xff0c;光纤可分为单模光纤和多模光纤&#xff0c;见《常用通信光纤是如何分类的》一文。 在传送网和有线接入网中&#xff0c;我们接触到的光纤类型主要有&#xff1a;G.652、G.654和G.657&#xff0c;这些都是单模光纤。多模光纤…

Linux 系统启动过程

linux启动时我们会看到许多启动信息。 Linux系统的启动过程并不是大家想象中的那么复杂&#xff0c;其过程可以分为5个阶段&#xff1a; 内核的引导。运行 init。系统初始化。建立终端 。用户登录系统。init程序的类型&#xff1a; SysV: init, CentOS 5之前, 配置文件&#…

通关算法题之 ⌈二叉树⌋ 上

二叉树深度 104、求二叉树最大深度 二叉树的深度为根节点到最远叶子节点的最长路径上的节点数&#xff0c;叶子节点是指没有子节点的节点。 示例&#xff1a; 给定二叉树[3,9,20,null,null,15,7]&#xff0c; 3/ \9 20/ \15 7返回它的最大深度 3。 解法一&#xff1a;递…

Linux进阶-Shell编程

目录 定义变量&#xff1a; 使用变量&#xff1a; 将命令的结果赋值给变量&#xff1a; 删除变量&#xff1a;unset 退出当前进程&#xff1a;exit 读取从键盘输入的数据 &#xff1a;read 对整数进行数字运算&#xff1a;(()) 逻辑与或&#xff1a; 检测某个条件是否成…

Qt QSqlQueryModel详解

1.功能概述 QSqlQueryModel是QSqlTableModel的父类。QSqlQueryModel封装了执行SELECT语句从数据库查询数据的功能&#xff0c;但是QSqlQueryModel只能作为只读数据源使用&#xff0c;不可以编辑数据。 2.常用API void clear() //清除数据模型&#xff0c;释放所有获得的数据…

投资有风险,入市需谨慎

投资有风险&#xff0c;入市需谨慎投资有风险&#xff0c;入市需谨慎股票的分类股票的分时图股票K线图股票交易规则股票趋势股票买卖机制投资有风险&#xff0c;入市需谨慎 感谢平台和大家支持&#xff0c;今天不聊技术&#xff0c;了解了解其他方面&#xff0c;比如股市&…

编程思维是一种什么思维?

hello wordl&#xff01;    keep coding&#xff01;&#x1f3c3; 学编程不是将来要当程序猿&#xff0c;而是在学习编程思维。比尔盖茨、扎克伯格、乔布斯用经验告诉我们&#xff0c;拥有编程思维的人&#xff0c;就相当于成功了一半——不但逻辑清晰心思缜密&#xff0c;…

vue + el-checkbox 单选功能

需求: 用 el-checkboc 实现单选功能并且当选中某一项时则回填到input框里,当点击 enter 键或者是 按下搜索图标按键,来实现页面搜索内容的同步展示;如图: <el-checkbox-group placeholder"请选择"size"small"v-model"checkedCols"clearablefi…

Android BLE HIDS Data ,从问询DB 到写入Android 节点的flow之二

问题点4&#xff1a;Android BLE具体连接flow 并问询DB的API flow 之第一阶段问询&#xff1b; 当前确认原生BT当作为GATT Client 连接上GATT Server时&#xff0c;在连接上后会有自动启动问询的动作(以下Tracing 基于Android 9(P), 测试 8.1的代码和Android 8.0有差异&#x…

Web大学生网页作业成品——抗击疫情网站设计与实现(HTML+CSS)实训素材

⛵ 源码获取 文末联系 ✈ Web前端开发技术 描述 网页设计题材&#xff0c;DIVCSS 布局制作,HTMLCSS网页设计期末课程大作业 | 击疫情致敬逆行者感人类题材 | 致敬逆行者网页设计作品 | 大学生抗疫感动专题网页设计作业模板 | 等网站的设计与制作 | HTML期末大学生网页设计作业 …

第3阶段-运维线上实战-3.2企业级nginx使用

企业级nginx使用 nginx实现平滑升级 [rootlnmp nginx-1.16.0]# cd /usr/local/nginx/sbin/ [rootlnmp sbin]# ls nginx nginx.old [rootlnmp sbin]# ./nginx -v nginx version: nginx/1.16.0 [rootlnmp sbin]# ./nginx.old -v nginx version: nginx/1.14.2 [rootlnmp sbin]#操…