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

时间:2022-11-20 21:54:00

目录

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 中打印的效果

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

 

因此,可以这么封装:

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 原理、使用场景及实现 | 木易杨前端进阶高级前端进阶之路call()、apply()、bind() 区别、使用场景、实现方式https://muyiy.cn/blog/3/3.3.html#%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF深度解析bind原理、使用场景及模拟实现 | 木易杨前端进阶高级前端进阶之路call()、apply()、bind() 区别、使用场景、实现方式https://muyiy.cn/blog/3/3.4.html#bind