一个有趣的原型继承实验:为什么“男人也会生孩子”?从对象赋值到构造函数继承的完整推演

0 阅读4分钟

在学习 JavaScript 面向对象时,很多人都会经历一个非常“离谱”的瞬间:

你给女人对象加了一个“生孩子”的方法
结果男人对象也能生孩子了

这不是业务 bug,这是原型和对象引用机制在给你上课。

这篇文章就带你完整复盘一个真实学习过程:

从最直觉的对象赋值继承
到出现诡异共享问题
再到构造函数 + 原型继承的正确解法

我们不仅看结果,更要看底层机制。


一、问题的起点:我只是想让两个对象拥有相同属性

假设我们有一个“人类基础属性”:

  • 两只眼睛
  • 一个脑袋

最直觉的写法:

const Person = {
  eyes: 2,
  head: 1
}

然后想让:

  • Woman 拥有这些属性
  • Man 也拥有这些属性

很多人第一反应是:

const Woman = Person
const Man = Person

或者:

Woman.__proto__ = Person
Man.__proto__ = Person

看起来很合理。

毕竟目标只是“复用”。

但问题马上出现。


二、诡异现象:给女人加方法,男人也会了

现在给 Woman 添加一个方法:

Woman.baby = function () {
  console.log('宝贝')
}

然后测试:

Man.baby()

居然也能调用。

为什么?

男人也会生孩子了。

这明显不合理。

但从 JavaScript 的角度,这是完全合理的。


三、本质原因:你根本没有创建多个对象

关键理解一句话:

对象赋值不是复制,而是引用传递

当你写:

const Woman = Person

本质是:

Woman 和 Person 指向同一个内存对象

内存结构:

Person  ─┐
         ├── 同一个对象
Woman  ──┘
Man    ───┘

你不是创建三个对象。

你只是创建三个“变量名”。

它们共同指向一个对象。

所以:

给 Woman 添加属性
就是给 Person 添加属性
自然 Man 也能访问

问题不是继承错了。

是你压根没继承。

你只是多起了几个别名。


四、真正的目标:结构相同,但对象独立

我们希望:

  • Woman 有 eyes / head
  • Man 有 eyes / head
  • 但互不影响
  • 方法也可独立扩展

这意味着:

必须创建多个独立对象

这就是构造函数存在的意义。


五、第一层解决方案:使用构造函数创建实例

构造函数的本质:

批量生产结构相同的对象

实现:

function Person() {
  this.eyes = 2
  this.head = 1
}

每次执行:

new Person()

都会发生:

1 创建新对象
2 绑定 this
3 执行函数
4 返回对象

关键点:

每次 new 都创建全新对象

互不影响。


六、现在我们来做真正的“继承”

目标:

Woman 继承 Person
Man 继承 Person

传统原型继承写法:

function Woman() {}
Woman.prototype = new Person()
Woman.prototype.constructor = Woman

同理:

function Man() {}
Man.prototype = new Person()
Man.prototype.constructor = Man

这行代码非常关键:

Woman.prototype = new Person()

它做了什么?

不是复制代码。

是创建一个 Person 实例,然后把它作为 Woman 的原型。

结构变成:

Woman实例
   ↓
Woman.prototypePerson实例)
   ↓
Object.prototype

Woman 的实例可以访问:

Person 实例里的属性。

这就是原型继承。


七、验证:现在给女人加能力,男人不会受影响

Woman.prototype.baby = function () {
  console.log('宝贝')
}

测试:

new Woman().baby()   ✔
new Man().baby()     ✘

终于正常了。

原因:

Woman.prototype 和 Man.prototype 是两个不同对象。

虽然都来自 new Person()

但:

是不同实例

互不干扰。


八、这就是关键转折:对象复用 vs 构造函数实例化

错误方案:

共享同一个对象

正确方案:

基于同一构造规则创建多个对象

这就是:

面向对象思想中的“实例化”。


九、构造函数为什么体现封装?

来看一个典型封装例子:

function Person() {
  this.name = '佚名'

  this.setName = function (name) {
    this.name = name
  }

  this.getName = function () {
    console.log(this.name)
  }
}

创建两个实例:

let p1 = new Person()
let p2 = new Person()

修改:

p1.setName('小明')

结果:

p1.name = 小明
p2.name = 佚名

互不影响。

这就是封装:

数据 + 操作数据的方法
打包在对象内部

并且实例独立。


十、但构造函数也有性能问题

如果方法写在构造函数里:

每个实例都会创建一份函数。

这很浪费内存。

解决方案:

使用原型。


十一、原型的真正作用:共享方法

function Person() {}

Person.prototype.sayHi = function () {
  console.log('Hi')
}

所有实例共享:

一份函数

而不是每人一份。

这就是原型的核心价值。


十二、实例是如何找到原型方法的?

访问对象属性时:

查找顺序:

1 先找实例自身
2 再找 prototype
3 再找上层原型
4 直到 null

这叫:

原型链查找