JavaScript 学习笔记:手写 bind 的实现与进阶
bind 是 JavaScript 里非常经典、也非常适合拿来深入理解 this、参数预置、构造函数行为、原型链这些知识点的一个方法。
这篇笔记,我会从一个最简单的 bind 开始,一步一步实现出一个更接近原生行为的版本,并分析每一步为什么还不够完善。
一、bind是什么
bind 的核心作用,是返回一个新的函数,并且把这个新函数内部的 this 固定住。
比如:
bind 是 JavaScript 里非常经典、也非常适合拿来深入理解 this、参数预置、构造函数行为、原型链这些知识点的一个方法。
这篇笔记,我会从一个最简单的 bind 开始,一步一步实现出一个更接近原生行为的版本,并分析每一步为什么还不够完善。
bind 的核心作用,是返回一个新的函数,并且把这个新函数内部的 this 固定住。
比如:
function add(a, b, c) {
console.log(this.x, a, b, c);
}
var obj = { x: 10 };
var bound = add.bind(obj, 1, 2);
bound(3); // 10 1 2 3
这里做了两件事:
把 this 绑定为 obj
预先保存了一部分参数 1, 2
之后调用 bound(3) 时,最终执行效果相当于:add.call(obj, 1, 2, 3);
所以,一个完整的 bind 至少要支持:
绑定 this
柯里化 / 参数预置
返回的新函数继续接收参数
兼容 new 关键字(作为构造函数调用)
保留原函数的原型链
Function.prototype.bind = function (context) {
var fn = this;
return function () {
return fn.apply(context);
};
};
// ================= 测试用例 =================
function add(a, b, c) {
console.log(this.x a b c
这个版本干了什么:
this 就是调用 bind 的原函数,也就是 add,用 fn 把它保存起来
返回一个新函数,调用新函数时,内部执行 fn.apply(context)
存在的问题: 完全丢失了参数预置(柯里化)的能力,并且如果在调用返回的新函数时传入了新参数,也会被直接丢弃忽略。
Function.prototype.bind = function (context) {
var fn = this;
var args = Array.prototype.slice.call(arguments, 1);
return function bound() {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.innerArgs
这个版本干了什么:
this 就是调用 bind 的原函数,用 fn 把它保存起来
从 arguments 中获取 bind 中剩余参数,用 args 保存
返回一个新函数,调用新函数时,内部将函数调用时传入的参数与之前保存的参数进行一个合并,然后通过 fn.apply(context, finalArgs) 执行。
存在的问题: 忽略了 new 操作符的情况。在 JavaScript 中,如果用 new 关键字去调用一个被 bind 过的函数,绑定的 this 应该失效,并指向 new 创建出来的新实例。而这个版本会死死地把 this 绑在传入的 context(即 obj)上,导致实例属性被错误地挂载到了外部对象上。
Function.prototype.bind = function (context) {
var fn = this;
var args = Array.prototype.slice.call(arguments, 1);
return function bound() {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = args.innerArgs
这个版本干了什么:
增加了 this instanceof bound ? this : context 判断。
识别函数是否被作为构造函数调用。如果是通过 new 调用的,this 应该是当前实例对象;如果是普通调用,才使用传入的 context。
存在的问题: 丢失了原函数的原型链。因为返回的 bound 是一个全新的函数,当执行 new BoundPerson() 时,实例的原型直接指向了 bound.prototype,而不是原函数 Person.prototype,这导致新实例无法访问原函数原型链上的方法(比如这里的 sayHi)。
Function.prototype.bind = function (context) {
if (typeof this !== "function") {
throw new TypeError("bind must be called on a function");
}
var fn = this;
var bindArgs = Array.prototype.slice.call(arguments, 1);
function bound
这个版本干了什么:
开头增加了 typeof this !== "function" 的校验,确保非函数对象调用时抛出 TypeError。
判断了 fn.prototype === "object",因为箭头函数没有 prototype 属性,直接 Object.create(undefined) 会报错。
使用 Object.create(fn.prototype) 建立原型链,让 bound 实例能访问原函数原型上的方法,并修复了 constructor 指向。
存在的问题: 无。这已经是一个非常健壮、且极其接近原生 Function.prototype.bind 行为的 Polyfill 实现了。
在一步步完善 bind 的过程中,我们实际上深度运用了以下几个 JavaScript 核心机制:
闭包 (Closures):利用函数嵌套,将原函数 (fn) 和预置的参数 (args) 保存起来,供后续调用时使用。
上下文绑定 (Context Binding):通过 apply 动态改变函数执行时的 this 指向。
参数处理与柯里化 (Currying):利用 arguments 对象和 Array.prototype.slice 实现参数的预置与合并,这是函数式编程中“偏函数(Partial Application)”的基础。
构造函数调用检测:利用 this instanceof bound 巧妙地判断函数是被作为普通函数调用,还是被 new 关键字作为构造函数调用。
原型链继承 (Prototype Chain):使用 Object.create(fn.prototype) 完美实现了基于原型的继承,确保新实例能顺着原型链找到原函数的方法。
边界条件处理:考虑到了非函数调用的异常抛出,以及箭头函数没有 prototype 的特殊情况。
如果你想顺着这套思路继续夯实 JavaScript 的底层基础,我建议可以从以下几个方向入手,每一个都可以尝试“手写实现”:
手撕 call 和 apply JavaScript 学习笔记:手写 call/apply 的实现与进阶
context.fn = this),执行完毕后再删除它。手撕 new 操作符
myNew(constructor, ...args) 函数,模拟:创建一个空对象 -> 链接原型链 -> 绑定 this 执行构造函数 -> 处理返回值的全过程。深入防抖与节流 (Debounce & Throttle)
this 和 arguments。函数柯里化 (Currying)
fn.length) 和已传入的参数长度进行递归对比,参数不够就返回新函数继续收集,参数够了就触发执行。彻底搞透 ES5 的继承模式
class extends 的底层语法糖)。