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 的根基。

相关推荐
Drift_Dream1 小时前
虚拟滚动:优化长列表性能的利器
前端
逃离疯人院1 小时前
前端性能深度解析:网络响应时间与实际渲染时间的鸿沟
前端
国服第二切图仔1 小时前
Electron for鸿蒙PC项目实战之天气预报应用
javascript·electron·harmonyos·鸿蒙pc
我是若尘1 小时前
🚀 深入理解 Claude Code:从入门到精通的能力全景图
前端
老前端的功夫1 小时前
Webpack 深度解析:从配置哲学到编译原理
前端·webpack·前端框架·node.js
重铸码农荣光1 小时前
🌟 Vibe Coding 时代:用自然语言打造你的专属 AI 单词应用
前端·vibecoding
MegatronKing1 小时前
SSL密钥协商导致抓包失败的原因分析
前端·https·测试
Kratzdisteln1 小时前
【TIDE DIARY 5】cursor; web; api-key; log
前端
Danny_FD1 小时前
使用docx库实现文档导出
前端·javascript