JavaScript 对象:从属性到原型与描述符
商品有名称、价格和库存,用户有昵称、地址和偏好,文章有标题、发布者信息和阅读数据。真实程序很少只处理一个孤立的值,它更常需要把一组彼此相关的信息放在一起,并通过有意义的名字找到它们。
对象正是 JavaScript 用来组织这类数据的核心结构。不过,对象并不只是一个“键值表”:属性值可以是函数,多个变量可能指向同一个对象,读取属性时还可能沿原型链继续查找。再往下一层,每个属性甚至有“是否可写、是否可枚举、是否可删除”等规则。
这篇教程从一个商品对象出发,逐层拆开创建、访问、嵌套、this、引用、原型和属性描述符。完成后,你不仅能写出对象,还能解释代码为什么会得到某个结果。
本文的控制台输出使用 Node.js v25.2.1 验证。正文讨论的是 JavaScript 语言本身,不依赖浏览器中的 window 或 DOM。
学习路线
完成本章后,你应该能够:
- 用对象字面量创建对象,并区分属性名与属性值。
- 根据属性名是否固定,选择点语法或方括号语法。
- 读取、添加、更新和删除属性,并安全地进入嵌套结构。
- 根据调用表达式判断普通方法中的
this。 - 解释对象赋值、浅拷贝和嵌套引用之间的差别。
- 画出一条简单的原型链,并区分自身属性与继承属性。
- 使用属性描述符控制可写、可枚举和可配置行为。
用对象字面量组织一组信息
假设商城页面需要展示一件商品。把名称、价格、库存分别放进三个无关变量当然可以,但数据一多,变量之间的关系就会越来越难追踪。对象字面量用一对 {} 把这些信息收进同一个值:
const title = "无线鼠标";
const stockField = "stock";
const product = {
id: "sku-001",
title,
price: 129,
[stockField]: 8,
tags: ["办公", "无线"],
available: true,
};
console.log(product.title);
console.log(product.stock);
console.log(Object.keys(product).join(" | "));运行环境:Node.js v25.2.1
无线鼠标
8
id | title | price | stock | tags | available每一项都由“属性名: 属性值”组成,项与项之间用逗号分隔。属性值可以是任意 JavaScript 值,例如字符串、数字、布尔值、数组、另一个对象或函数。
这里还用了两种简写:
- 当变量名和属性名相同时,
title等价于title: title。 [stockField]是计算属性名。JavaScript 先得到变量的值"stock",再用它创建属性。
普通属性名最终会成为字符串;Symbol 属性名是例外。合法标识符可以直接写成 price,带空格或连字符的名字则要写成字符串,例如 "unit price"。

对象字面量把同一件事的多项信息收进一个值。
const product 只表示变量 product 不能改指向另一个值,并不表示对象内部被冻结。只要属性本身允许写入,product.stock = 7 仍然合法。
读取、更新与删除属性
读取属性有两种常用写法。点语法更短,适合代码写下时就已确定、并且符合标识符规则的属性名;方括号语法会先计算括号内的表达式,适合动态属性名或带空格、连字符的名字。
const product = {
name: "无线鼠标",
stock: 8,
"unit price": 129,
};
const key = "unit price";
console.log(product.name);
console.log(product[key]);
console.log(product.key);运行环境:Node.js v25.2.1
无线鼠标
129
undefinedproduct[key] 会读取变量 key 的值,所以找到 "unit price"。product.key 则固定查找名字为 key 的属性;这个属性不存在,因此结果是 undefined,而不是抛出错误。

方括号接收表达式,点后面写的是固定属性名。
同样的语法也能更新或新增属性;delete 用来删除可配置的自身属性:
const product = { name: "无线鼠标", stock: 8 };
const field = "stock";
product[field] -= 2; // 更新已有属性
product.status = "selling"; // 新增属性
delete product.status; // 删除属性
console.log(product.stock);
console.log(product.status);
console.log(Object.hasOwn(product, "status"));运行环境:Node.js v25.2.1
6
undefined
falseObject.hasOwn(product, "status") 明确回答“status 是否是 product 自己的属性”。仅用 product.status === undefined 无法完成同样的判断,因为一个真实存在的属性也可能把 undefined 当作值。
JavaScript 允许把属性从数字改成字符串等其他类型,但“允许”不等于“合适”。业务代码最好保持字段含义和类型稳定,否则使用方必须增加许多额外判断。
在练习台里切换访问方式
修改 field 的值,再观察点语法与方括号语法分别查找哪个属性。
沿路径进入嵌套对象和数组
一件事内部还可能包含另一件事,或包含一组同类项目。对象适合表示“有多个命名特征的一件事”,数组适合表示“按顺序排列的一组项目”。二者可以自由组合。
const company = {
name: "星河商店",
address: {
city: "杭州",
street: "文一路",
},
activities: ["零售", "维修", "配送"],
warehouses: [
{ city: "杭州", stock: 24 },
{ city: "成都", stock: 18 },
],
};
company.address.street = "学院路";
company.warehouses[1].stock += 2;
console.log(company.address.city);
console.log(company.activities[1]);
console.log(`${company.warehouses[1].city}:${company.warehouses[1].stock}`);运行环境:Node.js v25.2.1
杭州
维修
成都:20![嵌套对象和数组访问路径示意图,展示 company.address.city、company.activities[1] 和 company.offices[0].city](https://media.edu-free.com/uploads/nested_object_array_path_39a0e533d0.png)
遇到对象就按属性名前进,遇到数组就按索引前进。
长路径的风险是中间某一段不存在。例如,直接读取 company.contact.email 会在 contact 为 undefined 时抛出 TypeError。只想安全读取时,可以使用可选链 ?.:
console.log(company.contact?.email);
console.log(company.warehouses?.[2]?.city ?? "暂未设置");undefined
暂未设置?. 遇到 null 或 undefined 就停止,并返回 undefined;?? 只在左侧为 null 或 undefined 时使用后备值。可选链适合读取,不会自动创建缺失的中间对象;要写入深层属性,必须先确保整条路径存在。
把函数放进对象,并正确判断 this
当属性值是函数时,我们通常把这个属性称为方法。方法可以使用对象的数据完成动作,例如扣减余额或生成一段描述。
"use strict";
const account = {
owner: "小周",
balance: 120,
describe() {
return `${this.owner}:${this.balance}`;
},
};
const guest = {
owner: "访客",
balance: 0,
describe: account.describe,
};
console.log(account.describe());
console.log(guest.describe());
console.log(account.describe.call(guest));运行环境:Node.js v25.2.1
小周:120
访客:0
访客:0同一个函数被两个对象使用,却得到不同结果。这是因为普通函数中的 this 主要由调用方式决定:
account.describe()中,this是点前面的account。guest.describe()中,this是点前面的guest。account.describe.call(guest)显式把本次调用的this指定为guest。

判断 this 时先看调用表达式,不要只看函数写在哪里。
把方法取出来再直接调用,会丢失接收者:
"use strict";
const account = {
owner: "小周",
describe() {
return this.owner;
},
};
const loose = account.describe;
try {
console.log(loose());
} catch (error) {
console.log(error.name);
}TypeError严格模式下,像 loose() 这样的普通函数调用会让 this 为 undefined,读取 this.owner 因而报错。可以保留 account.describe() 的调用形式,或在确实需要固定接收者时使用 bind(account)。
箭头函数没有自己的 this,它会沿外层词法作用域取得 this。需要用 this 读取“调用这个方法的对象”时,优先使用 describe() { ... } 这样的普通方法,而不是箭头函数。
观察调用形式如何改变 this
下面的模拟器把几种调用方式并排展示。重点观察调用表达式左侧有没有接收者,以及是否使用了 call。
看懂对象赋值、浅拷贝与嵌套引用
对象变量保存的是一个引用。把一个对象变量赋给另一个变量,不会复制对象本体,而是让两个变量指向同一个对象。
const original = {
title: "对象入门",
stats: { views: 10 },
};
const alias = original;
alias.title = "对象进阶";
console.log(original.title);
console.log(alias === original);运行环境:Node.js v25.2.1
对象进阶
truealias 和 original 指向同一对象,因此从任一入口修改,另一边都会看到变化。对象的 === 比较也是比较引用身份,而不是逐项比较属性内容。
展开语法可以创建一个新的外层对象,但它只做浅拷贝:
const original = {
title: "对象入门",
stats: { views: 10 },
};
const copy = { ...original };
copy.title = "副本";
copy.stats.views += 1;
console.log(original.title);
console.log(original.stats.views);
console.log(copy === original);
console.log(copy.stats === original.stats);对象入门
11
false
true外层的 copy 是新对象,所以改 copy.title 不影响原对象;但嵌套的 stats 引用仍被共享,所以改 views 会影响两边。Object.assign({}, original) 也具有同样的浅拷贝边界。
如果数据本身适合结构化克隆,可以使用 structuredClone(original) 创建更深层的独立副本。它能处理许多内置数据类型和循环引用,但函数不能被结构化克隆,某些平台对象也有额外限制。JSON.stringify 再 JSON.parse 也不是通用深拷贝方案,因为它会丢失或改变 undefined、函数、Date 等值。
沿原型链共享行为,并区分属性归属
读取 object.key 时,JavaScript 先查对象自身。找不到时,它会进入该对象的原型,再沿着原型的原型继续查找,直到找到属性或到达 null。
Object.create(proto) 可以直接创建一个以 proto 为原型的新对象:
const shared = {
category: "教程",
describe() {
return `${this.title} / ${this.category}`;
},
};
const article = Object.create(shared);
article.title = "理解对象";
console.log(article.describe());
console.log(Object.hasOwn(article, "describe"));
console.log("describe" in article);
console.log(Object.getPrototypeOf(article) === shared);运行环境:Node.js v25.2.1
理解对象 / 教程
false
true
truedescribe 不属于 article 自身,但能沿原型链找到。方法调用仍然是 article.describe(),所以方法里的 this 是 article,不是保存方法的 shared。

属性查找与属性归属是两个问题:能访问到,不代表它是自身属性。
同名自身属性会遮蔽原型属性
console.log(article.category);
article.category = "深度文章";
console.log(article.category);
console.log(shared.category);
delete article.category;
console.log(article.category);教程
深度文章
教程
教程赋值通常会在 article 自身创建 category,从而遮蔽原型上的同名属性。删除这个自身属性后,读取过程又会继续到原型,于是再次得到 "教程"。如果原型属性是不可写的数据属性或只有 getter 的访问器属性,普通赋值的结果会不同;是否能创建遮蔽属性要看原型属性的描述符和当前模式。
四种常用检查回答不同问题
枚举外部数据时,如果使用 for...in 并且只想处理自身属性,应在循环内用 Object.hasOwn 过滤。它也适用于 Object.create(null) 创建的无原型对象;这种对象没有继承 hasOwnProperty、toString 等方法。
不要给 Object.prototype、Array.prototype 等内置原型随意添加或替换属性。修改会影响大量不相关对象,也可能破坏第三方代码对枚举和方法行为的假设。
用属性描述符控制读写规则
属性不只有名称和值。数据属性还带有三项行为开关:writable、enumerable 和 configurable。Object.getOwnPropertyDescriptor 可以读取一个自身属性的完整描述符。
数据属性:值与三个行为开关
"use strict";
const badge = {};
Object.defineProperty(badge, "level", {
value: 3,
writable: false,
enumerable: false,
configurable: false,
});
console.log(badge.level);
console.log(Object.keys(badge).length);
try {
badge.level = 4;
} catch (error) {
console.log(error.name);
}
try {
delete badge.level;
} catch (error) {
console.log(error.name);
}
const descriptor = Object.getOwnPropertyDescriptor(badge, "level");
console.log(
descriptor.value,
descriptor.writable,
descriptor.enumerable,
descriptor.configurable
);运行环境:Node.js v25.2.1
3
0
TypeError
TypeError
3 false false falsevalue保存属性值。writable: false禁止通过普通赋值改变数据属性的值;严格模式下失败会抛出TypeError。enumerable: false让属性不出现在Object.keys、对象展开、for...in等只处理可枚举属性的操作中,但属性仍然可以直接读取。configurable: false禁止删除属性、改变enumerable或configurable,也禁止在数据属性和访问器属性之间转换。
configurable: false 不等同于“值永远不能改变”。如果一个不可配置的数据属性仍是 writable: true,它的值仍可写,并且 writable 还可以单向改成 false;一旦为 false,就不能再改回 true。
另一个常见边界是默认值:通过普通赋值或对象字面量创建的属性通常是可写、可枚举、可配置的;而 Object.defineProperty 中省略的布尔特性默认都是 false。
访问器属性:让读取和赋值经过逻辑
访问器属性不保存 value,而是用 get 和 set 响应读取与赋值。它的使用形式仍像普通属性,不需要在后面加括号。
const cart = {
_quantity: 1,
get quantity() {
return this._quantity;
},
set quantity(value) {
if (!Number.isInteger(value) || value < 0) {
throw new RangeError("quantity 必须是非负整数");
}
this._quantity = value;
},
};
cart.quantity = 3;
const descriptor = Object.getOwnPropertyDescriptor(cart, "quantity");
console.log(cart.quantity);
console.log(typeof descriptor.get, typeof descriptor.set);
console.log(descriptor.enumerable, descriptor.configurable);运行环境:Node.js v25.2.1
3
function function
true true对象字面量中的 getter 和 setter 默认可枚举、可配置。_quantity 的下划线只是一种“内部使用”的命名约定,并不提供真正的私有性。
数据描述符使用 value、writable;访问器描述符使用 get、set。同一个描述符不能把这两组字段混在一起,否则 Object.defineProperty 会抛出 TypeError。只有 getter 没有 setter 的属性可以读取但不能正常赋值;只有 setter 没有 getter 的属性读取时得到 undefined。
在实验台里调整属性特性
切换三个开关,再分别尝试改写、枚举和删除。注意观察“属性存在”和“属性出现在列表中”并不是一回事。
综合实践:构建一个小型库存模型
现在把本章机制组合起来,为命令行库存看板准备数据层。每件商品要拥有自己的数据,同时通过原型共享出售与摘要方法;商品编号应可读但不可改,也不出现在常规枚举结果里。
实现步骤
- 创建
productActions,提供sell(quantity)和summary()两个方法。sell只接受正整数,库存不足时返回false,成功时扣减库存并返回true。 - 编写
createProduct(data),通过Object.create(productActions)创建商品。 - 将
name、price、stock和嵌套的warehouse写成自身属性。 - 用
Object.defineProperty创建sku:可读、不可写、不可枚举、不可配置。 - 创建至少三件商品,放进
inventory数组。 - 在出售前创建快照。外层商品和嵌套的
warehouse都要复制,避免后续修改污染快照。 - 出售第一件商品的 2 个库存,并输出出售结果、商品摘要、快照库存、方法归属和可枚举键。
一种完整实现
验收标准
- 三件商品都能读取
sku,但Object.keys不包含sku。 sell和summary能通过商品访问,但不是商品的自身属性。- 出售 2 件后,实时库存从
5变为3,快照仍为5。 - 修改实时商品的
warehouse.city不会改变快照里的城市。 - 传入
0、负数或小数时抛出RangeError;库存不足时不扣库存并返回false。
本章自测
总结
- 对象把一件事的多个命名特征组织在一起,属性值可以是任意 JavaScript 值。
- 固定且合法的属性名适合点语法;动态或特殊属性名使用方括号语法。
- 嵌套路径要逐层读取;
?.能安全读取缺失路径,但不会自动创建对象。 - 普通方法中的
this由调用方式决定,方法脱离接收者后不会自动记住原对象。 - 对象赋值复制引用;对象展开只创建新的外层对象,嵌套对象仍可能共享。
- 属性查找从自身开始,再沿原型链向上;
Object.hasOwn与in回答的问题不同。 - 属性描述符把“是否可写、是否可枚举、是否可配置”与属性值分开控制,访问器属性则用 getter 和 setter 接管读写。