在学习 JavaScript 面向对象时,很多人都会经历一个非常"离谱"的瞬间:
你给女人对象加了一个"生孩子"的方法
结果男人对象也能生孩子了
这不是业务 bug,这是原型和对象引用机制在给你上课。
这篇文章就带你完整复盘一个真实学习过程:
从最直觉的对象赋值继承
到出现诡异共享问题
再到构造函数 + 原型继承的正确解法
我们不仅看结果,更要看底层机制。
一、问题的起点:我只是想让两个对象拥有相同属性
假设我们有一个"人类基础属性":
- 两只眼睛
- 一个脑袋
最直觉的写法:
ini
const Person = {
eyes: 2,
head: 1
}
然后想让:
- Woman 拥有这些属性
- Man 也拥有这些属性
很多人第一反应是:
ini
const Woman = Person
const Man = Person
或者:
ini
Woman.__proto__ = Person
Man.__proto__ = Person
看起来很合理。
毕竟目标只是"复用"。
但问题马上出现。
二、诡异现象:给女人加方法,男人也会了
现在给 Woman 添加一个方法:
javascript
Woman.baby = function () {
console.log('宝贝')
}
然后测试:
scss
Man.baby()
居然也能调用。
为什么?
男人也会生孩子了。
这明显不合理。
但从 JavaScript 的角度,这是完全合理的。
三、本质原因:你根本没有创建多个对象
关键理解一句话:
对象赋值不是复制,而是引用传递
当你写:
ini
const Woman = Person
本质是:
Woman 和 Person 指向同一个内存对象
内存结构:
markdown
Person ─┐
├── 同一个对象
Woman ──┘
Man ───┘
你不是创建三个对象。
你只是创建三个"变量名"。
它们共同指向一个对象。
所以:
给 Woman 添加属性
就是给 Person 添加属性
自然 Man 也能访问
问题不是继承错了。
是你压根没继承。
你只是多起了几个别名。
四、真正的目标:结构相同,但对象独立
我们希望:
- Woman 有 eyes / head
- Man 有 eyes / head
- 但互不影响
- 方法也可独立扩展
这意味着:
必须创建多个独立对象
这就是构造函数存在的意义。
五、第一层解决方案:使用构造函数创建实例
构造函数的本质:
批量生产结构相同的对象
实现:
csharp
function Person() {
this.eyes = 2
this.head = 1
}
每次执行:
scss
new Person()
都会发生:
1 创建新对象
2 绑定 this
3 执行函数
4 返回对象
关键点:
arduino
每次 new 都创建全新对象
互不影响。
六、现在我们来做真正的"继承"
目标:
Woman 继承 Person
Man 继承 Person
传统原型继承写法:
javascript
function Woman() {}
Woman.prototype = new Person()
Woman.prototype.constructor = Woman
同理:
javascript
function Man() {}
Man.prototype = new Person()
Man.prototype.constructor = Man
这行代码非常关键:
ini
Woman.prototype = new Person()
它做了什么?
不是复制代码。
是创建一个 Person 实例,然后把它作为 Woman 的原型。
结构变成:
javascript
Woman实例
↓
Woman.prototype(Person实例)
↓
Object.prototype
Woman 的实例可以访问:
Person 实例里的属性。
这就是原型继承。
七、验证:现在给女人加能力,男人不会受影响
javascript
Woman.prototype.baby = function () {
console.log('宝贝')
}
测试:
scss
new Woman().baby() ✔
new Man().baby() ✘
终于正常了。
原因:
Woman.prototype 和 Man.prototype 是两个不同对象。
虽然都来自 new Person()
但:
是不同实例
互不干扰。
八、这就是关键转折:对象复用 vs 构造函数实例化
错误方案:
共享同一个对象
正确方案:
基于同一构造规则创建多个对象
这就是:
面向对象思想中的"实例化"。
九、构造函数为什么体现封装?
来看一个典型封装例子:
javascript
function Person() {
this.name = '佚名'
this.setName = function (name) {
this.name = name
}
this.getName = function () {
console.log(this.name)
}
}
创建两个实例:
csharp
let p1 = new Person()
let p2 = new Person()
修改:
arduino
p1.setName('小明')
结果:
ini
p1.name = 小明
p2.name = 佚名
互不影响。
这就是封装:
数据 + 操作数据的方法
打包在对象内部
并且实例独立。
十、但构造函数也有性能问题
如果方法写在构造函数里:
每个实例都会创建一份函数。
这很浪费内存。
解决方案:
使用原型。
十一、原型的真正作用:共享方法
javascript
function Person() {}
Person.prototype.sayHi = function () {
console.log('Hi')
}
所有实例共享:
一份函数
而不是每人一份。
这就是原型的核心价值。
十二、实例是如何找到原型方法的?
访问对象属性时:
查找顺序:
1 先找实例自身
2 再找 prototype
3 再找上层原型
4 直到 null
这叫:
原型链查找