javascript 对象全知识解析《JavaScript 语言精粹》深度解析:第 3 章“对象”核心机制与避坑指南

javascript 对象全知识解析

  • [一、 对象字面量(Object Literals)](#一、 对象字面量(Object Literals))
    • [1.1 核心定义与语法](#1.1 核心定义与语法)
    • [1.2 属性名(键)的引号规则](#1.2 属性名(键)的引号规则)
    • [1.3 属性值的嵌套性](#1.3 属性值的嵌套性)
  • [二、 检索(Retrieval)](#二、 检索(Retrieval))
    • [2.1 两种检索方式](#2.1 两种检索方式)
    • [2.2 默认值与不存在的属性](#2.2 默认值与不存在的属性)
    • [2.3 核心防错:嵌套检索引发的 TypeError](#2.3 核心防错:嵌套检索引发的 TypeError)
  • [三、 更新(Update)](#三、 更新(Update))
    • [3.1 属性已存在:值替换](#3.1 属性已存在:值替换)
    • [3.2 属性不存在:对象扩充](#3.2 属性不存在:对象扩充)
  • [四、 引用(Reference)](#四、 引用(Reference))
    • [4.1 同一引用的联动效应](#4.1 同一引用的联动效应)
    • [4.2 独立对象与相同引用的对比](#4.2 独立对象与相同引用的对比)
  • [五、 原型(Prototype)](#五、 原型(Prototype))
    • [5.1 显式创建原型连接](#5.1 显式创建原型连接)
    • [5.2 原型的"读写分离"](#5.2 原型的“读写分离”)
    • [5.3 原型的两大核心特性](#5.3 原型的两大核心特性)
  • [六、 反射(Reflection)](#六、 反射(Reflection))
    • [6.1 使用 `typeof` 操作符探测类型](#6.1 使用 typeof 操作符探测类型)
    • [6.2 过滤原型链:处理非必要属性的两种方法](#6.2 过滤原型链:处理非必要属性的两种方法)
  • [七、 枚举(Enumeration)](#七、 枚举(Enumeration))
    • [7.1 for in 语句:遍历与过滤](#7.1 for in 语句:遍历与过滤)
    • [7.2 传统 for 语句:确保有序与纯净](#7.2 传统 for 语句:确保有序与纯净)
  • [八、 删除(Delete)](#八、 删除(Delete))
  • 九、减少全局变量污染
    • [9.1 为什么要避免全局变量?](#9.1 为什么要避免全局变量?)
    • [9.2 解法:唯一全局变量模式(Global Abatement)](#9.2 解法:唯一全局变量模式(Global Abatement))

重新认识 JavaScript 对象

在 JavaScript 中,除了数字字符串布尔值nullundefined 这五种基本类型之外,其他所有值都是对象(包括数组、函数、正则表达式)。

理解 JavaScript 的对象,需要牢记以下 4 个核心本质:

  • 万物皆对象("貌似"对象)

    数字、字符串和布尔值虽然是基本类型且不可变,但因为它们拥有方法,所以在行为上"貌似"对象。而真正的对象是可变的键控集合(Keyed Collections)

  • 属性的容器

    对象是属性的容器。

    名(键): 可以是包括空字符串在内的任意字符串。

    : 可以是除 undefined 之外的任何值(因为未定义的属性默认返回 undefined)。

  • 无类别约束(Class-free)

    JavaScript 的对象是"无类"的。它对新属性的名字和值没有任何约束,非常适合用来收集和管理数据。通过嵌套其他对象,它可以轻松表示树形图形结构。

  • 基于原型链(Prototype)继承

    JavaScript 包含一个原型链特性,允许一个对象直接继承另一个对象的属性。正确使用原型链,可以显著减少对象初始化的时间和内存消耗

一、 对象字面量(Object Literals)

对象字面量是 JavaScript 中最直观、最方便的创建新对象值的方法。

1.1 核心定义与语法

对象字面量就是包围在一对花括号 {} 中的零个或多个 "名/值"对。它可以出现在任何允许表达式出现的地方。

javascript 复制代码
// 空对象
var empty_object = {};

// 包含属性的对象
var stooge = {
    "first-name": "Jerome",
    "last-name": "Howard"
};

1.2 属性名(键)的引号规则

这是一个极易踩坑的细节。属性名可以是包括空字符串在内的任意字符串,但写不写引号取决于它是否合法:

  • 必须加引号: 如果属性名不是一个合法的 JavaScript 标识符(例如包含连字符 -、空格,或者是保留字)。

  • 可选加引号: 如果属性名是一个合法的 JavaScript 标识符且不是保留字。

javascript 复制代码
// An highlighted block
var person = {
    "first-name": "Jerome", // 必须加引号,因为连字符 '-' 在 JS 中是减号,非合法标识符
    first_name: "Howard",   // 可选加引号,下划线是合法标识符
    age: 28                 // 可选加引号
};

注意: 多个"名/值"对之间必须使用逗号 , 进行分隔。

1.3 属性值的嵌套性

属性的值可以从任何表达式中获得(包括另一个对象字面量)。这意味着对象是可嵌套的,非常适合用来表示树形或图形等复杂结构。

javascript 复制代码
// An highlighted block
var flight = {
    airline: "Oceanic",
    number: 815,
    // 嵌套对象
    departure: {
        IATA: "SYD",
        time: "2004-09-22 14:55",
        city: "Sydney"
    },
    // 嵌套对象
    arrival: {
        IATA: "LAX",
        time: "2004-09-23 10:42",
        city: "Los Angeles"
    }
};

二、 检索(Retrieval)

在 JavaScript 中,检索对象中包含的值主要有两种方式:\[\] 后缀表示法和 . 点表示法。

2.1 两种检索方式

  • \[\] 后缀法 : 在 [] 后缀中括住一个字符串表达式。适用于任何属性名。

  • . 点表示法: 如果属性名是一个常数,且是一个合法的 JavaScript 标识符(且不是保留字),则可以用点表示法代替。

最佳实践: 优先考虑使用.点表示法,因为它更紧凑且可读性更好。

javascript 复制代码
// An highlighted block
stooge["first-name"]     // "Joe" (包含连字符,必须用 [] 后缀法)
flight.departure.IATA    // "SYD" (合法标识符,优先用点表示法)

2.2 默认值与不存在的属性

如果你尝试检索一个不存在 的成员元素的值,JavaScript 不会报错,而是直接返回一个 undefined 值。

javascript 复制代码
// An highlighted block
stooge["middle-name"]    // undefined
flight.status            // undefined
stooge["FIRST-NAME"]     // undefined (注意:属性名区分大小写)

💡 避坑与填充技巧:使用 || 运算符

为了防止获取到 undefined,可以使用 || 运算符来填充默认值:

javascript 复制代码
// An highlighted block
var middle = stooge["middle-name"] || "(none)";
var status = flight.status || "unknown";

2.3 核心防错:嵌套检索引发的 TypeError

虽然检索对象本身不存在的属性只会返回 undefined,但如果尝试检索 undefined 值的属性,将会导致 TypeError 异常。这是前端开发中极常见的报错场景。

javascript 复制代码
// An highlighted block
flight.equipment         // undefined
flight.equipment.model   // 报错!throw "TypeError"(因为无法从 undefined 中读取属性 'model')

💡 解决方案:使用 && 运算符进行守卫

可以通过 && 运算符的短路特性来避免这种错误:

javascript 复制代码
// 如果 flight.equipment 不存在,直接返回 undefined,不会继续向后读取,从而避免报错
flight.equipment && flight.equipment.model  // undefined

三、 更新(Update)

对象中的值可以通过简单的赋值语句来更新。赋值时,JavaScript 会根据属性名是否存在,自动执行"替换"或"扩充"操作。

3.1 属性已存在:值替换

如果赋值的属性名已经存在于对象中,那么该属性的值会被新值直接替换

javascript 复制代码
// 假设 stooge['first-name'] 原本是 "Joe"
stooge['first-name'] = 'Jerome'; 
// 现已更新为 "Jerome"

3.2 属性不存在:对象扩充

如果对象之前并没有拥有那个属性名,那么该属性就会被直接扩充到该对象中。这种动态扩充的能力体现了 JavaScript 对象的灵活性。

javascript 复制代码
// 扩充一个需要用字符串表示的属性
stooge['middle-name'] = 'Lester';

// 用点表示法扩充一个合法标识符属性
stooge.nickname = 'Curly';

// 扩充一个对象值
flight.equipment = {
    model: 'Boeing 777'
};

// 扩充一个字符串属性
flight.status = 'overdue';

四、 引用(Reference)

这是理解 JavaScript 内存模型的关键知识点:对象通过引用来传递。它们永远不会被拷贝

4.1 同一引用的联动效应

当把一个对象赋值给另一个变量时,并没有创建该对象的副本,而是让两个变量指向内存中的同一个对象。因此,修改其中一个变量的属性,另一个变量也会受到影响。

javascript 复制代码
var x = stooge;
x.nickname = 'Curly';

var nick = stooge.nickname;
// 因为 x 和 stooge 是指向同一个对象的引用,所以 nick 的值为 'Curly'

4.2 独立对象与相同引用的对比

理解"字面量每次执行都会创建新对象"是避坑的关键:

  • 独立创建 : 每个 {} 字面量都会生成一个全新的、独立的内存空间。

  • 连续赋值: 通过链式赋值,可以让多个变量共享同一个内存空间。

javascript 复制代码
// 情况 A:a、b 和 c 每个都引用一个不同的、独立的空对象
var a = {}, b = {}, c = {}; 

// 情况 B:a、b 和 c 都引用同一个空对象
a = b = c = {};

五、 原型(Prototype)

每个对象都连接到一个原型对象,并且可以从中继承属性。

所有通过对象字面量创建的对象,都连接到 JavaScript 的标准对象:Object.prototype

5.1 显式创建原型连接

书中通过扩展 Object.beget 方法,展示了如何显式创建一个以特定对象为原型的新对象(注:此方法即现代 JS 中 Object.create 的雏形):

javascript 复制代码
if (typeof Object.beget !== 'function') {
    Object.beget = function (o) {
        var F = function () {};
        F.prototype = o;
        return new F();
    };
}

// another_stooge 的原型是 stooge
var another_stooge = Object.beget(stooge);

5.2 原型的"读写分离"

理解原型链的核心在于区分更新(写)检索(读)

规则 A:更新时不触发原型链(写操作)

原型连接在更新时是不起作用的。当我们对某个对象做出改变时,只会修改该对象自身的属性,永远不会触及并改变它的原型

javascript 复制代码
// 以下操作仅在 another_stooge 自身上添加或修改属性,原型的 stooge 毫无影响
another_stooge['first-name'] = 'Harry';
another_stooge['middle-name'] = 'Moses';
another_stooge.nickname = 'Moe';

规则 B:检索时触发原型链(读操作与"委托)

原型连接只有在检索值的时候才会被用到。

  1. 尝试获取对象属性时,若对象自身拥有该属性,直接返回。

  2. 若自身没有 该属性,JavaScript 会试着从它的原型对象中获取。

  3. 若原型也没有,则顺着原型链依次类推,直到终点 Object.prototype

  4. 如果想要的属性完全不存在于整条原型链中,最终返回 undefined

术语补充: 这种顺着原型链向上寻找属性的过程,被称为委托(Delegation)。

5.3 原型的两大核心特性

  • 动态性: 原型关系是一种动态的关系。如果我们在原型中添加一个新的属性,该属性会立即对所有基于该原型创建的对象可见。

  • 高容错性: 即使是在对象创建之后才加入原型的属性,依然能被顺着原型链即时检索到。

javascript 复制代码
// 1. 在原型 stooge 上动态添加属性
stooge.profession = 'actor';

// 2. 基于该原型创建的 another_stooge 能够立即读取到
another_stooge.profession // 返回 'actor'

六、 反射(Reflection)

反射是指在运行时检查对象并确定其拥有的属性和类型。JavaScript 提供了两种主要手段来实现反射。

6.1 使用 typeof 操作符探测类型

通过检索属性并验证其返回值,可以轻松确定属性的类型。

javascript 复制代码
typeof flight.number    // 'number'
typeof flight.status    // 'string'
typeof flight.arrival   // 'object'
typeof flight.manifest  // 'undefined'(属性不存在)

⚠️ 潜在陷阱:原型链污染

使用 typeof 时必须注意,原型链中的任何属性也会产生一个值。如果你只想检查对象自身的"数据",原型链上的非预期属性(如方法)会干扰你的判断:

javascript 复制代码
typeof flight.toString    // 'function' (来自 Object.prototype)
typeof flight.constructor // 'function' (来自 Object.prototype)

6.2 过滤原型链:处理非必要属性的两种方法

做反射的目标通常是获取数据,为了剔除不需要的属性(如原型链上的函数),有两种处理方法:

  • 方法 A:检测并剔除函数值

    在逻辑中通过typeof 判断,直接过滤掉类型为 'function' 的成员。

  • 方法 B:使用 hasOwnProperty 方法 (推荐)

    这是最可靠的方法。如果对象拥有独有的(自身定义的)属性,它将返回 true。最关键的是:hasOwnProperty 方法不会检查原型链

javascript 复制代码
flight.hasOwnProperty('number')       // true  (对象自身的属性)
flight.hasOwnProperty('constructor')  // false (来自原型链,成功被过滤)

七、 枚举(Enumeration)

在 JavaScript 中遍历对象的属性名主要有两种方式:for in 循环 和传统的 for 循环。它们在原型链过滤和顺序控制上有本质区别。

7.1 for in 语句:遍历与过滤

for in 语句可用来遍历一个对象中的所有属性名。

⚠️ 潜在陷阱:包含非预期属性
for in 的遍历过程会列出所有的属性------包括函数,以及原型链中可能你并不关心的属性。因此,必须配合过滤器来剔除不想要的值。

  • 常用过滤器hasOwnProperty 方法(过滤原型链属性)以及 typeof(排除函数)。
javascript 复制代码
var name;
for (name in another_stooge) {
    // 过滤器:排除掉类型为函数的属性
    if (typeof another_stooge[name] !== 'function') {
        document.writeln(name + ': ' + another_stooge[name]);
    }
}

7.2 传统 for 语句:确保有序与纯净

使用 for in 时,属性名出现的顺序是不确定的 。如果你想确保属性以特定的顺序出现,最完美的办法是完全避免使用 for in

💡 最佳实践:数组辅助遍历

创建一个数组,按正确的顺序包含你想要遍历的属性名,然后使用传统的 for 循环来读取。

javascript 复制代码
var i;
// 1. 自定义严格的属性顺序
var properties = [
    'first-name',
    'middle-name',
    'last-name',
    'profession'
];

// 2. 使用经典 for 循环遍历数组
for (i = 0; i < properties.length; i += 1) {
    document.writeln(properties[i] + ': ' + another_stooge[properties[i]]);
}

🎯 这种做法的两大优势:

  • 绝对有序: 严格按照你定义的数组顺序输出。
  • 免受干扰: 只获取指定的属性,不用担心挖掘出原型链中的其他非预期属

八、 删除(Delete)

delete 运算符可以用来删除对象中的属性。它的运行机制有两个核心要点:

  1. 核心机制:只触及自身,不影响原型
  • 精准移除: 它只会移除对象自身确定包含的属性。
  • 隔离性: 它不会触及并改变原型链中的任何对象。
  1. 核心现象:让原型链中的属性"浮现"
    由于 delete 只删除对象自身的遮蔽属性(Shadowing Property),删除后,原本被盖住的、来自原型链的同名属性会重新暴露出来。
javascript 复制代码
// 1. 此时 another_stooge 自身拥有 nickname 属性,值为 'Moe'
// 它"遮蔽"了原型 stooge 上的同名属性
another_stooge.nickname; // 'Moe'

// 2. 删除 another_stooge 自身的 nickname 属性
delete another_stooge.nickname;

// 3. 再次检索,自身属性已空,但由于原型链的"委托"机制,
// 原型 stooge 的 nickname 属性(假设为 'Curly')浮现了出来
another_stooge.nickname; // 'Curly'

九、减少全局变量污染

9.1 为什么要避免全局变量?

JavaScript 允许非常随意地定义全局变量,但这会削弱程序的灵活性:

随着项目变大,全局变量极易发生命名冲突(互相覆盖覆盖)。

多个组件或类库之间会产生糟糕的相互影响,让代码变得难以阅读和重构。

9.2 解法:唯一全局变量模式(Global Abatement)

最有效的最小化污染方法是:在应用中只创建唯一的一个全局变量。

第一步:创建唯一的"容器"

javascript 复制代码
var MYAPP = {};

第二步:把所有对象和资源变成它的属性

不要单独声明全局变量,全部收拢到 MYAPP 中:

javascript 复制代码
// 原本可能成为全局变量的 stooge 变成了属性
MYAPP.stooge = {
    "first-name": "Joe",
    "last-name": "Howard"
};

// 原本可能成为全局变量的 flight 变成了属性
MYAPP.flight = {
    airline: "Oceanic",
    number: 815,
    departure: {
        IATA: "SYD",
        time: "2004-09-22 14:55",
        city: "Sydney"
    },
    arrival: {
        IATA: "LAX",
        time: "2004-09-23 10:42",
        city: "Los Angeles"
    }
};

核心收益

  • 安全隔離: 只要确保 MYAPP 这个名字不与外部冲突,你容器内部的所有属性(stooge、flight)就绝对安全。

  • 清晰可读 : 任何人看代码都能一眼看MYAPP.stooge 指向的是应用的顶层结构。

相关推荐
胡志辉5 分钟前
深入浅出 call、apply、bind
前端·javascript·后端
十九画生3 小时前
parentID ``` JavaScript 是区分大小写的,所以这两个不是同一个字段。 第二,`parent` 没有声明。 应该先写: `
javascript
怕浪猫3 小时前
Electron 开发实战(十六):总结与展望|生态现状、框架对比、行业趋势与学习指南
前端·javascript·electron
ZengLiangYi4 小时前
批量导入 1000 条对话的性能优化实战
javascript·后端·架构
竹林8184 小时前
用 wagmi v2 + viem 监听合约事件时踩的坑,我花了两天才把"遗漏事件"修好
javascript
小花酱酱5 小时前
QQ群里只有你一个人?邪门歪道破局之路——AstrBot
javascript
bonechips5 小时前
JS 数组指南:从内存原理到二维矩阵
前端·javascript
mONESY5 小时前
前端零基础精讲:Canvas3D、CSS3D、文档流、定位全方位复盘
javascript
竹林81820 小时前
Web3表单签名验证:我用 wagmi 和 ethers 给 DApp 加了一个“免密登录”,踩坑记录全在这了
javascript