JavaScript 面向对象编程(OOP):从原始模式到原型继承

JavaScript 面向对象编程(OOP):从原始模式到原型继承

本文系统梳理 JavaScript 中面向对象编程的核心思想与实现方式,涵盖封装、原型链、继承等关键概念,并对比 ES5 与 ES6 的不同写法。适合初学者理解 JS OOP 的底层机制,也帮助进阶开发者巩固基础。


一、JavaScript 是"基于对象"的语言

JavaScript 虽然常被归类为"面向对象语言",但它没有传统意义上的类(Class) (ES6 之前)。它是一种 基于原型(Prototype-based) 的语言,几乎所有值(包括基本类型)在运行时都能表现为对象。

  • 早期 JS 没有 class 关键字,但可以通过函数模拟"构造器"。
  • ES6 引入的 class 只是语法糖,底层依然依赖原型链机制。

二、生成实例对象的原始模式

最朴素的方式是使用对象字面量

css 复制代码
var cat1 = {
  name: "加菲猫",
  color: "橘色"
};
var cat2 = {
  name: "coke",
  color: "黑色"
};

问题

  • 代码重复;
  • 无法批量创建;
  • 实例之间无关联,难以判断"是否属于同一类"。

三、构造函数模式:封装实例化过程

通过函数 + new 关键字实现"类"的效果:

ini 复制代码
function Cat(name, color) {
  this.name = name;
  this.color = color;
}

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

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

instanceof:

  • 作用 :检测构造函数的 prototype 是否出现在对象的原型链上。
  • 返回值:布尔值。
  • 用途:判断对象是否为某构造函数的实例。

new 的执行过程(关键!):

  1. 创建一个空对象 {}
  2. 将该对象的 __proto__ 指向 Cat.prototype
  3. 执行构造函数,this 指向新对象;
  4. 返回该对象(若构造函数无显式返回)。

⚠️ 若直接调用 Cat()(不加 new),this 指向全局对象(如 window),造成污染。


四、原型(Prototype)模式:解决方法重复问题

如果在构造函数中定义方法,每个实例都会拥有独立副本,浪费内存:

ini 复制代码
function Cat(name, color) {
  this.name = name;
  this.color = color;
  // ❌ 每个实例都新建一个 eat 函数
  this.eat = function() { console.log('吃鱼'); };
}

解决方案:将公共属性/方法挂载到 prototype

ini 复制代码
function Cat(name, color) {
  this.name = name;
  this.color = color;
}

Cat.prototype.type = '猫';
Cat.prototype.eat = function() {
  console.log('喜欢 eat jerry');
};

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

console.log(cat1.type); // '猫'
cat1.eat(); // 共享方法

原型链查找机制:

  • 访问属性时,先查实例自身(hasOwnPropertytrue);
  • 若未找到,则沿 __proto__ 向上查找 prototype
  • 直到 Object.prototype,最终为 null
arduino 复制代码
console.log(cat1.hasOwnProperty('name')); // true(自有属性)
console.log(cat1.hasOwnProperty('type')); // false(来自 prototype)
console.log('type' in cat1); // true(可访问,无论来源)

Object.prototype.hasOwnProperty(prop):

  • 作用 :判断对象自身(不包括原型链)是否拥有指定属性。
  • 返回值:布尔值。
  • 用途:区分自有属性和继承属性。

五、继承:让子类拥有父类的能力

1. 借用构造函数(仅继承实例属性)

ini 复制代码
function Animal() {
  this.species = '动物';
}

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

apply:

  • 作用 :调用一个函数,并显示指定其内部 this 的值,同时传入参数数组(或类数组)。
  • 常用于:在子类构造函数中"借用"父类构造函数,实现属性继承。

✅ 优点:可传承,支持多继承。

❌ 缺点:无法继承父类原型上的方法 (如 Animal.prototype.sayHi)。


2. 原型链继承(继承原型方法)

ini 复制代码
function Animal() {
  this.species = '动物';
}
Animal.prototype.sayHi = function() {
  console.log('你好,我是动物');
};

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

// 关键:让 Cat.prototype 指向 Animal 实例
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat; // 修复 constructor 指向

const cat = new Cat('加菲猫', '橘色');
cat.sayHi(); // ✅ 成功继承原型方法

这是经典的 组合继承(Combination Inheritance)

  • 构造函数继承属性(支持传参);
  • 原型链继承方法(共享、高效)。

六、ES6 Class:语法糖,更清晰的 OOP 写法

javascript 复制代码
class Animal {
  constructor() {
    this.species = '动物';
  }
  sayHi() {
    console.log('你好,我是动物');
  }
}

class Cat extends Animal {
  constructor(name, color) {
    super(); // 等价于 Animal.call(this)
    this.name = name;
    this.color = color;
  }
  eat() {
    console.log('喜欢 eat jerry');
  }
}

const cat1 = new Cat('tom', '黑色');
cat1.sayHi(); // 继承自 Animal
cat1.eat();   // 自身方法

注意:

  • class 本质仍是基于原型;
  • super() 必须在子类 constructor 中调用;
  • 方法自动绑定到 prototype,无需手动设置。

七、总结:JS OOP 的演进路径

方式 特点 适用场景
对象字面量 简单直接 单例、配置对象
构造函数 封装实例化 需要多个相似对象
原型模式 共享方法,节省内存 大量实例 + 公共行为
组合继承 属性 + 方法完整继承 传统 ES5 继承方案
ES6 Class 语法简洁,语义清晰 现代项目首选

八、延伸思考

  • 为什么 JS 不直接用类?

    因其动态性与灵活性,原型链更适合运行时修改行为(如 monkey patching)。

  • __proto__ vs prototype

    • prototype函数的属性,用于构建原型链;
    • __proto__对象 的内部属性,指向其构造函数的 prototype
  • 如何判断继承关系?

    javascript 复制代码
    cat1 instanceof Cat;      // true
    Cat.prototype.isPrototypeOf(cat1); // true

📌 记住 :无论用 function 还是 class,JavaScript 的 OOP 核心始终是 原型链 。理解 prototype__proto__constructor 的关系,是掌握 JS 面向对象的关键。


参考资料

相关推荐
OpenTiny社区1 小时前
低代码运行时渲染搞不懂?TinyEngine 从理论到实践全攻略,看完直接上手!
前端·vue.js·低代码
未央几许1 小时前
使用ffmpeg.wasm解码视频(avi,mpg等格式)问题
前端·ffmpeg
LJLJ1 小时前
BPMN的Activity节点渲染
前端
水水不水啊1 小时前
通过一个域名,借助IPV6免费远程访问自己家里的设备
前端·python·算法
圆弧YH1 小时前
edge + google
前端·edge
b***59431 小时前
【Nginx 】Nginx 部署前端 vue 项目
前端·vue.js·nginx
前端fighter1 小时前
全栈项目:旅游攻略系统
前端·后端·源码
我血条子呢1 小时前
【Vite】离线打包@iconify/vue的图标
前端·javascript·vue.js
一个有理想的摸鱼选手2 小时前
CesiumLite-一行代码让你在Cesium中实现标绘测量
前端·gis·cesium