JavaScript 对象通关指南:从字面量到原型链,一篇文章踩遍所有坑
本文适合对 JavaScript 有基础了解,但总在对象、原型链、引用传递上栽跟头的开发者。内容基于《JavaScript 语言精粹》第三章的学习实践,包含大量真实踩坑记录。
开篇:对象到底是什么
JavaScript 的简单类型包括数字、字符串、布尔值、null 和 undefined。其他所有的值都是对象。 ------ 《JavaScript 语言精粹》
数组是对象,函数是对象,正则表达式也是对象。这不仅是修辞,而是运行时的事实。
对象的本质是一个「属性的容器 」。每个属性是一对名/值,名字可以是任意字符串,值可以是除 undefined 外的任何东西。
相比之下,Java 或 C++ 的「类的实例」需要事先定义结构,JavaScript 不需要。这套设计让 JS 对象更像一个动态哈希表。
Part 1:创建、取值、改值------三个基本动作
对象字面量
最直接的方式是花括号:
javascript
var stooge = {
"first-name": "Joe",
"last-name": "Howard"
};
几个细节:
- 属性名是合法标识符(字母/数字/下划线/非保留字),引号可以省略
"first-name"含连字符,不是合法标识符,必须加引号- 对象可以嵌套,没有深度限制
javascript
var flight = {
airline: "Oceanic",
departure: {
IATA: "SYD",
city: "Sydney"
},
arrival: {
IATA: "LAX",
city: "Los Angeles"
}
};
取值:. 还是 []
两种语法,各有各的用法:
javascript
stooge["first-name"] // "Joe" ------ 含特殊字符,必须用 []
flight.departure.IATA // "SYD" ------ 常规场景,用 .
区分场景:属性名含特殊字符 或存在变量 里时用 [],其余情况用 . 更简洁。
访问不存在的属性不会报错,返回 undefined:
javascript
flight.status // undefined
防御写法
常⻅的两种安全模式:
javascript
// 给不存在的属性设默认值
var status = flight.status || "unknown";
// 安全访问深层嵌套
flight.equipment && flight.equipment.model // 不会因为访问 undefined.model 而报错
更新:JavaScript 不区分修改和新增
javascript
// 不管是修改还是新增,都是赋值
stooge['first-name'] = 'Jerome'; // 修改
stooge.nickname = 'Curly'; // 新增------之前没有这个属性
flight.equipment = { // 新增嵌套对象
model: 'Boeing 777'
};
JavaScript 只有一个操作:把这个键设成这个值。键已存在就覆盖,不存在就创建。
Part 2:一个让我翻车的坑------假值与 ||
这部分来自我的真实踩坑经历。当时有一道题:
javascript
var obj = { score: 0, name: "" };
var a = obj.score || 100;
var b = obj.name || "anonymous";
我的直觉是:score 属性存在啊,应该返回 0。
结果 a 是 100,b 是 "anonymous"。
错在哪?我把「属性是否存在」当成了 || 的判断条件。但 || 根本不关心存在性,它的判断标准很简单:左边的值是真值还是假值?
- 真值 → 返回左值
- 假值 → 返回右值
记住这 6 个假值
JavaScript 里只有 6 个假值:
| 假值 | 说明 |
|---|---|
false |
--- |
0 |
你以为"有值",JS 说"这是假" |
"" |
空字符串 |
null |
--- |
undefined |
访问不存在的属性得到这个 |
NaN |
非法数字运算 |
除此之外都是真值,包括:" "(空格字符串)、"false"(字符串)、[](空数组)、{}(空对象)。
|| vs && 一句话记
||:左值是假就去右边找保底&&:左值是假就停,不碰右边(短路保护)
javascript
// || 的经典场景:设置默认值(前提是值不会是假值)
var name = userName || "匿名";
// && 的经典场景:安全链式访问
var city = user && user.address && user.address.city;
注意:当你真正需要处理 0、""、false 时
|| 会误吞这些合法值。改用空值合并运算符:
javascript
var volume = config.volume ?? 10; // 0 保留 ✅
var loop = config.loop ?? true; // false 保留 ✅
?? 只检查 null 和 undefined,不关心真假。
Part 3:引用语义------对象是指针与引用,不是一个存储了属性与值的"数组"
javascript
var a = { value: 1 };
var b = a;
b.value = 999;
console.log(a.value); // 999,不是 1
为什么?因为 var b = a 没有复制对象,它复制了指向对象的引用。
做个类比:
- 基本类型(数字、字符串、布尔值)是箱子 :
var y = x把值复制一份放进新箱子,两个箱子互不干扰 - 对象是遥控器 :
var b = a复制了一个遥控器,两个遥控器操控同一个电视。按b遥控器改频道,a看到的也是同一个频道
ini
var x = 10; var obj1 = { count: 10 };
var y = x; var obj2 = obj1;
y = 20; obj2.count = 20;
console.log(x); // 10 console.log(obj1.count); // 20
这也是为什么 {} === {} 是 false ------两个独立对象,虽然"长得一样",但手上有两个不同的遥控器。
函数参数陷阱(我踩过的第二个坑)
javascript
function change(obj) {
obj.value = 100; // 通过引用改了外面的 data
obj = { value: 200 }; // 重新赋值局部变量 obj 的指向
}
var data = { value: 1 };
change(data);
console.log(data.value); // 100,不是 200
你有没有注意到矛盾的地方?函数内部对参数做了两件事:第一件改了对象的属性,生效了;第二件重新给参数赋值,没生效。
区别在于:obj.value = 100 是通过引用修改对象本身;obj = { value: 200 } 只是把局部变量指向新对象,外头的引用还在原地。
改属性影响外面,改指向不影响外面。
Part 4:原型链------对象之间的委托
一个"空对象"为什么能调用方法?
javascript
var empty = {};
empty.toString(); // "[object Object]"
empty.hasOwnProperty("x"); // false
答案藏在原型链里。
每个 JavaScript 对象内部有一个隐藏链接,指向另一个对象------它的原型。当你访问一个属性,引擎的查找过程是:
markdown
1. 在自己身上找
2. 没找到 → 去原型上找
3. 原型上也没有 → 去原型的原型上找
4. 一直走到 null(原型链终点)
javascript
empty {}
→ Object.prototype { toString, hasOwnProperty, ... }
→ null
这就是为什么 empty.toString() 能工作------它沿着原型链在 Object.prototype 上找到了 toString。
手写一个原型链
javascript
var grandparent = { surname: "张", origin: "中国" };
var parent = Object.create(grandparent);
parent.job = "工程师";
var me = Object.create(parent);
me.name = "小明";
查找 me.surname 的过程:
vbnet
me → 有 name,没有 surname
↓ 委托
parent → 有 job,没有 surname
↓ 委托
grandparent → 有 surname: "张" ✅
关键特性:原型链是动态的 。如果 grandparent.surname 改为"李",me.surname 也会变成"李"------因为查找链路没变。
另一个重要特性:单向性。 改子对象的属性不会影响父对象,因为原型链是子 → 父的单向委托,不会反向查找。
Part 5:反射与枚举------怎么安全地探索对象
当你有一个对象,怎么知道它有哪些属性?哪些是自己的、哪些是从原型来的?
检查单个属性
javascript
var child = Object.create({ shared: "原型属性" });
child.own = "自有属性";
child.hasOwnProperty("own"); // true ------ 自己的
child.hasOwnProperty("shared"); // false ------ 原型来的
"shared" in child; // true ------ in 会走整条原型链
⚠️ hasOwnProperty 返回的是布尔值,不是属性值。我之前在这个点上翻过车。
遍历所有属性
for...in 会遍历自身和原型链上所有可枚举属性:
javascript
for (var key in child) {
console.log(key);
}
// 输出:own, shared
如果你只想遍历自己的属性,加过滤:
javascript
for (var key in child) {
if (child.hasOwnProperty(key)) {
console.log("自己的:", key);
}
}
Object.keys 更简洁,直接返回自身可枚举属性的数组:
javascript
Object.keys(child); // ["own"] ------ 只有自己的,没有 shared
日常推荐优先用 Object.keys,不用手动过滤。
Part 6:删除与避免全局污染
delete 的正确用法
javascript
var obj = { a: 1, b: 2 };
delete obj.b;
console.log(obj.b); // undefined
delete 只删自己的属性,不碰原型链:
javascript
var proto = { shared: "原型" };
var obj = Object.create(proto);
delete obj.shared; // 删不掉,它不在自己身上
delete proto.shared; // 要去源头删
用 var 声明的变量不可 delete,没有 var 的隐式全局可以。
全局变量污染
JavaScript 的全局对象(浏览器里是 window)是所有全局变量的容器。在全局声明太多变量会导致:
javascript
var name = "Asize";
var version = "1.0";
var status = "active";
这些全挂在 window 上,和第三方库的变量可能冲突。更危险的是隐式全局 ------忘了写 var:
javascript
function test() {
leakVar = "我漏出去了"; // 没写 var → 隐式全局
}
命名空间模式
最简单的解法:只创造一个全局变量。
javascript
var MyApp = {};
MyApp.version = "1.0";
MyApp.utils = {
format: function(s) { return s.trim(); }
};
进阶版------用 IIFE(立即执行函数)隐藏私有变量:
javascript
var MyApp = (function() {
var privateCount = 0; // 外部无法访问
return {
increment: function() { privateCount++; },
getCount: function() { return privateCount; }
};
})();
速查表
创建对象
| 方式 | 示例 |
|---|---|
| 字面量 | var obj = { a: 1 }; |
| 以某对象为原型 | var child = Object.create(parent); |
访问属性
| 语法 | 适用场景 |
|---|---|
obj.prop |
属性名是合法标识符 |
obj["prop"] |
含特殊字符或用变量 |
检查属性
| 方法 | 只查自身 | 查原型链 |
|---|---|---|
obj.hasOwnProperty(k) |
✅ | ❌ |
k in obj |
✅ | ✅ |
遍历属性
| 方法 | 是否含原型链 |
|---|---|
for...in |
是(建议加 hasOwnProperty 过滤) |
Object.keys() |
否 |
Object.getOwnPropertyNames() |
否(含不可枚举) |
常见陷阱速查
| 场景 | 错误直觉 | 正解 | ||
|---|---|---|---|---|
| `0 | 100` | 返回 0 |
返回 100(0 是假值) |
|
函数内 obj = newObj |
外面也被改了 | 不改变外面,改的是局部变量指向 | ||
delete 原型属性 |
能删掉 | 删不掉,要去源头删 | ||
Object.keys(obj) |
返回值数组 | 返回键名数组 |
总结
JavaScript 的对象系统其实不复杂,但它和一些传统 OOP 语言的设计哲学完全不同:
- 对象是动态容器------增删属性不需要类定义
- 处理假值时小心
||------它不是存在性检查,是真假检查 - 对象是引用传递------函数参数不是例外,改属性影响外面,改指向不影响
- 原型链是委托机制------属性没找到就去问原型,链是单向、动态的
hasOwnProperty帮你在原型链中分清界限- 只创造一个全局变量------命名空间模式是你的朋友
如果你最近也在学 JavaScript 对象,或者在这些概念上栽过跟头,欢迎留言交流。