JavaScript 学习笔记:手写 bind 的实现与进阶
bind 是 JavaScript 里非常经典、也非常适合拿来深入理解 this、参数预置、构造函数行为、原型链这些知识点的一个方法。
这篇笔记,我会从一个最简单的 bind 开始,一步一步实现出一个更接近原生行为的版本,并分析每一步为什么还不够完善。
一、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关键字(作为构造函数调用) -
保留原函数的原型链
二、实现
实现绑定 this
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);
}
var obj = { x: 10 };
// 正确用例:成功固定 this
var bound1 = add.bind(obj);
bound1(); // 10 undefined undefined undefined
// 错误用例:无法预置参数,也无法接收新参数合并
var bound2 = add.bind(obj, 1, 2);
bound2(3); // 10 undefined undefined undefined
这个版本干了什么:
-
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.concat(innerArgs);
return fn.apply(context, finalArgs);
};
};
// ================= 测试用例 =================
function add(a, b, c) {
console.log(this.x, a, b, c);
}
function Person(name, age) {
this.name = name;
this.age = age;
}
var obj = { x: 10, name: "objName" };
// 正确用例:参数预置成功
var boundAdd = add.bind(obj, 1, 2);
boundAdd(3); // 10 1 2 3
// 错误用例:使用 new 调用时,this 绑定错误
var BoundPerson = Person.bind(obj, "muyu");
var p = new BoundPerson(18);
console.log("p.name:", p.name); // p.name: undefined (实例 p 上没有属性)
console.log("obj:", obj); // obj: { x: 10, name: 'muyu', age: 18 } (属性被错误添加到了 obj 上)
这个版本干了什么:
-
this就是调用bind的原函数,用fn把它保存起来 -
从
arguments中获取bind中剩余参数,用args保存 -
返回一个新函数,调用新函数时,内部将函数调用时传入的参数与之前保存的参数进行一个合并,然后通过
fn.apply(context, finalArgs)执行。
存在的问题: 忽略了 new 操作符的情况。在 JavaScript 中,如果用 new 关键字去调用一个被 bind 过的函数,绑定的 this 应该失效,并指向 new 创建出来的新实例。而这个版本会死死地把 this 绑在传入的 context(即 obj)上,导致实例属性被错误地挂载到了外部对象上。
兼容 new 操作符
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.concat(innerArgs);
var thisArg = this instanceof bound ? this : context;
return fn.apply(thisArg, finalArgs);
};
};
// ================= 测试用例 =================
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function () {
console.log("sayHi:", this.name, this.age);
};
var obj = { x: 10, name: "objName" };
// 正确用例:兼容了 new,属性成功添加到实例上,obj 也没有被污染
var BoundPerson = Person.bind(obj, "muyu");
var p = new BoundPerson(18);
console.log("p.name:", p.name); // p.name: muyu
console.log("p.age:", p.age); // p.age: 18
console.log("obj:", obj); // obj: { x: 10, name: 'objName' }
// 错误用例:丢失了原函数的原型链
try {
p.sayHi();
} catch (error) {
console.log(error.message); // p.sayHi is not a function
}
这个版本干了什么:
-
增加了
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() {
var innerArgs = Array.prototype.slice.call(arguments);
var finalArgs = bindArgs.concat(innerArgs);
var thisArg = this instanceof bound ? this : context;
return fn.apply(thisArg, finalArgs);
}
if (typeof fn.prototype === "object") {
bound.prototype = Object.create(fn.prototype);
bound.prototype.constructor = bound;
}
return bound;
};
// ================= 测试用例 =================
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function () {
console.log("sayHi:", this.name, this.age);
};
var obj = { x: 10 };
const arrow = () => { console.log("arrow"); };
// 正确用例 1:new 调用且成功继承原型链
var BoundPerson = Person.bind(obj, "muyu");
var p = new BoundPerson(18);
p.sayHi(); // sayHi: muyu 18
// 正确用例 2:兼容箭头函数调用 (箭头函数没有 prototype)
try {
arrow.bind(null)();
// arrow (不会因为找不到 prototype 而抛出异常)
} catch (e) {
console.log(e.message);
}
// 错误用例 3:对非函数调用 bind 会直接抛错
try {
Function.prototype.bind.call({}, null);
} catch (e) {
console.log(e.message); // bind must be called on a function
}
这个版本干了什么:
-
开头增加了
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和applyJavaScript 学习笔记:手写 call/apply 的实现与进阶- 核心思路:将函数临时作为对象的一个属性(例如
context.fn = this),执行完毕后再删除它。
- 核心思路:将函数临时作为对象的一个属性(例如
-
手撕
new操作符- 核心思路:尝试手写一个
myNew(constructor, ...args)函数,模拟:创建一个空对象 -> 链接原型链 -> 绑定this执行构造函数 -> 处理返回值的全过程。
- 核心思路:尝试手写一个
-
深入防抖与节流 (Debounce & Throttle)
- 进阶挑战:不仅要实现基础版,还要实现带有“首次立即执行”参数的完美版,以及在其中正确处理
this和arguments。
- 进阶挑战:不仅要实现基础版,还要实现带有“首次立即执行”参数的完美版,以及在其中正确处理
-
函数柯里化 (Currying)
- 核心思路:根据原函数的形参长度 (
fn.length) 和已传入的参数长度进行递归对比,参数不够就返回新函数继续收集,参数够了就触发执行。
- 核心思路:根据原函数的形参长度 (
-
彻底搞透 ES5 的继承模式
- 原型链继承、借用构造函数继承、组合继承。
- 以及目前 ES5 中最完美的寄生组合式继承(这也是 ES6
class extends的底层语法糖)。