《JavaScript 语言精粹》第三章精读:对象——最基础也最容易被误解的基石

3 阅读7分钟

《JavaScript 语言精粹》第三章精读:对象——最基础也最容易被误解的基石

在 JavaScript 的世界里,除了简单的六种基本类型,剩下的全都是对象。与其说它是“面向对象”,不如说它是“基于对象”。道格拉斯·克罗克福德在《JavaScript 语言精粹》中,把“对象”放在第三章,不是因为它简单,恰恰因为它是理解整门语言原型、函数、继承的底层钥匙。

这次,我们不光重读经典,更把文中的「笔记」梳理成一份随时可查的手册。


一、对象是什么?一个属性容器

书中开篇便给出了一个最简洁、也最接近本质的定义:

“对象是属性的容器,每个属性都是一个名字/值对。”

这意味着对象实际上就是一个动态的键值集合,键(属性名)可以是任何字符串(包括空字符串),值可以是除了 undefined 之外的任何数据类型。布尔值、数字、字符串、函数、数组、甚至另一个对象……统统可以放入这个“容器”之中。

对象字面量:唯一的创建方式

克罗克福德对字面量有着近乎偏执的推崇,因为:

// ✅ 极具表现力,一目了然
var flight = {
  airline: "Oceanic",
  number: 815,
  departure: {
    IATA: "SYD",
    city: "Sydney"
  }
};

// ❌ 拖沓、不直观,应避免
var flight = new Object();
flight.airline = "Oceanic";
...

{} 不仅代码更少,更重要的是它清晰地表达了“这是一个包含若干属性的对象”这一意图。属性名如果是合法的标识符,可以不加引号;但如果包含连字符等特殊字符,就必须用引号包裹并通过中括号访问。

📝 笔记卡①:对象字面量要点

  • 优先使用 {},不用 new Object()
  • 所有属性名最终都会被转为字符串
  • 特殊字符(如 -)必须带引号,并用 [] 访问

二、检索与更新:安全永远第一

对象的属性值可以通过 .[] 读取。点号方便,中括号灵活,两者各有用场:

flight.airline;        // "Oceanic"
flight["number"];      // 815
var key = "departure";
flight[key];           // { IATA: "SYD", city: "Sydney" }

设置默认值的优雅方式

当不确定属性是否存在时,我们常用 || 给出一个后备值:

var status = flight.status || "unknown";

这种写法利用了 undefined 被判定为“假值”的特性。但要注意,如果属性值本身就是 0"" 这类假值,它就会被后备值取代——好在这类场景在设计时应提前考虑。

深层访问的“短路保护”

更恼人的是访问深层嵌套的属性。如果某个中间对象为 undefinednull,代码会抛出 TypeError。书中给出的解药是 && 短路:

var city = flight && flight.departure && flight.departure.city;

每一个 && 如果碰到“假值”就会立即返回,从而安全地终止访问链。

更新和添加同样直接:对一个不存在的属性赋值,它就自动新增了。这表示对象是完全可变的,你随时可以往里面塞任何东西,但这种灵活性也要求你必须有更好的约定来避免失控。


三、引用:对象只有一个

引用是理解对象行为的关键。当你把一个对象赋值给另一个变量时,传递的是引用而不是副本:

var anotherFlight = flight;
anotherFlight.number = 616;
flight.number;       // 616,因为两者指向同一个对象

这是原型链、模块暴露等高级模式的基础,但也是调试时经常带来困惑的地方——一个地方的改动可能无意间影响了其它引用。

📝 笔记卡②:检索、更新与引用

  • 读取不存在属性 → undefined
  • || 设默认值;用 && 安全深层访问
  • 赋值即更新/添加
  • 对象通过引用传递,不是复制

四、原型:动态的委托关系,不是静态的类

这是第三章的真正灵魂,也是 JavaScript 区别于传统“类”语言的最大特异点。

每个对象都有一个秘密连接

书中写道:

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

当访问某个属性时,JavaScript 会先在对象自身寻找。如果找不到,就会顺着一个隐藏的内部链接 [[Prototype]](在浏览器的 __proto__Object.getPrototypeOf() 暴露出来)去它的原型上找,一直找到 Object.prototype 为止。这个过程不是“复制”,而是一种委托——对象把找不到的属性的责任委托给了它的原型。

克罗克福德提倡用一种纯净的方式来建立这种委托关系:Object.create(prototype)

var personProto = {
  species: "Homo sapiens",
  describe: function() {
    return this.name + " is a " + this.species;
  }
};

var alice = Object.create(personProto);
alice.name = "Alice";

alice.describe();  // "Alice is a Homo sapiens" —— describe 来自原型
alice.hasOwnProperty("name");     // true
alice.hasOwnProperty("species");  // false

这里 alice 本身并没有 species 属性,却能通过原型委托读到它。而 hasOwnProperty 正是用来甄别“这是自己的属性,还是从原型链上拿来的”——这在对对象进行循环遍历时至关重要。

真正的“继承”在运行时

“原型连接只在检索值时才会被用到。如果我们尝试获取一个对象的某个属性值,但该对象没有此属性名,那么 JavaScript 会试着从原型对象中获取属性值。”

更新和新增永远不会触及原型:当你给 alice.species = "Android" 时,它会在 alice 自身添加一个新属性 species,原型上的那个 species 依然存在,只是被“遮蔽”了。


五、枚举的陷阱:for in 不是你的朋友

遍历对象的属性时,for in 循环会把原型链上的所有可枚举属性一起拉出来,而且遍历的顺序没有任何保证。所以,书中的结论很坚决:

“永远使用 hasOwnProperty 去过滤,除非你明确想要遍历原型上的属性。”

for (var key in alice) {
  if (alice.hasOwnProperty(key)) {
    // 如果还想排除方法,可以再过滤
    if (typeof alice[key] !== 'function') {
      console.log(key + ": " + alice[key]);
    }
  }
}

如果你需要按特定顺序访问属性,可以先将属性名收集到一个数组中,再排序遍历。千万不要依赖 for in 的顺序。

反射 (typeof) 也需要谨慎。typeof 可以判断属性的数据类型,但有两个著名的陷阱:

  • typeof null 返回 "object" ——这是语言本身的错误。
  • 数组或者 null 会被误判,用 Array.isArray 和严格的 === null 来救场。

📝 笔记卡③:枚举与反射

  • for in 遍历所有可枚举属性,包括原型链
  • 强制使用 hasOwnProperty 做过滤
  • typeof 识别类型,但记住 typeof null === "object"
  • 属性遍历顺序不确定,不要依赖

六、删除:慢慢来,比较快

delete 操作符可以移除对象自身的属性,但它不会去动原型上的属性。如果原型上有同名属性,删除自身的属性后,那个被遮蔽的原型属性就“重见天日”了。

var bob = Object.create(personProto);
bob.name = "Bob";
bob.species = "Mutant";  // 遮蔽了原型上的 species

bob.species;               // "Mutant"
delete bob.species;
bob.species;               // "Homo sapiens" ——原型上的值浮现出来

由于原型链的动态性,这种“浮现”有时会让调试变得异常艰难。因此,除非真的需要删除某个属性,否则宁可将其设为 nullundefined 来明确表达“这个属性暂时没有值”,而不是直接删掉。


七、全局变量污染:用一个容器终结混乱

JavaScript 默认的全局作用域极易造成命名冲突,尤其是在加载了多个第三方脚本后。克罗克福德给出的解决方案简单而有效:为你的整个应用创建一个独一无二的全局变量,将所有功能都挂载在它下面

var MYAPP = MYAPP || {};

MYAPP.flight = {
  airline: "Oceanic"
};
MYAPP.utils = {
  sort: function() { /* ... */ }
};

这样,你只在全局作用域里引入了一个变量,所有自定义的功能都被收拢到这个“命名空间”里。这是一种极简却实用的模块化思想,也是后续模块模式(Module Pattern)的雏形。


总结:用好对象,就是用好 JavaScript

第三章表面在讲对象,实际是在教我们如何用最纯粹、最安全的方式组织数据与行为

要点一句话总结
创建{} 字面量,拒绝 new Object()
检索.[] 搭配,&& 短路保护深层访问
原型委托式复用,Object.create() 是银弹
自有属性hasOwnProperty 是你的护身符
枚举for in + hasOwnProperty 组合拳
删除尽量别删,用 = null 表达空值
全局一个唯一的全局容器,终结污染

记下这些笔记之后,再去看闭包、模块模式、函数调用的四种方式,你会猛然发现:原来这一切,都是从“对象”这个最朴素的概念慢慢生长出来的。

参考:《JavaScript 语言精粹》(Douglas Crockford)第三章·对象