JavaScript Study Notes: Implementing bind
bind is a classic JavaScript method and an excellent way to explore this, preset arguments, constructor behavior, and prototype chains.
In these notes, I'll start with a minimal bind, gradually move closer to native behavior, and explain what's missing at each step.
1. What Is bind?
The core purpose of bind is to return a new function with its this fixed to a particular value.
For example:
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 3This does two things:
Bind this to obj.
Save the preset arguments 1, 2.
A later call to bound(3) then behaves like add.call(obj, 1, 2, 3);.
A complete bind therefore needs at least:
Binding for this.
Currying / preset arguments.
A returned function that accepts additional arguments.
Compatibility with new, for constructor calls.
Preservation of the original function's prototype chain.
2. Implementation
Binding 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 undefinedWhat this version does:
this is the original function on which bind is called—here, add. Save it as fn.
Return a new function that calls fn.apply(context) when invoked.
Remaining problem: preset arguments (currying) are entirely unsupported, and any arguments passed to the returned function are discarded too.
Supporting Preset Arguments
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 上)What this version does:
Save this, the original function receiving the bind call, as fn.
Extract the remaining bind arguments from arguments and save them as args.
Return a function that combines arguments supplied at call time with the saved arguments and executes fn.apply(context, finalArgs).
Remaining problem: it ignores new. When a bound function is called with new, its bound this should be ignored in favor of the newly created instance. This version stubbornly binds this to the supplied context, or obj, causing instance properties to be added to the external object instead.
Supporting the new Operator
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
}What this version does:
Add the check this instanceof bound ? this : context.
Detect constructor calls. When invoked with new, use the current instance as this; for an ordinary call, use the supplied context.
Remaining problem: the original prototype chain is lost. The returned bound is a new function, so new BoundPerson() creates an instance whose prototype points to bound.prototype, not Person.prototype. The instance therefore cannot access the original prototype's methods, such as sayHi.
Fixing the Prototype Chain and Edge Cases: A More Complete Version
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
}What this version does:
Add typeof this !== "function" at the start so non-function receivers cause a TypeError.
Check fn.prototype === "object", because arrow functions have no prototype and Object.create(undefined) would throw.
Use Object.create(fn.prototype) to establish a prototype chain so bound instances can access the original prototype's methods, and fix the constructor reference.
Remaining problems: none. This is already a very robust polyfill whose behavior is extremely close to native Function.prototype.bind.
3. Summary
As we refine bind, we make extensive use of these core JavaScript mechanisms:
Closures: nested functions preserve the original function (fn) and preset arguments (args) for later calls.
Context binding: apply changes this dynamically at execution time.
Argument handling and currying: arguments and Array.prototype.slice support presetting and merging arguments, the basis of partial application in functional programming.
Constructor-call detection: this instanceof bound distinguishes ordinary calls from construction with new.
Prototype-chain inheritance: Object.create(fn.prototype) implements prototype-based inheritance so new instances can find the original function's methods.
Edge-case handling: throw for non-function receivers and account for arrow functions having no prototype.
@startuml
skinparam ArrowColor #666666
skinparam DefaultTextAlignment center
skinparam NoteBackgroundColor #FEFEFE
skinparam NoteBorderColor #CCCCCC
title Handwritten myBind vs. Native bind
start
:Call fn.bind(obj, ...args1) \nor fn.myBind(obj, ...args1);
fork
partition "Application-level Simulation (myBind)\nClosures, apply, and manual prototype management" #FFF0F5 {
:Check that the receiver is a function;
:Use a **closure** to retain fn and preset arguments args1;
:Create a **new wrapper function bound**;
:**Manually maintain the prototype chain** \nbound.prototype = Object.create(fn.prototype);
note left
**Key Limitation**
This adds a closure layer and a wrapper function.
It explicitly checks the prototype chain to simulate new.
With arrow functions or special host objects,
edge cases still remain.
end note
:=== Return bound and wait for a later call ===;
:**Later call bound(...args2)**\nCheck this instanceof bound;
if (Was the new operator used?) then (Yes)
:this points to the newly constructed instance,\nignoring the originally bound obj;
else (No)
:this points to obj retained by the closure;
endif
:**Invoke the original function through apply**\nfn.apply(this, args1.concat(args2));
}
fork again
partition "JS Engine (Native bind)\nInternal slots and exotic objects" #E0FFFF {
:The engine receives the call and validates the receiver;
:The engine creates in memory a\n**Bound Function Exotic Object**;
:**Internal storage (no closure)**\nStore fn in [[BoundTargetFunction]]\nStore obj in [[BoundThis]]\nStore args1 in [[BoundArguments]];
note right
**Key Advantage**
No closure and no wrapper function!
It is an engine-level exotic object.
The engine directly supports its [[Construct]] (new) behavior,
using less memory without modifying the prototype chain.
end note
:=== Return the exotic object and wait for a later call ===;
:**Later invoke the internal [[Call]] or [[Construct]] method**;
if (Is [[Construct]] invoked through new?) then (Yes)
:The engine allocates and initializes memory,\nthen constructs with the original function and preset arguments;
else (No)
:Read [[BoundThis]] and arguments from the internal slots,\nthen execute the original function's machine code;
endif
}
end fork
:Return the call or construction result;
stop
@endumlDiagram unavailable. Use Show code to inspect the source.