在 JavaScript 中,bind、call 和 apply 是函数对象的三个重要方法,用于显式绑定函数的 this 指向(无法改变箭头函数this指向,下面会解释原因)。三个函数都绑定在Function构造函数的prototype上,每个函数又都是Function的实例,所以每个函数都可以直接调用call、apply、bind(原型链继承向上查找)。
方法 | 调用方式 | 结果 |
---|---|---|
call | function.call(thisArg, arg1, arg2, ...) | 立即执行 |
apply | function.apply(thisArg, [arg1, arg2, ...]) | 立即执行 |
bind | function.bind(thisArg, arg1, arg2, ...) | 返回新函数,用到的时候调用 |
下面开始实现它们的自定义版本:
1. es6实现方式
1.实现call函数
Function.prototype.selfCall = function(context, ...args) {
// 1. 处理 context 为 null 或 undefined 的情况
context = context || window;
// 2. 创建唯一的键名避免冲突
const fnKey = Symbol('fn');
// 3. 将当前函数设置为 context 的属性
context[fnKey] = this;
// 4. 执行函数
const result = context[fnKey](...args);
// 5. 删除临时属性
delete context[fnKey];
// 6. 返回结果
return result;
};
// 测试
const person = { name: 'Alice' };
function greet(greeting) {
return `${greeting}, ${this.name}!`;
}
console.log(greet.selfCall(person, 'Hello')); // "Hello, Alice!"
2.实现apply函数
Function.prototype.selfApply = function(context, argsArray) {
// 1. 处理 context 为 null 或 undefined 的情况
context = context || window;
// 2. 创建唯一的键名避免冲突
const fnKey = Symbol('fn');
// 3. 将当前函数设置为 context 的属性
context[fnKey] = this;
// 4. 执行函数(处理参数数组)
const result = argsArray
? context[fnKey](...argsArray)
: context[fnKey]();
// 5. 删除临时属性
delete context[fnKey];
// 6. 返回结果
return result;
};
// 测试
const numbers = [1, 2, 3];
console.log(Math.max.selfApply(null, numbers)); // 3
3.实现bind函数
Function.prototype.selfBind = function(context, ...bindArgs) {
// 1. 保存原始函数引用
// this 指向调用 selfBind 的原始函数
// 例如:fn.selfBind() 中 this = fn
const originalFunc = this;
// 2. 返回绑定函数
// 创建闭包,捕获 context 和 bindArgs
return function boundFn(...callArgs) {
// 3. 判断是否作为构造函数调用
// new.target 检测函数是否通过 new 调用
// 如果使用 new 调用 boundFn,new.target 指向 boundFn
const isNewCall = new.target !== undefined;
// 4. 处理不同的调用场景
if (isNewCall) {
// 4.1 构造函数调用场景
// 忽略绑定的 this (context)
// 合并预设参数和调用时参数
// 使用 new 创建原始函数实例
return new originalFunc(...bindArgs, ...callArgs);
}
// 5. 普通函数调用场景
// 使用绑定的 this (context)
// 合并预设参数和调用时参数
return originalFunc.call(context, ...bindArgs, ...callArgs);
};
};
// 测试
const person = { name: 'Bob' };
function introduce(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const boundIntro = introduce.selfBind(person, 'Hi');
boundIntro('!'); // "Hi, Bob!"
// 构造函数测试
function Person(name, age) {
this.name = name;
this.age = age;
}
const BoundPerson = Person.selfBind(null, 'Alice');
const alice = new BoundPerson(25);
console.log(alice); // { name: 'Alice', age: 25 }
2. bind 的复杂点处理
场景 | 处理方式 |
---|---|
普通调用 | 使用绑定的 this 和合并后的参数 |
构造函数调用 | 忽略绑定的 this ,但保留参数 |
参数合并 | 绑定参数 + 调用参数 |
关键点解释:
闭包设计:
boundFn
函数捕获了三个关键变量:originalFunc
: 原始函数context
: 绑定的 this 值bindArgs
: 预设参数
- 即使外部函数执行完毕,这些变量仍然可用
new.target 检测:
- 当使用
new
调用绑定函数时:
const Bound = fn.selfBind(obj); const instance = new Bound(); // new.target 指向 Bound
- 普通调用时:
new.target
为undefined
- 当使用
构造函数处理:
- 当检测到
new
调用时,忽略绑定的this
值 - 将预设参数和调用参数合并后传递给原始函数
- 使用
new originalFunc(...)
创建实例
- 当检测到
普通调用处理:
- 使用
call
方法调用原始函数 - 传入绑定的
this
和合并后的参数
- 使用
3. 边界情况处理
- context 为空:默认指向全局对象(浏览器中为
window
) - Symbol 使用:避免覆盖对象原有属性
- 构造函数场景:通过
new.target
检测是否被new
调用 - 参数处理:支持剩余参数语法处理变长参数
完整实现代码(ES5 兼容版本)
// call 实现 (ES5)
Function.prototype.selfCall = function(context) {
context = context || window;
var fnKey = '__fn__' + Date.now();
context[fnKey] = this;
var args = [];
for (var i = 1; i < arguments.length; i++) {
args.push('arguments[' + i + ']');
}
var result = eval('context[fnKey](' + args + ')');
delete context[fnKey];
return result;
};
// apply 实现 (ES5)
Function.prototype.seflApply = function(context, argsArray) {
context = context || window;
var fnKey = '__fn__' + Date.now();
context[fnKey] = this;
var result;
if (!argsArray || !argsArray.length) {
result = context[fnKey]();
} else {
var args = [];
for (var i = 0; i < argsArray.length; i++) {
args.push('argsArray[' + i + ']');
}
result = eval('context[fnKey](' + args + ')');
}
delete context[fnKey];
return result;
};
// bind 实现 (ES5)
Function.prototype.selfBind = function(context) {
// 1. 保存原始函数
// this 指向调用 bind 的原始函数
var originalFunc = this;
// 2. 获取预设参数
// 将 arguments 转为数组,跳过第一个参数(context)
// 例如:fn.selfBind(obj, 1, 2) → bindArgs = [1, 2]
var bindArgs = Array.prototype.slice.call(arguments, 1);
// 3. 创建绑定函数
function BoundFn() {
// 4. 获取调用时参数
// 将当前函数的 arguments 转为数组
var callArgs = Array.prototype.slice.call(arguments);
// 5. 检测构造函数调用
// 检查 this 是否是 BoundFn 的实例
// 通过 new 调用时:this instanceof BoundFn 为 true
var isNewCall = this instanceof BoundFn;
// 6. 动态确定 this 值
// 构造函数调用:使用新创建的实例 (this)
// 普通调用:使用预设的 context
return originalFunc.apply(
isNewCall ? this : context,
bindArgs.concat(callArgs) // 合并参数
);
}
// 7. 维护原型链
// 复制原始函数的原型,确保 instanceof 正常工作
BoundFn.prototype = Object.create(originalFunc.prototype);
// 8. 修复构造函数指向
// 确保新实例的 constructor 属性正确指向原始函数
BoundFn.prototype.constructor = originalFunc;
// 9. 返回绑定函数
return BoundFn;
};
测试用例
// 测试 call
const obj1 = { value: 10 };
function add(a, b) {
return this.value + a + b;
}
console.log(add.selfCall(obj1, 5, 3)); // 18
// 测试 apply
const obj2 = { value: 20 };
console.log(add.selfApply(obj2, [5, 3])); // 28
// 测试 bind
const obj3 = { value: 30 };
const boundAdd = add.selfBind(obj3, 5);
console.log(boundAdd(3)); // 38
// 测试构造函数
function Point(x, y) {
this.x = x;
this.y = y;
}
const BoundPoint = Point.selfBind(null, 10);
const point = new BoundPoint(20);
console.log(point); // { x: 10, y: 20 }
实现总结
方法 | 实现难点 | 解决方案 |
---|---|---|
call | 参数传递 | 使用剩余参数或 eval |
apply | 数组参数处理 | 判断数组是否存在并展开 |
bind | 构造函数处理 | 检测 new.target 或 instanceof |
原型链 | 绑定函数原型 | 使用 Object.create 继承 |
下面特别为es6、es5selfBind方法关键点解释:
参数处理:
- 使用
Array.prototype.slice.call(arguments, 1)
将类数组对象转为真实数组 - 获取预设参数(除第一个
context
参数外的所有参数)
- 使用
构造函数检测:
- 通过
this instanceof BoundFn
检测是否被new
调用 new
调用时:this
指向新创建的BoundFn
实例- 普通调用时:
this
指向全局对象或undefined
(严格模式)
- 通过
- 原型链维护:
BoundFn.prototype = Object.create(originalFunc.prototype);
- 创建新对象继承原始函数的原型
- 确保
instanceof
操作符正常工作:
new BoundFn() instanceof originalFunc // true
- 构造函数修复:
BoundFn.prototype.constructor = originalFunc;
- 修正原型对象的
constructor
属性 - 确保继承关系正确:
new BoundFn().constructor === originalFunc // true
- 参数合并:
bindArgs.concat(callArgs)
- 将预设参数和调用时参数合并为单个数组
- 使用
apply
传递合并后的参数数组
两版本核心区别
特性 | ES6+ 版本 | ES5 版本 |
---|---|---|
参数处理 | 剩余参数 ...bindArgs | Array.prototype.slice.call |
构造函数检测 | new.target | this instanceof BoundFn |
原型处理 | 隐式处理 | 显式维护原型链 |
参数传递 | 扩展运算符 ... | apply + concat |
代码简洁性 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
浏览器兼容性 | 现代浏览器 | IE9+ |
使用示例
function Person(name, age) {
this.name = name;
this.age = age;
}
// 测试 ES6+ 版本
const BoundPerson6 = Person.myBind(null, "Alice");
const alice = new BoundPerson6(25);
console.log(alice); // { name: "Alice", age: 25 }
// 测试 ES5 版本
const BoundPerson5 = Person.myBind(null, "Bob");
const bob = new BoundPerson5(30);
console.log(bob); // { name: "Bob", age: 30 }
// 普通调用
function greet(greeting) {
return `${greeting}, ${this.name}`;
}
const boundGreet = greet.myBind({ name: "Charlie" }, "Hello");
console.log(boundGreet()); // "Hello, Charlie"
要点总结
双重调用模式处理:
- 普通调用:使用绑定的
this
值 - 构造函数调用:忽略绑定的
this
,创建新实例
- 普通调用:使用绑定的
参数合并:
- 预设参数 + 调用时参数
- 保持原生
bind
的参数传递顺序
原型链维护:
- 确保构造函数调用时,实例继承原始函数的原型
- 修复
constructor
属性指向
闭包应用:
- 捕获原始函数、绑定上下文和预设参数
- 保持函数调用的上下文信息
Comments | NOTHING