第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![Simple tech illustration expla](https://blog.xxyye.com/wp-content/uploads/2026/06/de3f7f06cf6de22.png)\n\n![AI comic creation scene, creat](https://blog.xxyye.com/wp-content/uploads/2026/06/2038c1572c1b9f8.png)\n\n是李四"

你照着抄了一遍,发现能用。但心里肯定有个疑问:

  • 这样每个对象都拷贝一份方法,不就重复代码了吗?
  • 为什么 arr.map() 我没定义,却直接就能用
  • user.__proto__ 是什么鬼?

这些问题,这篇文章全部给你掰开揉碎讲明白。学完你能:

  1. 理解 JavaScript 对象的「基因遗传」机制
  2. 搞懂为什么数组有那么多内置方法
  3. 能用 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 对象模型的关键。

先问你一个问题:为什么数组有 mapfilterpush 这些方法?

答案:因为数组的原型上有这些方法

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() 时:

  1. 先在 arr 自身找,没找到
  2. 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__ === behaviortrue。user1 本身只有 nameage,但 greetintroduce 是从原型链上「继承」来的。

说白了: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 继承后,每个分类都可以复用这些代码,不用每个分类都写一遍 completeprintAll


💪 进阶 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 原型,包含 namephonecategory 属性,以及 introduce 方法打印联系人简介
    2. 用 Object.create 创建多个联系人实例
    3. 实现分类筛选功能(朋友/同事/家人),打印出某个分类下的所有联系人
    4. 实现搜索功能:根据姓名关键词模糊查找
  • 加分项
    1. 给联系人添加「创建时间」属性(用 Object.defineProperty 设置不可修改)
    2. 统计每个分类的联系人数量并打印报表
  • 验收标准:能跑起来 + 输出符合预期 + 代码有注释
  • 提交方式:评论区贴代码或 GitHub 链接

📚 总结 + 资源

本章 3 个核心知识点

  1. 对象是键值对字典 —— 通过 obj.keyobj['key'] 访问,可以存任意类型
  2. 原型链是 JavaScript 的「遗传机制」 —— 对象没有的属性会顺着 __proto__ 往上找
  3. Object.create(父对象) 是继承的优雅写法 —— 不用拷贝代码,让对象共享方法

延伸学习资源

  1. MDN - 对象原型 —— 官方文档,配图清晰
  2. 《JavaScript 高级程序设计》第 4 章「变量、作用域与内存」—— 原型链讲得最透
  3. JavaScript Visualizer —— 可视化原型链结构,强烈推荐

互动钩子

你在项目中遇到过「明明没定义这个方法,却能调用」的情况吗?当时是怎么解决的?评论区聊聊,老粉优先回复!


下章预告:学会了对象和原型链,你可能会想——「每次都要用 Object.create 写一堆继承代码,太麻烦了!」别急,下一章「类与继承(ES6 Class)」 就是来解决这个问题的,让你用 class 关键字优雅地实现面向对象编程。敬请期待!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。