引言
在 JavaScript 开发中,我们经常需要处理函数执行上下文(this 指向)和对象原型链。call、apply、bind 和 Object.create 是四个至关重要的原生方法,理解它们的原理和实现方式,不仅有助于我们编写更优雅的代码,也是进阶 JavaScript 开发的必经之路。
本文将深入探讨这四个方法的核心原理,并提供完整的手写实现。通过自己实现这些方法,我们可以更好地理解 JavaScript 底层的工作机制。
一、call 方法
1.1 基本概念
call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。
语法:
func.call(thisArg, arg1, arg2, ...)
1.2 原生使用示例
function greet(message, punctuation) {
console.log(`${message}, ${this.name}${punctuation}`);
}
const person = { name: "Alice" };
// 使用 call 改变 this 指向
greet.call(person, "Hello", "!"); // 输出: Hello, Alice!
1.3 手写实现
Function.prototype.myCall = function (context = globalThis, ...args) {
// 确保 context 是对象(如果不是, 转换为对象包装)
context = Object(context);
// 使用 Symbol 创建唯一键,避免属性冲突
const fnKey = Symbol("fn");
// 将当前函数作为 context 的方法
context[fnKey] = this;
try {
// 执行函数
return context[fnKey](...args);
} finally {
// 确保删除临时添加的方法
delete context[fnKey];
}
};
// 测试
function showInfo(label) {
console.log(`${label}: ${this.name} - ${this.age}`);
}
const user = { name: "John", age: 25 };
showInfo.myCall(user, "用户信息"); // 输出: 用户信息: John - 25
1.4 实现解析
- 参数处理: 第一个参数是
thisArg,后续参数是传递给函数的参数 - 上下文绑定: 将函数临时添加到目标对象上
- 函数执行: 使用目标对象调用函数
- 清理工作: 删除临时添加的属性,避免污染目标对象
二、apply 方法
2.1 基本概念
apply() 方法调用一个具有给定 this 值的函数,以及以一个数组(或类数组对象)的形式提供的参数。
语法:
func.apply(thisArg, argsArray)
2.2原生使用示例
function sum(a, b, c) {
return a + b + c;
}
const numbers = [1, 2, 3];
// 使用 apply 传递数组参数
const result = sum.apply(null, numbers); // 6
console.log(result);
2.3 手写实现
Function.prototype.myApply = function (context = globalThis, argsArray) {
// 处理 context
context = Object(context);
// 如果 argsArray 不是数组或类数组, 则当作空数组处理
if (
!Array.isArray(argsArray) &&
!(argsArray && typeof argsArray === "object" && "length" in argsArray)
) {
argsArray = [];
}
// 创建唯一键
const fnKey = Symbol("fn");
// 将函数绑定到 context
context[fnKey] = this;
try {
// 执行函数,使用展开运算符传递参数
return context[fnKey](...argsArray);
} finally {
// 清理
delete context[fnKey];
}
};
// 测试
function calculate(operation, ...numbers) {
const ops = {
sum: (a, b) => a + b,
product: (a, b) => a * b,
};
return numbers.reduce(ops[operation]);
}
const nums = [2, 3, 4];
const sumResult = calculate.myApply(null, ["sum", ...nums]);
const productResult = calculate.myApply(null, ["product", ...nums]);
console.log("Sum: ", sumResult); // 输出: Sum: 9
console.log("Product: ", productResult); // 输出: Product: 24
2.4 Call vs Apply 的区别
2.4 call vs apply 的区别
| 特性 | 参数传递 | 性能 | 适用场景 |
|---|---|---|---|
| Call | 逐个传递 | 通常稍快 | 参数数量已知 |
| Apply | 数组形式传递 | 需要处理数组 | 参数数量动态或已存在数组中 |
三、bind 方法
3.1 基本概念
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
语法:
func.bind(thisArg[, arg1[, arg2[, ...]]])
3.2 原生使用示例
const module1 = {
x: 42,
getX: function () {
return this.x;
},
};
const unboundGetX = module1.getX;
console.log(unboundGetX()); // undefined (this 指向全局对象)
const boundGetX = unboundGetX.bind(module1);
console.log(boundGetX()); // 42
3.3 手写实现
Function.prototype.myBind = function (context, ...bindArgs) {
// 保存原函数
const originalFunc = this;
// 判断是否是构造函数
if (typeof originalFunc !== "function") {
throw new TypeError("Function.prototype.bind called on non-function");
}
// 返回的绑定函数
const boundFunc = function (...callArgs) {
// 判断是否通过 new 调用
// 通过 new 调用时, this 应该是 boundFunc 的实例
const isNewCall = this instanceof boundFunc;
// 确定执行上下文
const thisContext = isNewCall ? this : Object(context || globalThis);
// 合并参数
const allArgs = bindArgs.concat(callArgs);
// 调用原函数
return originalFunc.apply(thisContext, allArgs);
};
// 维护原型链
if (originalFunc.prototype) {
// 使用 Object.create 来创建原型链, 避免直接修改
boundFunc.prototype = Object.create(originalFunc.prototype);
// 修正 constructor 指向
boundFunc.prototype.constructor = boundFunc;
}
// 保留原函数的 length 属性(可选, 非标准)
try {
// 计算绑定函数的 length 属性(原函数参数个数 - 绑定的参数个数)
const originalLength = originalFunc.length;
const bindArgsLength = bindArgs.length;
const remainingArgs = Math.max(originalLength - bindArgsLength, 0);
// 使用 Object.defineProperty 来定义不可枚举的 length 属性
Object.defineProperty(boundFunc, "length", {
value: remainingArgs,
writable: false,
enumerable: false,
configurable: true,
});
} catch (e) {
// 忽略错误
}
// 保留原函数的 name 属性(可选)
try {
Object.defineProperty(boundFunc, "name", {
value: `bound ${originalFunc.name || ""}`,
writable: false,
enumerable: false,
configurable: true,
});
} catch (e) {
// 忽略错误
}
return boundFunc;
};
// 测试
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.introduce = function () {
console.log(`Hi, I'm ${this.name}, ${this.age} years old.`);
};
// 创建绑定函数
const BoundPerson = Person.myBind(null, "Alice");
const person = new BoundPerson(25); // 使用 new 调用
person.introduce(); // 输出: Hi, I'm Alice, 25 years old.
// 普通绑定使用
const obj = { x: 10 };
function multiply(y) {
return this.x * y;
}
const boundMultiply = multiply.myBind(obj);
console.log(boundMultiply(5)); // 输出: 50
3.4 bind的特殊之处
- 部分应用: 可以预先绑定部分参数
- 构造函数支持: 绑定后的函数可以作为构造函数使用
- 延迟执行: 返回新函数,不会立即执行
四、Object.create 方法
4.1 基本概念
Object.create()方法创建一个新对象,使用现有的对象作为新创建对象的原型。
语法:
Object.create(proto[, propertiesObject])
4.2 原生使用示例
// 创建一个原型为 null 的对象
const obj1 = Object.create(null);
console.log(obj1.toString); // undefined
// 使用原型创建对象
const person = {
greet() {
console.log(`Hello, ${this.name}`);
},
};
const john = Object.create(person);
john.name = "John";
john.greet(); // 输出: Hello, John
4.3 手写实现
Object.myCreate = function (proto, propertiesObject) {
// 参数校验
if (
proto !== null &&
typeof proto !== "object" &&
typeof proto !== "function"
) {
throw new TypeError("Object prototype may only be an Object or null");
}
// 创建一个临时构造函数
function TempFunc() {}
// 设置构造函数的原型
TempFunc.prototype = proto;
// 创建新实例
const obj = new TempFunc();
// 如果 proto 是 null, 手动设置原型为 null
if (proto === null) {
Object.setPrototypeOf(obj, null);
}
// 处理第二个参数: 属性描述符
if (propertiesObject !== undefined) {
Object.defineProperties(obj, propertiesObject);
}
return obj;
};
// 测试
const animal = {
makeSound() {
console.log(this.sound || "Some sound");
},
};
// 创建继承自 animal 的对象
const dog = Object.myCreate(animal, {
sound: {
value: "Woof!",
writable: true,
},
breed: {
value: "Labrador",
enumerable: true,
},
});
dog.makeSound(); // 输出: Woof!
console.log(dog.breed); // 输出: Labrador
console.log(Object.getPrototypeOf(dog) === animal); // 输出: true
4.4 Object.create的应用场景
- 原型继承: 实现对象之间的继承关系
- 纯净对象: 创建没有原型链的对象(
Object.create(null)) - 属性定义: 同时定义多个属性及其特性
五、综合应用与对比
5.1 实现一个完整的继承系统
// 父类
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function () {
console.log(`${this.name} is eating.`);
};
// 子类
function Dog(name, breed) {
// 使用 call 继承属性
Animal.call(this, name);
this.breed = breed;
}
// 使用 Object.create 继承原型
Dog.prototype = Object.myCreate(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function () {
console.log(`${this.name} (${this.breed} says: Woof!)`);
};
// 使用 bind 创建特定行为
const sleepyEat = Animal.prototype.eat.bind({ name: "Sleepy" });
// 测试
const myDog = new Dog("Buddy", "Golden Retriever");
myDog.eat(); // 输出: Buddy is eating.
myDog.bark(); // 输出: Buddy (Golden Retriever says: Woof!)
sleepyEat(); // 输出: Sleepy is eating.
5.2 四种方法对比总结
| 方法 | 作用 | 返回值 | 是否立即执行 |
|---|---|---|---|
| call | 改变函数this指向并立即调用 | 函数返回值 | 是 |
| apply | 改变函数this指向并以数组传参调用 | 函数返回值 | 是 |
| bind | 创建新的绑定函数 | 新函数 | 否 |
| Object.create | 创建指定原型的对象 | 新对象 | - |
5.3 性能考虑
- call/apply: 频繁使用会影响性能(每次都要创建/删除属性)
- bind: 适合重复调用的场景,只需绑定一次
- Object.create: 比
new Constructor()更轻量,适合纯原型继承
六、现代 JavaScript 中的替代方案
6.1 箭头函数
箭头函数没有自己的this,会捕获其所在上下文的this值。
// 传统方式
const obj1 = {
value: 10,
getValue: function () {
return this.value;
}.bind(this),
};
// 箭头函数方式
const obj2 = {
value: 10,
getValue: () => this.value,
};
6.2 类继承(ES6+)
// ES5 继承方式
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function () {
console.log(`${this.name} eats`);
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// ES6类继承
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} eats`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log(`${this.name} barks`);
}
}
6.3 属性描述符
// 替代 Object.create 的部分功能
const obj = {
// 普通属性
normalProp: "value",
// 使用 getter/setter
get computedProp() {
return this.normalProp.toUpperCase();
},
set computedProp(value) {
this.normalProp = value.toLowerCase();
},
};
// 使用 Object.defineProperty 定义属性特性
Object.defineProperty(obj, "readOnlyProp", {
value: "cannot change",
writable: false,
enumerable: true,
configurable: false,
});
七、常见面试题解析
7.1 手写new操作符
function myNew(Constructor, ...args) {
// 1. 创建一个新对象,继承 Constructor 的原型
const obj = Object.myCreate(Constructor.prototype);
// 2. 执行构造函数, 绑定 this 到新对象
const result = Constructor.apply(obj, args);
// 3. 如果构造函数返回对象,则返回该对象; 否则返回新对象
return result instanceof Object ? result : obj;
}
// 测试
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.introduce = function () {
console.log(`I'm ${this.name}, ${this.age} years old.`);
};
const p = myNew(Person, "Tim", 30);
p.introduce(); // 输出: I'm Tim, 30 years old.
7.2 实现深冻结对象
function deepFreeze(obj) {
// 获取对象的所有属性名
const propNames = Object.getOwnPropertyNames(obj);
// 冻结自身
Object.freeze(obj);
// 递归冻结所有属性
for (const name of propNames) {
const value = obj[name];
if (value && typeof value === "object") {
deepFreeze(value);
}
}
return obj;
}
// 使用 Object.create 创建不可变对象
const immutableProto = Object.freeze({
method() {
return "immutable method";
},
});
const immutableObj = Object.myCreate(immutableProto);
7.3 实现柯里化函数
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
// 参数足够,直接执行
return fn.apply(this, args);
} else {
// 参数不足,返回新函数继续收集参数
return function (...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
}
};
}
// 测试
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
八、总结
通过手写实现call、apply、bind和Object.create, 我们深入理解了 JavaScript 的核心机制:
- 执行上下文管理:
call和apply让我们可以显式控制函数的this指向 - 函数绑定与复用:
bind创建了可复用的函数,特别适合事件处理和回调函数 - 原型继承系统:
Object.create是JavaScript 原型继承的基础
虽然现代 JavaScript 提供了箭头函数、类语法等更高级的特性,但这些底层方法仍然是理解 JavaScript 运行机制的关键。掌握它们不仅能帮助我们写出更好的代码,还能在调试和性能优化时提供更多思路。
在实际开发中,我们应该根据具体场景选择合适的方法:
- 需要立即执行且参数明确时,使用
call - 参数在数组中时, 使用
apply - 需要创建可复用的函数时, 使用
bind - 需要创建特定原型的对象时, 使用
Object.create
理解这些方法的底层原理,将使我们成为一名更优秀的 JavaScript 开发者。
拓展阅读建议: