本文为面试专题之JavaScript进阶——this的显现绑定之callapplybind 的手写完成。

面经整理见:2024年,龙年大吉吧。裁人,内卷,逆流而上,万字面经整理

前言

这3个办法是能够显现的调用改变函数 this指向的。

  • applyapply 办法接纳两个参数,一个是 this 绑定的方针,一个是参数数组。
  • callcall 办法接纳的参数,第一个是 this 绑定的方针,后面的其他参数是传入函数履行的参数。
  • bind:语法和 call 类似,只不过 bind 办法是创立一个新的函数,而这个函数是经过 bind 绑定了 this 的,bind后面的其他参数会被固定在这个新函数内部,待履行调用时,会合并到新函数的参数中一并作为参数。

applycall的完成办法类似,差异便是传参办法不同,bind由于是回来一个新的未履行函数,需求特殊处理,在外部包一层函数。

call 函数的完成步骤

引证MDN对call的语法描绘:

call(thisArg)
call(thisArg, arg1)
call(thisArg, arg1, arg2)
call(thisArg, arg1, arg2, /* …, */ argN)

能够发现,参数1 是在调用 func 时要运用的 this 值。然后面的形参则都是函数的参数(这是和 apply 很大的一个差异)。

  • 判别调用方针是否为函数,即使是界说在函数的原型上的,可是或许呈现运用 call 等办法调用的状况。
  • 判别传入上下文方针是否存在,假如不存在,则设置为 window 。
  • 处理传入的参数,截取第一个参数后的所有参数。
  • 将函数作为上下文方针的一个特点。
  • 运用上下文方针来调用这个办法,并保存回来成果。
  • 删去方才新增的特点。
  • 回来成果。
Function.prototype.myCall = function(context) {
  // 判别调用方针
  if (typeof this !== "function") {
    console.error("type error");
  }
  // 获取参数
  let args = [...arguments].slice(1),
    result = null;
  // 判别 context 是否传入,假如未传入则设置为 window
  context = context || window;
  // 将调用函数设为方针的办法
  context.fn = this;
  // 调用函数
  result = context.fn(...args);
  // 将特点删去
  delete context.fn;
  return result;
};

apply 函数的完成步骤

引证MDN对apply的语法描绘:

apply(thisArg)
apply(thisArg, argsArray)

能够发现,参数1 是在调用 func 时要运用的 this 值。参数2 则是函数的参数(这是和 call 很大的一个差异)。

注:和call不同,apply只接受2个参数。扫除参数1 是this,只要参数2 才是方针函数的参数(全部放在一个数组中)。

argsArray 是一个类数组方针,用于指定调用 func 时的参数

  • 判别调用方针是否为函数,即使是界说在函数的原型上的,可是或许呈现运用 call 等办法调用的状况。
  • 判别传入上下文方针是否存在,假如不存在,则设置为 window 。
  • 将函数作为上下文方针的一个特点。
  • 判别参数值是否传入
  • 运用上下文方针来调用这个办法,并保存回来成果。
  • 删去方才新增的特点
  • 回来成果
Function.prototype.myApply = function(context) {
  // 判别调用方针是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  let result = null;
  // 判别 context 是否存在,假如未传入则为 window
  context = context || window;
  // 将函数设为方针的办法
  context.fn = this;
  // 调用办法
  // 1. 假如第2个参数存在的话,则进行解构传入方针函数,履行函数
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    // 2. 假如没有第2个参数,则无效传参,直接履行即可
    result = context.fn();
  }
  // 将特点删去
  delete context.fn;
  return result;
};

bind 函数的完成步骤

bind仅仅绑定 this 和固定参数,并不履行函数,回来一个新的待履行函数。

其实当你看到 bind 能够固定参数这一特性时,结合咱们前几章的内容,你应该能够联想到闭包柯里化这俩关键词(作用域与闭包)。

引证MDN对bind的语法描绘:

bind(thisArg)
bind(thisArg, arg1)
bind(thisArg, arg1, arg2)
bind(thisArg, arg1, arg2, /* …, */ argN)

能够发现,bind的用法和call还挺像的。

参数1 是在调用绑定函数时,作为 this 参数传入方针函数 func 的值。然后面的形参则在调用 func 时,刺进到传入绑定函数的参数前的参数。

  • 判别调用方针是否为函数,即使是界说在函数的原型上的,可是或许呈现运用 call 等办法调用的状况。
  • 保存当时函数的引证,获取其他传入参数值。
  • 创立一个函数回来
  • 函数内部运用 apply 来绑定函数调用,需求判别函数作为结构函数的状况,这个时候需求传入当时函数的 this 给 apply 调用,其他状况都传入指定的上下文方针
Function.prototype.myBind = function(context) {
  // 判别调用方针是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  // 获取参数
  var args = [...arguments].slice(1),
    fn = this;
  return function Fn() {
    // 依据调用办法,传入不同绑定值
    // ❗特别注意:这儿的 this 与 arguments,
    // 和函数外面的this、arguments现已不是同一个东西了
    const result = fn.apply(
      this instanceof Fn ? this : context,
      args.concat(...arguments)
    );
    return result
  };
};

bindcall 最大的差异,在于 call 是绑定 this 时直接履行函数,然后回来成果值;而 bind 是绑定this 和初始参数后,并不履行,因而,这儿的关键一点是 bind 回来的是一个 函数 而非履行成果值,它露出给用户自己选择履行时机(把 Fn 函数回来给用户,待履行)。

那么这儿就会有个问题:

Fn 和普通函数无异,它能够被 callapply 调用,也能够被当成结构函数履行 new 的操作,那么经过这样的处理之后,终究它会是什么样的呢?

call、apply 调用的影响

简单写个测验用例,剖析下:

function foo(a, ...args) {
  console.log('foo', this.name)
  return [a, ...args].reduce((prev, cur) => prev + cur, 0)
}
const obj = {
  name: 'A'
}
const fooBound = foo.bind(obj, 1, 2)
console.log('fooBound', fooBound(3))
const obj2 = {
  name: 'B'
}
const barBound = fooBound.bind(obj2, 3)
console.log('barBound', barBound(4))
const obj3 = {
  name: 'C'
}
const foo2 = foo.bind(obj3, 4)
console.log('foo2', foo2(5)) 
// 输出成果如下:
// foo A
// fooBound 6
// foo A
// barBound 10
// foo C
// aseBound 9

剖析成果能够发现:

  1. bind 绑定之后的绑定函数,再运用 bind 绑守时,传入的 thisArg 无效,可是之前传递的参数仍然有效
  2. bind 绑定之后的方针函数,能够被 bind 重新绑定,这时会回来一个新函数,之前绑定的参数无效

MDN官方是这样描绘的:

绑定函数能够经过调用 fooBound.bind(thisArg, /*more args*/) 进一步进行绑定,从而创立另一个绑定函数 barBound。新绑定的 thisArg 值会被疏忽,由于 barBound 的方针函数是 fooBound,而 fooBound 现已有一个绑定的 this 值了。当调用 barBound 时,它会调用 fooBound,而 fooBound 又会调用 foo

foo 终究接纳到的参数按次序为:fooBound 绑定的参数、barBound 绑定的参数,以及 barBound 接纳到的参数。

结构函数 new 的影响

function foo(a, ...args) {
  console.log('foo', this.name)
  const total = [a, ...args].reduce((prev, cur) => prev + cur, 0)
  this.val = `[${this.name}]:${total}`
  return this.val
}
foo.prototype.getSum = function () {
  return this.val
}
const obj = {
  name: 'A'
}
const fooBound = foo.bind(obj, 1, 2)
console.log('fooBound', fooBound(3))
const son = new fooBound(3, 4) 
console.log('son', son.val) 
console.log('son:sum', son.getSum()) 
console.log('son:prototype', son instanceof foo) 
// 输出成果如下:
// foo A    
// fooBound [A]:6
// foo undefined
// son [undefined]:10
// son:sum [undefined]:10
// true

运用 new 结构被 bind 绑定的函数时,bind供给的 this 值会被疏忽,参数会被正常传递履行。

从上一章的内容(new 的履行进程)咱们可知,当履行 new Function 这种结构写法的时候,new 的内部会以 Function 为原型新创立一个方针,并经过 apply(thisArg, args) 这种办法绑定到履行 Function 函数的 this上下文。

其实,在上一章中咱们也说过,new 的本质相当于对原型链的继承,首要是完成对 prototype 的绑定,而这儿的 bind 仅仅修改了履行上下文this,因而,这儿不管你怎么 new 结构几次函数,终究寻觅原型的时候仍是会回到最开始的 foo 函数上。

总结

本文首要介绍了显现绑定this的3个办法的相关完成,从代码来看,逻辑不算杂乱,重点在于对 this 的了解,而想要深入了解 this,就需求搞懂 JavaScript 中的 履行上下文、词法作用域,以及在剖析 bind 函数和其他用法场景时,又会触及闭包的相关知识,因而,掌握这些根底是重点中的重点,待完全了解吸收之后便可融会贯通。

沟通

好了,本文到此结束,欢迎来撩,一起学习‍♂️~

面试相关的文章及代码demo,后续打算在这个仓库(JS-banana/interview: 面试不完全指北 (github.com))进行维护,欢迎✨star,提建议,一起进步~