JavaScript 函数绑定:手写 bind、call 和 apply 方法解密 ——js手写系列(三)

164 阅读3分钟

前言

在JavaScript中,函数绑定是一项重要的技能。掌握了 bindcallapply,你将能够更加灵活地操作函数的上下文和参数。这不仅是深入理解JavaScript语言本质的一部分,也是成为一位优秀开发者的必备技能之一。 在这个系列中,我们将深入剖析这些函数绑定。通过手写 bind、模拟实现 callapply,我们将揭开它们神秘的面纱,逐步领略它们背后的原理。

什么是函数绑定

在js中,除了ES6之后出现的箭头函数之外,每个函数都有自己的this,函数绑定就是可以改变函数中的this指向,使this的值是开发过程中程序员所预期的值。函数绑定有三种方法,分别是 bindcallapply

bind

bind方法会创建一个新的函数,该函数的this会被绑定到指定的对象,并且该函数可以接收参数,并传递给原来的函数。bind也可以接受参数,且这些参数会插入到调用新函数时传入的参数的前面。

function sayHello(x, y, z) {
  console.log("函数中的", this);
  console.log("你好:", this.name);
  console.log(x + y + z);
}

let person = {
  name:'dante'
}

let bindFn = sayHello.bind(person,1,2)
bindFn(3)

image.png

根据这个例子,可以发现,手写bind需要以下需求

  • 绑定函数的this的上下文
  • 收集参数, bind时收集 ...args 执行时 ...args1 需要注意的是,args在前 [...args,...args1]
  • 闭包返回一个函数

手写bind

Function.prototype.myBind = function (context, ...args) {
  console.log(this);
  // 如果myBind被赋给一个变量 则报错
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  context = context || window; // 位于闭包之中
  let that = this;
  return function fn(...innerArgs) {
    // 将原先的函数执行 this手动指定为context
    if (this instanceof fn) {  // new的方式运行
      return new that(...args, ...innerArgs);
    }
    return that.apply(context, [...args, ...innerArgs]);
  };
};

以下是一些测试的用例

function sayHello(x, y, z) {
  console.log("函数中的", this);
  console.log("你好:", this.name);
  console.log(x + y + z);
}

let person = {
  name: "dante",
};
const arrowFn = () => {

}
// 此时就会报错 arrowFn.myBind是一个函数体
// const f2 = arrowFn.myBind
// f2()

let myBindFn = sayHello.myBind(person, 1, 2);
myBindFn(3);

call

call以给定的 this 值和逐个提供的参数调用该函数。这是MDN上对call的解释,不难理解,call方法就是可以改变this的指向,以及将收集的参数给调用它的函数并且执行这个函数。

let person = {
  name:'dante'
}
function sayName(x,y){
  console.log(this.name,x,y)
}


sayName.call(person,'hello','world')  // dante hello world

手写call

Function.prototype.myCall = function(context,...argments){
  if(typeof this !== 'function'){
    throw new TypeError('Error')
  }
  context = context || window
  context.fn = this
  let args = [...argments]
  return context.fn(...args)
}

let person = {
  name:'dante'
}
function sayName(x,y){
  console.log(this.name,x,y)
}


sayName.myCall(person,'hello','world')

apply

apply方法与call方法相似,但是apply接收的参数是数组或类数组。

let person = {
  name:'dante'
}
function sayName(x,y){
  console.log(this.name,x,y)
}


sayName.apply(person, ['hello','world'])

手写apply

Function.prototype.myApply = function(context,args = []){
  if(typeof this!== 'function'){
    throw new TypeError('Error')
  }
  context = context || window
  context.fn = this
  return context.fn(...args)
}

let person = {
  name:'dante'
}
function sayName(x,y){
  console.log(this.name,x,y)
}


const set = new Set(['hello','world'])

sayName.myApply(person, ['hello','world'])
sayName.myApply(person, set)

bind、call、apply的区别

  • bind会创一个新的函数,bind接收的参数会插入到新函数接受的参数前面,调用新函数,会将参数传递给原函数并执行
  • call方法不创建新的函数,接收多个参数,并传递给原函数执行
  • apply方法不创建新的函数,接收的参数为数组或类数组,传递给原函数执行