第3章 3.2 对象与原型链
上一章我们学会了
map / filter / reduce三剑客,用它们把数组玩出了花。但你有没有想过:数组凭什么能有那么多方便的方法?arr.map()是从哪来的?这一章我们要揭开这个秘密——答案就藏在对象与原型链里。搞懂这个,你不仅能理解 JavaScript 最核心的运行机制,还能写出更优雅的代码。
🎯 开场 3 分钟:为什么要学这个?
想象这样一个场景:
你刚入职新公司,接手一个老项目。看到一段代码:
const user = {
name: '张三',
age: 28,
greet: function() {
console.log('你好,我是' + this.name)
}
}
const admin = {
name: '李四',
role: '管理员'
}
// 老板说:admin 也要能 greet
admin.greet = user.greet
admin.greet() // 输出 "你好,我\n\n\n\n\n\n是李四"
你照着抄了一遍,发现能用。但心里肯定有个疑问:
- 这样每个对象都拷贝一份方法,不就重复代码了吗?
- 为什么
arr.map()我没定义,却直接就能用? user.__proto__是什么鬼?
这些问题,这篇文章全部给你掰开揉碎讲明白。学完你能:
- 理解 JavaScript 对象的「基因遗传」机制
- 搞懂为什么数组有那么多内置方法
- 能用
Object.create()实现更优雅的代码复用
🧱 基础 25 分钟:核心概念(小白视角)
3.2.1 对象是什么?—— 对象的本质是「字典」
先来生活类比。
你去快递站取件,工作人员让你报手机号后四位。她手里有本册子,翻到对应页码,找到你的名字,后面跟着一串信息:包裹大小、存放位置、是否已取。
对象就是一本字典:通过「键」查「值」,键值配对存储。
// 创建一个对象(字面量写法)
const phone_book = {
'张三': '138xxxx1234',
'李四': '139xxxx5678',
'王五': '137xxxx9012'
}
// 查一下李四的电话
console.log(phone_book['李四']) // 输出: 139xxxx5678
// 也可以用点语法(更简洁)
console.log(phone_book.张三) // 输出: 138xxxx1234
说白了:对象就是键值对(key-value)组成的集合,像一本查号的册子。
3.2.2 对象字面量与属性操作
创建对象最常见的方式是字面量——直接写 {}。
const student = {
name: '小明',
age: 16,
grade: '高二',
subjects: ['数学', '物理', '化学'] // 值可以是任意类型
}
// 读取属性
console.log(student.name) // 小明
console.log(student.subjects[0]) // 数学
// 修改属性
student.age = 17
console.log(student.age) // 17
// 添加新属性
student.hobby = '篮球'
console.log(student.hobby) // 篮球
// 删除属性
delete student.grade
console.log(student.grade) // undefined
注意! 访问不存在的属性返回
undefined,不会报错。
3.2.3 方法——对象的函数属性
对象的值可以是函数,这时候这个属性就叫做「方法」。
const calculator = {
a: 10,
b: 5,
// 这是一个方法
add: function() {
return this.a + this.b // this 指向 calculator 这个对象
},
// 简写形式(ES6)
multiply() {
return this.a * this.b
}
}
console.log(calculator.add()) // 15
console.log(calculator.multiply()) // 50
说白了:方法就是函数,只是它长在对象里面。
3.2.4 原型链的核心 —— __proto__
重头戏来了!这是理解 JavaScript 对象模型的关键。
先问你一个问题:为什么数组有 map、filter、push 这些方法?
答案:因为数组的原型上有这些方法。
const arr = [1, 2, 3]
// 数组的原型链是这样的:
// arr → Array.prototype → Object.prototype → null
console.log(arr.__proto__) // 指向 Array.prototype
console.log(arr.__proto__.__proto__) // 指向 Object.prototype
console.log(arr.__proto__.__proto__.__proto__) // null
生活类比:想象一个「基因遗传链」。
- 你有你爸的基因(
Object.prototype) - 你爸有你爷爷的基因(再上一层)
- 爷爷的爷爷传下来的基因就是「null」(遗传的尽头)
JavaScript 的对象也这样,每个对象都有一个 __proto__ 属性,指向它的「父对象」。当你访问 arr.map() 时:
- 先在
arr自身找,没找到 - 去
arr.__proto__(也就是Array.prototype)找,找到了!
// 证明:数组的 map 方法来自 Array.prototype
console.log(Array.prototype.map) // [Function: map]
console.log(arr.__proto__ === Array.prototype) // true
这就是原型链——像一条锁链,一环扣一环,找不到属性就顺着链子往上找。
3.2.5 属性描述符——给属性加「权限控制」
每个对象属性都有一些隐藏的「开关」,叫做属性描述符。
const person = {
name: '张三'
}
// 查看属性描述符
console.log(Object.getOwnPropertyDescriptor(person, 'name'))
// 输出: { value: '张三', writable: true, enumerable: true, configurable: true }
| 描述符 | 含义 |
|---|---|
value |
属性值 |
writable |
是否可写(修改) |
enumerable |
是否可遍历(for...in 能看到吗) |
configurable |
是否可删除或修改描述符本身 |
有什么用? 可以把属性「锁死」,防止被改。
const protected_data = {}
// 创建一个不可写的属性
Object.defineProperty(protected_data, 'secret', {
value: '密码是123456',
writable: false, // 不能修改
enumerable: false, // for...in 遍历不到
configurable: false // 不能删除
})
console.log(protected_data.secret) // 密码是123456
protected_data.secret = 'hacked!' // 静默失败,严格模式下才报错
console.log(protected_data.secret) // 还是 密码是123456
3.2.6 Object.create() —— 更灵活的对象创建
前面说过,「上一章的方法」那个例子,每个对象都要拷贝方法,很笨拙。有更优雅的方式吗?
// 创建一个「模板对象」,包含公共方法
const behavior = {
greet: function() {
console.log('你好,我是' + this.name)
},
introduce: function() {
console.log('我今年' + this.age + '岁')
}
}
// 用 Object.create 基于 behavior 创建新对象
const user1 = Object.create(behavior)
user1.name = '张三'
user1.age = 28
const user2 = Object.create(behavior)
user2.name = '李四'
user2.age = 35
// 测试
user1.greet() // 你好,我是张三
user2.greet() // 你好,我是李四
user1.introduce() // 我今年28岁
user2.introduce() // 我今年35岁
关键点:user1.__proto__ === behavior 为 true。user1 本身只有 name 和 age,但 greet 和 introduce 是从原型链上「继承」来的。
说白了:
Object.create(父对象)让你创建一个「继承自某个父对象」的新对象,不用拷贝代码。
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):实现一个「继承的方法」计数器
目标:理解原型链继承,统计一个对象调用了多少继承来的方法。
// ========== 完整可运行代码 ==========
const methodTracker = {
track: function(methodName) {
this._calledMethods = this._calledMethods || []
this._calledMethods.push(methodName)
console.log(`[追踪] 调用了方法: ${methodName}`)
}
}
const counter = Object.create(methodTracker)
counter.count = 0
// 模拟调用
counter.track('add')
counter.track('add')
counter.track('subtract')
console.log('累计调用次数:', counter._calledMethods.length)
console.log('调用的方法列表:', counter._calledMethods)
// ========== 预期输出 ==========
// [追踪] 调用了方法: add
// [追踪] 调用了方法: add
// [追踪] 调用了方法: subtract
// 累计调用次数: 3
// 调用的方法列表: ['add', 'add', 'subtract']
解释:
counter本身只有一个count属性,但track方法是从原型链上继承来的。
项目 2(15 分钟):解析 CSV 数据并按条件筛选
目标:从 CSV 格式的学生成绩数据中,筛选出不及格的学生并生成报告。
// ========== 完整可运行代码 ==========
const csvData = `name,math,english,science
小明,85,72,90
小红,45,60,55
小刚,92,88,95
小芳,33,41,50
小林,78,82,80`
// 1. 解析 CSV
function parseCSV(csvString) {
const lines = csvString.trim().split('\n')
const headers = lines[0].split(',')
return lines.slice(1).map(line => {
const values = line.split(',')
const obj = {}
headers.forEach((header, index) => {
const value = values[index]
// 成绩转为数字
obj[header] = isNaN(value) ? value : Number(value)
})
return obj
})
}
// 2. 定义筛选规则
const gradeRules = Object.create({
isFailing: function(subject, threshold = 60) {
return this[subject] < threshold
},
getFailingSubjects: function() {
const subjects = ['math', 'english', 'science']
return subjects.filter(subject => this.isFailing(subject))
}
})
// 3. 解析数据
const students = parseCSV(csvData)
// 4. 给每个学生附加筛选能力,并筛选不及格学生
const failingStudents = students
.map(student => Object.create(gradeRules, {
name: { value: student.name, writable: true },
math: { value: student.math, writable: true },
english: { value: student.english, writable: true },
science: { value: student.science, writable: true }
}))
.filter(student => student.getFailingSubjects().length > 0)
// 5. 输出报告
console.log('=== 不及格学生报告 ===')
failingStudents.forEach(student => {
const failing = student.getFailingSubjects()
console.log(`${student.name} 不及格科目: ${failing.join(', ')}`)
})
// ========== 预期输出 ==========
// === 不及格学生报告 ===
// 小红 不及格科目: science
// 小芳 不及格科目: math, english, science
解释:先用
parseCSV把原始数据转成对象数组,再用Object.create给每个学生「注入」筛选能力,最后用filter找出不及格学生。
项目 3(15 分钟):做一个简易的「待办事项管理器」
目标:组合原型链继承 + 对象操作,实现一个带分类、筛选、统计功能的待办事项管理工具。
// ========== 完整可运行代码 ==========
// 1. 定义基础功能(原型)
const todoBase = {
// 标记完成
complete(id) {
const item = this.items.find(i => i.id === id)
if (item) {
item.done = true
item.completedAt = new Date().toLocaleString()
console.log(`✓ 已完成: ${item.text}`)
}
},
// 打印所有事项
printAll() {
console.log(`\n📋 ${this.category} (共${this.items.length}项)`)
this.items.forEach(item => {
const status = item.done ? '✅' : '⬜'
const doneInfo = item.done ? ` [${item.completedAt}]` : ''
console.log(`${status} [${item.id}] ${item.text}${doneInfo}`)
})
},
// 统计完成率
getCompletionRate() {
const done = this.items.filter(i => i.done).length
return `${done}/${this.items.length}`
}
}
// 2. 创建一个分类(工作)
const workTodos = Object.create(todoBase)
workTodos.category = '工作'
workTodos.items = [
{ id: 1, text: '写周报', done: false },
{ id: 2, text: '回复邮件', done: false },
{ id: 3, text: '开会', done: true, completedAt: '2024-01-15 09:30' }
]
// 3. 创建一个分类(学习)
const studyTodos = Object.create(todoBase)
studyTodos.category = '学习'
studyTodos.items = [
{ id: 1, text: '学完原型链', done: false },
{ id: 2, text: '做练习题', done: false },
{ id: 3, text: '复习闭包', done: false }
]
// 4. 使用工具
console.log('=== 待办事项管理器 ===')
workTodos.printAll()
console.log('完成率:', workTodos.getCompletionRate())
studyTodos.printAll()
console.log('完成率:', studyTodos.getCompletionRate())
// 模拟操作
console.log('\n--- 模拟操作 ---')
studyTodos.complete(1)
// ========== 预期输出 ==========
// === 待办事项管理器 ===
// 📋 工作 (共3项)
// ⬜ [1] 写周报
// ⬜ [2] 回复邮件
// ✅ [3] 开会 [2024-01-15 09:30]
// 完成率: 1/3
// 📋 学习 (共3项)
// ⬜ [1] 学完原型链
// ⬜ [2] 做练习题
// ⬜ [3] 复习闭包
// 完成率: 0/3
// --- 模拟操作 ---
// ✓ 已完成: 学完原型链
解释:核心代码只有
todoBase里定义的 3 个方法,但通过Object.create继承后,每个分类都可以复用这些代码,不用每个分类都写一遍complete、printAll。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:__proto__ 和 prototype 傻傻分不清
// ❌ 错误理解
function User(name) {
this.name = name
}
const user = new User('张三')
console.log(user.prototype) // undefined!实例对象没有 prototype
// ✅ 正确理解
console.log(User.prototype) // 这是构造函数才有的
console.log(user.__proto__ === User.prototype) // true
记忆口诀:
prototype是「构造函数」的属性,__proto__是「实例对象」的属性。
坑 2:原型链太长导致性能问题
// ❌ 错误示范:嵌套太多层原型链
const level1 = { a: 1 }
const level2 = Object.create(level1)
const level3 = Object.create(level2)
const level4 = Object.create(level3)
console.log(level4.a) // 能找到,但链太长,访问慢
// ✅ 正确做法:减少原型链深度
const base = { shared: '公共数据' }
const obj = Object.create(base)
obj.own = '自己的数据'
console.log(obj.own) // 直接属性,最快
console.log(obj.shared) // 只跨一层原型链
坑 3:修改全局原型影响所有对象
// ❌ 危险操作:给 Array.prototype 加方法
Array.prototype.sum = function() {
return this.reduce((a, b) => a + b, 0)
}
console.log([1, 2, 3].sum()) // 6,能用
// 但如果多人协作,别人的代码可能也改了 Array.prototype
// 会造成难以排查的 bug
// ✅ 正确做法:用工具类或继承
class NumberArray extends Array {
sum() {
return this.reduce((a, b) => a + b, 0)
}
}
const arr = new NumberArray(1, 2, 3)
console.log(arr.sum()) // 6,不污染原生类型
坑 4:for...in 会遍历原型链上的属性
const parent = { inherited: '来自父对象' }
const child = Object.create(parent)
child.own = '自己的'
// ❌ 用 for...in 会遍历到 inherited
for (let key in child) {
console.log(key) // own, inherited
}
// ✅ 用 hasOwnProperty 过滤
for (let key in child) {
if (child.hasOwnProperty(key)) {
console.log(key) // 只打印 own
}
}
坑 5:直接修改对象的 __proto__(现代 JS 不推荐)
const parent = { name: 'parent' }
const child = { age: 18 }
// ❌ 直接改 __proto__(性能差,且在某些环境被禁用)
child.__proto__ = parent
// ✅ 正确做法:用 Object.create
const correctChild = Object.create(parent)
correctChild.age = 18
调试技巧:用 Object.getPrototypeOf() 代替 __proto__
const arr = [1, 2, 3]
// ❌ 直接打印 __proto__ 很乱
console.log(arr.__proto__)
// ✅ 用 Object.getPrototypeOf 更清晰
console.log(Object.getPrototypeOf(arr) === Array.prototype) // true
// 还可以顺着原型链往上查
let proto = Object.getPrototypeOf(arr)
let level = 0
while (proto) {
console.log(`层级${level}:`, Object.keys(proto).slice(0, 5))
proto = Object.getPrototypeOf(proto)
level++
}
✏️ 练习题 + 作业题
练习题(5 道,10 分钟内完成)
练习 1(2 分钟):读取对象属性
- 输入:给定 const book = { title: 'JavaScript高级程序设计', author: 'Nicholas', pages: 519 }
- 预期输出:打印出 JavaScript高级程序设计 和 519
- 提示:用点语法或方括号语法
练习 2(2 分钟):给对象添加方法
- 输入:在练习 1 的 book 对象上添加一个 info 方法,返回 "《JavaScript高级程序设计》由Nicholas著,共519页"
- 预期输出:调用 book.info() 返回指定字符串
- 提示:方法里用 this 指代对象本身
练习 3(3 分钟):用 Object.create 继承
- 输入:创建一个 animal 原型,有 eat 方法打印 "吃东西",再创建 dog 对象继承它,并添加 bark 方法
- 预期输出:dog.eat() 输出 吃东西,dog.bark() 输出 汪汪
- 提示:const dog = Object.create(animal) 后再给 dog 加属性
练习 4(3 分钟):筛选对象数组
- 输入:const products = [{name: '苹果', price: 5}, {name: '香蕉', price: 2}, {name: '西瓜', price: 10}]
- 预期输出:筛选出价格大于 3 的商品,打印 ['苹果', '西瓜']
- 提示:用 filter 结合 Object.create 体验原型链继承
练习 5(5 分钟):分析原型链
- 输入:给定报错 TypeError: obj.greet is not a function,已知 obj.greet() 报这个错
- 预期输出:分析可能的原因并给出解决方案
- 提示:检查 greet 是定义在对象自身还是在原型链上,Object.getPrototypeOf(obj) 能帮你诊断
作业题(30 分钟 - 2 小时)
做一个「通讯录管理系统」
- 需求描述:用对象 + 原型链实现一个通讯录,可以添加联系人、按分类管理、查找和删除
- 功能点:
1. 定义Contact原型,包含name、phone、category属性,以及introduce方法打印联系人简介
2. 用Object.create创建多个联系人实例
3. 实现分类筛选功能(朋友/同事/家人),打印出某个分类下的所有联系人
4. 实现搜索功能:根据姓名关键词模糊查找 - 加分项:
1. 给联系人添加「创建时间」属性(用Object.defineProperty设置不可修改)
2. 统计每个分类的联系人数量并打印报表 - 验收标准:能跑起来 + 输出符合预期 + 代码有注释
- 提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本章 3 个核心知识点
- 对象是键值对字典 —— 通过
obj.key或obj['key']访问,可以存任意类型 - 原型链是 JavaScript 的「遗传机制」 —— 对象没有的属性会顺着
__proto__往上找 Object.create(父对象)是继承的优雅写法 —— 不用拷贝代码,让对象共享方法
延伸学习资源
- MDN - 对象原型 —— 官方文档,配图清晰
- 《JavaScript 高级程序设计》第 4 章「变量、作用域与内存」—— 原型链讲得最透
- JavaScript Visualizer —— 可视化原型链结构,强烈推荐
互动钩子
你在项目中遇到过「明明没定义这个方法,却能调用」的情况吗?当时是怎么解决的?评论区聊聊,老粉优先回复!
下章预告:学会了对象和原型链,你可能会想——「每次都要用 Object.create 写一堆继承代码,太麻烦了!」别急,下一章「类与继承(ES6 Class)」 就是来解决这个问题的,让你用 class 关键字优雅地实现面向对象编程。敬请期待!

评论(0)