JavaScript 对象通关指南:从字面量到原型链,一篇文章踩遍所有坑

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 100b "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 保留 ✅

?? 只检查 nullundefined,不关心真假。


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 返回 1000 是假值)
函数内 obj = newObj 外面也被改了 不改变外面,改的是局部变量指向
delete 原型属性 能删掉 删不掉,要去源头删
Object.keys(obj) 返回值数组 返回键名数组

总结

JavaScript 的对象系统其实不复杂,但它和一些传统 OOP 语言的设计哲学完全不同:

  1. 对象是动态容器------增删属性不需要类定义
  2. 处理假值时小心 ||------它不是存在性检查,是真假检查
  3. 对象是引用传递------函数参数不是例外,改属性影响外面,改指向不影响
  4. 原型链是委托机制------属性没找到就去问原型,链是单向、动态的
  5. hasOwnProperty 帮你在原型链中分清界限
  6. 只创造一个全局变量------命名空间模式是你的朋友

如果你最近也在学 JavaScript 对象,或者在这些概念上栽过跟头,欢迎留言交流。

相关推荐
yingyima6 小时前
Docker 容器内定时任务秘诀全解
前端
moMo6 小时前
前后端模块化分离,web盒子布局思维
前端·后端
前端繁华如梦6 小时前
不写模型文件,用代码「捏」出 3D 世界:Vue3 + Three.js 程序化资产生成实战
前端·vue.js
灰子学技术6 小时前
Envoy OAuth2 过滤器功能实现分析
运维·服务器·前端·网络
LCG元6 小时前
MySQL慢查询分析与索引调优:从故障诊断到性能翻倍的进阶之路
android·前端·mysql
天涯明月19936 小时前
后端工程师全栈转型前端入门
前端·状态模式·全栈工程师
invicinble6 小时前
springboot出现的原因二---作为web的后端服务一站式整合器
前端·spring boot·后端
kyriewen6 小时前
前端初级岗位暴跌62%:我带了三年的实习生被裁了,而AI是他亲手教的
前端·面试·ai编程
zifengningyu6 小时前
【无标题】
前端·vue.js