自在学

我们与你共同进步

  • 分类课程
  • 文章
  • 工作台
  • 订阅

  • 关于我们
  • 隐私政策
  • 使用条款

探索

  • 分类课程
  • 文章
  • 工作台
  • 订阅

网站信息

  • 关于我们
  • 隐私政策
  • 使用条款

加入社区

自在学学习社区微信二维码

微信扫码,交流学习

株洲市自在学教育科技有限公司© 2025 - 2026 版权所有

© 2025 - 2026 株洲市自在学教育科技有限公司 版权所有

湘公网安备43020302000292号|湘ICP备2025148919号-1
分类课程工作台文章订阅
分类课程工作台文章价格

JavaScript

  1. 01JavaScript 入门
  2. 02类型、值与变量
  3. 03表达式与运算符
  4. 04语句
  5. 05对象
  6. 06数组
  7. 07函数
  8. 08类
  9. 09模块
  10. 10正则表达式
  11. 11浏览器环境
  12. 12文档对象模型 (DOM)
  13. 13Window 对象
  14. 14事件处理
  15. 15HTTP 通信
正在加载课程章节内容
课程编程JavaScript对象

JavaScript 对象:从属性到原型与描述符

商品有名称、价格和库存,用户有昵称、地址和偏好,文章有标题、发布者信息和阅读数据。真实程序很少只处理一个孤立的值,它更常需要把一组彼此相关的信息放在一起,并通过有意义的名字找到它们。

对象正是 JavaScript 用来组织这类数据的核心结构。不过,对象并不只是一个“键值表”:属性值可以是函数,多个变量可能指向同一个对象,读取属性时还可能沿原型链继续查找。再往下一层,每个属性甚至有“是否可写、是否可枚举、是否可删除”等规则。

这篇教程从一个商品对象出发,逐层拆开创建、访问、嵌套、this、引用、原型和属性描述符。完成后,你不仅能写出对象,还能解释代码为什么会得到某个结果。

本文的控制台输出使用 Node.js v25.2.1 验证。正文讨论的是 JavaScript 语言本身,不依赖浏览器中的 window 或 DOM。

学习路线

完成本章后,你应该能够:

  • 用对象字面量创建对象,并区分属性名与属性值。
  • 根据属性名是否固定,选择点语法或方括号语法。
  • 读取、添加、更新和删除属性,并安全地进入嵌套结构。
  • 根据调用表达式判断普通方法中的 this。
  • 解释对象赋值、浅拷贝和嵌套引用之间的差别。
  • 画出一条简单的原型链,并区分自身属性与继承属性。
  • 使用属性描述符控制可写、可枚举和可配置行为。

用对象字面量组织一组信息

假设商城页面需要展示一件商品。把名称、价格、库存分别放进三个无关变量当然可以,但数据一多,变量之间的关系就会越来越难追踪。对象字面量用一对 {} 把这些信息收进同一个值:

javascript
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

console
无线鼠标
8
id | title | price | stock | tags | available

每一项都由“属性名: 属性值”组成,项与项之间用逗号分隔。属性值可以是任意 JavaScript 值,例如字符串、数字、布尔值、数组、另一个对象或函数。

这里还用了两种简写:

  • 当变量名和属性名相同时,title 等价于 title: title。
  • [stockField] 是计算属性名。JavaScript 先得到变量的值 "stock",再用它创建属性。

普通属性名最终会成为字符串;Symbol 属性名是例外。合法标识符可以直接写成 price,带空格或连字符的名字则要写成字符串,例如 "unit price"。

JavaScript 对象字面量解剖图,标注对象外壳、属性名、属性值、冒号连接和逗号分隔。

对象字面量把同一件事的多项信息收进一个值。

const product 只表示变量 product 不能改指向另一个值,并不表示对象内部被冻结。只要属性本身允许写入,product.stock = 7 仍然合法。

1.1
代码 const field = 'score'; const student = { [field]: 90 }; 创建出的属性名是什么?

读取、更新与删除属性

读取属性有两种常用写法。点语法更短,适合代码写下时就已确定、并且符合标识符规则的属性名;方括号语法会先计算括号内的表达式,适合动态属性名或带空格、连字符的名字。

javascript
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

console
无线鼠标
129
undefined

product[key] 会读取变量 key 的值,所以找到 "unit price"。product.key 则固定查找名字为 key 的属性;这个属性不存在,因此结果是 undefined,而不是抛出错误。

点语法与方括号语法访问 JavaScript 对象属性的对比图

方括号接收表达式,点后面写的是固定属性名。

同样的语法也能更新或新增属性;delete 用来删除可配置的自身属性:

javascript
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

console
6
undefined
false

Object.hasOwn(product, "status") 明确回答“status 是否是 product 自己的属性”。仅用 product.status === undefined 无法完成同样的判断,因为一个真实存在的属性也可能把 undefined 当作值。

JavaScript 允许把属性从数字改成字符串等其他类型,但“允许”不等于“合适”。业务代码最好保持字段含义和类型稳定,否则使用方必须增加许多额外判断。

在练习台里切换访问方式

修改 field 的值,再观察点语法与方括号语法分别查找哪个属性。

2.1
变量 key 保存了运行时得到的属性名。下面哪些写法能使用 key 的值访问或更新该属性?

沿路径进入嵌套对象和数组

一件事内部还可能包含另一件事,或包含一组同类项目。对象适合表示“有多个命名特征的一件事”,数组适合表示“按顺序排列的一组项目”。二者可以自由组合。

javascript
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

console
杭州
维修
成都:20

嵌套对象和数组访问路径示意图,展示 company.address.city、company.activities[1] 和 company.offices[0].city

遇到对象就按属性名前进,遇到数组就按索引前进。

长路径的风险是中间某一段不存在。例如,直接读取 company.contact.email 会在 contact 为 undefined 时抛出 TypeError。只想安全读取时,可以使用可选链 ?.:

javascript
console.log(company.contact?.email);
console.log(company.warehouses?.[2]?.city ?? "暂未设置");
console
undefined
暂未设置

?. 遇到 null 或 undefined 就停止,并返回 undefined;?? 只在左侧为 null 或 undefined 时使用后备值。可选链适合读取,不会自动创建缺失的中间对象;要写入深层属性,必须先确保整条路径存在。

3.1
已知 data = { users: [{ profile: { city: '苏州' } }] },哪个表达式会得到苏州?

把函数放进对象,并正确判断 this

当属性值是函数时,我们通常把这个属性称为方法。方法可以使用对象的数据完成动作,例如扣减余额或生成一段描述。

javascript
"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

console
小周:120
访客:0
访客:0

同一个函数被两个对象使用,却得到不同结果。这是因为普通函数中的 this 主要由调用方式决定:

  • account.describe() 中,this 是点前面的 account。
  • guest.describe() 中,this 是点前面的 guest。
  • account.describe.call(guest) 显式把本次调用的 this 指定为 guest。

JavaScript 方法调用与 this 绑定示意图,展示 account.describe、other.describe 和普通函数调用下 this 的不同指向。

判断 this 时先看调用表达式,不要只看函数写在哪里。

把方法取出来再直接调用,会丢失接收者:

javascript
"use strict";
 
const account = {
  owner: "小周",
  describe() {
    return this.owner;
  },
};
 
const loose = account.describe;
 
try {
  console.log(loose());
} catch (error) {
  console.log(error.name);
}
console
TypeError

严格模式下,像 loose() 这样的普通函数调用会让 this 为 undefined,读取 this.owner 因而报错。可以保留 account.describe() 的调用形式,或在确实需要固定接收者时使用 bind(account)。

箭头函数没有自己的 this,它会沿外层词法作用域取得 this。需要用 this 读取“调用这个方法的对象”时,优先使用 describe() { ... } 这样的普通方法,而不是箭头函数。

观察调用形式如何改变 this

下面的模拟器把几种调用方式并排展示。重点观察调用表达式左侧有没有接收者,以及是否使用了 call。

4.1
fn 与 user.show 指向同一个普通函数。执行 user.show() 时,函数中的 this 通常是谁?
4.2
把依赖 this 的方法保存到变量后直接调用,仍然会自动保留原对象作为 this。

看懂对象赋值、浅拷贝与嵌套引用

对象变量保存的是一个引用。把一个对象变量赋给另一个变量,不会复制对象本体,而是让两个变量指向同一个对象。

javascript
const original = {
  title: "对象入门",
  stats: { views: 10 },
};
 
const alias = original;
alias.title = "对象进阶";
 
console.log(original.title);
console.log(alias === original);

运行环境:Node.js v25.2.1

console
对象进阶
true

alias 和 original 指向同一对象,因此从任一入口修改,另一边都会看到变化。对象的 === 比较也是比较引用身份,而不是逐项比较属性内容。

展开语法可以创建一个新的外层对象,但它只做浅拷贝:

javascript
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);
console
对象入门
11
false
true

外层的 copy 是新对象,所以改 copy.title 不影响原对象;但嵌套的 stats 引用仍被共享,所以改 views 会影响两边。Object.assign({}, original) 也具有同样的浅拷贝边界。

如果数据本身适合结构化克隆,可以使用 structuredClone(original) 创建更深层的独立副本。它能处理许多内置数据类型和循环引用,但函数不能被结构化克隆,某些平台对象也有额外限制。JSON.stringify 再 JSON.parse 也不是通用深拷贝方案,因为它会丢失或改变 undefined、函数、Date 等值。

5.1
执行 const copy = { ...source } 后,下面哪些说法正确?

沿原型链共享行为,并区分属性归属

读取 object.key 时,JavaScript 先查对象自身。找不到时,它会进入该对象的原型,再沿着原型的原型继续查找,直到找到属性或到达 null。

Object.create(proto) 可以直接创建一个以 proto 为原型的新对象:

javascript
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

console
理解对象 / 教程
false
true
true

describe 不属于 article 自身,但能沿原型链找到。方法调用仍然是 article.describe(),所以方法里的 this 是 article,不是保存方法的 shared。

对象自身属性、原型链查找顺序与属性描述符特性的关系图

属性查找与属性归属是两个问题:能访问到,不代表它是自身属性。

同名自身属性会遮蔽原型属性

javascript
console.log(article.category);
 
article.category = "深度文章";
console.log(article.category);
console.log(shared.category);
 
delete article.category;
console.log(article.category);
console
教程
深度文章
教程
教程

赋值通常会在 article 自身创建 category,从而遮蔽原型上的同名属性。删除这个自身属性后,读取过程又会继续到原型,于是再次得到 "教程"。如果原型属性是不可写的数据属性或只有 getter 的访问器属性,普通赋值的结果会不同;是否能创建遮蔽属性要看原型属性的描述符和当前模式。

四种常用检查回答不同问题

写法检查范围是否要求可枚举
Object.hasOwn(obj, key)仅自身否
key in obj自身与整条原型链否
Object.keys(obj)仅自身的字符串属性是
for...in自身与原型链上的字符串属性是

枚举外部数据时,如果使用 for...in 并且只想处理自身属性,应在循环内用 Object.hasOwn 过滤。它也适用于 Object.create(null) 创建的无原型对象;这种对象没有继承 hasOwnProperty、toString 等方法。

不要给 Object.prototype、Array.prototype 等内置原型随意添加或替换属性。修改会影响大量不相关对象,也可能破坏第三方代码对枚举和方法行为的假设。

6.1
属性 run 只存在于 task 的原型上。哪组结果正确?
6.2
Object.keys(obj) 会把原型链上所有可枚举属性也放进结果。

用属性描述符控制读写规则

属性不只有名称和值。数据属性还带有三项行为开关:writable、enumerable 和 configurable。Object.getOwnPropertyDescriptor 可以读取一个自身属性的完整描述符。

数据属性:值与三个行为开关

javascript
"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

console
3
0
TypeError
TypeError
3 false false false
  • value 保存属性值。
  • writable: false 禁止通过普通赋值改变数据属性的值;严格模式下失败会抛出 TypeError。
  • enumerable: false 让属性不出现在 Object.keys、对象展开、for...in 等只处理可枚举属性的操作中,但属性仍然可以直接读取。
  • configurable: false 禁止删除属性、改变 enumerable 或 configurable,也禁止在数据属性和访问器属性之间转换。

configurable: false 不等同于“值永远不能改变”。如果一个不可配置的数据属性仍是 writable: true,它的值仍可写,并且 writable 还可以单向改成 false;一旦为 false,就不能再改回 true。

另一个常见边界是默认值:通过普通赋值或对象字面量创建的属性通常是可写、可枚举、可配置的;而 Object.defineProperty 中省略的布尔特性默认都是 false。

7.1
某自身数据属性的 enumerable 为 false。下面哪些说法一定成立?

访问器属性:让读取和赋值经过逻辑

访问器属性不保存 value,而是用 get 和 set 响应读取与赋值。它的使用形式仍像普通属性,不需要在后面加括号。

javascript
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

console
3
function function
true true

对象字面量中的 getter 和 setter 默认可枚举、可配置。_quantity 的下划线只是一种“内部使用”的命名约定,并不提供真正的私有性。

数据描述符使用 value、writable;访问器描述符使用 get、set。同一个描述符不能把这两组字段混在一起,否则 Object.defineProperty 会抛出 TypeError。只有 getter 没有 setter 的属性可以读取但不能正常赋值;只有 setter 没有 getter 的属性读取时得到 undefined。

在实验台里调整属性特性

切换三个开关,再分别尝试改写、枚举和删除。注意观察“属性存在”和“属性出现在列表中”并不是一回事。

7.2
对象有 getter fullName。正确的读取方式是什么?

综合实践:构建一个小型库存模型

现在把本章机制组合起来,为命令行库存看板准备数据层。每件商品要拥有自己的数据,同时通过原型共享出售与摘要方法;商品编号应可读但不可改,也不出现在常规枚举结果里。

实现步骤

  1. 创建 productActions,提供 sell(quantity) 和 summary() 两个方法。sell 只接受正整数,库存不足时返回 false,成功时扣减库存并返回 true。
  2. 编写 createProduct(data),通过 Object.create(productActions) 创建商品。
  3. 将 name、price、stock 和嵌套的 warehouse 写成自身属性。
  4. 用 Object.defineProperty 创建 sku:可读、不可写、不可枚举、不可配置。
  5. 创建至少三件商品,放进 inventory 数组。
  6. 在出售前创建快照。外层商品和嵌套的 warehouse 都要复制,避免后续修改污染快照。
  7. 出售第一件商品的 2 个库存,并输出出售结果、商品摘要、快照库存、方法归属和可枚举键。

一种完整实现

javascript
"use strict";
 
const productActions = {
  sell(quantity) {
    if (!Number.isInteger(quantity) || quantity <= 0) {
      throw new RangeError("quantity 必须是正整数");
    }
 
    if (quantity > this.stock) {
      return false;
    }
 
    this.stock -= quantity;
    return true;
  },
 
  summary() {
    return `${this.name} / ¥${this.price} / 库存 ${this.stock}`;
  },
};
 
function createProduct({ sku, name, price, stock, warehouse }) {
  const product = Object.create(productActions);
 
  Object.assign(product, {
    name,
    price,
    stock,
    warehouse: { ...warehouse },
  });
 
  Object.defineProperty(product, "sku", {
    value: sku,
    writable: false,
    enumerable: false,
    configurable: false,
  });
 
  return product;
}
 
const inventory = [
  createProduct({
    sku: "M-001",
    name: "无线鼠标",
    price: 129,
    stock: 5,
    warehouse: { city: "杭州", shelf: "A-3" },
  }),
  createProduct({
    sku: "K-002",
    name: "机械键盘",
    price: 399,
    stock: 4,
    warehouse: { city: "苏州", shelf: "B-1" },
  }),
  createProduct({
    sku: "H-003",
    name: "桌面支架",
    price: 89,
    stock: 9,
    warehouse: { city: "成都", shelf: "C-2" },
  }),
];
 
const snapshot = inventory.map((item) => ({
  sku: item.sku,
  ...item,
  warehouse: { ...item.warehouse },
}));
 
console.log(inventory[0].sell(2));
console.log(inventory[0].summary());
console.log(snapshot[0].stock);
console.log(Object.hasOwn(inventory[0], "sell"));
console.log("sell" in inventory[0]);
console.log(Object.keys(inventory[0]).join(" | "));

运行环境:Node.js v25.2.1

console
true
无线鼠标 / ¥129 / 库存 3
5
false
true
name | price | stock | warehouse

验收标准

  • 三件商品都能读取 sku,但 Object.keys 不包含 sku。
  • sell 和 summary 能通过商品访问,但不是商品的自身属性。
  • 出售 2 件后,实时库存从 5 变为 3,快照仍为 5。
  • 修改实时商品的 warehouse.city 不会改变快照里的城市。
  • 传入 0、负数或小数时抛出 RangeError;库存不足时不扣库存并返回 false。

本章自测

8.1
属性名保存在变量 field 中时,哪种读取方式会使用变量的值?
8.2
填写输出:const a = { nested: {} }; const b = { ...a }; console.log(a.nested === b.nested);
8.3
关于属性描述符,哪些说法正确?
8.4
一个属性能通过 in 运算符找到,就说明它一定是对象自身的属性。
8.5
严格模式下,方法依赖 this.name。把方法取出为 const fn = user.show,再执行 fn(),最可能发生什么?

总结

  • 对象把一件事的多个命名特征组织在一起,属性值可以是任意 JavaScript 值。
  • 固定且合法的属性名适合点语法;动态或特殊属性名使用方括号语法。
  • 嵌套路径要逐层读取;?. 能安全读取缺失路径,但不会自动创建对象。
  • 普通方法中的 this 由调用方式决定,方法脱离接收者后不会自动记住原对象。
  • 对象赋值复制引用;对象展开只创建新的外层对象,嵌套对象仍可能共享。
  • 属性查找从自身开始,再沿原型链向上;Object.hasOwn 与 in 回答的问题不同。
  • 属性描述符把“是否可写、是否可枚举、是否可配置”与属性值分开控制,访问器属性则用 getter 和 setter 接管读写。
上一章语句下一章数组