JavaScript 核心方法深度解析:手写 call、apply、bind 和 Object.create

143 阅读6分钟

引言

在 JavaScript 开发中,我们经常需要处理函数执行上下文(this 指向)和对象原型链。callapplybindObject.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 实现解析
  1. 参数处理: 第一个参数是 thisArg,后续参数是传递给函数的参数
  2. 上下文绑定: 将函数临时添加到目标对象上
  3. 函数执行: 使用目标对象调用函数
  4. 清理工作: 删除临时添加的属性,避免污染目标对象

二、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的特殊之处
  1. 部分应用: 可以预先绑定部分参数
  2. 构造函数支持: 绑定后的函数可以作为构造函数使用
  3. 延迟执行: 返回新函数,不会立即执行

四、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的应用场景
  1. 原型继承: 实现对象之间的继承关系
  2. 纯净对象: 创建没有原型链的对象(Object.create(null))
  3. 属性定义: 同时定义多个属性及其特性

五、综合应用与对比

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 性能考虑
  1. call/apply: 频繁使用会影响性能(每次都要创建/删除属性)
  2. bind: 适合重复调用的场景,只需绑定一次
  3. 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

八、总结

通过手写实现callapplybindObject.create, 我们深入理解了 JavaScript 的核心机制:

  1. 执行上下文管理: callapply让我们可以显式控制函数的this指向
  2. 函数绑定与复用: bind创建了可复用的函数,特别适合事件处理和回调函数
  3. 原型继承系统: Object.create 是JavaScript 原型继承的基础

虽然现代 JavaScript 提供了箭头函数、类语法等更高级的特性,但这些底层方法仍然是理解 JavaScript 运行机制的关键。掌握它们不仅能帮助我们写出更好的代码,还能在调试和性能优化时提供更多思路。

在实际开发中,我们应该根据具体场景选择合适的方法:

  • 需要立即执行且参数明确时,使用call
  • 参数在数组中时, 使用apply
  • 需要创建可复用的函数时, 使用bind
  • 需要创建特定原型的对象时, 使用Object.create

理解这些方法的底层原理,将使我们成为一名更优秀的 JavaScript 开发者。


拓展阅读建议: