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 调用构造函数时,引擎在背后依次执行以下四步:
- 创建一个空对象
{},并将this指向该对象 - 将该对象的原型链接 到构造函数的
prototype属性(即this.__proto__ = User.prototype) - 执行构造函数体,为新对象添加属性
- 返回 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"(污染了全局对象!)
在严格模式下,this 为 undefined,会立即报错:
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 问题背景
访问深层嵌套对象的属性时,如果中间的某个值为 null 或 undefined,会抛出 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 引入了可选链操作符 ?.,当 ?. 左侧的值为 null 或 undefined 时,表达式会短路 返回 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 + value、obj == value |
由对象自行决定 |
注意 :
"default"是一个特殊 hint。大多数内置对象将"default"等同于"number"处理。
11.4.3 转换算法的三步查找
当需要将对象转换为原始值时,引擎按以下顺序查找转换方法:
对于 hint = "string":
- 调用
obj[Symbol.toPrimitive]('string')(如果存在) - 调用
obj.toString()(如果存在且返回原始值) - 调用
obj.valueOf()(如果存在且返回原始值)
对于 hint = "number" 或 "default":
- 调用
obj[Symbol.toPrimitive](hint)(如果存在) - 调用
obj.valueOf()(如果存在且返回原始值) - 调用
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
如果没有自定义转换方法,对象使用默认的 toString 和 valueOf:
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
通过定义 toString 和 valueOf 方法,可以控制对象的转换行为:
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.toPrimitive → toString → valueOf |
alert(obj) |
"string" |
同上 |
Number(obj) |
"number" |
Symbol.toPrimitive → valueOf → toString |
+obj |
"number" |
同上 |
obj + value |
"default" |
Symbol.toPrimitive → valueOf → toString |
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 规范