JavaScript 手写Function.prototype.bind() 底层逻辑以及解析

JavaScript Code:

Function.prototype.myCall = function(thisArg,...args) {
  if(thisArg == null) {
    thisArg = typeof window === 'undefined'? global : window;
  }
  const temp = Symbol('tempKey');
  Object.defineProperty(thisArg,temp,{
    value: this,
    enumerable: false,
    configurable: true,
  })
  const result = thisArg[temp](...args)
  delete thisArg[temp];
  return result;
}//手写的call方法
Function.prototype.myBind = function(thisArg,...args) {
  if(thisArg == null) {
    thisArg = typeof window === 'undefined'? global : window;
  }
  //返回一个绑定了this的新函数这里是箭头函数!应为this需要穿透出去拿到上一层作用域的this值也就是调用myBind的函数!
  return (...parames)=>{
    //这里的this指向的是调用myBind的函数
    //这里需要用call来改变this指向然后我们这里手写一下call方法
    return this.myCall(thisArg,...args,...parames)
  }
}
//测试代码
const person = {
  name: 'Alice',
}
function func(num1, num2, num3, num4) {
  console.log(this)
  console.log(num1, num2, num3, num4)
  return num1 + num2 + num3 + num4;
}
const bindFunc = func.myBind(person, 10, 20)
const result = bindFunc(30, 40)
console.log(`返回值:${result}`)

1. 引言:理解原生Function.prototype.bind()

在解析手写实现前,需先明确原生bind()的核心功能——它是Function.prototype上的方法,用于创建一个新函数,该新函数的this被永久绑定到指定的thisArg,同时可预先传入部分参数(实现“柯里化”特性),后续调用新函数时传入的参数会与预传参数合并。

原生bind()的典型用法:

const obj = { name: "Bob" };
function sayHi(age) { console.log(`Hi ${this.name}, ${age}`); }
const boundSayHi = sayHi.bind(obj, 20); // 绑定this为obj,预传参数20
boundSayHi(); // 输出 "Hi Bob, 20岁"(this不变,参数生效)

手写myBind()的目标,就是模拟原生bind()的this绑定参数柯里化核心逻辑,同时处理边界情况(如thisArgnull/undefined)。

2. 手写前置知识

实现myBind()需依赖以下JavaScript核心特性,需先明确其作用:

  • this绑定规则:函数的this指向由调用方式决定,需通过“间接调用”改变this(如obj.fn()fnthisobj)。
  • 剩余参数(…args):收集函数的不定长参数,转为数组,方便参数合并。
  • Symbol:创建唯一的属性名,避免给thisArg添加临时属性时覆盖其原有属性。
  • Object.defineProperty:精细化配置对象属性(如enumerable: false确保临时属性不被遍历,configurable: true确保后续可删除)。
  • 箭头函数:无自身this,继承外层作用域的this,用于固定myBind()返回函数的this指向。

3. 依赖实现:手写Function.prototype.myCall()

myBind()的核心逻辑依赖“改变函数this指向”,而call()是最直接的“改变this”的方法。因此需先实现myCall(),作为myBind()的底层依赖。

3.1 myCall()完整代码

Function.prototype.myCall = function(thisArg, ...args) {
  // 1. 处理thisArg为null/undefined的情况
  if (thisArg == null) {
    thisArg = typeof window === 'undefined' ? global : window;
  }
  // 2. 创建唯一临时属性,避免覆盖thisArg原有属性
  const temp = Symbol('tempKey');
  // 3. 给thisArg添加临时属性,值为调用myCall()的函数(即原函数)
  Object.defineProperty(thisArg, temp, {
    value: this,       // this指向调用myCall()的函数(如func.myCall()中this是func)
    enumerable: false, // 临时属性不可枚举,避免遍历thisArg时暴露
    configurable: true // 允许后续删除该属性
  });
  // 4. 间接调用原函数:thisArg[temp]() 使原函数的this指向thisArg,传入args参数
  const result = thisArg[temp](...args);
  // 5. 删除临时属性,避免污染thisArg
  delete thisArg[temp];
  // 6. 返回原函数的执行结果
  return result;
};

3.2 myCall()逐行逻辑解析

代码片段 核心作用 细节说明
if (thisArg == null) { ... } 处理边界:thisArgnull/undefined 原生call()中,若thisArgnull/undefinedthis会指向全局对象(浏览器端window,Node.js端global),此处模拟该行为。
const temp = Symbol('tempKey') 避免属性名冲突 Symbol生成的属性名是唯一的,不会覆盖thisArg上已有的属性(如thisArg.name不会被影响)。
Object.defineProperty(...) 配置临时属性 - value: this:将调用myCall()的函数(如func)挂载到thisArgtemp属性上;
- enumerable: false:防止for...inObject.keys()遍历到临时属性,符合原生call()“不污染thisArg”的特性;
- configurable: true:确保后续能通过delete删除临时属性。
thisArg[temp](...args) 改变this并执行函数 函数调用时,this指向调用者(即thisArg),同时传入args参数,实现“改变this+传参”的核心需求。
delete thisArg[temp] 清理临时属性 执行完函数后删除临时属性,避免thisArg被永久污染。
return result 传递返回值 原生call()会返回原函数的执行结果,此处保持一致。

4. 核心实现:手写Function.prototype.myBind()

myBind()的核心是返回一个绑定了this和预传参数的新函数,后续调用新函数时,会合并预传参数和新参数,最终通过myCall()执行原函数。

4.1 myBind()完整代码

Function.prototype.myBind = function(thisArg, ...args) {
  // 1. 处理thisArg为null/undefined的情况(同myCall()逻辑)
  if (thisArg == null) {
    thisArg = typeof window === 'undefined' ? global : window;
  }
  // 2. 返回箭头函数:固定this指向,避免后续调用时this被改变
  return (...parames) => {
    // 3. 调用myCall():绑定this为thisArg,合并预传参数args和新参数parames
    return this.myCall(thisArg, ...args, ...parames);
  };
};

4.2 myBind()核心逻辑拆解

(1)边界处理:thisArgnull/undefined

myCall()逻辑一致:若thisArgnullundefined,则将this指向全局对象(window/global),模拟原生bind()的行为。

(2)返回箭头函数:固定this指向的关键

此处必须使用箭头函数,而非普通函数,原因是:

  • 普通函数的this由调用方式决定(如boundFunc.call(otherObj)会改变this);
  • 箭头函数无自身this,其this继承自外层作用域(即myBind()执行时的this);
  • myBind()执行时的this,正是调用myBind()的原函数(如func.myBind()thisfunc)。

→ 最终效果:无论后续如何调用返回的新函数(如bindFunc()bindFunc.call(otherObj)),其内部this始终指向原函数(func),确保myCall()能正确绑定thisArg

(3)参数合并:实现柯里化特性
  • ...args:收集myBind()调用时传入的预传参数(如func.myBind(person, 10, 20)中的10, 20);
  • ...parames:收集新函数调用时传入的后续参数(如bindFunc(30, 40)中的30, 40);
  • ...args, ...parames:通过“展开运算符”合并两组参数,最终传给原函数(如合并为[10, 20, 30, 40])。

→ 这就是bind()的“柯里化”特性:允许分多次传入参数,最终合并执行。

(4)调用myCall()并返回结果

通过this.myCall(...)

  • this指向原函数(func);
  • 绑定thisthisArgperson);
  • 传入合并后的参数;
  • 返回原函数的执行结果,确保新函数的返回值与原函数一致。

5. 测试代码执行流程解析

通过测试代码验证myBind()的正确性,需分步拆解执行过程:

5.1 测试代码

// 1. 定义被绑定的this对象
const person = { name: 'Alice' };

// 2. 定义原函数(需绑定this和参数)
function func(num1, num2, num3, num4) {
  console.log(this); // 打印绑定后的this
  console.log(num1, num2, num3, num4); // 打印合并后的参数
  return num1 + num2 + num3 + num4; // 返回参数总和
}

// 3. 调用myBind():绑定this为person,预传参数10、20
const bindFunc = func.myBind(person, 10, 20);

// 4. 调用新函数:传入后续参数30、40,接收返回值
const result = bindFunc(30, 40);

// 5. 打印最终返回值
console.log(`返回值:${result}`);

5.2 分步执行流程

  1. 执行func.myBind(person, 10, 20)

    • myBind()this指向func(原函数);
    • thisArgperson(非null/undefined,无需指向全局);
    • args收集预传参数[10, 20]
    • 返回一个箭头函数(即bindFunc),该函数继承myBind()thisfunc)。
  2. 执行bindFunc(30, 40)

    • 箭头函数的this继承自myBind(),即func
    • parames收集后续参数[30, 40]
    • 调用this.myCall(...) → 即func.myCall(person, 10, 20, 30, 40)
  3. 执行func.myCall(...)

    • thisArgpersonfuncthis指向person
    • 传入参数[10, 20, 30, 40],执行func
    • 打印this{ name: 'Alice' }
    • 打印参数 → 10 20 30 40
    • 返回参数总和 → 10+20+30+40=100
  4. 最终输出

    • console.log(result) → 打印返回值:100

6. 核心关键点总结

手写myBind()的核心是解决“固定this”和“参数柯里化”两大问题,需牢记以下关键点:

  1. thisArg边界处理null/undefined时指向全局对象,符合原生行为;
  2. 箭头函数固定this:利用箭头函数无自身this的特性,确保返回函数的this不被后续调用改变;
  3. Symbol避免属性冲突myCall()中用Symbol创建临时属性,防止污染thisArg
  4. 参数合并(柯里化):通过剩余参数和展开运算符,分多次传入的参数最终合并执行;
  5. 无副作用myCall()中删除临时属性,避免thisArg被永久修改。

7. 与原生bind()的对比及注意事项

手写myBind()实现了原生bind()的核心功能,但存在以下简化(原生bind()更复杂):

  • 基本类型的装箱处理:原生bind()thisArg是基本类型(如123"test"),会自动转为对应包装对象(new Number(123)new String("test")),手写版本未处理;
  • 构造函数场景:原生bind()返回的函数若作为构造函数(new boundFunc()),this会指向新创建的实例(而非thisArg),手写版本未处理;
  • 原型链继承:原生bind()返回的函数会继承原函数的原型链,手写版本未处理。

→ 手写版本适用于理解bind()的核心逻辑,生产环境需使用原生bind()(兼容性和功能更完善)。

最终测试输出结果

执行测试代码后,控制台会依次打印:

{ name: 'Alice' } // func的this指向person
10 20 30 40       // 合并后的参数
返回值:100        // 参数总和

该结果符合预期,证明手写myCall()myBind()的逻辑正确。

更多推荐