JavaScript|第11章:构造器与 new 操作符

JavaScript|第11章:构造器与 new 操作符

构造器与new操作符

  • [JavaScript|第11章:构造器与 new 操作符](#JavaScript|第11章:构造器与 new 操作符)
    • 引言
    • [11.1 构造函数与 new 操作符](#11.1 构造函数与 new 操作符)
      • [11.1.1 什么是构造函数?](#11.1.1 什么是构造函数?)
      • [11.1.2 new 操作符的执行过程](#11.1.2 new 操作符的执行过程)
      • [11.1.3 构造函数的 return 行为](#11.1.3 构造函数的 return 行为)
      • [11.1.4 构造函数 vs 工厂函数](#11.1.4 构造函数 vs 工厂函数)
      • [11.1.5 忘记 new 的后果](#11.1.5 忘记 new 的后果)
      • [11.1.6 构造函数的方法共享](#11.1.6 构造函数的方法共享)
    • [11.2 可选链 ?.](#11.2 可选链 ?.)
      • [11.2.1 问题背景](#11.2.1 问题背景)
      • [11.2.2 可选链语法](#11.2.2 可选链语法)
      • [11.2.3 可选链的三种用法](#11.2.3 可选链的三种用法)
      • [11.2.4 注意事项](#11.2.4 注意事项)
    • [11.3 Symbol 类型](#11.3 Symbol 类型)
      • [11.3.1 Symbol 是什么?](#11.3.1 Symbol 是什么?)
      • [11.3.2 Symbol 作为对象属性键](#11.3.2 Symbol 作为对象属性键)
      • [11.3.3 全局 Symbol 注册表](#11.3.3 全局 Symbol 注册表)
      • [11.3.4 内置 Symbol(Well-known Symbols)](#11.3.4 内置 Symbol(Well-known Symbols))
      • [11.3.5 Symbol 的特性总结](#11.3.5 Symbol 的特性总结)
    • [11.4 对象 --- 原始值转换](#11.4 对象 — 原始值转换)
      • [11.4.1 转换的触发场景](#11.4.1 转换的触发场景)
      • [11.4.2 转换的三种 hint](#11.4.2 转换的三种 hint)
      • [11.4.3 转换算法的三步查找](#11.4.3 转换算法的三步查找)
      • [11.4.4 默认的 toString 和 valueOf](#11.4.4 默认的 toString 和 valueOf)
      • [11.4.5 自定义 toString 和 valueOf](#11.4.5 自定义 toString 和 valueOf)
      • [11.4.6 使用 Symbol.toPrimitive 完全控制转换](#11.4.6 使用 Symbol.toPrimitive 完全控制转换)
      • [11.4.7 Date 对象的特殊转换](#11.4.7 Date 对象的特殊转换)
      • [11.4.8 转换规则速查表](#11.4.8 转换规则速查表)
    • [11.5 实战](#11.5 实战)
      • [11.5.1 实战1:手写 new 操作符](#11.5.1 实战1:手写 new 操作符)
      • [11.5.2 实战2:手写 instanceof](#11.5.2 实战2:手写 instanceof)
      • [11.5.3 实战3:this 指向与可选链综合题](#11.5.3 实战3:this 指向与可选链综合题)
      • [11.5.4 实战4:Symbol + 对象转换综合应用](#11.5.4 实战4:Symbol + 对象转换综合应用)
    • 本章小结

上篇回顾第10章:对象方法与 this

下篇预告第12章:属性配置与 getter/setter


引言

在上一章中,我们深入理解了 this 的四种绑定规则。其中 new 绑定 是创建可复用对象实例的基础方式。本章我们将正式展开这个主题:什么是构造函数?new 操作符在背后做了哪些事情?

除了构造函数,本章还将覆盖三个重要的关联主题:

  • 可选链 ?. :安全地访问深层嵌套属性,避免 TypeError
  • Symbol 类型:JavaScript 中独特的原始类型,用于创建不可冲突的属性标识
  • 对象 --- 原始值转换:当对象被用于运算或比较时,JavaScript 引擎如何将其转换为原始值

#mermaid-svg-wCnjSMf0QuYsqDKz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wCnjSMf0QuYsqDKz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wCnjSMf0QuYsqDKz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wCnjSMf0QuYsqDKz .error-icon{fill:#552222;}#mermaid-svg-wCnjSMf0QuYsqDKz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wCnjSMf0QuYsqDKz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wCnjSMf0QuYsqDKz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wCnjSMf0QuYsqDKz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wCnjSMf0QuYsqDKz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wCnjSMf0QuYsqDKz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wCnjSMf0QuYsqDKz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wCnjSMf0QuYsqDKz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wCnjSMf0QuYsqDKz .marker.cross{stroke:#333333;}#mermaid-svg-wCnjSMf0QuYsqDKz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wCnjSMf0QuYsqDKz p{margin:0;}#mermaid-svg-wCnjSMf0QuYsqDKz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wCnjSMf0QuYsqDKz .cluster-label text{fill:#333;}#mermaid-svg-wCnjSMf0QuYsqDKz .cluster-label span{color:#333;}#mermaid-svg-wCnjSMf0QuYsqDKz .cluster-label span p{background-color:transparent;}#mermaid-svg-wCnjSMf0QuYsqDKz .label text,#mermaid-svg-wCnjSMf0QuYsqDKz span{fill:#333;color:#333;}#mermaid-svg-wCnjSMf0QuYsqDKz .node rect,#mermaid-svg-wCnjSMf0QuYsqDKz .node circle,#mermaid-svg-wCnjSMf0QuYsqDKz .node ellipse,#mermaid-svg-wCnjSMf0QuYsqDKz .node polygon,#mermaid-svg-wCnjSMf0QuYsqDKz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wCnjSMf0QuYsqDKz .rough-node .label text,#mermaid-svg-wCnjSMf0QuYsqDKz .node .label text,#mermaid-svg-wCnjSMf0QuYsqDKz .image-shape .label,#mermaid-svg-wCnjSMf0QuYsqDKz .icon-shape .label{text-anchor:middle;}#mermaid-svg-wCnjSMf0QuYsqDKz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wCnjSMf0QuYsqDKz .rough-node .label,#mermaid-svg-wCnjSMf0QuYsqDKz .node .label,#mermaid-svg-wCnjSMf0QuYsqDKz .image-shape .label,#mermaid-svg-wCnjSMf0QuYsqDKz .icon-shape .label{text-align:center;}#mermaid-svg-wCnjSMf0QuYsqDKz .node.clickable{cursor:pointer;}#mermaid-svg-wCnjSMf0QuYsqDKz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wCnjSMf0QuYsqDKz .arrowheadPath{fill:#333333;}#mermaid-svg-wCnjSMf0QuYsqDKz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wCnjSMf0QuYsqDKz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wCnjSMf0QuYsqDKz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wCnjSMf0QuYsqDKz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wCnjSMf0QuYsqDKz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wCnjSMf0QuYsqDKz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wCnjSMf0QuYsqDKz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wCnjSMf0QuYsqDKz .cluster text{fill:#333;}#mermaid-svg-wCnjSMf0QuYsqDKz .cluster span{color:#333;}#mermaid-svg-wCnjSMf0QuYsqDKz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wCnjSMf0QuYsqDKz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wCnjSMf0QuYsqDKz rect.text{fill:none;stroke-width:0;}#mermaid-svg-wCnjSMf0QuYsqDKz .icon-shape,#mermaid-svg-wCnjSMf0QuYsqDKz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wCnjSMf0QuYsqDKz .icon-shape p,#mermaid-svg-wCnjSMf0QuYsqDKz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wCnjSMf0QuYsqDKz .icon-shape .label rect,#mermaid-svg-wCnjSMf0QuYsqDKz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wCnjSMf0QuYsqDKz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wCnjSMf0QuYsqDKz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wCnjSMf0QuYsqDKz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第11章:构造器与 new 操作符
构造函数与 new
可选链 ?.
Symbol 类型
对象原始值转换
手写 new 操作符
安全访问深层属性
唯一标识与元编程
toString / valueOf

本章目标

  • 理解构造函数的概念与 new 操作符的四步执行过程
  • 掌握 return 对构造函数行为的影响
  • 熟练使用可选链 ?. 安全访问嵌套属性
  • 理解 Symbol 类型的特点与应用场景
  • 掌握对象到原始值的转换规则(toString / valueOf)
  • 实战:手写 new 操作符

11.1 构造函数与 new 操作符

11.1.1 什么是构造函数?

构造函数本质上是一个普通函数,它按照约定new 操作符调用,用于创建一类对象的实例。构造函数没有语法上的特殊标记,它的"特殊性"完全来自于调用方式。

命名约定:构造函数名首字母大写(PascalCase),以区别于普通函数。

javascript 复制代码
// 构造函数(首字母大写)
function User(name, age) {
  this.name = name;
  this.age = age;
}

// 使用 new 调用
const alice = new User('Alice', 25);
const bob = new User('Bob', 30);

console.log(alice.name); // "Alice"
console.log(bob.age);    // 30

11.1.2 new 操作符的执行过程

当使用 new 调用构造函数时,引擎在背后依次执行以下四步:

  1. 创建一个空对象 {},并将 this 指向该对象
  2. 将该对象的原型链接 到构造函数的 prototype 属性(即 this.__proto__ = User.prototype
  3. 执行构造函数体,为新对象添加属性
  4. 返回 this(除非构造函数显式返回了一个对象)

#mermaid-svg-Ip8WLeDivobCPUhq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Ip8WLeDivobCPUhq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Ip8WLeDivobCPUhq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Ip8WLeDivobCPUhq .error-icon{fill:#552222;}#mermaid-svg-Ip8WLeDivobCPUhq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Ip8WLeDivobCPUhq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Ip8WLeDivobCPUhq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Ip8WLeDivobCPUhq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Ip8WLeDivobCPUhq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Ip8WLeDivobCPUhq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Ip8WLeDivobCPUhq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Ip8WLeDivobCPUhq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Ip8WLeDivobCPUhq .marker.cross{stroke:#333333;}#mermaid-svg-Ip8WLeDivobCPUhq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Ip8WLeDivobCPUhq p{margin:0;}#mermaid-svg-Ip8WLeDivobCPUhq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Ip8WLeDivobCPUhq .cluster-label text{fill:#333;}#mermaid-svg-Ip8WLeDivobCPUhq .cluster-label span{color:#333;}#mermaid-svg-Ip8WLeDivobCPUhq .cluster-label span p{background-color:transparent;}#mermaid-svg-Ip8WLeDivobCPUhq .label text,#mermaid-svg-Ip8WLeDivobCPUhq span{fill:#333;color:#333;}#mermaid-svg-Ip8WLeDivobCPUhq .node rect,#mermaid-svg-Ip8WLeDivobCPUhq .node circle,#mermaid-svg-Ip8WLeDivobCPUhq .node ellipse,#mermaid-svg-Ip8WLeDivobCPUhq .node polygon,#mermaid-svg-Ip8WLeDivobCPUhq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ip8WLeDivobCPUhq .rough-node .label text,#mermaid-svg-Ip8WLeDivobCPUhq .node .label text,#mermaid-svg-Ip8WLeDivobCPUhq .image-shape .label,#mermaid-svg-Ip8WLeDivobCPUhq .icon-shape .label{text-anchor:middle;}#mermaid-svg-Ip8WLeDivobCPUhq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Ip8WLeDivobCPUhq .rough-node .label,#mermaid-svg-Ip8WLeDivobCPUhq .node .label,#mermaid-svg-Ip8WLeDivobCPUhq .image-shape .label,#mermaid-svg-Ip8WLeDivobCPUhq .icon-shape .label{text-align:center;}#mermaid-svg-Ip8WLeDivobCPUhq .node.clickable{cursor:pointer;}#mermaid-svg-Ip8WLeDivobCPUhq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Ip8WLeDivobCPUhq .arrowheadPath{fill:#333333;}#mermaid-svg-Ip8WLeDivobCPUhq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Ip8WLeDivobCPUhq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Ip8WLeDivobCPUhq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ip8WLeDivobCPUhq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Ip8WLeDivobCPUhq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ip8WLeDivobCPUhq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Ip8WLeDivobCPUhq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Ip8WLeDivobCPUhq .cluster text{fill:#333;}#mermaid-svg-Ip8WLeDivobCPUhq .cluster span{color:#333;}#mermaid-svg-Ip8WLeDivobCPUhq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Ip8WLeDivobCPUhq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Ip8WLeDivobCPUhq rect.text{fill:none;stroke-width:0;}#mermaid-svg-Ip8WLeDivobCPUhq .icon-shape,#mermaid-svg-Ip8WLeDivobCPUhq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ip8WLeDivobCPUhq .icon-shape p,#mermaid-svg-Ip8WLeDivobCPUhq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Ip8WLeDivobCPUhq .icon-shape .label rect,#mermaid-svg-Ip8WLeDivobCPUhq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ip8WLeDivobCPUhq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Ip8WLeDivobCPUhq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Ip8WLeDivobCPUhq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} new User('Alice', 25)
Step 1: 创建空对象 this = {}
Step 2: 链接原型 this.proto = User.prototype
Step 3: 执行构造函数体

this.name = 'Alice'

this.age = 25
Step 4: return this
返回实例对象

我们可以手动模拟这个过程:

javascript 复制代码
function User(name, age) {
  this.name = name;
  this.age = age;
}

// 手动模拟 new 的过程
// Step 1:创建空对象
const obj = {};

// Step 2:链接原型
Object.setPrototypeOf(obj, User.prototype);

// Step 3:执行构造函数(将 this 绑定到 obj)
User.call(obj, 'Alice', 25);

// Step 4:返回 obj
console.log(obj);        // { name: 'Alice', age: 25 }
console.log(obj.name);   // "Alice"

11.1.3 构造函数的 return 行为

构造函数中的 return 有特殊的规则:

return 的值 实际返回 说明
无 return / undefined this(新对象) 默认行为
原始类型(number、string 等) this(新对象) 原始类型被忽略
对象(Object、Array 等) 该对象 覆盖了 this
javascript 复制代码
// 情况1:无 return → 返回 this
function Person1(name) {
  this.name = name;
}
console.log(new Person1('Alice')); // Person1 { name: 'Alice' }

// 情况2:return 原始类型 → 仍然返回 this
function Person2(name) {
  this.name = name;
  return 42;  // 被忽略
}
console.log(new Person2('Bob')); // Person2 { name: 'Bob' }

// 情况3:return 对象 → 返回该对象
function Person3(name) {
  this.name = name;
  return { role: 'admin', name: '覆盖' };
}
console.log(new Person3('Charlie')); // { role: 'admin', name: '覆盖' }

最佳实践 :构造函数中不要使用 return 语句。如果你确实需要控制返回值,说明这个场景可能不适合构造函数。

11.1.4 构造函数 vs 工厂函数

构造函数并不是创建对象的唯一方式。另一种常见模式是工厂函数------直接返回对象的普通函数。

javascript 复制代码
// 构造函数方式
function UserConstructor(name, age) {
  this.name = name;
  this.age = age;
}

const alice = new UserConstructor('Alice', 25);
console.log(alice instanceof UserConstructor); // true

// 工厂函数方式
function createUser(name, age) {
  return { name, age };
}

const bob = createUser('Bob', 30);
console.log(bob instanceof createUser); // false(没有原型链关系)
特性 构造函数 工厂函数
调用方式 new User() createUser()
instanceof 检查 ✅ 支持 ❌ 不支持
原型链共享方法 ✅ 通过 prototype ❌ 每个实例独立(除非手动处理)
忘记 new 可能导致 Bug 不存在此问题
适用场景 面向对象、类式编程 函数式编程、简单对象创建

11.1.5 忘记 new 的后果

如果忘记使用 new 调用构造函数,在非严格模式下,this 会指向全局对象:

javascript 复制代码
function User(name) {
  this.name = name;
}

// 忘记 new
const result = User('Alice'); // this 指向全局对象(window)
console.log(result);           // undefined(函数没有返回值)
console.log(window.name);      // "Alice"(污染了全局对象!)

在严格模式下,thisundefined,会立即报错:

javascript 复制代码
'use strict';

function User(name) {
  this.name = name;
}

// User('Alice'); // TypeError: Cannot set property 'name' of undefined

防护技巧 :ES6 的 class 语法天然要求 new,忘记 new 会直接报错。推荐在现代代码中使用 class 代替手写构造函数。

11.1.6 构造函数的方法共享

如果将方法定义在构造函数内部,每个实例都会创建一份独立的函数副本,浪费内存。正确做法是将共享方法放在 prototype 上。

javascript 复制代码
// ❌ 方法在构造函数内部:每个实例都有一份副本
function User(name) {
  this.name = name;
  this.greet = function() {
    console.log(`Hi, ${this.name}`);
  };
}

const a = new User('Alice');
const b = new User('Bob');
console.log(a.greet === b.greet); // false(不同的函数对象)

// ✅ 方法放在 prototype 上:所有实例共享
function Person(name) {
  this.name = name;
}

Person.prototype.greet = function() {
  console.log(`Hi, ${this.name}`);
};

const c = new Person('Alice');
const d = new Person('Bob');
console.log(c.greet === d.greet); // true(同一个函数对象)

#mermaid-svg-Wr1nSIhmqthZBAoc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Wr1nSIhmqthZBAoc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Wr1nSIhmqthZBAoc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Wr1nSIhmqthZBAoc .error-icon{fill:#552222;}#mermaid-svg-Wr1nSIhmqthZBAoc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Wr1nSIhmqthZBAoc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Wr1nSIhmqthZBAoc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Wr1nSIhmqthZBAoc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Wr1nSIhmqthZBAoc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Wr1nSIhmqthZBAoc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Wr1nSIhmqthZBAoc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Wr1nSIhmqthZBAoc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Wr1nSIhmqthZBAoc .marker.cross{stroke:#333333;}#mermaid-svg-Wr1nSIhmqthZBAoc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Wr1nSIhmqthZBAoc p{margin:0;}#mermaid-svg-Wr1nSIhmqthZBAoc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Wr1nSIhmqthZBAoc .cluster-label text{fill:#333;}#mermaid-svg-Wr1nSIhmqthZBAoc .cluster-label span{color:#333;}#mermaid-svg-Wr1nSIhmqthZBAoc .cluster-label span p{background-color:transparent;}#mermaid-svg-Wr1nSIhmqthZBAoc .label text,#mermaid-svg-Wr1nSIhmqthZBAoc span{fill:#333;color:#333;}#mermaid-svg-Wr1nSIhmqthZBAoc .node rect,#mermaid-svg-Wr1nSIhmqthZBAoc .node circle,#mermaid-svg-Wr1nSIhmqthZBAoc .node ellipse,#mermaid-svg-Wr1nSIhmqthZBAoc .node polygon,#mermaid-svg-Wr1nSIhmqthZBAoc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Wr1nSIhmqthZBAoc .rough-node .label text,#mermaid-svg-Wr1nSIhmqthZBAoc .node .label text,#mermaid-svg-Wr1nSIhmqthZBAoc .image-shape .label,#mermaid-svg-Wr1nSIhmqthZBAoc .icon-shape .label{text-anchor:middle;}#mermaid-svg-Wr1nSIhmqthZBAoc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Wr1nSIhmqthZBAoc .rough-node .label,#mermaid-svg-Wr1nSIhmqthZBAoc .node .label,#mermaid-svg-Wr1nSIhmqthZBAoc .image-shape .label,#mermaid-svg-Wr1nSIhmqthZBAoc .icon-shape .label{text-align:center;}#mermaid-svg-Wr1nSIhmqthZBAoc .node.clickable{cursor:pointer;}#mermaid-svg-Wr1nSIhmqthZBAoc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Wr1nSIhmqthZBAoc .arrowheadPath{fill:#333333;}#mermaid-svg-Wr1nSIhmqthZBAoc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Wr1nSIhmqthZBAoc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Wr1nSIhmqthZBAoc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Wr1nSIhmqthZBAoc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Wr1nSIhmqthZBAoc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Wr1nSIhmqthZBAoc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Wr1nSIhmqthZBAoc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Wr1nSIhmqthZBAoc .cluster text{fill:#333;}#mermaid-svg-Wr1nSIhmqthZBAoc .cluster span{color:#333;}#mermaid-svg-Wr1nSIhmqthZBAoc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Wr1nSIhmqthZBAoc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Wr1nSIhmqthZBAoc rect.text{fill:none;stroke-width:0;}#mermaid-svg-Wr1nSIhmqthZBAoc .icon-shape,#mermaid-svg-Wr1nSIhmqthZBAoc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Wr1nSIhmqthZBAoc .icon-shape p,#mermaid-svg-Wr1nSIhmqthZBAoc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Wr1nSIhmqthZBAoc .icon-shape .label rect,#mermaid-svg-Wr1nSIhmqthZBAoc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Wr1nSIhmqthZBAoc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Wr1nSIhmqthZBAoc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Wr1nSIhmqthZBAoc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} proto
proto
Person.prototype
greet()
实例 c
实例 d


11.2 可选链 ?.

11.2.1 问题背景

访问深层嵌套对象的属性时,如果中间的某个值为 nullundefined,会抛出 TypeError

javascript 复制代码
const user = {};

// console.log(user.address.city); // TypeError: Cannot read properties of undefined

传统的防御写法非常冗长:

javascript 复制代码
const city = user && user.address && user.address.city;
console.log(city); // undefined

11.2.2 可选链语法

ES2020 引入了可选链操作符 ?.,当 ?. 左侧的值为 nullundefined 时,表达式会短路 返回 undefined,不再继续访问后续属性。

javascript 复制代码
const user = {};

// 使用可选链:安全访问
console.log(user?.address?.city); // undefined(不报错)

// 等价的完整写法
console.log(user == null ? undefined : user.address == null ? undefined : user.address.city);

#mermaid-svg-VP25Cf9Ss4kxQy3B{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VP25Cf9Ss4kxQy3B .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VP25Cf9Ss4kxQy3B .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VP25Cf9Ss4kxQy3B .error-icon{fill:#552222;}#mermaid-svg-VP25Cf9Ss4kxQy3B .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VP25Cf9Ss4kxQy3B .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VP25Cf9Ss4kxQy3B .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VP25Cf9Ss4kxQy3B .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VP25Cf9Ss4kxQy3B .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VP25Cf9Ss4kxQy3B .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VP25Cf9Ss4kxQy3B .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VP25Cf9Ss4kxQy3B .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VP25Cf9Ss4kxQy3B .marker.cross{stroke:#333333;}#mermaid-svg-VP25Cf9Ss4kxQy3B svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VP25Cf9Ss4kxQy3B p{margin:0;}#mermaid-svg-VP25Cf9Ss4kxQy3B .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VP25Cf9Ss4kxQy3B .cluster-label text{fill:#333;}#mermaid-svg-VP25Cf9Ss4kxQy3B .cluster-label span{color:#333;}#mermaid-svg-VP25Cf9Ss4kxQy3B .cluster-label span p{background-color:transparent;}#mermaid-svg-VP25Cf9Ss4kxQy3B .label text,#mermaid-svg-VP25Cf9Ss4kxQy3B span{fill:#333;color:#333;}#mermaid-svg-VP25Cf9Ss4kxQy3B .node rect,#mermaid-svg-VP25Cf9Ss4kxQy3B .node circle,#mermaid-svg-VP25Cf9Ss4kxQy3B .node ellipse,#mermaid-svg-VP25Cf9Ss4kxQy3B .node polygon,#mermaid-svg-VP25Cf9Ss4kxQy3B .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VP25Cf9Ss4kxQy3B .rough-node .label text,#mermaid-svg-VP25Cf9Ss4kxQy3B .node .label text,#mermaid-svg-VP25Cf9Ss4kxQy3B .image-shape .label,#mermaid-svg-VP25Cf9Ss4kxQy3B .icon-shape .label{text-anchor:middle;}#mermaid-svg-VP25Cf9Ss4kxQy3B .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VP25Cf9Ss4kxQy3B .rough-node .label,#mermaid-svg-VP25Cf9Ss4kxQy3B .node .label,#mermaid-svg-VP25Cf9Ss4kxQy3B .image-shape .label,#mermaid-svg-VP25Cf9Ss4kxQy3B .icon-shape .label{text-align:center;}#mermaid-svg-VP25Cf9Ss4kxQy3B .node.clickable{cursor:pointer;}#mermaid-svg-VP25Cf9Ss4kxQy3B .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VP25Cf9Ss4kxQy3B .arrowheadPath{fill:#333333;}#mermaid-svg-VP25Cf9Ss4kxQy3B .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VP25Cf9Ss4kxQy3B .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VP25Cf9Ss4kxQy3B .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VP25Cf9Ss4kxQy3B .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VP25Cf9Ss4kxQy3B .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VP25Cf9Ss4kxQy3B .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VP25Cf9Ss4kxQy3B .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VP25Cf9Ss4kxQy3B .cluster text{fill:#333;}#mermaid-svg-VP25Cf9Ss4kxQy3B .cluster span{color:#333;}#mermaid-svg-VP25Cf9Ss4kxQy3B div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VP25Cf9Ss4kxQy3B .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VP25Cf9Ss4kxQy3B rect.text{fill:none;stroke-width:0;}#mermaid-svg-VP25Cf9Ss4kxQy3B .icon-shape,#mermaid-svg-VP25Cf9Ss4kxQy3B .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VP25Cf9Ss4kxQy3B .icon-shape p,#mermaid-svg-VP25Cf9Ss4kxQy3B .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VP25Cf9Ss4kxQy3B .icon-shape .label rect,#mermaid-svg-VP25Cf9Ss4kxQy3B .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VP25Cf9Ss4kxQy3B .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VP25Cf9Ss4kxQy3B .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VP25Cf9Ss4kxQy3B :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是



user?.address?.city
user 为 null/undefined?
短路:返回 undefined
user.address 为 null/undefined?
短路:返回 undefined
返回 user.address.city

11.2.3 可选链的三种用法

?. 不仅可以用于属性访问,还可以用于方法调用和数组索引。

javascript 复制代码
const user = {
  name: 'Alice',
  address: { city: '北京' },
  hobbies: ['读书', '游泳']
};

// 1. 可选属性访问:obj?.prop
console.log(user?.name);              // "Alice"
console.log(user?.address?.city);     // "北京"
console.log(user?.email?.domain);     // undefined

// 2. 可选方法调用:obj?.method()
console.log(user?.sayHi?.());         // undefined(sayHi 不存在,不报错)
console.log(user?.greet?.('你好'));    // undefined

// 3. 可选计算属性访问:obj?.[expr]
const key = 'name';
console.log(user?.[key]);             // "Alice"

const missing = null;
console.log(missing?.[key]);          // undefined
语法 含义 等价写法
obj?.prop 安全访问属性 obj == null ? undefined : obj.prop
obj?.[expr] 安全访问计算属性 obj == null ? undefined : obj[expr]
obj?.method() 安全调用方法 obj?.method == null ? undefined : obj.method()

11.2.4 注意事项

1. 不能在赋值左侧使用可选链

javascript 复制代码
// ❌ 错误
// user?.name = 'Alice'; // SyntaxError

2. 不要过度使用 :可选链应该只用在"值可能不存在"的场景。如果某个属性一定存在,不要加 ?.,否则会掩盖 Bug。

javascript 复制代码
// ✅ 合理使用:address 可能不存在
console.log(user?.address?.city);

// ❌ 过度使用:user 一定是对象,不需要 ?.
// console.log(user?.name); // 如果 user 不应该是 null,加 ?. 反而不好

3. 短路求值?. 后面的代码不会执行。

javascript 复制代码
let count = 0;
const user = null;

console.log(user?.address?.[count++]); // undefined
console.log(count); // 0(count++ 未执行)

11.3 Symbol 类型

11.3.1 Symbol 是什么?

Symbol 是 ES6 引入的原始类型,表示一个唯一的、不可变的标识符。每个 Symbol 值都是独一无二的,即使描述相同。

javascript 复制代码
// 创建 Symbol
const sym1 = Symbol('id');
const sym2 = Symbol('id');

console.log(sym1 === sym2); // false(每个 Symbol 都是唯一的)
console.log(typeof sym1);   // "symbol"

类比理解:如果把字符串比作"名字"(同名的人很多),那么 Symbol 就是"身份证号"------即使描述相同,实际标识也绝不重复。

11.3.2 Symbol 作为对象属性键

Symbol 最常见的用途是作为对象的属性键,确保不会与其他属性冲突。

javascript 复制代码
const ID_KEY = Symbol('id');

const user = {
  name: 'Alice',
  [ID_KEY]: 12345  // 用 Symbol 作为属性键
};

console.log(user[ID_KEY]); // 12345
console.log(user.name);    // "Alice"

// Symbol 属性不会被常规遍历方式发现
console.log(Object.keys(user));  // ['name'](不包含 Symbol 属性)

// 使用 Object.getOwnPropertySymbols 获取 Symbol 属性
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id)]

// Reflect.ownKeys 获取所有属性(包括 Symbol)
console.log(Reflect.ownKeys(user)); // ['name', Symbol(id)]

#mermaid-svg-7LiyiAXEIMMGAGGY{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7LiyiAXEIMMGAGGY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7LiyiAXEIMMGAGGY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7LiyiAXEIMMGAGGY .error-icon{fill:#552222;}#mermaid-svg-7LiyiAXEIMMGAGGY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7LiyiAXEIMMGAGGY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7LiyiAXEIMMGAGGY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7LiyiAXEIMMGAGGY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7LiyiAXEIMMGAGGY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7LiyiAXEIMMGAGGY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7LiyiAXEIMMGAGGY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7LiyiAXEIMMGAGGY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7LiyiAXEIMMGAGGY .marker.cross{stroke:#333333;}#mermaid-svg-7LiyiAXEIMMGAGGY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7LiyiAXEIMMGAGGY p{margin:0;}#mermaid-svg-7LiyiAXEIMMGAGGY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7LiyiAXEIMMGAGGY .cluster-label text{fill:#333;}#mermaid-svg-7LiyiAXEIMMGAGGY .cluster-label span{color:#333;}#mermaid-svg-7LiyiAXEIMMGAGGY .cluster-label span p{background-color:transparent;}#mermaid-svg-7LiyiAXEIMMGAGGY .label text,#mermaid-svg-7LiyiAXEIMMGAGGY span{fill:#333;color:#333;}#mermaid-svg-7LiyiAXEIMMGAGGY .node rect,#mermaid-svg-7LiyiAXEIMMGAGGY .node circle,#mermaid-svg-7LiyiAXEIMMGAGGY .node ellipse,#mermaid-svg-7LiyiAXEIMMGAGGY .node polygon,#mermaid-svg-7LiyiAXEIMMGAGGY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7LiyiAXEIMMGAGGY .rough-node .label text,#mermaid-svg-7LiyiAXEIMMGAGGY .node .label text,#mermaid-svg-7LiyiAXEIMMGAGGY .image-shape .label,#mermaid-svg-7LiyiAXEIMMGAGGY .icon-shape .label{text-anchor:middle;}#mermaid-svg-7LiyiAXEIMMGAGGY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7LiyiAXEIMMGAGGY .rough-node .label,#mermaid-svg-7LiyiAXEIMMGAGGY .node .label,#mermaid-svg-7LiyiAXEIMMGAGGY .image-shape .label,#mermaid-svg-7LiyiAXEIMMGAGGY .icon-shape .label{text-align:center;}#mermaid-svg-7LiyiAXEIMMGAGGY .node.clickable{cursor:pointer;}#mermaid-svg-7LiyiAXEIMMGAGGY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7LiyiAXEIMMGAGGY .arrowheadPath{fill:#333333;}#mermaid-svg-7LiyiAXEIMMGAGGY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7LiyiAXEIMMGAGGY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7LiyiAXEIMMGAGGY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7LiyiAXEIMMGAGGY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7LiyiAXEIMMGAGGY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7LiyiAXEIMMGAGGY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7LiyiAXEIMMGAGGY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7LiyiAXEIMMGAGGY .cluster text{fill:#333;}#mermaid-svg-7LiyiAXEIMMGAGGY .cluster span{color:#333;}#mermaid-svg-7LiyiAXEIMMGAGGY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7LiyiAXEIMMGAGGY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7LiyiAXEIMMGAGGY rect.text{fill:none;stroke-width:0;}#mermaid-svg-7LiyiAXEIMMGAGGY .icon-shape,#mermaid-svg-7LiyiAXEIMMGAGGY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7LiyiAXEIMMGAGGY .icon-shape p,#mermaid-svg-7LiyiAXEIMMGAGGY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7LiyiAXEIMMGAGGY .icon-shape .label rect,#mermaid-svg-7LiyiAXEIMMGAGGY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7LiyiAXEIMMGAGGY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7LiyiAXEIMMGAGGY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7LiyiAXEIMMGAGGY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 对象属性键类型
字符串键
Symbol 键
Object.keys 可枚举
for...in 可枚举
Object.keys 不可见
Object.getOwnPropertySymbols 可获取

11.3.3 全局 Symbol 注册表

有时候我们需要在不同模块间共享同一个 Symbol。Symbol.for() 提供了全局注册机制:

javascript 复制代码
// Symbol.for:从全局注册表获取或创建 Symbol
const globalId = Symbol.for('app.id');   // 创建并注册
const globalId2 = Symbol.for('app.id');  // 从注册表获取(同一个 Symbol)

console.log(globalId === globalId2); // true

// Symbol.keyFor:根据全局 Symbol 获取其描述字符串
console.log(Symbol.keyFor(globalId)); // "app.id"

// 注意:Symbol.keyFor 只适用于通过 Symbol.for 创建的全局 Symbol
const localSym = Symbol('local');
console.log(Symbol.keyFor(localSym)); // undefined(非全局 Symbol)
方法 说明 唯一性
Symbol('desc') 创建局部 Symbol 每次都唯一
Symbol.for('key') 从全局注册表获取/创建 同 key 返回同一个
Symbol.keyFor(sym) 获取全局 Symbol 的描述 仅全局 Symbol 有效

11.3.4 内置 Symbol(Well-known Symbols)

JavaScript 引擎预定义了一系列内置 Symbol,用于定制对象的内部行为:

javascript 复制代码
// Symbol.iterator:使对象可迭代
const range = {
  from: 1,
  to: 3,

  [Symbol.iterator]() {
    let current = this.from;
    const last = this.to;

    return {
      next() {
        return current <= last
          ? { value: current++, done: false }
          : { value: undefined, done: true };
      }
    };
  }
};

for (const num of range) {
  console.log(num); // 1, 2, 3
}
javascript 复制代码
// Symbol.toPrimitive:自定义对象到原始值的转换
const money = {
  value: 100,
  currency: 'CNY',

  [Symbol.toPrimitive](hint) {
    if (hint === 'number') return this.value;
    if (hint === 'string') return `${this.currency} ${this.value}`;
    return this.value; // hint === 'default'
  }
};

console.log(+money);             // 100
console.log(`${money}`);         // "CNY 100"
console.log(money + 50);         // 150

常见的 Well-known Symbols:

Symbol 用途
Symbol.iterator 使对象可被 for...of 遍历
Symbol.toPrimitive 自定义对象到原始值的转换
Symbol.toStringTag 自定义 Object.prototype.toString 的标签
Symbol.hasInstance 自定义 instanceof 的行为
Symbol.species 指定派生类的构造函数

11.3.5 Symbol 的特性总结

javascript 复制代码
// 1. Symbol 是原始类型
console.log(typeof Symbol('id')); // "symbol"

// 2. Symbol 不可变
const sym = Symbol('test');
// sym.description = 'changed'; // 无效(严格模式下报错)

// 3. Symbol 不会被 JSON 序列化
const obj = {
  name: 'Alice',
  [Symbol('id')]: 123
};
console.log(JSON.stringify(obj)); // '{"name":"Alice"}'(Symbol 被忽略)

// 4. 可以通过 description 获取描述
const sym = Symbol('my description');
console.log(sym.description); // "my description"

11.4 对象 --- 原始值转换

当 JavaScript 需要将对象用于运算比较字符串拼接 时,会自动将对象转换为原始值(number、string 或 boolean)。这个过程称为对象到原始值的转换(Object-to-Primitive Conversion)。

11.4.1 转换的触发场景

javascript 复制代码
const obj = { value: 42 };

// 1. 数学运算
console.log(obj + 10);  // 触发转换

// 2. 比较运算
console.log(obj > 10);  // 触发转换

// 3. 字符串拼接
console.log('Value: ' + obj);  // 触发转换

// 4. 显式转换
console.log(Number(obj));   // 触发转换
console.log(String(obj));   // 触发转换

11.4.2 转换的三种 hint

JavaScript 引擎在转换时会传递一个 hint 参数,表示期望得到的类型:

hint 触发场景 期望结果
"string" String(obj)alert(obj)、对象作为属性键 字符串
"number" Number(obj)、数学运算(+obj)、比较(> < 数字
"default" obj + valueobj == value 由对象自行决定

注意"default" 是一个特殊 hint。大多数内置对象将 "default" 等同于 "number" 处理。

11.4.3 转换算法的三步查找

当需要将对象转换为原始值时,引擎按以下顺序查找转换方法:

对于 hint = "string"

  1. 调用 obj[Symbol.toPrimitive]('string')(如果存在)
  2. 调用 obj.toString()(如果存在且返回原始值)
  3. 调用 obj.valueOf()(如果存在且返回原始值)

对于 hint = "number" 或 "default"

  1. 调用 obj[Symbol.toPrimitive](hint)(如果存在)
  2. 调用 obj.valueOf()(如果存在且返回原始值)
  3. 调用 obj.toString()(如果存在且返回原始值)

#mermaid-svg-YayzHVy8sDOM0v2l{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YayzHVy8sDOM0v2l .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YayzHVy8sDOM0v2l .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YayzHVy8sDOM0v2l .error-icon{fill:#552222;}#mermaid-svg-YayzHVy8sDOM0v2l .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YayzHVy8sDOM0v2l .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YayzHVy8sDOM0v2l .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YayzHVy8sDOM0v2l .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YayzHVy8sDOM0v2l .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YayzHVy8sDOM0v2l .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YayzHVy8sDOM0v2l .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YayzHVy8sDOM0v2l .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YayzHVy8sDOM0v2l .marker.cross{stroke:#333333;}#mermaid-svg-YayzHVy8sDOM0v2l svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YayzHVy8sDOM0v2l p{margin:0;}#mermaid-svg-YayzHVy8sDOM0v2l .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YayzHVy8sDOM0v2l .cluster-label text{fill:#333;}#mermaid-svg-YayzHVy8sDOM0v2l .cluster-label span{color:#333;}#mermaid-svg-YayzHVy8sDOM0v2l .cluster-label span p{background-color:transparent;}#mermaid-svg-YayzHVy8sDOM0v2l .label text,#mermaid-svg-YayzHVy8sDOM0v2l span{fill:#333;color:#333;}#mermaid-svg-YayzHVy8sDOM0v2l .node rect,#mermaid-svg-YayzHVy8sDOM0v2l .node circle,#mermaid-svg-YayzHVy8sDOM0v2l .node ellipse,#mermaid-svg-YayzHVy8sDOM0v2l .node polygon,#mermaid-svg-YayzHVy8sDOM0v2l .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YayzHVy8sDOM0v2l .rough-node .label text,#mermaid-svg-YayzHVy8sDOM0v2l .node .label text,#mermaid-svg-YayzHVy8sDOM0v2l .image-shape .label,#mermaid-svg-YayzHVy8sDOM0v2l .icon-shape .label{text-anchor:middle;}#mermaid-svg-YayzHVy8sDOM0v2l .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YayzHVy8sDOM0v2l .rough-node .label,#mermaid-svg-YayzHVy8sDOM0v2l .node .label,#mermaid-svg-YayzHVy8sDOM0v2l .image-shape .label,#mermaid-svg-YayzHVy8sDOM0v2l .icon-shape .label{text-align:center;}#mermaid-svg-YayzHVy8sDOM0v2l .node.clickable{cursor:pointer;}#mermaid-svg-YayzHVy8sDOM0v2l .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YayzHVy8sDOM0v2l .arrowheadPath{fill:#333333;}#mermaid-svg-YayzHVy8sDOM0v2l .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YayzHVy8sDOM0v2l .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YayzHVy8sDOM0v2l .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YayzHVy8sDOM0v2l .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YayzHVy8sDOM0v2l .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YayzHVy8sDOM0v2l .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YayzHVy8sDOM0v2l .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YayzHVy8sDOM0v2l .cluster text{fill:#333;}#mermaid-svg-YayzHVy8sDOM0v2l .cluster span{color:#333;}#mermaid-svg-YayzHVy8sDOM0v2l div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YayzHVy8sDOM0v2l .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YayzHVy8sDOM0v2l rect.text{fill:none;stroke-width:0;}#mermaid-svg-YayzHVy8sDOM0v2l .icon-shape,#mermaid-svg-YayzHVy8sDOM0v2l .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YayzHVy8sDOM0v2l .icon-shape p,#mermaid-svg-YayzHVy8sDOM0v2l .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YayzHVy8sDOM0v2l .icon-shape .label rect,#mermaid-svg-YayzHVy8sDOM0v2l .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YayzHVy8sDOM0v2l .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YayzHVy8sDOM0v2l .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YayzHVy8sDOM0v2l :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

'string'
'number'/'default'




对象转原始值
有 Symbol.toPrimitive?
调用它,使用返回值
hint 类型?
先 toString(),再 valueOf()
先 valueOf(),再 toString()
返回原始值?
返回原始值?
使用返回值
调用 valueOf()
使用返回值
调用 toString()

11.4.4 默认的 toString 和 valueOf

如果没有自定义转换方法,对象使用默认的 toStringvalueOf

javascript 复制代码
const obj = { name: 'Alice', age: 25 };

// 默认 toString:返回 "[object Object]"
console.log(String(obj));  // "[object Object]"

// 默认 valueOf:返回对象自身
console.log(obj.valueOf()); // { name: 'Alice', age: 25 }
console.log(obj.valueOf() === obj); // true

当 valueOf 返回对象本身时 (即默认行为),引擎会尝试 toString

javascript 复制代码
const obj = {};

// obj + 0:hint 为 "default"
// 1. valueOf() → 返回 obj 本身(不是原始值,跳过)
// 2. toString() → 返回 "[object Object]"(是原始值,使用它)
console.log(obj + 0); // "[object Object]0"

11.4.5 自定义 toString 和 valueOf

通过定义 toStringvalueOf 方法,可以控制对象的转换行为:

javascript 复制代码
const counter = {
  count: 0,

  // 当需要数字时,返回 count
  valueOf() {
    return this.count;
  },

  // 当需要字符串时,返回描述
  toString() {
    return `Counter: ${this.count}`;
  }
};

// 数学运算 → 调用 valueOf
console.log(counter + 5);  // 5(counter.valueOf() + 5 = 0 + 5)

// 字符串拼接 → 先调用 valueOf(hint 为 "default")
console.log('Count: ' + counter); // "Count: 0"

// 显式字符串转换 → 调用 toString
console.log(String(counter)); // "Counter: 0"

// 显式数字转换 → 调用 valueOf
console.log(Number(counter)); // 0

11.4.6 使用 Symbol.toPrimitive 完全控制转换

Symbol.toPrimitive 是最优先的转换方法,可以一次性处理所有 hint:

javascript 复制代码
const product = {
  name: 'MacBook Pro',
  price: 14999,

  [Symbol.toPrimitive](hint) {
    console.log(`hint: ${hint}`);

    if (hint === 'string') {
      return `{Product: ${this.name}}`;
    }

    if (hint === 'number') {
      return this.price;
    }

    // hint === 'default'
    return this.price;
  }
};

console.log(String(product));    // hint: string → "{Product: MacBook Pro}"
console.log(Number(product));    // hint: number → 14999
console.log(product + 1);        // hint: default → 15000
console.log(`${product}`);       // hint: string → "{Product: MacBook Pro}"

11.4.7 Date 对象的特殊转换

Date 对象是少数对 "default" hint 有特殊处理的内置类型------它的 "default" 等同于 "string"

javascript 复制代码
const now = new Date('2024-06-15T10:30:00');

// hint: "default"(+ 运算符)
console.log(now + '');
// "Sat Jun 15 2024 10:30:00 GMT+0800 (中国标准时间)"(字符串,不是数字)

// hint: "number"
console.log(+now); // 1718447400000(时间戳)

// hint: "string"
console.log(String(now)); // "Sat Jun 15 2024 10:30:00 GMT+0800 (中国标准时间)"

11.4.8 转换规则速查表

操作 hint 转换顺序
String(obj) "string" Symbol.toPrimitivetoStringvalueOf
alert(obj) "string" 同上
Number(obj) "number" Symbol.toPrimitivevalueOftoString
+obj "number" 同上
obj + value "default" Symbol.toPrimitivevalueOftoString
obj == value "default" 同上

11.5 实战

11.5.1 实战1:手写 new 操作符

这是面试中的高频题目。我们需要精确模拟 new 操作符的四步过程。

javascript 复制代码
/**
 * 手写 new 操作符
 * @param {Function} Constructor - 构造函数
 * @param {...*} args - 传给构造函数的参数
 * @returns {Object} 新创建的实例对象
 */
function myNew(Constructor, ...args) {
  // 1. 创建一个空对象,原型指向 Constructor.prototype
  const obj = Object.create(Constructor.prototype);

  // 2. 以 obj 为 this 执行构造函数
  const result = Constructor.apply(obj, args);

  // 3. 如果构造函数返回了对象,则使用返回的对象;否则使用 obj
  return (result !== null && typeof result === 'object') ? result : obj;
}

// ===== 测试 =====

function User(name, age) {
  this.name = name;
  this.age = age;
}

User.prototype.greet = function() {
  return `Hi, I'm ${this.name}, ${this.age} years old.`;
};

// 使用 myNew 创建实例
const alice = myNew(User, 'Alice', 25);
console.log(alice.name);           // "Alice"
console.log(alice.age);            // 25
console.log(alice.greet());        // "Hi, I'm Alice, 25 years old."
console.log(alice instanceof User); // true

// 与原生 new 对比
const bob = new User('Bob', 30);
console.log(bob.greet());          // "Hi, I'm Bob, 30 years old."
console.log(bob instanceof User);  // true

测试构造函数返回对象的情况

javascript 复制代码
function Special(name) {
  this.name = name;
  return { role: 'admin', name: '覆盖' };
}

const s1 = myNew(Special, 'test');
const s2 = new Special('test');

console.log(s1); // { role: 'admin', name: '覆盖' }
console.log(s2); // { role: 'admin', name: '覆盖' }
console.log(s1.name === s2.name); // true

11.5.2 实战2:手写 instanceof

instanceof 通过原型链检查对象是否由指定构造函数创建:

javascript 复制代码
/**
 * 手写 instanceof
 * @param {Object} obj - 要检查的对象
 * @param {Function} Constructor - 构造函数
 * @returns {boolean}
 */
function myInstanceof(obj, Constructor) {
  // 原始类型不是任何构造函数的实例
  if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) {
    return false;
  }

  // 沿原型链向上查找
  let proto = Object.getPrototypeOf(obj);
  const target = Constructor.prototype;

  while (proto !== null) {
    if (proto === target) {
      return true;
    }
    proto = Object.getPrototypeOf(proto);
  }

  return false;
}

// 测试
console.log(myInstanceof([], Array));        // true
console.log(myInstanceof([], Object));       // true
console.log(myInstanceof('hello', String));  // false(原始类型)
console.log(myInstanceof(null, Object));     // false

11.5.3 实战3:this 指向与可选链综合题

分析以下代码的输出:

javascript 复制代码
const company = {
  name: 'TechCorp',
  departments: {
    engineering: {
      lead: {
        name: 'Alice',
        getName() {
          return this.name;
        }
      }
    }
  }
};

// 题目
console.log(company.departments?.engineering?.lead?.getName());      // ?
console.log(company.departments?.marketing?.lead?.getName());        // ?

const fn = company.departments.engineering.lead.getName;
console.log(fn?.());                                                 // ?

console.log(company.departments?.['engineering']?.lead?.name);       // ?

解析

javascript 复制代码
// 1. 通过 company 链式调用,this 指向 lead
// 输出:"Alice"

// 2. marketing 不存在,可选链短路,返回 undefined
// 输出:undefined

// 3. fn 是提取出来的函数,独立调用(默认绑定),this 指向全局/undefined
// 输出:undefined(非严格模式下可能是全局的 name)

// 4. 可选链支持计算属性访问
// 输出:"Alice"

11.5.4 实战4:Symbol + 对象转换综合应用

实现一个 Temperature 类,支持自定义转换和唯一标识:

javascript 复制代码
const UNIT_SYMBOL = Symbol('unit');

class Temperature {
  constructor(celsius, unit = 'C') {
    this.celsius = celsius;
    this[UNIT_SYMBOL] = unit;
  }

  // 自定义字符串转换
  toString() {
    return `${this.celsius}°${this[UNIT_SYMBOL]}`;
  }

  // 自定义数字转换
  valueOf() {
    return this.celsius;
  }

  // 完全控制原始值转换
  [Symbol.toPrimitive](hint) {
    if (hint === 'string') return this.toString();
    if (hint === 'number') return this.celsius;
    // default:用于 + 运算符
    return this.celsius;
  }

  // 自定义迭代器(Symbol.iterator)
  [Symbol.iterator]() {
    const scales = ['celsius', 'fahrenheit', 'kelvin'];
    let index = 0;
    const self = this;

    return {
      next() {
        if (index >= scales.length) {
          return { done: true };
        }

        const scale = scales[index++];
        let value;

        switch (scale) {
          case 'celsius':
            value = self.celsius;
            break;
          case 'fahrenheit':
            value = self.celsius * 9 / 5 + 32;
            break;
          case 'kelvin':
            value = self.celsius + 273.15;
            break;
        }

        return { value: { scale, value }, done: false };
      }
    };
  }
}

// ===== 测试 =====

const temp = new Temperature(100);

// 字符串转换
console.log(`水的沸点:${temp}`);       // "水的沸点:100°C"
console.log(String(temp));              // "100°C"

// 数字转换
console.log(+temp);                     // 100
console.log(Number(temp));              // 100

// 数学运算
console.log(temp + 50);                 // 150

// 遍历温度转换
for (const { scale, value } of temp) {
  console.log(`${scale}: ${value}`);
}
// celsius: 100
// fahrenheit: 212
// kelvin: 373.15

本章小结

知识点 要点
构造函数 普通函数 + new 调用,首字母大写是约定
new 四步 创建空对象 → 链接原型 → 执行函数体 → 返回 this
构造函数 return 返回对象时覆盖 this,返回原始类型时忽略
可选链 ?. 安全访问嵌套属性,null/undefined 时短路返回 undefined
Symbol 唯一标识符,作为属性键避免冲突
Symbol.for 全局 Symbol 注册表,跨模块共享
Well-known Symbols 内置 Symbol 用于定制对象行为(iterator、toPrimitive 等)
对象转原始值 hint 为 "string" / "number" / "default" 三种
Symbol.toPrimitive 最高优先级的转换方法,覆盖 toString/valueOf
toString vs valueOf string hint 先 toString,number/default hint 先 valueOf

一句话总结new 操作符通过"创建对象 → 链接原型 → 绑定 this 执行 → 返回实例"四步构建可复用的对象体系;可选链 ?. 让深层属性访问更安全;Symbol 提供了全局唯一的标识能力;理解对象到原始值的转换规则,是掌控隐式类型转换的基础。


下篇预告第12章:属性配置与 getter/setter --- 我们将深入 JavaScript 的属性系统,学习属性描述符、Object.defineProperty,以及 getter / setter 的定义与应用。


作者 :xhmico | CSDN 博客https://blog.csdn.net/xhmico

参考资源The Modern JavaScript Tutorial | MDN Web Docs | ECMAScript 规范

相关推荐
多加点辣也没关系19 小时前
JavaScript|第24章:事件循环与并发模型
开发语言·javascript·ecmascript
ん贤19 小时前
怎么设计,才算一个优秀审计模块
大数据·开发语言·设计·审计
l1t19 小时前
测试用rust重写的postgresql: pgrust
开发语言·postgresql·rust
2501_9481069119 小时前
计算机毕业设计之jsp-智慧旅游分享平台
java·开发语言·spark·汽车·课程设计·旅游
阿里云云原生20 小时前
Agent 不再是“玩具”!AgentScope Java 2.0 GA 发布:构建企业级分布式智能体底座
java·开发语言·分布式·agentscope
BIM云平台开发20 小时前
【App.vue里跟踪页面跳转和用户ID】
开发语言·前端·javascript
NiceCloud喜云20 小时前
ClaudeAPI 接入 n8n / Dify / Open WebUI 实战:Base URL 配置、调用示例与排错
开发语言·ai·chatgpt·aigc
庵中十三居士20 小时前
【纯AI无人工修改】AI Agent从0到1实战:50行Python手写核心循环,一次看懂所有Agent框架的底层逻辑
开发语言·人工智能·python
甄同学20 小时前
第二十篇:MCP协议集成,Claude Code如何扩展AI Agent的能力边界
开发语言·人工智能·qt
有同事要进步20 小时前
python常见问题--2
开发语言·python