《JS 没有 class 时怎么实现继承?一文搞懂原型链与 OOP 演进》

32 阅读4分钟

JavaScript 中的面向对象与继承:从原型到 class

“JS 是基于对象的语言,但不是传统意义上的面向对象语言。”

这句话道出了 JavaScript 面向对象(OOP)机制的独特之处。它没有 Java 或 C++ 那样原生的类系统,却通过原型(prototype) 实现了强大的对象模型。本文将带你从最原始的对象字面量出发,一步步理解 JS 如何实现封装、实例化、继承,并最终演进到 ES6 的 class 语法。


一、一切皆对象?JS 的“类”从何而来?

在 JavaScript 中,你遇到的几乎所有东西都可以看作对象——函数是对象,数组是对象,甚至字符串、数字等基本类型也有对应的包装对象(如 StringNumber)。

但早期的 JS 并没有 class 关键字(直到 ES6 才引入),也没有传统 OOP 中的“构造器”概念。那么,如何创建具有相同结构的多个对象呢?

最原始的方式:对象字面量

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

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

这种方式虽然简单,但存在明显问题:

  • 代码重复;
  • cat1cat2 之间没有任何“关系”;
  • 无法体现“模板”或“类”的抽象概念。

二、封装实例化过程:构造函数登场

为了解决上述问题,开发者开始使用函数 + new 来模拟“类”:

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

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

new 背后发生了什么?

当你执行 new Cat(...) 时,JS 引擎会自动完成以下四步:

  1. 创建一个空对象 {}
  2. 将该对象的 __proto__ 指向 Cat.prototype
  3. 执行 Cat 函数,this 指向新对象;
  4. 如果函数没有显式返回对象,则返回这个新对象。

这样,我们就实现了封装实例化


三、方法复用:prototype 的妙用

如果每个 Cat 实例都拥有自己的 sayHello 方法,会造成内存浪费。于是,JS 引入了 prototype

Cat.prototype.sayHello = function() {
  console.log("我是" + this.name);
};

cat1.sayHello(); // 我是加菲猫

所有通过 new Cat() 创建的实例,都会通过原型链共享 prototype 上的方法。这正是 JS 实现继承和多态的基础。

核心思想:把不变的属性和公用的方法,放到原型对象上。


四、如何实现继承?

JS 没有原生的“类继承”,但我们可以通过多种方式模拟。

1. 原型链继承(基础但有缺陷)

function Animal(name) {
  this.name = name;
}
Animal.prototype.eat = function() { console.log("eating..."); };

function Cat(name, color) {
  this.color = color;
}
Cat.prototype = new Animal(); // 继承 Animal

问题:无法向父类传参;引用属性会被所有实例共享。


2. 构造函数绑定(借用构造器)

function Cat(name, color) {
  Animal.call(this, name); // 绑定 this,调用父构造器
  this.color = color;
}

优点:可传参,避免属性共享。
缺点:无法继承父类原型上的方法。


3. 组合继承(经典方案)

结合以上两种方式:

function Cat(name, color) {
  Animal.call(this, name); // 属性继承
}
Cat.prototype = new Animal(); // 方法继承
Cat.prototype.constructor = Cat; // 修正 constructor

这是 ES5 时代最常用的继承模式。


4. 寄生组合继承(最优解)

避免两次调用父构造函数:

Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

5. ES6 Class:语法糖,但更优雅

class Animal {
  constructor(name) {
    this.name = name;
  }
  eat() { console.log("eating..."); }
}

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

虽然 class 本质仍是基于原型,但它让代码更清晰、更接近传统 OOP 语言。

📌 重要提醒:ES6 的 class 只是语法糖,底层依然是原型链机制!


五、辅助工具:判断对象关系

  • instanceof:判断实例是否属于某构造函数

    cat1 instanceof Cat; // true
    
  • hasOwnProperty:判断是否是自身属性(非原型)

  • for...in:遍历所有可枚举属性(包括原型链上的,需配合 hasOwnProperty 过滤)


结语

JavaScript 的面向对象机制看似“另类”,实则灵活而强大。从对象字面量 → 构造函数 → prototype → class,每一步都是对“如何更好地组织代码”的探索。

理解原型链和 new 的工作原理,不仅能写出更健壮的代码,还能在面试中从容应对 OOP 相关问题。

记住:JS 不是“没有类”,而是“万物皆可为类”——只要你懂得原型。