JavaScript 面向对象的本质:从对象模板到组合继承的完整演进

JavaScript 并不是传统意义上的面向对象语言,它没有类(class)的原生支持(直到 ES6 才引入语法糖),而是基于原型(prototype-based) 的动态语言。但开发者很早就开始用各种方式模拟"类"和"继承"。本文将沿着你代码中的演进路径,系统梳理 JS 面向对象的核心机制,并重点解析 原型共享、属性遮蔽、组合继承 这三大关键环节。


一、最初的"类":对象字面量只是模板

css 复制代码
var Cat = {
  name: '',
  color: ''
}

这段代码看似定义了一个"类",但实际上它只是一个普通对象,也可以说更像是一个默认属性模版。它不能被 new,也无法生成多个独立实例。开发者只能手动创建:

ini 复制代码
var cat1 = {}; cat1.name = '加菲猫'
var cat2 = {}; cat2.name = '黑猫警长'

问题很明显:

  • 代码重复
  • 实例无关联
  • 无法共享方法 ------ 如果要给所有猫加 sayHello(),每个实例都得写一遍。

这促使我们思考:能否封装实例化过程


二、构造函数:封装创建逻辑

为了解决上面的问题,于是有了构造函数 + 原型链 来模拟类:

ini 复制代码
function Cat(name, color) {
  this.name = name
  this.color = color
}
const cat1 = new Cat('加菲猫', '橘色')

使用 new 时,JS 引擎会:

  1. 创建一个新的空对象 {}
  2. 将其 __proto__ 指向 Cat.prototype
  3. 绑定 this 到该对象并执行函数体;
  4. 隐式返回该对象。

此时,cat1.constructor === Cat 成立,因为 Cat.prototype.constructor 默认指向 Cat。而 instanceof 的判断依据是原型链上是否存在构造函数的 prototype ,与 constructor 属性无关。

这说明:实例与构造函数的关联,靠的是 __proto__prototype 的链条,而非 constructor 字段。

但若把公共方法写在构造函数内部:

javascript 复制代码
this.eat = function() { ... }

每次 new 都会创建新函数,浪费内存 。我们需要一种共享机制


三、原型模式:解决复用与统一修改

这才是 JS 面向对象的精髓所在:

ini 复制代码
function Cat(name, color) {
  this.name = name
  this.color = color
}
Cat.prototype.type = '猫科动物'
Cat.prototype.eat = function() { alert('喜欢Jerry') }

所有实例通过 __proto__ 共享 Cat.prototype 上的属性和方法。

关键机制:属性查找与遮蔽

当你执行:

ini 复制代码
cat1.type = '铲屎官'

不会修改原型 ,而是在 cat1 自身新增一个 type 属性,遮蔽(shadow) 了原型上的同名属性。

  • cat1.type'铲屎官'(自身属性)
  • cat2.type'猫科动物'(从原型读取)

但如果你希望统一修改所有实例的公共属性,只需:

ini 复制代码
Cat.prototype.type = '哺乳动物'

此时,只要实例没有遮蔽该属性,就会立即反映新值。

这正是原型模式的核心优势:既能共享,又能局部覆盖;既能统一更新,又不影响隔离性。

如何区分属性来源?

  • hasOwnProperty('type')false(来自原型)
  • hasOwnProperty('name')true(来自实例)
  • 'type' in cat1true(检查整个原型链)
  • for...in → 遍历所有可枚举属性(包括原型链)

这些工具让我们能精确控制属性行为,是理解原型链的关键。


四、继承:从借用构造函数到组合继承

JS 没有 extends,但可以通过两种方式实现继承:

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

javascript 复制代码
function Animal() {
  this.species = '动物'
}
function Cat(name, color) {
  Animal.apply(this) // 借用父类构造函数
  this.name = name
  this.color = color
}

这行 Animal.apply(this) 的本质是:

  • 立即执行 Animal 函数
  • 强制其内部 this 指向当前 Cat 实例

结果:cat 实例拥有了 species 属性。

注意:这只是属性拷贝不建立原型链 ,因此无法继承 Animal.prototype 上的方法。

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

ini 复制代码
Cat.prototype = new Animal()

这一步让 Cat.prototype 成为一个 Animal 实例,从而:

  • 拥有 species(来自 Animal 构造函数);
  • __proto__ 指向 Animal.prototype,可访问 sayHi()

但问题来了:所有 Cat 实例共享同一个 species,且无法在创建时传参。

3. 组合继承:两者结合(经典方案)

javascript 复制代码
function Cat(name, color) {
  Animal.apply(this) // 实例属性隔离
  this.name = name
  this.color = color
}
Cat.prototype = new Animal() // 方法复用 + 原型链

这样:

  • 每个 Cat 实例都有自己的 species(来自 apply);
  • 所有实例共享 sayHi()(来自原型链)。

但存在一个经典问题:父类构造函数被调用了两次

  1. Cat.prototype = new Animal() → 第一次调用,species 被写入原型
  2. new Cat()Animal.apply(this) → 第二次调用,species 被写入实例

虽然实例属性会遮蔽原型属性,但原型上仍残留一个无用的 species,属于轻微内存浪费

"原型上多了一个无用的 species 属性(会被实例自身的 species 遮蔽)"。

尽管如此,组合继承仍是早期最完整的继承方案,因为它同时解决了:

  • 实例属性的独立性(通过构造函数调用);
  • 方法的复用性(通过原型链)。

五、ES6 class:只是语法糖

javascript 复制代码
class Cat {
  constructor(name, color) {
    this.name = name
    this.color = color
  }
  eat() { console.log('eat') }
}

这等价于:

javascript 复制代码
function Cat(name, color) { ... }
Cat.prototype.eat = function() { ... }

typeof Cat 返回 'function',证明 class 本质仍是函数。它不能被提升(有暂时性死区),也不能像普通函数那样随意调用。

所以,class 不是真正的类,JS 依然是基于原型的语言


结语:OOP 在 JS 中的本质

"JS 是一种基于原型的面向对象语言,哪怕 ES6 有了 class,仍然是原型式的。"

这句话道出了全部真相。

从对象字面量 → 构造函数 → 原型共享 → 组合继承,每一步都是为了解决前一步的缺陷:

  • 封装(构造函数);
  • 复用(prototype);
  • 继承(apply + 原型链)。

而贯穿始终的核心机制只有两个:

  1. __proto__ 指向构造函数的 prototype
  2. 属性查找沿原型链向上,直到 Object.prototype

理解这两点,就能看透 JS 面向对象的全貌。无论未来语法如何演进,原型链永远是 JavaScript OOP 的根基。

相关推荐
代码搬运媛6 小时前
Jest 测试框架详解与实现指南
前端
counterxing6 小时前
我把 Codex 里的 Skills 做成了一个 MCP,还支持分享
前端·agent·ai编程
wangqiaowq7 小时前
windows下nginx的安装
linux·服务器·前端
之歆7 小时前
DAY_12JavaScript DOM 完全指南(二):实战与性能篇
开发语言·前端·javascript·ecmascript
发现一只大呆瓜7 小时前
Vite凭什么这么快?3分钟带你彻底搞懂 Vite 热更新的幕后黑手
前端·面试·vite
Maimai108087 小时前
React如何用 @microsoft/fetch-event-source 落地 SSE:比原生 EventSource 更灵活的实时推送方案
前端·javascript·react.js·microsoft·前端框架·reactjs·webassembly
candyTong7 小时前
Claude Code 的 Edit 工具是怎么工作的
javascript·后端·架构
kyriewen9 小时前
产品经理把PRD写成“天书”,我用AI半小时重写了一遍,他当场愣住
前端·ai编程·cursor
humcomm10 小时前
元框架的工作原理详解
前端·前端框架
canonical_entropy10 小时前
Attractor Before Harness: AI 大规模开发的方法论
前端·aigc·ai编程