当前位置: 首页 > news >正文

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

目录

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

相关文章:

  • python3 爬虫(初试牛刀)
  • excel的frequency函数的用法和实例
  • 程序员这个身份,比你想象的还值钱!
  • Feng Office 3.7.0.5 - 文件上传
  • C#编程流程控制与集合类型
  • JADE: Adaptive Differential Evolution withOptional External Archive
  • Python学习基础笔记五——列表
  • 【深度学习】使用深度学习框架来简洁地实现线性回归模型
  • 超神之路 数据结构 3 —— Stack栈实现及应用
  • 面试官问:Spring 如何解决循环依赖?
  • 动力节点索引优化解决方案学习笔记——查询优化
  • 400Gbps 网络面临的挑战
  • 【 C++ 】特殊类设计
  • 【UML】活动图Activity Diagram、状态机图State Machine Diagram、顺序图Sequence Diagram
  • 微信小程序|从零动手实现俄罗斯方块
  • ES6指北【2】—— 箭头函数
  • 【译】React性能工程(下) -- 深入研究React性能调试
  • 【译】理解JavaScript:new 关键字
  • CAP 一致性协议及应用解析
  • iBatis和MyBatis在使用ResultMap对应关系时的区别
  • iOS 系统授权开发
  • Netty 4.1 源代码学习:线程模型
  • OSS Web直传 (文件图片)
  • STAR法则
  • text-decoration与color属性
  • weex踩坑之旅第一弹 ~ 搭建具有入口文件的weex脚手架
  • 闭包--闭包作用之保存(一)
  • 互联网大裁员:Java程序员失工作,焉知不能进ali?
  • 前端面试总结(at, md)
  • 视频flv转mp4最快的几种方法(就是不用格式工厂)
  • 自制字幕遮挡器
  • nb
  • 看到一个关于网页设计的文章分享过来!大家看看!
  • LIGO、Virgo第三轮探测告捷,同时探测到一对黑洞合并产生的引力波事件 ...
  • ​【原创】基于SSM的酒店预约管理系统(酒店管理系统毕业设计)
  • ​sqlite3 --- SQLite 数据库 DB-API 2.0 接口模块​
  • ​如何在iOS手机上查看应用日志
  • ​软考-高级-系统架构设计师教程(清华第2版)【第12章 信息系统架构设计理论与实践(P420~465)-思维导图】​
  • #《AI中文版》V3 第 1 章 概述
  • #pragma once
  • (17)Hive ——MR任务的map与reduce个数由什么决定?
  • (Matalb时序预测)PSO-BP粒子群算法优化BP神经网络的多维时序回归预测
  • (Matalb时序预测)WOA-BP鲸鱼算法优化BP神经网络的多维时序回归预测
  • (Note)C++中的继承方式
  • (板子)A* astar算法,AcWing第k短路+八数码 带注释
  • (超简单)使用vuepress搭建自己的博客并部署到github pages上
  • (大众金融)SQL server面试题(1)-总销售量最少的3个型号的车及其总销售量
  • (附源码)springboot课程在线考试系统 毕业设计 655127
  • (附源码)小程序 交通违法举报系统 毕业设计 242045
  • (三) diretfbrc详解
  • (十八)devops持续集成开发——使用docker安装部署jenkins流水线服务
  • (四)c52学习之旅-流水LED灯
  • (已解决)vue+element-ui实现个人中心,仿照原神
  • (译) 函数式 JS #1:简介
  • (转)Android学习系列(31)--App自动化之使用Ant编译项目多渠道打包