参考规范:ECMAScript 2023 (ES14) · MDN Web Docs
官方文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects
ECMAScript 规范:https://tc39.es/ecma262/


前言:理解 JavaScript 的设计哲学

JavaScript 于 1995 年诞生,其设计深受 Self 语言(基于原型的对象系统)和 Scheme 语言(函数式特性)的影响。与 Java、C++ 等采用"类(Class)"的面向对象语言不同,JavaScript 最初选择了一条更为动态、灵活的路径——基于原型(Prototype-based)的对象模型

这一设计决定了 JavaScript 的诸多核心特性:

  • 对象不需要从类实例化,可以直接从其他对象"克隆"
  • 属性查找通过原型链动态进行,而非编译期确定
  • 函数本身也是对象,可以作为构造函数使用

理解这些底层设计理念,是真正掌握 JavaScript 的关键。

1995 Brendan Eich 设计 JS,引入原型继承 1997 ECMAScript 1 规范发布,标准化原型链 1999 ES3 发布,完善正则、异常处理 2009 ES5 发布,新增 Object.create、严格模式 2015 ES6 (ES2015) 发布,引入 class 语法糖、Symbol、Map、Set 2017 ES2017,新增 Object.values/entries、padStart/padEnd 2019 ES2019,新增 flat/flatMap、Object.fromEntries 2020 ES2020,新增 BigInt、Optional Chaining、Nullish Coalescing 2023 ES2023,新增 Array.toSorted/toReversed(非破坏性方法) JavaScript 核心对象模型发展史

目录

第一章 · 原型机制

第二章 · 内存与类型

第三章 · 内置对象

第四章 · 数组

第五章 · 进阶专题

第六章 · 速查与参考


1 原型与原型链

1.1 理论基础:为什么需要原型?

在传统面向对象语言(如 Java)中,对象的行为由**类(Class)**定义,所有实例共享同一份方法描述,方法存储在类结构中。JavaScript 采用了不同的策略:

原型(Prototype)的本质是对象之间的委托关系(Delegation)。

当我们访问一个对象的属性时,JavaScript 引擎不仅仅查找对象本身,还会沿着原型链向上委托查找,直到找到该属性或到达链的顶端(null)为止。这种机制实现了属性和方法的共享与复用,是 JavaScript 内存效率的重要保障。

ECMAScript 规范说明:每个对象都有一个内部槽 [[Prototype]],其值要么是 null,要么是另一个对象。这是原型链的规范表达。__proto__ 是访问 [[Prototype]] 的历史遗留方式,ES6 规范通过 Object.getPrototypeOf()Object.setPrototypeOf() 提供了标准 API。

深层理论:委托模型 vs 类继承——两种截然不同的对象哲学

理解原型链,需要先从根本上区分两种面向对象范式:

维度 类继承(Class-based) 委托模型(Prototype-based)
代表语言 Java、C++、C# JavaScript、Self、Lua
对象来源 必须从类实例化,类是对象的"蓝图" 对象直接从其他对象"委托",无需蓝图
方法共享 编译期绑定,存储在类的方法表中 运行期委托查找,存储在原型对象上
扩展方式 通过类继承扩展(extends 通过修改原型链扩展(动态、灵活)
内存结构 每个实例持有所有继承链上的数据副本 实例只持有自身属性,方法通过链共享
多态实现 虚函数表(vtable)调度 属性遮蔽(Property Shadowing)

Self 语言的影响:JavaScript 的原型系统直接受到 Self 语言(1986 年,施乐 PARC 研究中心)的启发。Self 的核心哲学是:“原型比类更简单,因为只有一个概念——对象,而不是类和对象两个概念。” Brendan Eich 在 1995 年用 10 天设计 JavaScript 时,借鉴了这一思想。

对象组合优于类继承(Composition over Inheritance):这是软件设计的经典原则。原型式继承天然支持"组合"——一个对象可以从任意对象获取能力,而不必被迫接受整个类层次结构。这使 JavaScript 的代码复用方式比 Java 的深层继承树更灵活。

// 类继承的问题:层次固化,难以拆解
// 假设需求:会飞的汽车。在类继承中,Car 和 Aircraft 都是类,多继承导致"菱形问题"
// JS 的对象组合解决方案:
var canFly  = { fly:  function() { return this.name + ' is flying';  } };
var canDrive= { drive:function() { return this.name + ' is driving'; } };

// 任意组合能力,无需继承层次结构
var flyingCar = Object.assign(
    Object.create(null),  // 纯净对象,无原型污染
    canFly,
    canDrive,
    { name: 'FutureCar' }
);
console.log(flyingCar.fly());   // 'FutureCar is flying'
console.log(flyingCar.drive()); // 'FutureCar is driving'

💡 代码解析

代码片段 含义
var canFly = { fly: fn } 将"飞行"能力封装为独立对象(Mixin),与任何具体类型解耦,可以被任意对象复用
Object.create(null) 创建无原型的纯净对象作为基础,避免从 Object.prototype 继承 toString 等方法产生干扰
Object.assign(..., canFly, canDrive, {...}) 将多个能力对象的属性混入目标对象,实现"组合"而非"继承",解决多继承的菱形问题
flyingCar.fly() 能调用 fly 方法已通过 Object.assign 直接复制到 flyingCar 上,不需要原型链查找
深层理论:ES6 class 语法糖的本质

ES6 引入了 class 关键字,让 JavaScript 看起来像类继承语言,但它只是原型链的语法糖,底层机制完全没有变化:

// ES6 class 写法
class Animal {
    constructor(name) {
        this.name = name;
    }
    speak() {
        return this.name + ' makes a sound';
    }
    static create(name) { return new Animal(name); }
}

class Dog extends Animal {
    speak() {
        return this.name + ' barks';
    }
}

var d = new Dog('Rex');
console.log(d.speak()); // 'Rex barks'

// ====== 以下是 class 的等价原型写法 ======
function Animal2(name) { this.name = name; }
Animal2.prototype.speak = function() { return this.name + ' makes a sound'; };
Animal2.create = function(name) { return new Animal2(name); };

function Dog2(name) { Animal2.call(this, name); }          // 继承实例属性
Dog2.prototype = Object.create(Animal2.prototype);          // 继承原型方法
Dog2.prototype.constructor = Dog2;                          // 修复 constructor 指向
Dog2.prototype.speak = function() { return this.name + ' barks'; }; // 覆盖方法

// 验证:class 和原型写法的原型链结构完全相同
console.log(Object.getPrototypeOf(Dog.prototype) === Animal.prototype);   // true
console.log(Object.getPrototypeOf(Dog2.prototype) === Animal2.prototype); // true

💡 代码解析

代码片段 含义
class Dog extends Animal 建立继承关系:① Dog.prototype[[Prototype]] 指向 Animal.prototype;② Dog.__proto__ 指向 Animal(静态方法继承)
Dog2.prototype = Object.create(Animal2.prototype) 等价于 extends 的原型链部分:手动创建一个 [[Prototype]] 指向 Animal2.prototype 的新对象
Animal2.call(this, name) 等价于 super(name):在子类构造函数中调用父类构造函数,确保父类的实例属性(如 this.name)被正确初始化
Dog2.prototype.constructor = Dog2 修复因替换 prototype 导致 constructor 属性丢失的问题;若不修复,new Dog2() instanceof Dog2 的内部判断逻辑仍正确,但 obj.constructor 会指向错误的函数
Dog2.prototype.speak 覆盖父类方法 属性遮蔽(Property Shadowing):子类原型上的同名属性遮蔽了父类原型上的属性,实现多态;super.speak() 可访问被遮蔽的父类方法

规范层面的差异class 与纯原型写法并非完全等价。class 内部方法是不可枚举的(for...in 不会遍历到),而直接赋值给 prototype 的方法是可枚举的。此外,class 的方法自动启用严格模式,且必须通过 new 调用,否则报错。

1.2 名词解释

术语 定义 规范表达
原型(Prototype) 每个对象内部都有一个隐式引用指向另一个对象,这个被引用的对象就是原型。原型为对象提供共享的属性和方法。 [[Prototype]] 内部槽
__proto__ 对象访问其原型的非标准属性(浏览器几乎全部支持),ES6 后官方推荐使用 Object.getPrototypeOf() Object.prototype.__proto__ 的访问器属性
prototype 函数/构造函数才有的属性,指向该构造函数所创建实例的原型对象 仅函数对象拥有
原型链(Prototype Chain) 对象沿着 [[Prototype]] 一层一层向上查找属性的链式结构,顶端是 Object.prototype,再往上是 null 属性查找算法的核心
构造函数(Constructor) 用于创建对象实例的函数,通常首字母大写,通过 new 调用 函数的 [[Construct]] 内部方法
实例(Instance) 通过 new 构造函数创建出来的对象 new F() 调用 F.[[Construct]]()
hasOwnProperty() 判断属性是否为对象自身拥有(不含原型链上的属性),返回布尔值 检查对象自身的 [[OwnProperty]]
Object.create() 以指定对象为原型创建新对象,可精确控制原型链 直接设置新对象的 [[Prototype]]
instanceof 检查构造函数的 prototype 是否存在于对象的原型链上 遍历 [[Prototype]] 链判断

1.3 new 操作符的底层执行过程

理解 new 做了什么,是理解原型链的关键。当执行 new F() 时,引擎按以下步骤执行:

① 创建一个全新的空对象 obj = {}
② 将 obj 的 [[Prototype]] 设置为 F.prototype
③ 以 obj 作为 this,执行构造函数 F 的函数体
④ 如果 F 没有显式返回对象,则返回 obj
   如果 F 显式 return 了一个对象,则返回那个对象
// 手动模拟 new 的过程(帮助理解原理)
function myNew(Constructor, ...args) {
    // 步骤①②:创建对象并设置原型
    var obj = Object.create(Constructor.prototype);
    // 步骤③:执行构造函数
    var result = Constructor.apply(obj, args);
    // 步骤④:判断返回值
    return (result !== null && typeof result === 'object') ? result : obj;
}

function Point(x, y) {
    this.x = x;
    this.y = y;
}
Point.prototype.toString = function() {
    return '(' + this.x + ', ' + this.y + ')';
};

var p1 = myNew(Point, 3, 4);
var p2 = new Point(3, 4);
console.log(p1.toString()); // (3, 4)
console.log(p2.toString()); // (3, 4)

💡 代码解析

行为 说明
Object.create(Constructor.prototype) 步骤①②合并:创建空对象,同时将其 [[Prototype]] 指向构造函数的 prototype,建立原型链
Constructor.apply(obj, args) 步骤③:以新对象作为 this 执行构造函数体,将属性挂载到新对象上
typeof result === 'object' 步骤④:若构造函数显式返回一个对象,则以该对象为结果;否则返回我们创建的 obj
p1.toString() 能调用 p1[[Prototype]] 指向 Point.prototype,查找 toString 时从原型上找到

🏢 经典使用场景 & 业务价值

理解 new 的底层原理在以下业务场景中至关重要:

场景 应用
框架开发 React、Vue 等框架内部大量使用 new 创建组件实例,理解其过程有助于调试复杂问题
插件系统 开发可配置的插件架构时,需要控制实例化行为,有时需要劫持/代理 new 操作
单元测试 Mock 在测试中需要 mock 某个构造函数,理解 new 过程可以精准拦截
工厂模式 封装 myNew 类似的工厂函数,根据参数动态决定创建哪种类型的对象

1.4 三角关系图解

对象实例 (instance)
    │
    │  [[Prototype]] / __proto__
    ▼
构造函数.prototype  ◄─────── 构造函数 (Constructor)
    │                              │
    │  [[Prototype]]           .prototype
    ▼                              │
Object.prototype  ◄───────────────┘ (大多数情况)
    │
    │  [[Prototype]]
    ▼
   null  ← 原型链的终点

1.5 原型链结构(Mermaid)

__proto__

__proto__

__proto__

prototype

__proto__

__proto__

prototype

__proto__

实例对象 f
new F()

F.prototype
(F的原型对象)

Object.prototype
(顶层原型)

null ← 链的终点

构造函数 F

Function.prototype

Object 构造函数

1.6 核心代码演示

// ① 自定义构造函数 —— 将方法添加到原型上(节省内存)
// 若将方法写在构造函数体内,每次 new 都会创建新函数对象,浪费内存
// 写在 prototype 上,所有实例共享同一个函数对象
function Animal(name, sound) {
    this.name  = name;   // 每个实例独有(实例属性)
    this.sound = sound;
}
Animal.prototype.speak = function() {  // 所有实例共享(原型属性)
    return this.name + ' 说:' + this.sound;
};

var dog = new Animal('Dog', 'Woof');
var cat = new Animal('Cat', 'Meow');

console.log(dog.speak());   // Dog 说:Woof
console.log(cat.speak());   // Cat 说:Meow

// 验证:所有实例的 speak 方法是同一个函数对象
console.log(dog.speak === cat.speak); // true(共享!)

// ② 访问原型
console.log(dog.__proto__ === Animal.prototype);      // true
console.log(Animal.prototype.constructor === Animal); // true

// ③ hasOwnProperty —— 区分自身属性与原型属性
console.log(dog.hasOwnProperty('name'));   // true(自身属性)
console.log(dog.hasOwnProperty('speak'));  // false(原型属性)

// for...in 遍历包含原型链属性,配合 hasOwnProperty 过滤
for (var key in dog) {
    if (dog.hasOwnProperty(key)) {
        console.log('自身属性:', key); // name, sound
    }
}

// ④ Object.create() —— 指定原型创建对象(寄生式原型链)
var baseProto = {
    greet: function() { return 'Hello, I am ' + this.name; }
};
var person = Object.create(baseProto);
person.name = 'Alice';
console.log(person.greet());  // Hello, I am Alice
console.log(Object.getPrototypeOf(person) === baseProto); // true

// ⑤ 创建无原型的纯净对象(原型链为 null)
var pure = Object.create(null);
// pure 没有 toString、hasOwnProperty 等继承方法,常用于纯数据存储
console.log(pure.__proto__); // undefined(没有原型)

💡 代码解析

代码片段 含义
Animal.prototype.speak = function(){...} 方法挂在原型上,dog/cat 所有实例共享同一个函数对象,而非每个实例独自持有一份,大幅节省内存
dog.speak === cat.speak → true 直接证明了原型方法共享:两个不同对象的 speak 是同一个引用
dog.hasOwnProperty('name') → true name 是构造函数体内用 this.name= 赋值的,属于实例自身属性
dog.hasOwnProperty('speak') → false speak 在原型上,不属于实例自身,for...in 会遍历到它但 hasOwnProperty 返回 false
Object.create(null) 创建无原型链的纯净对象,没有 toStringvalueOf 等继承方法,常用于创建安全的字典/哈希表,避免原型污染攻击

🏢 经典使用场景 & 业务价值

场景 技术手段 业务收益
构建组件库 将公共方法(如 renderupdate)放在 prototype 100个组件实例只存一份方法,内存占用减少 90%+
权限过滤 for...in + hasOwnProperty 遍历只读取自身属性 避免枚举到原型链上的方法,输出干净的数据
防原型污染 配置解析时用 Object.create(null) 存储键值对 避免恶意输入通过 __proto__ 污染原型,提升安全性
继承链设计 Object.create(BaseClass.prototype) 实现原型继承 无需调用父类构造函数即可建立原型链,灵活设计继承体系

1.7 Function 与 Object 的特殊关系

这是 JavaScript 原型链中最令人困惑也最精妙的部分:

// Function 是所有函数(包括自身)的构造函数
// 这是 JS 引擎的一个自举(bootstrapping)特殊处理
console.log(Function.__proto__ === Function.prototype);  // true(自己是自己的实例)

// Object 的原型链经过 Function.prototype
console.log(Object.__proto__ === Function.prototype);     // true

// Function.prototype 的原型是 Object.prototype
console.log(Function.prototype.__proto__ === Object.prototype); // true

// 验证数组实例原型链
var arr = [];
console.log(arr.__proto__ === Array.prototype);           // true
console.log(arr.__proto__.__proto__ === Object.prototype); // true
// Function.prototype 不在 arr 的原型链上!
console.log(arr instanceof Function);                      // false
// 但 Function.prototype 在 Array(构造函数)的原型链上
console.log(Array instanceof Function);                    // true

// instanceof 的真正判断逻辑
// arr instanceof Array:检查 Array.prototype 是否在 arr 的原型链上
// 等同于:Object.getPrototypeOf(arr) === Array.prototype

💡 代码解析

代码片段 含义
Function.__proto__ === Function.prototypetrue 自举循环:Function 本身也是函数,是通过自身构造的,JS 引擎启动时特殊初始化此循环引用
Object.__proto__ === Function.prototypetrue Object 作为一个构造函数(即函数对象),其 [[Prototype]] 指向 Function.prototype,说明 Object instanceof Functiontrue
Function.prototype.__proto__ === Object.prototypetrue Function.prototype 虽然是函数原型,但它本质上也是一个对象,其 [[Prototype]] 指向 Object.prototype,链到普通对象的顶端
arr instanceof Functionfalse 数组实例 arr 的原型链:arr → Array.prototype → Object.prototype → null,不经过 Function.prototype
Array instanceof Functiontrue Array 是构造函数(函数对象),其原型链:Array → Function.prototype → Object.prototype → null,经过 Function.prototype

鸡与蛋的哲学问题:
Object 是函数,所以 Object.__proto__ === Function.prototype
Function.prototype 是对象,所以 Function.prototype.__proto__ === Object.prototype
这两者互为依存,是 JS 引擎在初始化时通过"特殊引导"建立的循环引用,无法用纯粹的 JS 代码描述。

1.8 原型链属性查找流程(Mermaid)

否,已到达 null

访问 obj.prop

obj 自身
[[OwnProperty]] 有 prop?

✅ 返回 obj.prop

obj.[[Prototype]]
不为 null?

❌ 返回 undefined

obj.[[Prototype]]
有 prop?

✅ 返回该原型上的 prop

继续向上:obj = obj.[[Prototype]]

1.9 原型链的性能注意事项

// 访问越深的原型链属性,性能越低
// 引擎需要遍历更多层次

// 好的做法:经常访问的属性,缓存到局部变量
var speak = dog.speak; // 缓存
speak.call(dog);       // 使用缓存,避免重复查找

// 不推荐:频繁访问深层原型链属性(在高频循环中)
for (var i = 0; i < 1000000; i++) {
    dog.speak(); // 每次都要查找原型链
}

💡 代码解析

代码片段 含义
var speak = dog.speak 将原型链查找结果缓存到局部变量,后续直接通过局部变量调用,跳过原型链查找过程
speak.call(dog) 使用 call 明确指定 thisdog,确保方法内部 this.name 取到正确值
高频循环中的 dog.speak() 每次调用都触发原型链查找(尽管 V8 有内联缓存优化,但动态属性结构仍会致缓存失效)

V8 引擎优化:现代 JS 引擎(如 V8)使用**隐藏类(Hidden Class)内联缓存(Inline Cache)**来优化原型链查找。当对象的形状(属性结构)固定时,V8 会缓存属性查找结果,大幅提升性能。因此,保持对象结构稳定(不要动态增减属性)是 V8 性能优化的重要原则。


📌 原型与原型链 — 知识特点总结

特点 描述
委托模型 属性查找是一种委托行为,由对象逐级"委托"给其原型处理,而非"继承"数据拷贝
动态性 原型对象的修改会立即反映到所有以其为原型的对象上(实时生效)
单一原型链 每个对象只有一条原型链(单继承),与多继承语言不同
顶端终止 所有普通对象的原型链最终指向 Object.prototype,再往上是 null
内存效率 方法放在原型上,所有实例共享同一函数对象,比将方法放在构造函数内节省大量内存
自举悖论 Function.__proto__ === Function.prototype 是引擎初始化时的特殊处理,表明 JS 中所有函数(包括 Function 本身)都是 Function 的实例
instanceof 本质 a instanceof B 实质是检查 B.prototype 是否在 a 的原型链上,与构造函数无关
class 是语法糖 ES6 的 class 关键字并未改变 JS 的原型本质,只是提供了更清晰的语法表达

2 值类型与引用类型

2.1 理论基础:内存模型与类型系统

JavaScript 的类型系统将所有数据分为两大阵营,这一区分源于计算机底层的内存管理机制。

栈内存(Stack Memory)

  • 由编译器/运行时自动分配和释放
  • 内存空间连续,访问速度极快
  • 大小固定,存储的数据类型大小必须在编译期已知
  • 函数调用帧(Call Frame)存储在栈上

堆内存(Heap Memory)

  • 由垃圾回收器(GC)负责管理生命周期
  • 内存空间不连续,需要通过指针/引用访问
  • 大小动态,可以存储任意大小的复杂数据结构
  • JavaScript 引擎的垃圾回收主要针对堆内存

ECMAScript 规范:规范中将数据类型分为 Primitive Value(原始值)Object(对象) 两大类。规范并未强制要求引擎使用特定的内存结构,但 V8 等主流引擎均采用栈+堆的经典模型。

深层理论:V8 引擎的垃圾回收机制(GC)

JavaScript 开发者无需手动管理内存,但理解 GC 机制有助于写出内存友好的代码,避免内存泄漏。

V8 的分代假说(Generational Hypothesis):大多数对象"年轻即死亡"——绝大多数对象在创建后很快变得不可达。基于这一假说,V8 将堆内存分为两代:

V8 堆内存布局
┌─────────────────────────────────────────┐
│              新生代(Young Generation)  │
│  ┌──────────────┬──────────────────────┐ │
│  │  From Space  │     To Space         │ │
│  │(当前激活区) │  (复制目标区,初始空)│ │
│  └──────────────┴──────────────────────┘ │
│  大小:约 1~8 MB,GC 频率高(Minor GC)  │
├─────────────────────────────────────────┤
│              老年代(Old Generation)    │
│  ┌────────────────────────────────────┐  │
│  │  存活经过 2 次 Minor GC 的对象      │  │
│  │  大小:数百 MB ~ GB,GC 频率低      │  │
│  └────────────────────────────────────┘  │
└─────────────────────────────────────────┘

① 新生代 GC:Scavenger(清道夫)算法

新生代使用 Cheney’s Algorithm(切尼复制算法),又称 Semi-Space 收集器:

Step 1: 对象最初分配在 From Space
Step 2: 触发 Minor GC 时,扫描所有根(全局变量、调用栈等)
Step 3: 存活对象复制到 To Space(按顺序紧密排列,消除碎片)
Step 4: 清空 From Space(直接整块释放,极快)
Step 5: From Space ↔ To Space 互换角色

优点:分配速度极快(只需移动指针),GC 暂停时间短(< 1ms)
缺点:只有一半空间可用,适合短命对象(新生代本来就小,可接受)

② 老年代 GC:Mark-Sweep + Mark-Compact

对象在 From Space 中存活超过 2 次 Minor GC 后晋升到老年代。老年代对象多且生命周期长,使用三色标记法:

三色标记(Tri-color Marking):
  ⚪ 白色:未访问(GC 结束后仍为白色 = 垃圾)
  🔘 灰色:已发现但其引用的对象未完全扫描
  ⚫ 黑色:已完全扫描,本身及其引用均已处理

标记阶段(Mark):
  从 GC Roots 出发,BFS 遍历所有可达对象,标为黑色
  
清除阶段(Sweep):
  扫描整个堆,释放所有仍为白色的对象
  问题:产生内存碎片
  
整理阶段(Compact,选择性执行):
  将存活对象移动到内存一端,消除碎片
  代价:移动对象耗时,需更新所有引用(指针修复)

③ 增量标记(Incremental Marking):V8 不会一次性完成所有标记(这会导致长时间暂停),而是将标记工作分成小片段,穿插在 JS 代码执行之间(三色标记保证了这种"暂停后可安全恢复"的特性),大幅减少 GC 导致的页面卡顿。

④ 并发标记(Concurrent Marking)(V8 6.4+):标记工作在后台线程进行,主线程继续运行 JS,实现真正的并发 GC,进一步减少停顿。

对象分配

Minor GC (Scavenger)

是,晋升

Major GC (Mark-Sweep)

Major GC (Mark-Compact)

JS 代码执行

新生代 From Space

存活 2 次?

老年代

释放无引用对象

整理碎片(选择性)

常见内存泄漏场景(理论 → 实践)

场景 原因 解决方案
全局变量持有大对象 全局变量是 GC Root,永不被回收 用完后显式 obj = null 断开引用
闭包持有无用外部变量 内部函数保持对外部作用域的引用 及时解除不需要的闭包引用
事件监听器未移除 DOM 元素已移除,但 Handler 持有其引用 removeEventListener 清理
定时器未清除 setInterval 回调持有外部对象引用 clearInterval 及时清理
WeakMap/WeakSet 持有 DOM 节点引用但节点已删除 改用 WeakMap 存储对 DOM 的关联数据,允许 GC 回收

2.2 名词解释

术语 定义 包含类型
值类型(Value Type) 又称原始类型(Primitive Type),存储在内存中,赋值时复制整个值。 numberstringbooleannullundefinedsymbolbigint
引用类型(Reference Type) 对象类型,实际数据存储在内存中,变量中存储的是堆内存地址(引用/指针)。 ObjectArrayFunctionDateRegExpMapSet
栈(Stack) 内存中的一块区域,特点是先进后出(LIFO),存储局部变量和函数调用信息,访问速度快。
堆(Heap) 内存中的另一块区域,用于存储动态分配的对象,大小不固定。
引用传递(Pass by Reference) 传递的是内存地址,通过该地址可以修改堆中的同一个对象。
值传递(Pass by Value) 传递的是值的副本,修改副本不影响原始值。
浅拷贝(Shallow Copy) 只复制对象的第一层属性,嵌套的引用类型属性仍然共享地址。
深拷贝(Deep Copy) 递归地复制对象的所有层次,完全独立的副本。
垃圾回收(GC) 自动释放不再被引用的堆内存,JavaScript 主流算法为标记-清除(Mark-and-Sweep)

2.3 内存结构精解(Mermaid)

堆内存(Heap)

调用栈(Call Stack)

引用

初始引用

obj2 重新赋值后

函数帧 main()

a = 100(直接存储值)

b = 200(直接存储值)

obj1 → 0x4A2F(存储堆地址)

obj2 → 0x4A2F(同一地址!)

地址 0x4A2F
{ age: 100 }

地址 0x6B31
{ age: 400 }(新对象)

2.4 四大区别对照表

维度 值类型 引用类型
内存位置 栈(Stack) 堆(Heap),栈中存地址
赋值方式 复制值(互不影响) 复制地址(共享同一对象)
可变性 不可变(Immutable) 可变(Mutable)
判等方式 值相同即相等(=== 比较值) 地址相同才相等(=== 比较引用)
参数传递 传递副本,函数内修改不影响外部 传递地址,函数内修改属性影响外部
GC 影响 随栈帧自动释放 无引用时由 GC 回收
typeof 结果 各自的类型名('number'等) 'object'(除函数外)

2.5 不可变性(Immutability)深入理解

// ════════ 字符串的不可变性 ════════
// 字符串是值类型,每次"修改"实际上创建了新字符串
var s = 'hello';
s[0] = 'H';      // 无效!字符串不可变
console.log(s);  // 'hello'(未改变)

// 字符串操作方法都返回新字符串,原字符串不变
var s2 = s.toUpperCase();
console.log(s);  // 'hello'(未改变)
console.log(s2); // 'HELLO'(新字符串)

// ════════ 数字的不可变性 ════════
var n = 42;
// 不存在 n.someProperty = 100 的有效操作
// 每次运算都产生新值,原值不变
var n2 = n + 1; // 产生新值 43
console.log(n); // 42(不变)

// ════════ 引用类型的可变性 ════════
var arr = [1, 2, 3];
arr[0] = 100;   // 可以修改内部元素
console.log(arr); // [100, 2, 3](原数组被修改)

var obj = { x: 1 };
obj.x = 100;    // 可以修改属性
console.log(obj); // { x: 100 }(原对象被修改)

💡 代码解析

代码片段 含义
s[0] = 'H' 无效 字符串是值类型,底层 V8 字符串对象是不可变的(SeqString 内容只读),索引赋值在非严格模式下静默失败
s.toUpperCase() 不改变 s 所有字符串方法都返回新字符串;s 仍指向原始字符串 'hello's2 指向新的字符串 'HELLO'
var n2 = n + 1 数值运算产生新值并赋给 n2,原变量 n 未被修改;原始值的"不可变"正是这个意思
arr[0] = 100 有效 数组是引用类型(对象),内部元素可以被修改,arr 仍指向同一个对象,只是对象的内容改变了
obj.x = 100 有效 对象属性可以被动态修改,堆中该对象的 x 属性值被更新,而对象引用(地址)本身不变

2.6 课堂案例精解

// ════════ 值类型:互不影响 ════════
var a = 100;
var b = a;   // 复制了 100 这个"值"给 b
b = 200;
console.log(a); // 100 —— a 不受影响

// ════════ 引用类型:共享地址 ════════
var obj1 = { age: 100 };
var obj2 = obj1;       // 复制了"地址"给 obj2,两者指向同一堆对象
obj2.age = 200;        // 通过 obj2 修改了堆中对象的属性
console.log(obj1.age); // 200 —— obj1 也受影响!

obj2 = { age: 400 };   // obj2 重新指向了一个新对象
console.log(obj1.age); // 200 —— obj1 依然指向原对象,不受影响

// ════════ 引用类型判等:地址相同才相等 ════════
console.log('hello' === 'hello');                  // true(值相同)
console.log({ name: 'Alice' } === { name: 'Alice' }); // false(不同地址)
console.log([10, 20] === [10, 20]);               // false(不同地址)

var arr  = [1, 2, 3];
var arr2 = arr;              // 同一地址
console.log(arr === arr2);   // true

💡 代码解析

代码片段 含义
var b = a; b = 200; 值类型赋值是完整的值复制,b 得到一个独立的数值 100,修改 ba 毫无影响
var obj2 = obj1; obj2.age = 200; 引用类型赋值只复制地址,obj2obj1 指向堆中同一个对象,通过任意一个变量修改属性,另一个也能"看到"变化
obj2 = { age: 400 } 这是重新赋值,让 obj2 指向一个新对象,与原对象断开联系,obj1 仍指向原对象不受影响
{ name: 'Alice' } === { name: 'Alice' } 两个字面量对象是在堆中分配的两块不同内存,地址不同,即使内容完全相同,=== 也返回 false

2.7 函数参数传递的深层理解

重要认知:JavaScript 中函数参数传递永远是值传递(Pass by Value)。对于引用类型,传递的"值"是地址本身,因此能通过地址修改堆中的对象。这不叫"引用传递",准确说是**“传值,值是引用”(Pass by Value, where the value is a reference)**。

// 值类型参数:函数内修改不影响外部
function incrementNum(n) {
    n += 10;
    console.log('函数内:', n); // 110
}
var x = 100;
incrementNum(x);
console.log('函数外:', x);    // 100 —— 不受影响

// 引用类型参数:函数内修改属性会影响外部(通过共享地址)
function addScore(user) {
    user.score += 100;  // 通过地址访问并修改堆中对象
}
var player = { name: 'Bob', score: 50 };
addScore(player);
console.log(player.score); // 150 —— 受影响!

// 引用类型参数:函数内重新赋值不影响外部
// 重新赋值只是改变了函数局部变量的指向,外部变量不变
function resetUser(user) {
    user.score = 999;              // 修改属性(影响外部)
    user = { name: 'New', score: 0 }; // 重新赋值(不影响外部)
}
var player2 = { name: 'Carol', score: 80 };
resetUser(player2);
console.log(player2.score); // 999 —— 属性修改生效,重新赋值无效

💡 代码解析

代码片段 含义
incrementNum(x)x 仍为 100 值类型传入函数,函数得到的是一个独立副本 n,修改 n 不会影响外部变量 x
user.score += 100 影响外部 引用类型传入函数,函数局部变量 user 持有的是同一个堆地址,修改属性就是修改堆中的数据,外部 player 能感知到
user = { name: 'New', score: 0 } 不影响外部 重新赋值让函数局部变量 user 指向一个新对象,但这只是修改了局部变量的指向,外部 player2 的指向没有改变

🏢 经典使用场景 & 业务价值

场景 技术手段 业务收益
状态管理(Redux/Vuex) 每次状态更新必须返回新对象(引用类型判等),框架通过比较对象引用来检测变化 避免不必要的重渲染,精确触发组件更新
不可变数据(Immer.js) 利用值类型不可变的语义,确保数据流单向传递,不在组件内直接修改 props 数据可预测,bug 更容易定位和复现
对象比较工具函数 理解引用类型判等,实现深比较函数(如 _.isEqual 处理表单"是否有改动"、配置"是否变化"等判断
函数参数防御性拷贝 在函数内对引用类型参数进行浅拷贝 {...obj},避免意外修改调用方数据 函数行为可预测,减少副作用导致的难以追踪的 bug

2.8 浅拷贝与深拷贝

var original = { a: 1, b: { c: 2 } };

// ════════ 浅拷贝(Shallow Copy)════════
var shallow1 = Object.assign({}, original);  // ES6 Object.assign
var shallow2 = { ...original };              // ES6 展开运算符

shallow1.a = 100;       // 不影响 original
console.log(original.a); // 1

shallow1.b.c = 999;     // 影响 original!因为 b 是引用类型,浅拷贝只复制了地址
console.log(original.b.c); // 999(受影响)

// ════════ 深拷贝(Deep Copy)════════
// 方案一:JSON 序列化(有局限:不能处理函数、undefined、Symbol、循环引用)
var deep1 = JSON.parse(JSON.stringify(original));

// 方案二:递归实现
function deepClone(obj) {
    if (obj === null || typeof obj !== 'object') return obj;
    if (Array.isArray(obj)) return obj.map(deepClone);
    var clone = {};
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            clone[key] = deepClone(obj[key]);
        }
    }
    return clone;
}

// 方案三:ES2023 structuredClone(原生深拷贝,现代环境可用)
var deep2 = structuredClone(original);

💡 代码解析

代码片段 含义
Object.assign({}, original) original自身可枚举属性逐一复制到新空对象,只复制第一层,嵌套对象仍是地址复制(浅拷贝)
shallow1.b.c = 999 影响原始 b 属性是对象,浅拷贝只复制了地址,shallow1.boriginal.b 指向同一个堆对象
JSON.parse(JSON.stringify(...)) 先序列化为 JSON 字符串(值类型),再解析为全新对象,实现深拷贝;但无法处理 undefinedFunctionSymbolDate(会变字符串)、循环引用
deepClone 递归函数 对每个属性值递归判断,若是对象则继续克隆,若是原始值则直接返回,实现真正意义的深层独立副本
structuredClone(original) ES2023 标准 API,基于结构化克隆算法,支持 DateMapSetArrayBuffer 等,但不支持函数和 Symbol

🏢 经典使用场景 & 业务价值

场景 推荐方案 原因
复制简单配置对象(无嵌套) { ...obj } 展开运算符 语法简洁,性能最佳,一行代码
合并组件 props(第一层) Object.assign({}, defaults, props) 合并时对每层的覆盖行为可控
保存表单编辑前的快照(含嵌套) JSON.parse(JSON.stringify(...)) 表单数据通常无函数,简单可靠
Redux action 传递 State 快照 structuredClone(state) 原生 API,支持复杂类型,无需引入 lodash
游戏存档/撤销重做功能 自定义 deepClonestructuredClone 需要完全独立副本,任何属性修改不影响历史记录

2.9 完整可运行示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>值类型与引用类型演示</title>
  <style>
    body { font-family: monospace; padding: 20px; background: #1e1e1e; color: #d4d4d4; }
    .section { background: #252526; padding: 16px; margin: 12px 0; border-radius: 8px; border-left: 4px solid #569cd6; }
    h3 { color: #4ec9b0; margin-top: 0; }
    button { background: #0e639c; color: #fff; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin: 4px; }
    button:hover { background: #1177bb; }
    #output { background: #1e1e1e; padding: 12px; border-radius: 6px; min-height: 80px; white-space: pre; color: #9cdcfe; }
  </style>
</head>
<body>
  <h2 style="color:#569cd6">值类型与引用类型交互演示</h2>
  <div class="section">
    <h3>值类型(Number)</h3>
    <button onclick="runValueType()">运行演示</button>
  </div>
  <div class="section">
    <h3>引用类型(Object)</h3>
    <button onclick="runRefType()">运行演示</button>
  </div>
  <div class="section">
    <h3>引用类型判等</h3>
    <button onclick="runRefEqual()">运行演示</button>
  </div>
  <div class="section">
    <h3>浅拷贝 vs 深拷贝</h3>
    <button onclick="runCopy()">运行演示</button>
  </div>
  <div class="section">
    <h3>输出结果</h3>
    <div id="output">点击按钮查看结果...</div>
  </div>
  <script>
    var out = document.getElementById('output');
    function log(msg) { out.textContent += msg + '\n'; }
    function clear() { out.textContent = ''; }

    function runValueType() {
      clear();
      var a = 100; var b = a; b = 200;
      log('=== 值类型演示 ===');
      log('var a = 100; var b = a; b = 200;');
      log('a = ' + a + '  (不受影响)');
      log('b = ' + b);
    }
    function runRefType() {
      clear();
      var obj1 = { age: 100 }; var obj2 = obj1;
      obj2.age = 200;
      log('=== 引用类型演示 ===');
      log('obj2.age = 200  →  obj1.age = ' + obj1.age + '(受影响!)');
      obj2 = { age: 400 };
      log('obj2 = {age:400} →  obj1.age = ' + obj1.age + '(不受影响)');
    }
    function runRefEqual() {
      clear();
      log('=== 引用类型判等 ===');
      log('"hello" === "hello" → ' + ('hello' === 'hello'));
      log('{} === {} → ' + ({} === {}));
      log('[] === [] → ' + ([] === []));
      var arr = [1,2,3]; var arr2 = arr;
      log('arr === arr2(同地址)→ ' + (arr === arr2));
    }
    function runCopy() {
      clear();
      var orig = { a: 1, b: { c: 2 } };
      var shallow = Object.assign({}, orig);
      shallow.b.c = 999;
      log('=== 浅拷贝 ===');
      log('浅拷贝后修改嵌套属性:orig.b.c = ' + orig.b.c + '(受影响!)');
      var orig2 = { a: 1, b: { c: 2 } };
      var deep = JSON.parse(JSON.stringify(orig2));
      deep.b.c = 999;
      log('=== 深拷贝(JSON) ===');
      log('深拷贝后修改嵌套属性:orig2.b.c = ' + orig2.b.c + '(不受影响)');
    }
  </script>
</body>
</html>

在这里插入图片描述


📌 值类型与引用类型 — 知识特点总结

特点 描述
值类型不可变 原始值本身无法被修改,每次"修改"都是创建新值。字符串看起来可以索引访问,但无法通过索引修改。
引用类型可变 对象的属性可以随时增删改,修改后仍是同一个对象(地址不变)
JS 只有值传递 函数参数传递本质上都是值传递;对引用类型,传递的"值"是地址,这与"引用传递"有本质区别
判等行为差异 值类型 === 比较实际值;引用类型 === 比较内存地址,内容相同的两个对象不相等
null 的特殊性 typeof null === 'object' 是 JS 历史 bug(永久保留),null 本质是值类型(原始值)
字符串的特殊行为 字符串虽是值类型,但访问属性时引擎会自动创建临时的包装对象(String 实例),调用后立即销毁
浅拷贝陷阱 Object.assign 和展开运算符 {...obj} 均为浅拷贝,嵌套对象仍共享引用
垃圾回收 当堆中的对象不再被任何变量引用时,GC 会自动回收其内存(标记-清除算法)

3 内置构造函数 Boolean

3.1 理论基础:类型包装机制

JavaScript 有一个巧妙的设计:原始值(如 42"hello"true)本身没有方法,但我们可以在数字或字符串上调用方法(如 "hello".toUpperCase())。这是怎么实现的?

包装对象(Wrapper Object)机制:当访问原始值的属性或方法时,JavaScript 引擎会临时创建一个对应的包装对象(NumberStringBoolean 的实例),执行属性访问或方法调用后,立即销毁该临时对象。

// 看似在字符串上调用方法
var s = 'hello';
s.toUpperCase(); // 'HELLO'

// 引擎内部实际发生的过程(伪代码):
// 1. temp = new String('hello');   // 创建临时包装对象
// 2. temp.toUpperCase();           // 调用方法
// 3. temp = null;                  // 销毁临时对象

// 这也解释了为什么给原始值设置属性不会报错,但读取时总是 undefined
var str = 'hello';
str.custom = 'world'; // 设置在临时对象上,立即销毁
console.log(str.custom); // undefined(每次都是新的临时对象)

💡 代码解析

代码片段 含义
s.toUpperCase() 引擎自动执行:① 创建 new String('hello') 临时包装对象;② 调用其 toUpperCase() 方法;③ 销毁包装对象;④ 返回结果。整个过程对开发者透明
str.custom = 'world' 不报错 属性赋值作用在临时包装对象上,不报错;但包装对象立即销毁,属性丢失
再次访问 str.customundefined 每次访问属性都会创建全新的临时包装对象,上次赋值的属性早已随前一个临时对象销毁,因此永远是 undefined
深层理论:ECMAScript 规范的类型转换算法

JavaScript 的类型转换行为完全由 ECMAScript 规范中的抽象操作(Abstract Operations)定义,理解这些算法是读懂 JS 隐式转换"魔法"的钥匙。

① ToBoolean 算法(规范 §7.1.2)

将任意值转为布尔值的完整规则,顺序遍历以下情况:

输入类型 输入值 结果
Undefined undefined false
Null null false
Boolean false false
Boolean true true
Number +0-0NaN false
Number 其他任何数字 true
String "" (空字符串) false
String 其他任何字符串 true
BigInt 0n false
BigInt 其他任何 BigInt true
Object 任何对象(含 {}[] true
Symbol 任何 Symbol true

关键洞察:Object 类型的 ToBoolean 结果永远是 true,不存在例外。这解释了为什么 new Boolean(false) 在条件判断中是 truthy——它是一个对象。

② ToNumber 算法(规范 §7.1.4)

将任意值转为数字,这是 +-*/ 等运算符的基础:

输入 结果 说明
undefined NaN 无法转为数字
null 0 历史设计,null + 1 === 1
true 1
false 0
"" 0 空字符串转 0,常见陷阱
"42" 42 纯数字字符串直接转
"3.14abc" NaN 非纯数字字符串
"0x1F" 31 十六进制字符串识别
[] 0 先 ToPrimitive → ""0
[1] 1 先 ToPrimitive → "1"1
[1,2] NaN 先 ToPrimitive → "1,2"NaN
{} NaN 先 ToPrimitive → "[object Object]"NaN

③ 抽象相等比较(Abstract Equality ==)算法流程

== 的"魔法行为"来自规范 §7.2.14 的复杂规则,以下是简化后的决策树:

两者都是 null/undefined

一个是 null,另一个不是 null/undefined

x == y

类型相同?

使用严格相等 === 比较

其中一个是 null 或 undefined?

✅ true

❌ false

其中一个是数字?

将另一个 ToNumber 后再比较

其中一个是字符串?

将字符串 ToNumber 后再比较

其中一个是布尔值?

将布尔值 ToNumber(1/0) 后再比较

一个是对象,另一个是原始值?

对象 ToPrimitive 后再比较

❌ false

// 理解了算法,这些"奇怪"行为都有规律可循:
console.log(null == undefined);  // true(规范特殊处理)
console.log(null == 0);          // false(null 只与 undefined 宽松相等)
console.log('' == false);        // true:false→ToNumber→0,''→ToNumber→0,0==0
console.log([] == false);        // true:false→0,[]→ToPrimitive→''→0,0==0
console.log({} == false);        // false:false→0,{}→ToPrimitive→'[object Object]'→NaN,NaN≠0
console.log([] == ![]);          // true:![]→false→0,[]→0,0==0(经典面试题)

💡 代码解析

表达式 完整推导步骤
null == undefinedtrue 规范特殊处理:这两个值互相宽松相等,且与其他任何值宽松不等
null == 0false null 只与 null/undefined 宽松相等,不触发 ToNumber
'' == falsetrue false 是布尔值,ToNumber → 0;② '' 是字符串,ToNumber → 0;③ 0 === 0true
[] == falsetrue false → ToNumber → 0;② [] 是对象,ToPrimitive → ''(数组调用 join())→ ToNumber → 0;③ 0 === 0true
{} == falsefalse false0;② {} ToPrimitive → '[object Object]'→ ToNumber → NaN;③ NaN === 0false
[] == ![]true ![] 先求值:[] 是 truthy,取反 → false;② false0;③ [] ToPrimitive → ''0;④ 0 === 0true

最佳实践:在实际工程中,始终使用 === 严格相等,避免 == 带来的隐式转换歧义。只有在明确需要同时匹配 nullundefined 时,才使用 x == null 这一惯用写法。

3.2 名词解释

术语 定义
布尔值(Boolean) JavaScript 最基础的数据类型之一,只有 truefalse 两个值
包装对象(Wrapper Object) 用构造函数 new Boolean(...) 创建的对象,与布尔原始值不同,是对象类型
隐式类型转换(Type Coercion) JavaScript 在特定上下文(条件判断、运算符)中自动将值转换为特定类型
Falsy 值 转换为布尔值时得到 false 的值:false0-00n""nullundefinedNaN
Truthy 值 除 Falsy 外,所有值转换为布尔值时都得到 true包括空对象 {}、空数组 []
短路求值(Short-circuit Evaluation) && 遇到 Falsy 立即返回,|| 遇到 Truthy 立即返回,不继续求值
空值合并运算符(?? ES2020,仅当左侧为 nullundefined 时才使用右侧值,比 || 更精确

3.3 三种创建方式

// 方式一:直接量(推荐日常使用)
var b1 = true;
var b2 = false;

// 方式二:Boolean() 函数转换(用于类型转换)
console.log(Boolean(1));         // true
console.log(Boolean(0));         // false
console.log(Boolean('hello'));   // true
console.log(Boolean(''));        // false
console.log(Boolean(null));      // false
console.log(Boolean(undefined)); // false
console.log(Boolean({}));        // true(注意!空对象也是 true)
console.log(Boolean([]));        // true(注意!空数组也是 true)

// 方式三:new Boolean() 构造函数(几乎不用,会产生包装对象)
var b3 = new Boolean(true);
var b4 = new Boolean(false);
console.log(typeof b3);          // "object"(不是 boolean!)

// 陷阱:包装对象在条件判断中总是 truthy
if (new Boolean(false)) {
    console.log('这里会执行!因为对象是 truthy');
}

💡 代码解析

代码片段 含义
Boolean({})true 空对象是引用类型,是一个有效的内存地址,任何对象(含空对象、空数组)转布尔都是 true
Boolean([])true 空数组同理,这是初学者最常见的误判:if ([]) 的条件永远成立
typeof b3'object' new Boolean(true) 创建的是包装对象,不是原始布尔值,类型是 object
if (new Boolean(false)) {} 中代码执行 new Boolean(false) 虽然"包着"false,但整体是一个对象(truthy),条件恒真,这是一个严重的逻辑陷阱

3.4 布尔运算符深度解析

// ════════ && (逻辑与):短路求值 ════════
// 遇到 Falsy 值立即返回该值,否则返回最后一个值
console.log(1 && 2);          // 2(都是 truthy,返回最后一个)
console.log(0 && 'hello');    // 0(遇到 falsy,返回 0)
console.log('' && 'hello');   // ''(遇到 falsy,返回 '')
console.log(null && 'hello'); // null
// 实际应用:条件执行
var user = { name: 'Alice' };
user && user.name && console.log(user.name); // 'Alice'(链式安全访问)

// ════════ || (逻辑或):短路求值 ════════
// 遇到 Truthy 值立即返回该值,否则返回最后一个值
console.log(1 || 'default');    // 1(遇到 truthy,返回 1)
console.log(0 || 'default');    // 'default'(0 是 falsy,继续)
console.log('' || 'default');   // 'default'
// 实际应用:默认值(注意:0 和 '' 也会触发默认值,是缺陷)
var name = '' || 'Anonymous';   // 'Anonymous'(可能不是期望的)

// ════════ ?? (空值合并)ES2020 ════════
// 仅当左侧为 null 或 undefined 时使用右侧
console.log(0 ?? 'default');    // 0(0 不是 null/undefined,保留)
console.log('' ?? 'default');   // ''('' 不是 null/undefined,保留)
console.log(null ?? 'default'); // 'default'
console.log(undefined ?? 'default'); // 'default'

// ════════ ! (逻辑非)════════
console.log(!true);    // false
console.log(!false);   // true
console.log(!0);       // true
console.log(!'');      // true
console.log(!{});      // false(对象是 truthy,取反得 false)

// !! 双重否定:将任意值转为布尔值(比 Boolean() 简洁)
console.log(!!0);    // false
console.log(!!1);    // true
console.log(!!'');   // false
console.log(!!{});   // true

💡 代码解析

代码片段 含义
1 && 22 && 遇到第一个 truthy 值不停止,继续向右求值,返回最后一个被求值的结果 2
0 && 'hello'0 && 遇到第一个 falsy 值立即返回它,后续不再求值(短路),所以 'hello' 根本不被执行
user && user.name && console.log(user.name) 链式安全访问模式:先确认 user 存在,再确认 user.name 存在,最后执行操作,等价于 user?.name(ES2020 可选链)
`‘’
0 ?? 'default'0 ?? 只对 null/undefined 触发,0 是有效值被保留;这是 || 处理 0 时产生 bug 的解决方案
!!obj 第一个 ! 将值转为布尔值并取反,第二个 ! 再次取反,最终得到值对应的布尔值,等价于 Boolean(obj)

🏢 经典使用场景 & 业务价值

场景 推荐写法 不推荐写法 说明
渲染用户名称(可能为空) name ?? '匿名用户' name || '匿名用户' 允许用户名为 0 或空字符串时,?? 不会意外替换
权限按钮的条件渲染 isAdmin && <AdminButton> if(isAdmin){...} React 中 && 短路渲染是最简洁的条件渲染方式
表单字段有效性检测 !!value.trim() value.trim().length > 0 !! 快速将字符串转为布尔值,用于表单验证
API 响应数据安全访问 data?.user?.name ?? '未知' data && data.user && data.user.name ES2020 可选链+空值合并,极大简化深层属性访问
配置项默认值合并 config.timeout ?? 5000 config.timeout || 5000 timeout 明确设为 0(立即超时),|| 会错误地替换为 5000

3.5 Falsy / Truthy 速查(Mermaid)

✅ Truthy 值(其余所有)

true

非零数字(正/负)

非空字符串(含 '0' 和 'false')

{} 空对象 ← 常见陷阱

[] 空数组 ← 常见陷阱

函数

Infinity / -Infinity

❌ Falsy 值(共 8 个)

false

0(数字零)

-0(负零)

0n(BigInt 零)

''(空字符串)

null

undefined

NaN


📌 Boolean — 知识特点总结

特点 描述
只有两个值 truefalse,逻辑运算的基础
包装对象陷阱 new Boolean(false) 是对象(truthy),不要用 new Boolean() 做条件判断
空容器是 truthy {}[] 虽然"空",但都是 truthy,初学者常见误区
'false' 是 truthy 字符串 'false' 是非空字符串,转换为布尔值是 true
短路特性的双重用途 && 可替代简单 if|| 可提供默认值,?? 处理 null/undefined 默认值
!! 双重取反 Boolean() 更简洁的类型转换写法,在工程实践中广泛使用
类型强制转换 JS 在 if、三元运算符、逻辑运算中自动触发布尔转换,理解 Falsy 列表至关重要

4 内置构造函数 Number

4.1 理论基础:IEEE 754 双精度浮点数

JavaScript 使用 IEEE 754-2008 双精度 64 位浮点数(binary64) 标准存储所有数值(ES2020 之前没有整数类型)。

64 位的分配:

符号位(1位) | 指数位(11位) | 尾数位(52位)
    S         |      E        |       M
  • 符号位:0 为正数,1 为负数
  • 指数位:11 位,表示 2 的幂次(偏置量为 1023)
  • 尾数位:52 位,加上隐含的 1 位,共 53 位有效精度

这解释了 0.1 + 0.2 !== 0.3 的根本原因0.10.2 在二进制中是无限循环小数,存储时被截断,产生舍入误差,累加后误差超过 Number.EPSILON

0.1 的二进制表示(无限循环):
0.0001100110011001100110011001100110011001100110011001101...
                                           ↑ 53 位处截断
深层理论:0.1 的二进制推导与 NaN 的 IEEE 754 编码

① 为什么 0.1 是无限循环小数?

十进制小数转二进制的方法是"乘 2 取整",0.1 的推导过程:

0.1 × 2 = 0.2  → 整数位 0
0.2 × 2 = 0.4  → 整数位 0
0.4 × 2 = 0.8  → 整数位 0
0.8 × 2 = 1.6  → 整数位 1
0.6 × 2 = 1.2  → 整数位 1
0.2 × 2 = 0.4  → 整数位 0  ← 开始循环!
...

0.1₁₀ = 0.0001100110011001100110011...₂(无限循环)

② 0.1 在 64 位中的精确表示

IEEE 754 在尾数位截断后,0.1 实际存储的值为:

0.1 精确值 ≈ 0.1000000000000000055511151231257827021181583404541015625

0.2 精确值 ≈ 0.200000000000000011102230246251565404236316680908203125

0.1 + 0.2   ≈ 0.3000000000000000444089209850062616169452667236328125

而 0.3 精确值 ≈ 0.299999999999999988897769753748434595763683319091796875

所以 0.1 + 0.2 ≠ 0.3
// 用 toPrecision(21) 看到足够多的位数
console.log((0.1).toPrecision(21)); // "0.100000000000000005551"
console.log((0.2).toPrecision(21)); // "0.200000000000000011102"
console.log((0.3).toPrecision(21)); // "0.299999999999999988898"
console.log((0.1 + 0.2).toPrecision(21)); // "0.300000000000000044409"

💡 代码解析

代码片段 含义
(0.1).toPrecision(21)"0.100000000000000005551" 展示 0.1 的真实存储值:比精确的 0.1 稍大,多了约 5.55e-18 的误差(IEEE 754 舍入引入)
(0.3).toPrecision(21)"0.299999..." 直接字面量 0.3 存储的是比 0.3 稍小的值
(0.1 + 0.2).toPrecision(21)"0.300000...04" 两个正误差累加,结果比 0.3 大;而 0.3 字面量比 0.3 小,两者之差约 5.55e-17,超过显示精度,所以 0.1+0.2 !== 0.3

③ 特殊数值的 IEEE 754 编码

符号 指数位(11位) 尾数位(52位) 说明
+0 0 全 0 全 0 正零
-0 1 全 0 全 0 负零(+0 === -0true
+Infinity 0 全 1 全 0 正无穷
-Infinity 1 全 1 全 0 负无穷
NaN 任意 全 1 非零 非数字(有多种编码形式)

④ NaN 的特殊性

// NaN 是 IEEE 754 规范的一类特殊值,有以下独特行为:
console.log(NaN === NaN);          // false(NaN 不等于任何值,包括自身)
console.log(NaN !== NaN);          // true
console.log(typeof NaN);           // "number"(历史遗留设计)
console.log(isNaN('hello'));       // true(全局 isNaN 先调用 ToNumber)
console.log(Number.isNaN('hello'));// false(Number.isNaN 不做类型转换,更严格)

// NaN 的产生场景:
console.log(0 / 0);          // NaN(不定式)
console.log(Math.sqrt(-1));  // NaN(负数开方无实数解)
console.log(parseInt('abc')); // NaN(无法解析)
console.log(undefined + 1);  // NaN(undefined ToNumber 为 NaN)

// IEEE 754 规定:NaN 与任何数(包括自身)的比较运算结果均为 false
// 因此 NaN 的检测必须用 isNaN() 或 Number.isNaN()

💡 代码解析

代码片段 含义
NaN === NaNfalse IEEE 754 规范的强制规定:NaN(Not a Number)是不可比较的,任何含 NaN 的比较(</>/==/===)均返回 false
typeof NaN'number' 历史遗留 bug:NaN 是数字类型(数值空间中的特殊值),typeof 无法区分正常数字和 NaN
isNaN('hello')true 全局 isNaN 先调用 ToNumber:'hello'NaN,再判断是否为 NaN,结果 true;容易误判字符串
Number.isNaN('hello')false ES6 新版:不做类型转换,只对确实是 NaN 值的情况返回 true;字符串不是 NaN,返回 false
0 / 0NaN 数学上的"不定式"(0/0 没有确定答案),IEEE 754 规定结果为 NaN
undefined + 1NaN undefined ToNumber 得到 NaN,任何数与 NaN 的算术运算结果仍为 NaN(NaN 的传染性)

⑤ 正零与负零

// JavaScript 中存在 +0 和 -0,大多数场景无差别,但有细微差异
console.log(+0 === -0);                // true(相等比较忽略符号)
console.log(Object.is(+0, -0));        // false(Object.is 识别负零)
console.log(1 / +0);                   // Infinity
console.log(1 / -0);                   // -Infinity(通过除法可区分)
console.log((-0).toString());          // "0"(字符串化时负零变成"0")
console.log(JSON.stringify(-0));       // "0"(JSON 序列化也不保留符号)

💡 代码解析

代码片段 含义
+0 === -0true === 使用 Abstract Equality,不区分正负零;但 IEEE 754 内存中它们的编码不同(符号位 0 vs 1)
Object.is(+0, -0)false ES6 的 Object.is 使用 SameValue 算法,能区分 +0-0;这是判断精确相等的最可靠方式
1 / +0Infinity1 / -0-Infinity 除以正零得正无穷,除以负零得负无穷,通过除法可以检测负零
(-0).toString()'0' toString 规范要求忽略负零的符号;同样 JSON.stringify(-0) 也返回 '0'

实际影响:负零在物理方向(如速度的方向)建模时有意义,但在日常开发中几乎不会遇到。Object.is() 是 ES6 提供的精确相等比较,能正确区分 +0/-0NaN/NaN,是实现严格相等语义的最佳工具。

4.2 名词解释

术语 定义
IEEE 754 JavaScript 使用的浮点数标准,64 位双精度,这是 0.1 + 0.2 !== 0.3 的根本原因
toFixed() 将数字格式化为指定小数位数的字符串,采用四舍五入(注意:部分边界值的舍入行为依赖实现)
toString(radix) 将数字转为指定进制的字符串,进制范围 2~36
Number.MAX_VALUE JS 能表示的最大正数:约 1.7976931348623157e+308
Number.MIN_VALUE JS 能表示的最小正数(最接近 0 的正数):约 5e-324
Number.MAX_SAFE_INTEGER 最大安全整数 2^53 - 1 = 9007199254740991,超出此范围整数运算不精确
Number.EPSILON 机器精度:2^-52 ≈ 2.22e-16,用于浮点数比较的误差容限
NaN Not a Number,表示非法数值运算的结果,typeof NaN === 'number'(历史设计)
Infinity 正无穷大,超出 MAX_VALUE 的运算结果;-Infinity 为负无穷大
BigInt ES2020 引入,用于表示任意精度整数,如 9007199254740992n,解决了大整数精度问题

4.3 实例方法

var price  = 19.985;
var amount = 100.904;

// ① toFixed(n) —— 保留 n 位小数(四舍五入),返回字符串
console.log(price.toFixed(2));   // "19.99"
console.log(price.toFixed(0));   // "20"
console.log(price.toFixed());    // "20"(无参数返回整数字符串)

// ② toString(radix) —— 转换为指定进制字符串(radix: 2~36)
var n = 255;
console.log(n.toString(2));   // "11111111"(二进制)
console.log(n.toString(8));   // "377"(八进制)
console.log(n.toString(16));  // "ff"(十六进制,小写)
console.log(n.toString(36));  // "73"(三十六进制)

// ③ toPrecision(n) —— 指定有效数字位数(包含整数部分)
console.log((1234.567).toPrecision(4)); // "1235"
console.log((0.000123).toPrecision(2)); // "0.00012"

// ④ toExponential(n) —— 科学计数法表示
console.log((12345).toExponential(2));  // "1.23e+4"
console.log((0.00123).toExponential()); // "1.23e-3"

// ⑤ valueOf() —— 返回原始数值(包装对象转换时使用)
var numObj = new Number(42);
console.log(numObj.valueOf()); // 42
console.log(numObj + 1);       // 43(自动调用 valueOf)

💡 代码解析

代码片段 含义
price.toFixed(2) 返回保留 2 位小数的字符串,注意是字符串而非数字,做数学运算前需用 +Number() 转换
n.toString(16) 将十进制整数转为十六进制字符串,255'ff';配合 parseInt('ff', 16) 可反向转回
(1234.567).toPrecision(4) 指定 总有效数字位数(整数+小数),而非小数位数;4 位有效数字 1235 覆盖到个位
(12345).toExponential(2) 科学计数法,适合显示超大或超小数字,保留 2 位小数精度
numObj + 1 自动调用 valueOf() JS 在数学运算时会自动调用包装对象的 valueOf() 取出原始值,这是 JS 隐式类型转换链的一部分

🏢 经典使用场景 & 业务价值

场景 方法 业务收益
电商价格展示 price.toFixed(2) 确保所有价格显示为两位小数,如 ¥19.90 而非 ¥19.9
CSS 颜色值生成 n.toString(16).padStart(6,'0') 将随机数转为十六进制颜色 #3a7bc8,用于主题色生成、图表颜色分配
科学数据展示 value.toExponential(3) 天文距离、基因序列数量等超大数值的可读展示
进制转换工具 toString(2/8/16/36) Base64 编码辅助、权限位运算展示、短链接ID生成(36进制)
数据精度控制 toPrecision(n) 传感器数据、测量结果的精度统一展示,避免不必要的精度噪音

4.4 静态属性与方法

// 静态属性
console.log(Number.MAX_VALUE);          // 1.7976931348623157e+308
console.log(Number.MIN_VALUE);          // 5e-324
console.log(Number.MAX_SAFE_INTEGER);   // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER);   // -9007199254740991
console.log(Number.POSITIVE_INFINITY);  // Infinity
console.log(Number.NEGATIVE_INFINITY);  // -Infinity
console.log(Number.NaN);               // NaN
console.log(Number.EPSILON);           // 2.220446049250313e-16

// 静态方法
console.log(Number.isInteger(42));     // true
console.log(Number.isInteger(42.0));   // true(42.0 在 JS 中就是 42)
console.log(Number.isInteger(42.5));   // false
console.log(Number.isNaN(NaN));        // true
console.log(Number.isNaN('NaN'));      // false(不做转换,严格判断)
// 对比全局 isNaN:会先转换类型
console.log(isNaN('NaN'));             // true('NaN' 转换后是 NaN)
console.log(Number.isFinite(Infinity));// false
console.log(Number.isSafeInteger(Number.MAX_SAFE_INTEGER));     // true
console.log(Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1)); // false
console.log(Number.parseInt('42px'));  // 42
console.log(Number.parseFloat('3.14abc')); // 3.14

💡 代码解析

代码片段 含义
Number.MAX_VALUE1.8e+308 JS 能表示的最大正数,超过此值运算结果变为 Infinity
Number.MAX_SAFE_INTEGER = 2^53-1 安全整数上限,超过此值的整数运算不保证精确(如 9007199254740992 + 1 === 9007199254740992
Number.EPSILON2.22e-16 机器精度,用于判断两个浮点数是否"足够接近":Math.abs(a-b) < Number.EPSILON
Number.isNaN(NaN)trueNumber.isNaN('NaN')false Number.isNaN 不做类型转换,比全局 isNaN 更严格可靠;全局 isNaN('NaN') 先转数字再判断,会返回 true
Number.isSafeInteger 判断是否在安全整数范围内,处理来自后端的大整数 ID 时必须检查此项
Number.parseInt('42px')42 从字符串开头解析整数,遇到非数字字符停止,常用于解析 CSS 值、用户输入

🏢 经典使用场景 & 业务价值

场景 API 业务价值
后端大整数 ID 安全检查 Number.isSafeInteger(id) Java/Go 的 long 类型 ID(64位)传入 JS 时,若超过 MAX_SAFE_INTEGER 会丢精度,此检查可提前预警
传感器数据有效性校验 Number.isFinite(val) && !Number.isNaN(val) 过滤传感器上报的 Infinity/NaN 异常值,防止图表渲染崩溃
浮点数相等判断 Math.abs(a-b) < Number.EPSILON 财务计算中比较两个金额是否相等,避免 0.1+0.2 !== 0.3 的精度 bug
CSS 值解析 Number.parseFloat('1.5rem') 从样式字符串提取数值,动态计算布局尺寸

4.5 浮点数精度问题与解决方案

// 经典问题:IEEE 754 舍入误差
console.log(0.1 + 0.2);           // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3);   // false

// 解决方案一:toFixed 后转 Number
console.log(+(0.1 + 0.2).toFixed(1)); // 0.3

// 解决方案二:乘以整数倍后运算再除回来(适合金融场景)
function safeAdd(a, b, decimals) {
    var factor = Math.pow(10, decimals || 10);
    return Math.round((a + b) * factor) / factor;
}
console.log(safeAdd(0.1, 0.2, 1)); // 0.3

// 解决方案三:使用 Number.EPSILON 判等
function floatEqual(a, b) {
    return Math.abs(a - b) < Number.EPSILON;
}
console.log(floatEqual(0.1 + 0.2, 0.3)); // true

// 解决方案四:BigInt(ES2020,仅整数)
// 将小数乘以精度因子转为 BigInt 运算
function addCents(a, b) { // a, b 以分为单位(整数)
    return BigInt(a) + BigInt(b);
}

// 解决方案五:Intl.NumberFormat(格式化显示)
var formatter = new Intl.NumberFormat('zh-CN', { 
    style: 'currency', currency: 'CNY', minimumFractionDigits: 2 
});
console.log(formatter.format(0.1 + 0.2)); // ¥0.30(显示上看起来正确)

💡 代码解析

代码片段 含义
0.1 + 0.2 → 0.30000000000000004 IEEE 754 舍入误差累积的结果,这不是 JS bug,而是所有使用该标准的语言的共同现象
+(0.1 + 0.2).toFixed(1) toFixed 在内部用更高精度计算并四舍五入后返回字符串,一元 + 将字符串转回数字
Math.round((a + b) * factor) / factor 将小数转为整数倍后运算(整数运算精确),再除回,绕过浮点精度问题
Math.abs(a - b) < Number.EPSILON 不直接比较是否相等,而是判断差值是否在机器精度范围内,这是工程中比较浮点数的标准做法
Intl.NumberFormat 国际化数字格式化 API,自动处理货币符号、千分位、小数位等,适合面向用户展示

🏢 经典使用场景 & 业务价值

场景 推荐方案 业务价值
电商购物车总计 整数分为单位运算(商品价格×100存储),最终展示时÷100 完全消除浮点误差,金额绝对准确
税率计算 safeAdd(price, price * 0.13, 2) 13% 税率计算后展示两位小数,不出现 ¥19.240000000000001
股票/汇率价格显示 Intl.NumberFormat('zh-CN', {minimumFractionDigits: 4}).format(val) 自动处理千分位和小数位,国际化友好
科学实验数据比较 floatEqual(measured, expected) 测量值和期望值受仪器精度影响,使用 EPSILON 判等而非严格相等

4.6 完整可运行示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Number 内置构造函数演示</title>
  <style>
    * { box-sizing: border-box; }
    body { font-family: sans-serif; max-width: 800px; margin: 0 auto; padding: 24px; background: #f5f5f5; }
    .card { background: #fff; border-radius: 10px; padding: 20px; margin: 16px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
    h2 { color: #1565c0; border-bottom: 2px solid #e3f2fd; padding-bottom: 8px; }
    input[type=number] { padding: 8px; border: 1px solid #90caf9; border-radius: 6px; font-size: 16px; width: 160px; }
    button { background: #1976d2; color: #fff; border: none; padding: 8px 20px; border-radius: 6px; cursor: pointer; margin: 4px; font-size: 14px; }
    .result { background: #e8f5e9; padding: 12px; border-radius: 6px; margin-top: 12px; font-family: monospace; min-height: 40px; white-space: pre-wrap; }
    .theory { background: #fff3e0; padding: 12px; border-radius: 6px; margin: 10px 0; font-size: 13px; color: #e65100; }
  </style>
</head>
<body>
  <h1 style="color:#0d47a1">Number 构造函数全解析</h1>
  <div class="card">
    <h2>IEEE 754 精度演示</h2>
    <div class="theory">⚠️ 理论:0.1 和 0.2 在二进制中是无限循环小数,存储时被截断,累加产生精度误差</div>
    <button onclick="showPrecision()">演示浮点精度问题</button>
    <div class="result" id="precResult">点击查看</div>
  </div>
  <div class="card">
    <h2>toFixed —— 格式化小数</h2>
    <input type="number" id="numInput" value="19.985" step="0.001">
    <button onclick="runToFixed(0)">0位</button>
    <button onclick="runToFixed(2)">2位</button>
    <button onclick="runToFixed(4)">4位</button>
    <div class="result" id="fixedResult">结果将在此显示</div>
  </div>
  <div class="card">
    <h2>toString —— 进制转换</h2>
    <input type="number" id="radixInput" value="255" step="1">
    <button onclick="runToString(2)">二进制</button>
    <button onclick="runToString(8)">八进制</button>
    <button onclick="runToString(16)">十六进制</button>
    <div class="result" id="radixResult">结果将在此显示</div>
  </div>
  <div class="card">
    <h2>Number 静态属性</h2>
    <button onclick="showStaticProps()">查看所有静态属性</button>
    <div class="result" id="staticResult">点击查看</div>
  </div>
  <script>
    function showPrecision() {
      var r = document.getElementById('precResult');
      r.textContent = [
        '0.1 + 0.2 = ' + (0.1 + 0.2),
        '0.1 + 0.2 === 0.3 → ' + (0.1 + 0.2 === 0.3),
        '',
        '解决方案1 toFixed: ' + +(0.1 + 0.2).toFixed(1),
        '解决方案2 EPSILON判等: ' + (Math.abs(0.1+0.2-0.3) < Number.EPSILON),
        '解决方案3 整数运算: ' + (Math.round((0.1+0.2)*10)/10),
      ].join('\n');
    }
    function runToFixed(n) {
      var num = parseFloat(document.getElementById('numInput').value);
      document.getElementById('fixedResult').textContent = num + '.toFixed(' + n + ') = "' + num.toFixed(n) + '"';
    }
    function runToString(radix) {
      var num = parseInt(document.getElementById('radixInput').value);
      document.getElementById('radixResult').textContent = num + '.toString(' + radix + ') = "' + num.toString(radix) + '"';
    }
    function showStaticProps() {
      var props = [
        ['MAX_VALUE', Number.MAX_VALUE], ['MIN_VALUE', Number.MIN_VALUE],
        ['MAX_SAFE_INTEGER', Number.MAX_SAFE_INTEGER], ['MIN_SAFE_INTEGER', Number.MIN_SAFE_INTEGER],
        ['EPSILON', Number.EPSILON], ['POSITIVE_INFINITY', Number.POSITIVE_INFINITY],
      ];
      document.getElementById('staticResult').textContent = props.map(function(p){ return 'Number.' + p[0] + ' = ' + p[1]; }).join('\n');
    }
  </script>
</body>
</html>

在这里插入图片描述


📌 Number — 知识特点总结

特点 描述
唯一数值类型(ES2020前) 所有数字(整数和浮点)都用同一种 64 位双精度浮点数表示,不区分 int/float
IEEE 754 精度限制 0.1 + 0.2 !== 0.3 是底层存储格式决定的,非语言 bug,所有使用 IEEE 754 的语言都有此问题
安全整数范围 ±(2^53-1) 范围外的整数运算结果不可信,处理大 ID 时需注意(如 Java long 类型的 ID 传入 JS 后精度丢失)
NaN 的自反性 NaN !== NaN 是 IEEE 754 规定,NaN 不等于任何值(包括自身),必须用 Number.isNaN() 检测
toFixed 返回字符串 注意 toFixed() 返回的是字符串,需要转换后才能做数学运算
进制转换双向 num.toString(16) 转十六进制,parseInt('ff', 16) 从十六进制转回,常用于颜色处理
typeof NaN === 'number' 历史遗留设计,NaN 属于 number 类型,但表示"不是有效数字"
BigInt 补充 ES2020 引入 BigInt 类型(后缀 n),用于任意精度整数,不能与普通 Number 混用

5 内置构造函数 String

5.1 理论基础:字符串的编码与不可变性

Unicode 与 UTF-16:JavaScript 字符串在内存中以 UTF-16 编码存储。大多数常用字符占用 2 字节(1 个代码单元),但某些特殊字符(如部分 emoji、某些汉字变体)需要 4 字节(2 个代码单元,称为代理对 Surrogate Pair)。

这导致了一个有趣的问题:

var emoji = '😀';
console.log(emoji.length);        // 2(不是 1!占用 2 个 UTF-16 代码单元)
console.log(emoji.charCodeAt(0)); // 55357(高代理)
console.log(emoji.charCodeAt(1)); // 56832(低代理)
// ES6+ 解决方案:codePointAt 正确处理代理对
console.log(emoji.codePointAt(0)); // 128512(正确的 Unicode 码点)

💡 代码解析

代码片段 含义
emoji.length2 emoji 😀 的 Unicode 码点 U+1F600 超过 U+FFFF,需要两个 UTF-16 代码单元(代理对)表示,所以 length 为 2
charCodeAt(0)55357 读取高代理(High Surrogate)的编码,范围 0xD800~0xDBFF,单独使用无意义
charCodeAt(1)56832 读取低代理(Low Surrogate)的编码,范围 0xDC00~0xDFFF,单独使用无意义
codePointAt(0)128512 ES6 正确处理代理对,将高低代理合并计算出真实的 Unicode 码点 U+1F600(即十进制 128512)

字符串不可变性(Immutability):字符串一旦创建,其内容不能被修改。所有字符串方法都返回新字符串,原始字符串保持不变。这是字符串作为值类型的本质体现,也使字符串可以安全地共享(字符串驻留/intern 优化)。

字符串驻留(String Interning):相同的字符串字面量在引擎内部可能指向同一块内存,这是引擎的优化手段,解释了为什么 'hello' === 'hello'true(值类型按值比较,且引擎可能复用同一对象)。

深层理论:V8 引擎的字符串内部表示

V8 并非用单一结构存储所有字符串,而是根据字符串的创建方式和使用场景,选择最高效的内部表示。理解这些机制有助于编写高性能字符串处理代码。

① V8 字符串的五种内部类型

类型 中文名 适用场景 内存特点
SeqOneByteString 顺序单字节字符串 纯 ASCII 字符串(每个字符一字节) 内存最紧凑,访问最快
SeqTwoByteString 顺序双字节字符串 含非 ASCII 字符(每个字符两字节) 标准 UTF-16
ConsString 拼接字符串 a + b + c 字符串连接 树形结构,延迟展开
SlicedString 切片字符串 str.slice(start, end) 截取 保存原字符串引用+偏移,无内存拷贝
ExternalString 外部字符串 从 C++ 层传入的字符串 内存由宿主环境管理

② ConsString(拼接字符串树)的关键影响

// 字符串拼接不会立即复制内存,V8 创建一个"树节点"
var a = 'Hello';
var b = ', ';
var c = 'World';
var result = a + b + c;
// V8 内部:ConsString { left: 'Hello', right: ConsString { left: ', ', right: 'World' } }
// 实际字符串内容是懒求值的——只有真正需要遍历字符时才展开(如 console.log)

// 影响:大量字符串拼接时,V8 不会每次都复制内存(避免 O(n²) 的内存操作)
// 但最终遍历时需要展开树,若树过深会有额外开销

💡 代码解析

代码片段 含义
var result = a + b + c V8 不立即分配连续内存,而是创建 ConsString 树节点:{left:'Hello', right:{left:', ', right:'World'}},延迟展开
懒求值的时机 当需要逐字符遍历(如 console.log、正则匹配、slice)时,V8 才会将树"展开"(flatten)为连续内存的 SeqString
O(n²) 问题 若用旧引擎或不支持 ConsString 的环境,每次 += 都复制整个字符串,10000次拼接总操作量为 1+2+…+10000 = O(n²)

③ SlicedString(切片字符串)的零拷贝特性

var longStr = 'A'.repeat(100000); // 10 万字符
var slice   = longStr.slice(0, 100); // 不复制内存!
// SlicedString: { parent: longStr, offset: 0, length: 100 }
// 只存储对原字符串的引用 + 偏移量 + 长度,内存开销几乎为零

// 注意:这也意味着 slice 保持对原字符串的引用
// 若原字符串很大,即使只用了 slice,原字符串也无法被 GC 回收
// 解决方案:用 String(slice) 或 '' + slice 强制复制,切断对原字符串的引用
var safeCopy = String(slice); // 强制创建新的 SeqString

💡 代码解析

代码片段 含义
'A'.repeat(100000) 创建 10 万字符的长字符串,V8 存储为 SeqOneByteString(ASCII,每字符1字节,共 100KB)
longStr.slice(0, 100) 不复制内存 V8 创建 SlicedString{parent: longStr, offset: 0, length: 100},只分配极小的元数据,不复制 10 万字符
String(slice) 强制复制 调用 String() 触发字符串展平,创建新的独立 SeqString,切断对 longStr 的引用,允许 longStr 被 GC 回收
内存泄漏风险 若保留大量 SlicedString,它们各自持有对原始长字符串的引用,导致大块内存无法释放

④ 字符串驻留(String Interning)的本质

// V8 对"短字符串"和"标识符样"字符串进行驻留(Interning)
// 相同内容的字符串会共享同一内存地址
var s1 = 'hello';
var s2 = 'hello';
// V8 内部:s1 和 s2 可能指向同一个 SeqString 对象
// 但 JavaScript 语义上不暴露这个细节(===比较的是值,不是引用)

// 动态生成的字符串通常不会被驻留(除非显式调用 String.prototype.intern 等不标准 API)
var s3 = 'hel' + 'lo'; // 编译期常量折叠,可能驻留
var s4 = 'hel';
var s5 = s4 + 'lo';    // 运行时拼接,通常不驻留(ConsString)

💡 代码解析

代码片段 含义
s1 = 'hello's2 = 'hello' 字面量字符串由编译器处理,V8 在字符串哈希表中查找或创建,s1s2 可能指向同一内存地址
s3 = 'hel' + 'lo' 编译器在编译阶段将常量折叠为 'hello',结果可能驻留,与 s1/s2 共享内存
s5 = s4 + 'lo' 运行时动态拼接,V8 创建新的 ConsString,通常不参与驻留池(Intern Pool)
=== 比较 JavaScript 字符串的 === 比较的是字符串值(内容),不是引用地址,与是否驻留无关;驻留只影响内存使用,不影响语义

⑤ 高性能字符串拼接的最佳实践

// 不推荐:循环中 += 会产生 O(n²) 的内存操作(旧引擎)或大量 ConsString 树
var result1 = '';
for (var i = 0; i < 10000; i++) {
    result1 += 'item' + i + ',';  // 每次都可能产生中间字符串
}

// 推荐:先收集到数组,最后一次 join
var parts = [];
for (var i = 0; i < 10000; i++) {
    parts.push('item' + i);
}
var result2 = parts.join(','); // join 在 V8 内部高度优化,一次性分配内存并写入

// 现代 V8 对 += 也有优化,差距已缩小,但 join 的语义更清晰

💡 代码解析

代码片段 含义
循环中 result1 += 'item' + i + ',' 每次迭代:① 'item' + i 创建中间 ConsString;② + ',' 再创建一层 ConsString;③ result1 += 继续累积,最终展开时有大量中间对象
parts.push('item' + i) 每次只生成当前迭代的字符串片段,推入数组,无积累效应
parts.join(',') V8 在 C++ 层预先计算总长度,一次性分配连续内存,然后逐段写入,时间复杂度 O(n),无中间对象
现代优化说明 V8 对 += 也做了 ConsString 优化,在字符串较短时几乎等价;但当单个字符串极长(>64KB)时,join 仍明显更优

5.2 名词解释

术语 定义
字符串(String) 由零个或多个 UTF-16 代码单元组成的不可变序列
索引(Index) 字符串中每个代码单元的位置编号,从 0 开始
Unicode 码点(Code Point) Unicode 为每个字符分配的唯一编号(U+0000 ~ U+10FFFF)
charCodeAt() 返回指定位置 UTF-16 代码单元的编码(0~65535)
codePointAt() ES6,正确处理代理对,返回完整 Unicode 码点
fromCharCode() 根据 UTF-16 编码返回字符
fromCodePoint() ES6,根据 Unicode 码点返回字符,正确处理代理对
模板字面量(Template Literal) ES6,反引号语法,支持多行字符串和 ${} 插值表达式
trimStart() / trimEnd() ES2019,分别去除字符串开头/结尾的空白字符
matchAll() ES2020,返回所有正则匹配结果的迭代器
replaceAll() ES2021,替换所有匹配项(无需正则 /g 标志)
at() ES2022,支持负数索引访问字符(str.at(-1) 获取最后一个字符)

5.3 实例方法全解

var msg = 'Hello,开发者,欢迎学习 JavaScript!';

// ════════ 属性 ════════
console.log(msg.length);    // 字符个数(准确是 UTF-16 代码单元数)

// ════════ 访问字符 ════════
console.log(msg.charAt(0));     // 'H'
console.log(msg[0]);            // 'H'(ES5+)
console.log(msg.at(-1));        // '!'(ES2022,负数索引)

// ════════ 查找位置 ════════
console.log(msg.indexOf(','));       // 5(第一次出现)
console.log(msg.lastIndexOf(','));   // 9(最后一次出现)
console.log(msg.indexOf('Python'));   // -1(不存在返回 -1)
console.log(msg.includes('JavaScript'));  // true(ES6)
console.log(msg.startsWith('Hello'));     // true(ES6)
console.log(msg.endsWith('!'));          // true(ES6)

// ════════ 截取字符串 ════════
// slice(start, end):支持负数索引,推荐使用
console.log(msg.slice(0, 5));    // 'Hello'
console.log(msg.slice(-12));     // 从倒数第12个到末尾

// substring(start, end):不支持负数(负数转0),参数顺序可互换
console.log(msg.substring(0, 5)); // 'Hello'

// substr(start, length):已废弃,不推荐
console.log(msg.substr(0, 5));    // 'Hello'

// ════════ 分割 ════════
var parts = 'apple,banana,cherry';
console.log(parts.split(','));    // ['apple', 'banana', 'cherry']
console.log(parts.split('', 3)); // ['a', 'p', 'p'](限制数量)
console.log(parts.split());      // ['apple,banana,cherry']

// ════════ 大小写 ════════
console.log('Hello World'.toUpperCase()); // 'HELLO WORLD'
console.log('Hello World'.toLowerCase()); // 'hello world'

// ════════ 去空格 ════════
console.log('  hello  '.trim());       // 'hello'
console.log('  hello  '.trimStart()); // 'hello  '
console.log('  hello  '.trimEnd());   // '  hello'

// ════════ 填充(ES2017)════════
console.log('5'.padStart(3, '0'));  // '005'(数字补零)
console.log('hi'.padEnd(6, '.'));   // 'hi....'

// ════════ 重复 ════════
console.log('abc'.repeat(3));  // 'abcabcabc'

// ════════ 替换 ════════
console.log('hello world'.replace('o', '0'));    // 'hell0 world'(只替换第一个)
console.log('hello world'.replace(/o/g, '0'));   // 'hell0 w0rld'(全部,正则)
console.log('hello world'.replaceAll('o', '0')); // 'hell0 w0rld'(ES2021,全部)

> **💡 代码解析(String 扩展方法)**
>
> | 代码片段 | 含义 |
> |---------|------|
> | `trim()` | 去除首尾空格(含 `\t``\n``\r`);`trimStart/trimEnd` 只去一侧,ES2019 |
> | `'5'.padStart(3, '0')``'005'` | 在字符串**左侧**填充,达到指定长度;常用于数字/日期补零、对齐文本 |
> | `'hi'.padEnd(6, '.')``'hi....'` | 在字符串**右侧**填充;常用于格式化对齐输出(如进度条)|
> | `'abc'.repeat(3)``'abcabcabc'` | 将字符串重复 n 次并拼接,`repeat(0)` 返回空字符串 |
> | `replace('o', '0')` | 字符串参数只替换**第一个**匹配项;需替换全部须用正则加 `g` 标志 |
> | `replace(/o/g, '0')` | 正则 `/o/g``g` 标志(global)匹配**全部**,这是 ES2021 `replaceAll` 出现前的标准做法 |
> | `replaceAll('o', '0')` | ES2021,直接替换全部字符串匹配,比正则更直观(不需要记忆 `g` 标志)|
>
> **🏢 业务价值**
>
> | 场景 | 方法 | 收益 |
> |------|------|------|
> | **表单输入预处理** | `input.trim()` | 去除用户无意输入的首尾空格,防止空格导致的校验失败或数据存储不一致 |
> | **进度条/表格对齐** | `padEnd(width, ' ')` | 纯文本终端/等宽字体环境中对齐输出,如 CLI 工具的状态信息 |
> | **数字/编号格式化** | `padStart(6, '0')` | 订单号 `42``000042`,保证所有编号等宽,数据库排序和展示更规范 |
> | **模板批量生成** | `baseStr.repeat(n)` | 生成重复的占位内容(如 Skeleton 屏幕的 HTML 结构、密码强度条的填充色块)|
> | **敏感词过滤** | `replaceAll(badWord, '***')` | 批量替换用户输入中的违规词汇,比循环更简洁 |

// ════════ Unicode 操作 ════════
console.log('A'.charCodeAt(0));           // 65
console.log('a'.charCodeAt(0));           // 97
console.log('0'.charCodeAt(0));           // 48
console.log(String.fromCharCode(65));     // 'A'
console.log(String.fromCharCode(20013));  // '中'
console.log('😀'.codePointAt(0));         // 128512(ES6,正确获取码点)
console.log(String.fromCodePoint(128512)); // '😀'(ES6,正确还原字符)

💡 代码解析

代码片段 含义
msg.charAt(2) / msg[2] 两种方式等价,均返回索引 2 处的字符;charAt 兼容性更好,[] 语法更简洁
indexOf(',') vs lastIndexOf(',') 前者从左找第一个,后者从右找最后一个,两者返回索引值,不存在返回 -1
includes('JavaScript') ES6 语法,直接返回布尔值,比 indexOf(...) !== -1 更语义化
slice(0, 5)substring(0, 5) 两者结果相同,区别在于 slice 支持负数索引(slice(-3) 取最后3个),substring 不支持
split(',') 按逗号分割为数组,split('') 分割为单个字符数组,无参数则返回含整个字符串的数组
'A'.charCodeAt(0)65 获取 ASCII/Unicode 编码,常用于密码强度校验(大写 65-90,小写 97-122,数字 48-57)
'😀'.codePointAt(0)128512 emoji 占2个 UTF-16 单元,charCodeAt 只读到高代理(55357),codePointAt 才能正确读取完整码点

🏢 经典使用场景 & 业务价值

场景 方法组合 业务价值
搜索高亮 indexOf + slice 提取匹配位置前后文本 在搜索结果中高亮关键词,提升用户体验
文件类型检测 filename.slice(filename.lastIndexOf('.')) 获取后缀 前端上传文件时验证扩展名,拒绝不允许的文件类型
密码强度校验 charCodeAt 判断字符属于大写/小写/数字/特殊字符 实时反馈密码强度,引导用户设置安全密码
国际化截断(…省略) slice(0, maxLen) + '...' 列表页标题超长时截断,确保布局不被撑破
手机号脱敏 tel.slice(0,3) + '****' + tel.slice(-4) 展示 138****5678,保护用户隐私
URL 参数解析 split('&').map(...) ?a=1&b=2 解析为对象,替代 URLSearchParams 在旧环境中使用

5.4 截取方法对比表

方法 第二参数含义 负数支持 参数互换 状态
slice(start, end) 结束位置(不含) ✅ 两个参数均支持 推荐使用
substring(start, end) 结束位置(不含) ❌ 负数视为 0 ✅ 自动处理 可用
substr(start, length) 截取长度 ✅ 第一参数支持 已废弃

5.5 模板字面量(ES6)

// 基本用法:插值表达式
var name = 'Alice';
var score = 95;
var msg = `你好,${name}!你的得分是 ${score} 分,等级为 ${score >= 90 ? 'A' : 'B'}`;

// 多行字符串(无需 \n)
var html = `
  <div class="card">
    <h2>${name}</h2>
    <p>Score: ${score}</p>
  </div>
`;

// 标签模板(Tagged Template)
function highlight(strings, ...values) {
    return strings.reduce(function(acc, str, i) {
        return acc + str + (values[i] !== undefined ? '<mark>' + values[i] + '</mark>' : '');
    }, '');
}
var result = highlight`Hello ${name}, your score is ${score}!`;
// 'Hello <mark>Alice</mark>, your score is <mark>95</mark>!'

💡 代码解析

代码片段 含义
`你好,${name}!` 模板字面量语法:反引号包裹,${} 内可放任意 JS 表达式,表达式结果会被转为字符串插入
${score >= 90 ? 'A' : 'B'} ${} 内支持三元表达式、函数调用等任意表达式,比字符串拼接更简洁、可读
多行 html 变量 模板字面量原生支持多行,换行符被保留;传统字符串需要 \n 或字符串拼接来实现多行
highlight\Hello…`` 标签模板语法:highlight 函数被调用时,第一个参数是字符串片段数组(['Hello ', ', your score is ', '!']),后续参数是各插值的值(name, score
strings.reduce(...) 将字符串片段与高亮后的值交替合并,实现将所有插值用 <mark> 标签包裹的效果
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐