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 分钟前
Vue如何集成封装Axios
前端·javascript·vue.js
康一夏5 分钟前
Next.js 13变化有多大?
前端·react·nextjs
糖炒栗子03265 分钟前
前端项目标准环境搭建与启动
前端
不是az6 分钟前
CSS知识点记录
前端·javascript·css
爱分享的阿Q14 分钟前
GPT6-Spud-AGI前夜的豪赌
前端·easyui·agi
昵称暂无143 分钟前
.NET 高级开发 | i18n 原理、实现一个 i18n 框架
javascript·c#·.net
西西小飞龙1 小时前
Less/Sass Mixins vs. Extend
前端·less·sass
syjy21 小时前
(含下载)BeTheme WordPress主题使用教程
前端·wordpress·wordpress建站
Misnice1 小时前
shadcn如何使用
前端·reactjs
h_jQuery1 小时前
vue使用gm-crypto对数据进行sm4加密处理
前端·javascript·vue.js