JavaScript 面向对象编程OOP:从原型到继承的深度解析

60 阅读6分钟

JavaScript 面向对象编程:从原型到继承的深度解析

在 JavaScript 的世界里,一切皆对象。但它的“面向对象”方式却与众不同——没有类的束缚,只有灵活的原型链。本文将带你深入理解 JS 面向对象的本质,从对象创建到继承实现,层层剖析,助你打通 OOP 的任督二脉。


🔍 为什么说 JavaScript 的 OOP 很特别?

在 Java 或 C++ 这样的传统语言中,我们通过 class 定义模板,再用 new 创建实例,是一种典型的 类式继承(Class-based Inheritance)

而 JavaScript 是一门 基于原型(Prototype-based) 的语言。它没有真正的“类”,只有对象之间的委托关系 —— 即通过 原型链(Prototype Chain) 实现属性查找和方法共享。

📌 关键点:ES6 的 class 只是语法糖,底层仍是原型机制。

这使得 JavaScript 的 OOP 更加动态、灵活,但也更容易让人困惑。要真正掌握它,我们必须回到“原型”这个核心概念上来。


🏗️ 第一步:如何创建一个对象?

方式一:对象字面量(适合单例)

const cat1 = {
    name: "加菲猫",
    color: "橘色"
};

简单直观,但无法复用,不适合批量创建。

方式二:构造函数模式(实现封装)

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

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

✅ 使用 new 关键字时发生了什么?
JavaScript 引擎会自动完成以下四步:

  1. 创建一个新的空对象 {}
  2. 将构造函数内部的 this 指向该新对象;
  3. 执行构造函数体内的代码,为对象添加属性;
  4. 返回这个新对象(除非构造函数显式返回其他对象)。

🧠 这个过程实现了“封装”——把创建对象的逻辑集中起来,避免重复。


⚠️ 构造函数的问题:方法重复浪费内存

如果我们给每个猫都加上 eat() 方法:

function Cat(name, color) {
    this.name = name;
    this.color = color;
    this.eat = function() {
        console.log(this.name + " 喜欢吃 jerry!");
    };
}

那么每创建一个实例,都会重新生成一个 eat 函数,造成内存浪费!

cat1.eat !== cat2.eat; // true → 两个不同的函数副本

💡 解决方案:让所有实例 共享同一个方法 —— 这就是 原型(prototype) 的用武之地。


🌀 原型机制:共享的秘密武器

什么是 prototype?

每个函数都有一个 prototype 属性,指向一个对象(称为“原型对象”),该对象中的属性和方法可以被所有实例共享。

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

// 共享的方法放在这里
Cat.prototype.eat = function() {
    console.log(this.name + " 喜欢吃 jerry!");
};

Cat.prototype.type = "猫科动物";

此时:

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

cat1.eat === cat2.eat; // true → 同一个函数引用
console.log(cat1.type); // 猫科动物(来自原型)

🧩 图解:构造函数、实例与原型的关系

graph TD
    A[Cat 构造函数] -->|prototype| B(Cat.prototype 对象)
    B -->|constructor| A
    C[cat1 实例] -->|__proto__| B
    D[cat2 实例] -->|__proto__| B

🔹 Cat.prototype 是原型对象
🔹 所有由 new Cat() 创建的实例,其 __proto__ 都指向 Cat.prototype
🔹 constructor 属性反向指向构造函数本身

💡 注意:现代开发中应避免直接操作 __proto__,它是非标准但广泛支持的属性。推荐使用 Object.getPrototypeOf(obj) 获取原型。


🔍 原型链:属性查找的路径

当你访问 cat1.name,JS 引擎按如下顺序查找:

  1. 先看 cat1 自身有没有 name
  2. 没有?就去 cat1.__proto__(即 Cat.prototype)找
  3. 还没有?继续往上找 Cat.prototype.__proto__(即 Object.prototype
  4. 最终到达 null,停止搜索

这就是 原型链(Prototype Chain)

📊 属性查找流程图

flowchart LR
    Start[开始访问 obj.prop] --> HasOwn{obj 有 prop?}
    HasOwn -- 是 --> Return[返回值]
    HasOwn -- 否 --> HasProto{存在 __proto__?}
    HasProto -- 否 --> NotFound[undefined]
    HasProto -- 是 --> CheckProto[检查 __proto__ 上是否有 prop]
    CheckProto --> HasOwn

如何判断属性在哪一层?

方法说明
obj.hasOwnProperty('prop')是否为自身属性(不包括原型)
'prop' in obj是否存在于自身或原型链中
Cat.prototype.isPrototypeOf(obj)判断某对象是否是另一个对象的原型
console.log(cat1.hasOwnProperty('name'));     // true → 自身属性
console.log(cat1.hasOwnProperty('type'));      // false → 来自原型
console.log('type' in cat1);                   // true → 存在于原型链
console.log(Cat.prototype.isPrototypeOf(cat1)); // true

🧬 继承:子类复用父类的能力

JavaScript 中的继承,本质上是 原型链的连接。常见的实现方式有多种。


方式一:构造函数绑定(仅继承属性)

利用 call / apply 改变 this 指向,在子类中调用父类构造函数。

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

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

const cat1 = new Cat("加菲猫", "橘色");
console.log(cat1.species); // 动物

✅ 优点:能正确继承父类实例属性
❌ 缺点:无法继承父类原型上的方法


方式二:原型链继承(继承方法)

将子类的原型设置为父类的一个实例。

function Animal() {
    this.species = '动物';
}
Animal.prototype.sayHi = function() {
    console.log('哪哪哪啦~');
};

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

// 核心:继承整个原型链
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat; // 修复 constructor 指向

const cat1 = new Cat("加菲猫", "橘色");
cat1.sayHi(); // 哪哪哪啦~(成功继承!)

⚠️ 注意:

  • Cat.prototype = new Animal() 会导致父类构造函数被提前执行
  • 必须手动修复 constructor,否则 cat1.constructor === Animal

方式三:组合继承(最常用)

结合前两种方式,既继承属性,又继承方法。

function Animal(species) {
    this.species = species || '动物';
}
Animal.prototype.sayHi = function() {
    console.log('哪哪哪啦~');
};

function Cat(name, color, species) {
    Animal.call(this, species); // 继承属性(第二次调用 Animal)
    this.name = name;
    this.color = color;
}

Cat.prototype = new Animal(); // 继承方法(第一次调用 Animal)
Cat.prototype.constructor = Cat;

const cat1 = new Cat("加菲猫", "橘色");

✅ 优点:功能完整,兼容性好
⚠️ 缺点:父类构造函数被执行了两次(性能略差)


方式四:寄生组合式继承(终极方案)

解决“调用两次构造函数”的问题,是目前最高效的继承方式。

function inherit(Child, Parent) {
    // 创建一个干净的对象,其原型为 Parent.prototype
    const F = function() {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
}

// 使用
inherit(Cat, Animal);

或者更现代的方式(ES5+):

Object.setPrototypeOf(Child.prototype, Parent.prototype);
// 或
Child.prototype.__proto__ = Parent.prototype;

但这不是重点。重点是:ES6 的 class 已经帮你做好了一切。


✨ ES6 Class:优雅的语法糖

虽然底层还是原型,但 class 让代码更清晰、易读、易维护。

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

    sayHi() {
        console.log('哪哪哪啦~');
    }
}

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

    eat() {
        console.log(`${this.name} 喜欢吃 jerry`);
    }
}

const cat1 = new Cat("加菲猫", "橘色");
cat1.sayHi(); // 哪哪哪啦~
cat1.eat();   // 加菲猫 喜欢吃 jerry

🔧 extends 背后做了什么?

classDiagram
    class Animal {
        +species: string
        +sayHi()
    }

    class Cat {
        +name: string
        +color: string
        +eat()
    }

    Cat --|> Animal : extends

✅ 编译后等价于:

  • Cat.prototype.__proto__ = Animal.prototype
  • Cat.__proto__ = Animal
  • super 指向父类构造函数

🎯 总结:class 不是新机制,而是对原型系统的良好封装,极大提升了开发效率。


📚 总结:JavaScript 面向对象的核心思想

概念说明
构造函数用于初始化实例属性,相当于“类”的定义入口
prototype实现方法共享,避免内存浪费
proto / [[Prototype]]实例指向原型的链接,构成原型链
原型链属性查找的路径,也是继承的基础
继承本质修改原型链,使子类能访问父类的属性和方法
class 是语法糖提供更友好的写法,底层仍依赖原型机制

🎯 写在最后

JavaScript 的面向对象看似复杂,实则简洁统一 —— 一切归于对象,一切基于原型

一旦你理解了:

“对象通过 __proto__ 连接成链,属性沿着这条链向上查找”

你就掌握了 JS OOP 的灵魂。

无论你是使用原生构造函数、手动搭建继承体系,还是直接使用 class,背后的机制始终如一。

🚀 掌握原型,才能驾驭 JavaScript 的灵活性;理解继承,方能写出高质量、可扩展的应用架构。


🔖 点赞 + 收藏 + 关注,不错过更多前端深度解析!