从零实现 JavaScript 中的 `new` 和 ES6 的 `class`:ES5 模拟面向对象编程

77 阅读3分钟

在 JavaScript 中,虽然 ES6 引入了 class 关键字让面向对象编程更加直观和易读,但其底层机制仍然是基于原型(prototype)的继承模型。本文将带你一步步理解:

  • 如何手动实现类似 new 的功能
  • ES6 中 class 的核心特性
  • 如何用 ES5 手动模拟 ES6 的 class 和继承机制

通过这篇文章,你不仅能掌握 JavaScript 面向对象的核心原理,还能深入理解类和原型之间的关系。


一、JavaScript 中的 new 运算符详解

new 的作用

当你使用 new 创建一个对象时,JavaScript 引擎会执行以下步骤:

  1. 创建一个新的空对象。
  2. 将这个新对象的内部 [[Prototype]](即 __proto__)指向构造函数的 prototype 属性。
  3. 构造函数被调用,并且 this 被绑定到新对象。
  4. 如果构造函数返回一个对象,则返回该对象;否则返回新创建的对象。

🛠️ 手动实现 myNew 函数

我们可以模仿 new 的行为,实现一个自定义的 myNew 函数:

function myNew(constructor, ...args) {
  // 1. 创建一个空对象,并继承构造函数的 prototype
  const obj = Object.create(constructor.prototype);

  // 2. 执行构造函数,并绑定 this 到 obj
  const result = constructor.apply(obj, args);

  // 3. 如果构造函数返回了一个对象,则返回它;否则返回新对象
  return (typeof result === 'object' && result !== null) ? result : obj;
}

✅ 使用示例

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log(`Hello, I'm ${this.name}`);
};

// 原生 new
const p1 = new Person('Alice', 25);
p1.sayHello(); // 输出: Hello, I'm Alice

// 自定义 myNew
const p2 = myNew(Person, 'Bob', 30);
p2.sayHello(); // 输出: Hello, I'm Bob

🔍 注意事项

  • Object.create(proto) 用于设置原型链。
  • 构造函数可能返回一个对象(如单例模式),此时应返回该对象。
  • 若未显式返回对象,默认返回我们创建的新对象。

二、ES6 中的 class 简介与使用

ES6 的 class 是一种语法糖,本质上是基于原型的封装。它提供了更清晰、更接近传统面向对象语言(如 Java、C++)的写法。

✅ 最简 class 示例

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }

  sayHello() {
    console.log(`Hello, I'm ${this.name}`);
  }
}

const p = new Person('Alice', 25);
p.sayHello(); // Hello, I'm Alice

🧱 类的组成部分

特性示例
构造函数constructor()
实例方法直接写在类体中的方法
静态方法static info()
getter/setterget fullName() / set fullName(value)
私有字段#privateField(以 # 开头)

✅ 继承与 super

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 调用父类构造函数
    this.breed = breed;
  }

  speak() {
    console.log(`${this.name} barks.`);
  }

  static info() {
    console.log("This is a dog class.");
  }

  #privateData = "Secret Info";

  showPrivateData() {
    console.log(this.#privateData);
  }
}

✅ 使用子类

const d = new Dog("Buddy", "Golden Retriever");
d.speak();            // Buddy barks.
d.showPrivateData();  // Secret Info
Dog.info();           // This is a dog class.

console.log(d instanceof Dog);      // true
console.log(d instanceof Animal);   // true

三、用 ES5 实现 ES6 的 class 与继承机制

为了更深入地理解 class 的本质,我们可以使用 ES5 来模拟其实现。

🎯 模拟目标

我们将模拟以下 ES6 类结构:

class Animal { /* ... */ }
class Dog extends Animal { /* ... */ }

✅ ES5 实现完整代码

// ========== Animal 构造函数 ==========
function Animal(name) {
  if (!(this instanceof Animal)) {
    throw new TypeError("Animal must be called with new");
  }
  this.name = name;
}

Animal.prototype.speak = function () {
  console.log(`${this.name} makes a noise.`);
};

// ========== Dog 构造函数 ==========
function Dog(name, breed) {
  if (!(this instanceof Dog)) {
    throw new TypeError("Dog must be called with new");
  }

  // 模拟 super()
  Animal.call(this, name);
  this.breed = breed;
}

// 设置原型链继承
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

// 重写 speak 方法
Dog.prototype.speak = function () {
  console.log(`${this.name} barks.`);
};

✅ 使用示例

var animal = new Animal("Generic Animal");
animal.speak(); // Generic Animal makes a noise.

var dog = new Dog("Buddy", "Golden Retriever");
dog.speak(); // Buddy barks.

console.log(dog instanceof Dog);      // true
console.log(dog instanceof Animal);   // true

🔍 关键点解析

ES6 语法ES5 模拟方式
class构造函数 + prototype
extendsObject.create(Parent.prototype) 设置原型链
super()Parent.call(this, ...) 调用父类构造函数
method()添加到 prototype 上的方法
instanceof正确设置原型链和 constructor

四、进阶技巧:封装继承工具函数

为了简化继承逻辑,我们可以封装一个通用的 extend 工具函数:

function extend(subClass, superClass) {
  subClass.prototype = Object.create(superClass.prototype);
  subClass.prototype.constructor = subClass;
  subClass.super = superClass;
}

然后这样使用:

extend(Dog, Animal);

这可以让你更清晰地组织代码结构,尤其是在构建大型应用或库时非常有用。


五、注意事项与限制

  • 必须使用 new 调用构造函数:否则 this 会指向全局对象(非严格模式下)或 undefined(严格模式下)。
  • 不支持私有字段:除非你使用闭包或 Symbol 属性进行封装。
  • 静态方法不会自动继承:需要手动绑定。
  • getter/setter 需要使用 Object.defineProperty 来定义。

六、总结

通过这篇文章,你应该已经掌握了以下几个关键知识点:

✅ 如何手动实现 JavaScript 中的 new
✅ ES6 class 的基本语法与特性
✅ 如何用 ES5 模拟 ES6 的类和继承机制
✅ 如何封装工具函数简化继承逻辑

这些知识不仅有助于你更好地理解 JavaScript 的原型系统,也能帮助你在没有现代语法支持的环境中编写兼容性强的代码。


📚 延伸阅读建议

如果你对下面的内容感兴趣,可以继续学习:

  • JavaScript 原型链与继承的深入解析
  • 闭包与模块化设计
  • ES6+ 新特性(如 Symbol, Proxy, Reflect
  • 使用 Babel 编译 ES6+ 代码为 ES5
  • 构建自己的类工厂函数或 OOP 库