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

相关推荐
CharlesYu018 小时前
前端性能优化的第一性原理,是不断缩短“用户发起意图 → 获得可用结果”之间的时间
前端
平头哥技术团队8 小时前
Day 21 _ 页内锚点_给每段起个 id,目录写 href=_#id_,点一下页面就滚到那一段
前端·html·html5
子兮曰9 小时前
Bun v1.4.1 深度解析:从 Zig 到 Rust,一场 11 天、64 个 AI 代理的语言迁徙
前端·后端·bun
人民广场吃泡面9 小时前
什么是AI Agent?它又能给前端带来哪些效率提升?
前端·人工智能
中科三方10 小时前
两家域名注册商资质被ICANN终止:企业域名资产安全再受关注
前端·网络·安全·域名
bug总结10 小时前
uniapp vue3全局方法注册使用
前端·javascript·uni-app
华无丽言11 小时前
如何在宜搭中实现获取子表中的字段值赋值到父表中?
前端·javascript·低代码
IT_陈寒11 小时前
Vue的computed属性竟然坑了我一把
前端·人工智能·后端
威斯软科的老司机11 小时前
通俗讲解 CNN 图像识别、向量 Embedding、Softmax 概率计算这三块的简化原理
前端·人工智能·ui·数字孪生
2601_9623824312 小时前
剑指大前端全栈工程师(全2册)主要情节
javascript·实战案例·全栈工程师·剑指大前端·html5+css3