DAY_10 JavaScript 深度解析:原型链 · 引用类型 · 内置对象 · 数组方法全攻略(上)
参考规范: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 的关键。
目录
第一章 · 原型机制
第二章 · 内存与类型
第三章 · 内置对象
第四章 · 数组
第五章 · 进阶专题
第六章 · 速查与参考
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'步骤④:若构造函数显式返回一个对象,则以该对象为结果;否则返回我们创建的 objp1.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)
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') → truename是构造函数体内用this.name=赋值的,属于实例自身属性dog.hasOwnProperty('speak') → falsespeak在原型上,不属于实例自身,for...in会遍历到它但hasOwnProperty返回falseObject.create(null)创建无原型链的纯净对象,没有 toString、valueOf等继承方法,常用于创建安全的字典/哈希表,避免原型污染攻击🏢 经典使用场景 & 业务价值
场景 技术手段 业务收益 构建组件库 将公共方法(如 render、update)放在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.prototype→true自举循环: Function本身也是函数,是通过自身构造的,JS 引擎启动时特殊初始化此循环引用Object.__proto__ === Function.prototype→trueObject作为一个构造函数(即函数对象),其[[Prototype]]指向Function.prototype,说明Object instanceof Function为trueFunction.prototype.__proto__ === Object.prototype→trueFunction.prototype虽然是函数原型,但它本质上也是一个对象,其[[Prototype]]指向Object.prototype,链到普通对象的顶端arr instanceof Function→false数组实例 arr的原型链:arr → Array.prototype → Object.prototype → null,不经过Function.prototypeArray instanceof Function→trueArray是构造函数(函数对象),其原型链: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)
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明确指定this为dog,确保方法内部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,进一步减少停顿。
常见内存泄漏场景(理论 → 实践):
| 场景 | 原因 | 解决方案 |
|---|---|---|
| 全局变量持有大对象 | 全局变量是 GC Root,永不被回收 | 用完后显式 obj = null 断开引用 |
| 闭包持有无用外部变量 | 内部函数保持对外部作用域的引用 | 及时解除不需要的闭包引用 |
| 事件监听器未移除 | DOM 元素已移除,但 Handler 持有其引用 | removeEventListener 清理 |
| 定时器未清除 | setInterval 回调持有外部对象引用 |
clearInterval 及时清理 |
| WeakMap/WeakSet | 持有 DOM 节点引用但节点已删除 | 改用 WeakMap 存储对 DOM 的关联数据,允许 GC 回收 |
2.2 名词解释
| 术语 | 定义 | 包含类型 |
|---|---|---|
| 值类型(Value Type) | 又称原始类型(Primitive Type),存储在栈内存中,赋值时复制整个值。 | number、string、boolean、null、undefined、symbol、bigint |
| 引用类型(Reference Type) | 对象类型,实际数据存储在堆内存中,变量中存储的是堆内存地址(引用/指针)。 | Object、Array、Function、Date、RegExp、Map、Set 等 |
| 栈(Stack) | 内存中的一块区域,特点是先进后出(LIFO),存储局部变量和函数调用信息,访问速度快。 | — |
| 堆(Heap) | 内存中的另一块区域,用于存储动态分配的对象,大小不固定。 | — |
| 引用传递(Pass by Reference) | 传递的是内存地址,通过该地址可以修改堆中的同一个对象。 | — |
| 值传递(Pass by Value) | 传递的是值的副本,修改副本不影响原始值。 | — |
| 浅拷贝(Shallow Copy) | 只复制对象的第一层属性,嵌套的引用类型属性仍然共享地址。 | — |
| 深拷贝(Deep Copy) | 递归地复制对象的所有层次,完全独立的副本。 | — |
| 垃圾回收(GC) | 自动释放不再被引用的堆内存,JavaScript 主流算法为标记-清除(Mark-and-Sweep)。 | — |
2.3 内存结构精解(Mermaid)
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,修改b对a毫无影响var obj2 = obj1; obj2.age = 200;引用类型赋值只复制地址, obj2和obj1指向堆中同一个对象,通过任意一个变量修改属性,另一个也能"看到"变化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不会影响外部变量xuser.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.b和original.b指向同一个堆对象JSON.parse(JSON.stringify(...))先序列化为 JSON 字符串(值类型),再解析为全新对象,实现深拷贝;但无法处理 undefined、Function、Symbol、Date(会变字符串)、循环引用deepClone递归函数对每个属性值递归判断,若是对象则继续克隆,若是原始值则直接返回,实现真正意义的深层独立副本 structuredClone(original)ES2023 标准 API,基于结构化克隆算法,支持 Date、Map、Set、ArrayBuffer等,但不支持函数和Symbol🏢 经典使用场景 & 业务价值
场景 推荐方案 原因 复制简单配置对象(无嵌套) { ...obj }展开运算符语法简洁,性能最佳,一行代码 合并组件 props(第一层) Object.assign({}, defaults, props)合并时对每层的覆盖行为可控 保存表单编辑前的快照(含嵌套) JSON.parse(JSON.stringify(...))表单数据通常无函数,简单可靠 Redux action 传递 State 快照 structuredClone(state)原生 API,支持复杂类型,无需引入 lodash 游戏存档/撤销重做功能 自定义 deepClone或structuredClone需要完全独立副本,任何属性修改不影响历史记录
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 引擎会临时创建一个对应的包装对象(Number、String、Boolean 的实例),执行属性访问或方法调用后,立即销毁该临时对象。
// 看似在字符串上调用方法
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.custom→undefined每次访问属性都会创建全新的临时包装对象,上次赋值的属性早已随前一个临时对象销毁,因此永远是 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、-0、NaN |
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 的复杂规则,以下是简化后的决策树:
// 理解了算法,这些"奇怪"行为都有规律可循:
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 == undefined→true规范特殊处理:这两个值互相宽松相等,且与其他任何值宽松不等 null == 0→falsenull只与null/undefined宽松相等,不触发 ToNumber'' == false→true① false是布尔值,ToNumber →0;②''是字符串,ToNumber →0;③0 === 0→true[] == false→true① false→ ToNumber →0;②[]是对象,ToPrimitive →''(数组调用join())→ ToNumber →0;③0 === 0→true{} == false→false① false→0;②{}ToPrimitive →'[object Object]'→ ToNumber →NaN;③NaN === 0→false[] == ![]→true① ![]先求值:[]是 truthy,取反 →false;②false→0;③[]ToPrimitive →''→0;④0 === 0→true
最佳实践:在实际工程中,始终使用
===严格相等,避免==带来的隐式转换歧义。只有在明确需要同时匹配null和undefined时,才使用x == null这一惯用写法。
3.2 名词解释
| 术语 | 定义 |
|---|---|
| 布尔值(Boolean) | JavaScript 最基础的数据类型之一,只有 true 和 false 两个值 |
| 包装对象(Wrapper Object) | 用构造函数 new Boolean(...) 创建的对象,与布尔原始值不同,是对象类型 |
| 隐式类型转换(Type Coercion) | JavaScript 在特定上下文(条件判断、运算符)中自动将值转换为特定类型 |
| Falsy 值 | 转换为布尔值时得到 false 的值:false、0、-0、0n、""、null、undefined、NaN |
| Truthy 值 | 除 Falsy 外,所有值转换为布尔值时都得到 true,包括空对象 {}、空数组 [] |
| 短路求值(Short-circuit Evaluation) | && 遇到 Falsy 立即返回,|| 遇到 Truthy 立即返回,不继续求值 |
空值合并运算符(??) |
ES2020,仅当左侧为 null 或 undefined 时才使用右侧值,比 || 更精确 |
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空对象是引用类型,是一个有效的内存地址,任何对象(含空对象、空数组)转布尔都是 trueBoolean([])→true空数组同理,这是初学者最常见的误判: if ([])的条件永远成立typeof b3→'object'new Boolean(true)创建的是包装对象,不是原始布尔值,类型是objectif (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 && 2→2&&遇到第一个 truthy 值不停止,继续向右求值,返回最后一个被求值的结果20 && '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.nameES2020 可选链+空值合并,极大简化深层属性访问 配置项默认值合并 config.timeout ?? 5000config.timeout || 5000若 timeout明确设为0(立即超时),||会错误地替换为 5000
3.5 Falsy / Truthy 速查(Mermaid)
📌 Boolean — 知识特点总结
特点 描述 只有两个值 true和false,逻辑运算的基础包装对象陷阱 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.1 和 0.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 === -0 为 true) |
+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 === NaN→falseIEEE 754 规范的强制规定:NaN(Not a Number)是不可比较的,任何含 NaN 的比较( </>/==/===)均返回falsetypeof NaN→'number'历史遗留 bug:NaN 是数字类型(数值空间中的特殊值), typeof无法区分正常数字和 NaNisNaN('hello')→true全局 isNaN先调用 ToNumber:'hello'→NaN,再判断是否为 NaN,结果true;容易误判字符串Number.isNaN('hello')→falseES6 新版:不做类型转换,只对确实是 NaN值的情况返回true;字符串不是 NaN,返回false0 / 0→NaN数学上的"不定式"(0/0 没有确定答案),IEEE 754 规定结果为 NaN undefined + 1→NaNundefinedToNumber 得到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 === -0→true===使用 Abstract Equality,不区分正负零;但 IEEE 754 内存中它们的编码不同(符号位 0 vs 1)Object.is(+0, -0)→falseES6 的 Object.is使用 SameValue 算法,能区分+0和-0;这是判断精确相等的最可靠方式1 / +0→Infinity,1 / -0→-Infinity除以正零得正无穷,除以负零得负无穷,通过除法可以检测负零 (-0).toString()→'0'toString规范要求忽略负零的符号;同样JSON.stringify(-0)也返回'0'
实际影响:负零在物理方向(如速度的方向)建模时有意义,但在日常开发中几乎不会遇到。
Object.is()是 ES6 提供的精确相等比较,能正确区分+0/-0和NaN/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.9CSS 颜色值生成 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_VALUE≈1.8e+308JS 能表示的最大正数,超过此值运算结果变为 InfinityNumber.MAX_SAFE_INTEGER=2^53-1安全整数上限,超过此值的整数运算不保证精确(如 9007199254740992 + 1 === 9007199254740992)Number.EPSILON≈2.22e-16机器精度,用于判断两个浮点数是否"足够接近": Math.abs(a-b) < Number.EPSILONNumber.isNaN(NaN)→true,Number.isNaN('NaN')→falseNumber.isNaN不做类型转换,比全局isNaN更严格可靠;全局isNaN('NaN')先转数字再判断,会返回trueNumber.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的精度 bugCSS 值解析 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.30000000000000004IEEE 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.length→2emoji 😀的 Unicode 码点 U+1F600 超过 U+FFFF,需要两个 UTF-16 代码单元(代理对)表示,所以length为 2charCodeAt(0)→55357读取高代理(High Surrogate)的编码,范围 0xD800~0xDBFF,单独使用无意义charCodeAt(1)→56832读取低代理(Low Surrogate)的编码,范围 0xDC00~0xDFFF,单独使用无意义codePointAt(0)→128512ES6 正确处理代理对,将高低代理合并计算出真实的 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 + cV8 不立即分配连续内存,而是创建 ConsString树节点:{left:'Hello', right:{left:', ', right:'World'}},延迟展开懒求值的时机 当需要逐字符遍历(如 console.log、正则匹配、slice)时,V8 才会将树"展开"(flatten)为连续内存的SeqStringO(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 在字符串哈希表中查找或创建, s1和s2可能指向同一内存地址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(',')vslastIndexOf(',')前者从左找第一个,后者从右找最后一个,两者返回索引值,不存在返回 -1includes('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)→128512emoji 占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>标签包裹的效果
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)