day16 实现call,apply和bind

55 阅读4分钟

每日一名

I know I have this great potential.

释义:我知道我有巨大的潜力。

前言

image.png

我们知道它们最主要的功能是改变this的指向,只是用法写会有些区别。在开发中除了在一些基础库或是在公共库中会用到,其他场景很少会用到,但在面试中会被经常问到,三者的区别用法。

我们来重新捋一波...

基本介绍

call

Function.call(obj, [param1[,param2[,…]]])
  • 调用者Function
  • 第一个参数obj,this就指向obj
  • 后面可以传入多个参数以,分隔

使用场景

  • 实现对象继承
var obj = {
  name: '小明',
}
function play() {
  console.log(`${this.name}会打🏀`)
}

play.call(obj)
  • 实现函数继承
function play() {
  console.log(`我不会打🏀`)
  console.log('他会!')
}

function say() {
  console.log('你会打🏀不?')
  play.call(null)
}
say();
  • DOM实现借用数组方法

Array.prototype.slice.call(document.querySelectorAll('div'))

apply

Function.call(obj[,arrayArg])
  • 调用者Function
  • 第一个参数obj,this就指向obj
  • 后面的参数是一个数组或伪数组,这也是 call 和 apply 之间,很重要的一个区别

类数组无法使用 forEach、splice、push 等数组原型链上的方法,毕竟它不是真正的数组。

bind

Function.bind(obj[, arg1[, arg2[, ...]]])
  • 调用者Function
  • this指向obj
  • 后面可以传入多个参数以,分隔

与call、apply区别是:

  • bind不会立即执行,而call、apply会立即执行
  • bind只会对this作一次绑定,传的参数会当默认参数,后续无需再bind,默认参数也无需再传,而call,apply每次都要调用和传参

apply的妙用

  • 查找数组中的最大或最小值
Math.max.apply(null, [4,32,1,5])
Math.min.apply(null, [4,32,1,5])
  • 实现多个数组的合并
let arr = [3]
Array.prototype.push.apply(arr, [4,2,5]);
console.log(arr); // 3,4,2,5

手写call,apply,bind

  • call
Function.prototype.mycall = function(ctx) {
    // 这里不能用箭头函数,因为箭头没有arguments
    ctx = ctx || window
    // 只是给ctxt新增一个独一无二的属性,避免覆盖ctx原属性
    let fn = Symbol()
    ctx[fn] = this;
    let arg = [...arguments].slice(1) // 取到参数
    const result = ctx[fn](...arg); // 执行
    delete ctx[fn] // 删除此属性
    return result // 返回结果
}
  • apply

就相当简单了,只要把参数改成数组形式就好了。

Function.prototype.mycall = function(ctx) {
    // 这里不能用箭头函数,因为箭头没有arguments
    ctx = ctx || window
    // 只是给ctxt新增一个独一无二的属性,避免覆盖ctx原属性
    let fn = Symbol()
    ctx[fn] = this;
    let arg = [...arguments].slice(1) // 取到参数
    const result = ctx[fn](arg); // 执行
    delete ctx[fn] // 删除此属性
    return result // 返回结果
}
  • bind
Function.prototype.myBind = function(ctx) {
  ctx = ctx || window
  const args = Array.from(arguments).slice(1)
  return function ()  {
    return fn.apply(ctx, [...args, ...arguments])
  }
}

面试:为什么call会比apply快

  • call被调用后经历了啥?

1、如果 IsCallable(Function)为false,即 Function 不可以被调用,则抛出一个 TypeError 异常。
2、如果 argArray 为 null 或未定义,则返回调用 Function 的 [[Call]] 内部方法的结果,提供thisArg 和一个空数组作为参数。
3、如果 Type(argArray)不是 Object,则抛出 TypeError 异常。
4、获取 argArray 的长度。调用 argArray 的 [[Get]] 内部方法,找到属性 length。 赋值给 len。
5、定义 n 为 ToUint32(len)。
6、初始化 argList 为一个空列表。
7、初始化 index 为 0。
8、循环迭代取出 argArray。重复循环 while(index < n)
a、将下标转换成String类型。初始化 indexName 为 ToString(index).
b、定义 nextArg 为 使用 indexName 作为参数调用argArray的[[Get]]内部方法的结果。
c、将 nextArg 添加到 argList 中,作为最后一个元素。
d、设置 index = index+1
9、返回调用 Function 的 [[Call]] 内部方法的结果,提供 thisArg 作为该值,argList 作为参数列表。

  • apply调用后,经历了啥?

1、如果 IsCallable(Function)为 false,即 Function 不可以被调用,则抛出一个 TypeError 异常。
2、定义 argList 为一个空列表。
3、如果使用超过一个参数调用此方法,则以从arg1开始的从左到右的顺序将每个参数附加为 argList 的最后一个元素
4、返回调用func的[[Call]]内部方法的结果,提供 thisArg 作为该值,argList 作为参数列表。

我们可以看到,明显 apply 比 call 的步骤多很多。
由于 apply 中定义的参数格式(数组),使得被调用之后需要做更多的事,需要将给定的参数格式改变(步骤8)。 同时也有一些对参数的检查(步骤2),在 call 中却是不必要的。
另外一个很重要的点:在 apply 中不管有多少个参数,都会执行循环,也就是步骤 6-8,在 call 中也就是对应步骤3 ,是有需要才会被执行。

综上,call 方法比 apply 快的原因是 call 方法的参数格式正是内部方法所需要的格式。

具体可参考:call和apply的性能对比,这里比较全面。