商品有名称、价格和库存,用户有昵称、地址和偏好,文章有标题、发布者信息和阅读数据。真实程序很少只处理一个孤立的值,它更常需要把一组彼此相关的信息放在一起,并通过有意义的名字找到它们。
对象正是 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 = "学院路";
运行环境: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 时使用后备值。可选链适合读取,不会自动创建缺失的中间对象;要写入深层属性,必须先确保整条路径存在。
当属性值是函数时,我们通常把这个属性称为方法。方法可以使用对象的数据完成动作,例如扣减余额或生成一段描述。
"use strict";
const account = {
owner: "小周",
balance: 120,
describe() {
return `${this.owner}:${this.balance}`;
},
};
const guest = {
owner: "访客",
balance: 0,
describe: account.describe,
};
运行环境: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() { ... } 这样的普通方法,而不是箭头函数。
下面的模拟器把几种调用方式并排展示。重点观察调用表达式左侧有没有接收者,以及是否使用了 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 对象入门
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.
运行环境: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 =
运行环境: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 =
运行环境: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 都要复制,避免后续修改污染快照。sku,但 Object.keys 不包含 sku。sell 和 summary 能通过商品访问,但不是商品的自身属性。5 变为 3,快照仍为 5。warehouse.city 不会改变快照里的城市。0、负数或小数时抛出 RangeError;库存不足时不扣库存并返回 false。?. 能安全读取缺失路径,但不会自动创建对象。this 由调用方式决定,方法脱离接收者后不会自动记住原对象。Object.hasOwn 与 in 回答的问题不同。运行环境:Node.js v25.2.1
true
无线鼠标 / ¥129 / 库存 3
5
false
true
name | price | stock | warehouse