JavaScript 面向对象编程的演进:从原始模式到 ES6 class

66 阅读3分钟

在前端开发中,JavaScript 的面向对象编程(OOP)一直是一个绕不开的话题。虽然 JS 是一种基于对象的语言,但它不像 Java 或 C++ 那样天生支持类(class),而是通过原型链(prototype chain)实现了一套独特的 OOP 机制。今天我们就来一起回顾一下 JS 中面向对象编程的发展历程,从最原始的对象字面量,到构造函数、原型模式,再到 ES6 引入的 class 语法糖。


🧱 1. 原始模式:对象字面量

最开始我们可能这样写一个“猫”:

var Cat = {
  name: "",
  color: ""
};

var cat1 = {};
cat1.name = "加菲猫";
cat1.color = "橘色";

var cat2 = {};
cat2.name = "黑猫警长";
cat2.color = "黑色";

这种方式虽然直观,但存在明显问题:

  • 每次创建实例都要手动赋值,重复代码多;
  • 实例之间毫无关联,无法共享行为;
  • 不利于维护和扩展。

💡 小结:这是面向对象思想的雏形,但不够优雅,也不够“面向对象”。


🔧 2. 构造函数模式:封装实例化过程

为了解决上述问题,我们引入了构造函数模式:

function Cat(name, color) {
  this.name = name;
  this.color = color;
}

const cat1 = new Cat('加菲猫', '橘色');
const cat2 = new Cat('黑猫警长', '黑色');

✅ new 关键字做了什么?

  1. 创建一个空对象 {}
  2. 将这个空对象的 __proto__ 指向构造函数的 prototype
  3. 执行构造函数内部代码,给 this 添加属性;
  4. 返回这个新对象。

此时我们可以通过 instanceof 判断类型:

console.log(cat1 instanceof Cat); // true

⚠️ 但是!每个实例都拥有自己的方法副本,比如你给 Cat.prototype.eat = function(){...},那所有实例都能调用,但如果直接在构造函数里定义方法,就会造成内存浪费。


🔄 3. 原型模式:共享方法与属性

为了优化性能,我们将公共方法放到 prototype 上:

function Cat(name, color) {
  this.name = name;
  this.color = color;
}

Cat.prototype.type = '猫科动物';
Cat.prototype.eat = function() {
  alert('喜欢jerry');
};

const cat1 = new Cat('tom', '黑色');
console.log(cat1.type); // 猫科动物
cat1.eat(); // 调用原型上的方法

🔍 如何判断属性来源?

console.log(cat1.hasOwnProperty('name')); // true(实例自有)
console.log(cat1.hasOwnProperty('type'));  // false(来自原型)
console.log('type' in cat1);               // true(可访问)

💡 原型链查找机制:当访问某个属性时,先查实例,再查原型,直到找到或到达顶层。


📚 4. 继承:让子类复用父类功能

假设我们要实现“猫是动物的一种”,可以使用 apply 和原型链结合的方式:

function Animal() {
  this.species = '动物';
}

Animal.prototype.sayHi = function() {
  console.log('哪哪哪啦');
};

function Cat(name, color) {
  Animal.apply(this); // 绑定 this,继承属性
  this.name = name;
  this.color = color;
}

// 设置原型链
Cat.prototype = new Animal(); // 注意:这会丢失 constructor

然后就可以:

const cat = new Cat('加菲猫', '橘色');
cat.sayHi(); // 哪哪哪啦

❗️ 问题来了:

  • Cat.prototype.constructor 变成了 Animal,需要手动修复;
  • 这种方式容易出错,逻辑复杂。

🤔 所以早期开发者很痛苦,继承机制不直观。


✨ 5. ES6 class:语法糖登场!

终于,在 ES6 中,JavaScript 引入了 class 关键字,让我们可以用更清晰的语法写出面向对象代码:

class Cat {
  constructor(name, color) {
    this.name = name;
    this.color = color;
  }

  eat() {
    console.log('喜欢jerry');
  }
}

const cat1 = new Cat('tom', '黑色');
cat1.eat();

✅ 优点:

  • 语义清晰,符合传统 OOP 思维;
  • 支持继承(extends)、静态方法、getter/setter 等;
  • 底层依然是原型链,只是包装得更好用了。
class Animal {
  constructor() {
    this.species = '动物';
  }
  sayHi() {
    console.log('哪哪哪啦');
  }
}

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

🧩 总结:JS OOP 的演变之路

阶段方式特点缺点
初始对象字面量直观简单无封装,无共享
第一代构造函数封装实例化方法重复,浪费内存
第二代原型模式共享方法复杂,难理解
第三代继承 + prototype实现继承逻辑混乱,易错
当代ES6 class清晰简洁本质仍是原型

🌟 核心结论class 是语法糖,底层依然是原型链。它让 JS 更接近传统的面向对象语言,降低了学习成本,提升了可读性。


🚀 写在最后

作为开发者,我们不仅要会写 class,更要理解它的背后——原型链才是 JS 面向对象的根基。当你看到 cat.__proto__.constructor === Cat 时,你就真正懂了 JS 的灵魂。