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

相关推荐
写代码的【黑咖啡】4 分钟前
Python中的文件操作详解
java·前端·python
Moment6 分钟前
一文搞懂 Tailwind CSS v4 主题变量映射背后的原理
前端·javascript·面试
我命由我123458 分钟前
JavaScript WebGL - WebGL 引入(获取绘图上下文、获取最大支持纹理尺寸)
开发语言·前端·javascript·学习·ecmascript·学习方法·webgl
辛-夷10 分钟前
2025年高频面试题整理(vue系列一)
前端·javascript·vue.js·前端框架
GISer_Jing10 分钟前
ByteDance AI战略:前端生态的颠覆者
前端·人工智能·aigc
大布布将军22 分钟前
⚡️ 性能加速器:利用 Redis 实现接口高性能缓存
前端·数据库·经验分享·redis·程序人生·缓存·node.js
Change!!25 分钟前
uniapp写的h5,怎么根据页面详情,设置不同的标题
前端·uni-app·标题
浅箬26 分钟前
uniapp 打包之后出现shadow-grey.png去除
前端·uni-app
梵得儿SHI30 分钟前
(第五篇)Spring AI 核心技术攻坚:流式响应与前端集成实现【打字机】效果
前端·webflux·springai·流式响应技术·低延迟ai交互·reactive编程原理·streamapi设计
鹏多多32 分钟前
一文搞懂柯里化:函数式编程技巧的解析和实践案例
前端·javascript·vue.js