参考规范 :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 核心对象模型发展史
目录
- [前言:理解 JavaScript 的设计哲学](#前言:理解 JavaScript 的设计哲学)
第一章 · 原型机制
- [1 原型与原型链](#1 原型与原型链)
- [1.1 理论基础:为什么需要原型?](#1.1 理论基础:为什么需要原型?)
- [1.2 名词解释](#1.2 名词解释)
- [1.3
new操作符的底层执行过程](#1.3 new 操作符的底层执行过程) - [1.4 三角关系图解](#1.4 三角关系图解)
- [1.5 原型链结构(Mermaid)](#1.5 原型链结构(Mermaid))
- [1.6 核心代码演示](#1.6 核心代码演示)
- [1.7 Function 与 Object 的特殊关系](#1.7 Function 与 Object 的特殊关系)
- [1.8 原型链属性查找流程(Mermaid)](#1.8 原型链属性查找流程(Mermaid))
- [1.9 原型链的性能注意事项](#1.9 原型链的性能注意事项)
第二章 · 内存与类型
- [2 值类型与引用类型](#2 值类型与引用类型)
- [2.1 理论基础:内存模型与类型系统](#2.1 理论基础:内存模型与类型系统)
- [2.2 名词解释](#2.2 名词解释)
- [2.3 内存结构精解(Mermaid)](#2.3 内存结构精解(Mermaid))
- [2.4 四大区别对照表](#2.4 四大区别对照表)
- [2.5 不可变性(Immutability)深入理解](#2.5 不可变性(Immutability)深入理解)
- [2.6 课堂案例精解](#2.6 课堂案例精解)
- [2.7 函数参数传递的深层理解](#2.7 函数参数传递的深层理解)
- [2.8 浅拷贝与深拷贝](#2.8 浅拷贝与深拷贝)
- [2.9 完整可运行示例](#2.9 完整可运行示例)
第三章 · 内置对象
- [3 内置构造函数 Boolean](#3 内置构造函数 Boolean)
- [3.1 理论基础:类型包装机制](#3.1 理论基础:类型包装机制)
- [3.2 名词解释](#3.2 名词解释)
- [3.3 三种创建方式](#3.3 三种创建方式)
- [3.4 布尔运算符深度解析](#3.4 布尔运算符深度解析)
- [3.5 Falsy / Truthy 速查(Mermaid)](#3.5 Falsy / Truthy 速查(Mermaid))
- [4 内置构造函数 Number](#4 内置构造函数 Number)
- [4.1 理论基础:IEEE 754 双精度浮点数](#4.1 理论基础:IEEE 754 双精度浮点数)
- [4.2 名词解释](#4.2 名词解释)
- [4.3 实例方法](#4.3 实例方法)
- [4.4 静态属性与方法](#4.4 静态属性与方法)
- [4.5 浮点数精度问题与解决方案](#4.5 浮点数精度问题与解决方案)
- [4.6 完整可运行示例](#4.6 完整可运行示例)
- [5 内置构造函数 String](#5 内置构造函数 String)
- [5.1 理论基础:字符串的编码与不可变性](#5.1 理论基础:字符串的编码与不可变性)
- [5.2 名词解释](#5.2 名词解释)
- [5.3 实例方法全解](#5.3 实例方法全解)
- [5.4 截取方法对比表](#5.4 截取方法对比表)
- [5.5 模板字面量(ES6)](#5.5 模板字面量(ES6))
- [5.6 经典应用场景](#5.6 经典应用场景)
- [5.7 完整可运行示例](#5.7 完整可运行示例)
- [6 内置对象 Math](#6 内置对象 Math)
- [6.1 理论基础:Math 不是构造函数](#6.1 理论基础:Math 不是构造函数)
- [6.2 名词解释](#6.2 名词解释)
- [6.3 方法速查表](#6.3 方法速查表)
- [6.4 取随机数公式推导](#6.4 取随机数公式推导)
- [6.5 取整方法对比(负数行为差异)](#6.5 取整方法对比(负数行为差异))
- [6.6 完整可运行示例(随机颜色生成器)](#6.6 完整可运行示例(随机颜色生成器))
- [7 内置构造函数 Date](#7 内置构造函数 Date)
- [7.1 理论基础:时间的表示与时区](#7.1 理论基础:时间的表示与时区)
- [7.2 名词解释](#7.2 名词解释)
- [7.3 实例化方式全解](#7.3 实例化方式全解)
- [7.4 获取与设置方法](#7.4 获取与设置方法)
- [7.5 日期格式化与工具函数](#7.5 日期格式化与工具函数)
- [7.6 完整可运行示例(数字时钟)](#7.6 完整可运行示例(数字时钟))
第四章 · 数组
- [8 数组 Array:修改器方法](#8 数组 Array:修改器方法)
- [8.1 理论基础:数组的内存模型](#8.1 理论基础:数组的内存模型)
- [8.2 名词解释](#8.2 名词解释)
- [8.3 方法详解与对比](#8.3 方法详解与对比)
- [8.4 push/pop vs unshift/shift 性能差异](#8.4 push/pop vs unshift/shift 性能差异)
- [8.5 sort 比较函数原理](#8.5 sort 比较函数原理)
- [8.6 完整可运行示例(待办列表)](#8.6 完整可运行示例(待办列表))
- [9 数组 Array:访问器方法与迭代方法](#9 数组 Array:访问器方法与迭代方法)
- [9.1 理论基础:函数式编程思想](#9.1 理论基础:函数式编程思想)
- [9.2 名词解释](#9.2 名词解释)
- [9.3 访问器方法详解](#9.3 访问器方法详解)
- [9.4 迭代方法详解](#9.4 迭代方法详解)
- [9.5 reduce 的执行过程图解(Mermaid)](#9.5 reduce 的执行过程图解(Mermaid))
- [9.6 迭代方法选择决策树(Mermaid)](#9.6 迭代方法选择决策树(Mermaid))
- [9.7 完整可运行示例(商品筛选器)](#9.7 完整可运行示例(商品筛选器))
第五章 · 进阶专题
- [10 经典笔试题精讲](#10 经典笔试题精讲)
- [10.1 原型链经典题一:方法归属判断](#10.1 原型链经典题一:方法归属判断)
- [10.2 原型链经典题二:原型替换](#10.2 原型链经典题二:原型替换)
- [10.3 原型链经典题三:f 和 F 的原型链(综合)](#10.3 原型链经典题三:f 和 F 的原型链(综合))
- [10.4 引用类型经典题:函数参数传递综合](#10.4 引用类型经典题:函数参数传递综合)
- [10.5 值类型经典练习集](#10.5 值类型经典练习集)
- [11 综合实战案例](#11 综合实战案例)
- [11.1 驼峰命名转换(作业一)](#11.1 驼峰命名转换(作业一))
- [11.2 字符串翻转(作业二)](#11.2 字符串翻转(作业二))
- [11.3 随机抽取(作业三)](#11.3 随机抽取(作业三))
- [11.4 日期格式化输出(作业四)](#11.4 日期格式化输出(作业四))
第六章 · 速查与参考
- [12 知识点总结与速查表](#12 知识点总结与速查表)
- [12.1 JavaScript 对象模型总览(Mermaid 思维导图)](#12.1 JavaScript 对象模型总览(Mermaid 思维导图))
- [12.2 各类型 typeof / instanceof 速查](#12.2 各类型 typeof / instanceof 速查)
- [12.3 数组方法是否改变原数组速查](#12.3 数组方法是否改变原数组速查)
- [12.4 常见反模式与最佳实践](#12.4 常见反模式与最佳实践)
- [12.5 经典使用场景总结](#12.5 经典使用场景总结)
- 附录:参考资料
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 的深层继承树更灵活。
js
// 类继承的问题:层次固化,难以拆解
// 假设需求:会飞的汽车。在类继承中,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 看起来像类继承语言,但它只是原型链的语法糖,底层机制完全没有变化:
js
// 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 了一个对象,则返回那个对象
js
// 手动模拟 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)
proto
proto
proto
prototype
proto
proto
prototype
proto
实例对象 f
new F()
F.prototype
(F的原型对象)
Object.prototype
(顶层原型)
null ← 链的终点
构造函数 F
Function.prototype
Object 构造函数
1.6 核心代码演示
js
// ① 自定义构造函数 ------ 将方法添加到原型上(节省内存)
// 若将方法写在构造函数体内,每次 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 原型链中最令人困惑也最精妙的部分:
js
// 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)
是
否
否,已到达 null
是
是
否
访问 obj.prop
obj 自身
\[OwnProperty\]\] 有 prop? ✅ 返回 obj.prop obj.\[\[Prototype\]
不为 null?
❌ 返回 undefined
obj.[[Prototype]]
有 prop?
✅ 返回该原型上的 prop
继续向上:obj = obj.[[Prototype]]
1.9 原型链的性能注意事项
js
// 访问越深的原型链属性,性能越低
// 引擎需要遍历更多层次
// 好的做法:经常访问的属性,缓存到局部变量
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,进一步减少停顿。
对象分配
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),存储在栈内存中,赋值时复制整个值。 | 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)
堆内存(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)深入理解
js
// ════════ 字符串的不可变性 ════════
// 字符串是值类型,每次"修改"实际上创建了新字符串
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 课堂案例精解
js
// ════════ 值类型:互不影响 ════════
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)**。
js
// 值类型参数:函数内修改不影响外部
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 浅拷贝与深拷贝
js
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 完整可运行示例
html
<!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 的实例),执行属性访问或方法调用后,立即销毁该临时对象。
js
// 看似在字符串上调用方法
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 的复杂规则,以下是简化后的决策树:
是
否
两者都是 null/undefined
一个是 null,另一个不是 null/undefined
否
是
否
是
否
是
否
是
否
x == y
类型相同?
使用严格相等 === 比较
其中一个是 null 或 undefined?
✅ true
❌ false
其中一个是数字?
将另一个 ToNumber 后再比较
其中一个是字符串?
将字符串 ToNumber 后再比较
其中一个是布尔值?
将布尔值 ToNumber(1/0) 后再比较
一个是对象,另一个是原始值?
对象 ToPrimitive 后再比较
❌ false
js
// 理解了算法,这些"奇怪"行为都有规律可循:
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 立即返回,` |
空值合并运算符(??) |
ES2020,仅当左侧为 null 或 undefined 时才使用右侧值,比 ` |
3.3 三种创建方式
js
// 方式一:直接量(推荐日常使用)
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 布尔运算符深度解析
js
// ════════ && (逻辑与):短路求值 ════════
// 遇到 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是有效值被保留;这是 `!!obj第一个 !将值转为布尔值并取反,第二个!再次取反,最终得到值对应的布尔值,等价于Boolean(obj)🏢 经典使用场景 & 业务价值
场景 推荐写法 不推荐写法 说明 渲染用户名称(可能为空) name ?? '匿名用户'`name 权限按钮的条件渲染 isAdmin && <AdminButton>if(isAdmin){...}React 中 &&短路渲染是最简洁的条件渲染方式表单字段有效性检测 !!value.trim()value.trim().length > 0!!快速将字符串转为布尔值,用于表单验证API 响应数据安全访问 data?.user?.name ?? '未知'data && data.user && data.user.nameES2020 可选链+空值合并,极大简化深层属性访问 配置项默认值合并 config.timeout ?? 5000`config.timeout
3.5 Falsy / Truthy 速查(Mermaid)
✅ Truthy 值(其余所有)
true
非零数字(正/负)
非空字符串(含 '0' 和 'false')
{} 空对象 ← 常见陷阱
\] 空数组 ← 常见陷阱 函数 Infinity / -Infinity ❌ Falsy 值(共 8 个) false 0(数字零) -0(负零) 0n(BigInt 零) ''(空字符串) null undefined NaN *** ** * ** *** > #### 📌 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 ```js // 用 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 的特殊性** ```js // 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` → `false` | 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 / 0` → `NaN` | 数学上的"不定式"(0/0 没有确定答案),IEEE 754 规定结果为 NaN | > | `undefined + 1` → `NaN` | `undefined` ToNumber 得到 `NaN`,任何数与 NaN 的算术运算结果仍为 NaN(NaN 的传染性) | **⑤ 正零与负零** ```js // 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)` → `false` | ES6 的 `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 实例方法 ```js 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 静态属性与方法 ```js // 静态属性 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+308` | JS 能表示的最大正数,超过此值运算结果变为 `Infinity` | > | `Number.MAX_SAFE_INTEGER` = `2^53-1` | 安全整数上限,超过此值的整数运算不保证精确(如 `9007199254740992 + 1 === 9007199254740992`) | > | `Number.EPSILON` ≈ `2.22e-16` | 机器精度,用于判断两个浮点数是否"足够接近":`Math.abs(a-b) < Number.EPSILON` | > | `Number.isNaN(NaN)` → `true`,`Number.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 浮点数精度问题与解决方案 ```js // 经典问题: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 完整可运行示例 ```html
Number 构造函数全解析
IEEE 754 精度演示
toFixed ------ 格式化小数
toString ------ 进制转换
Number 静态属性
${name}
Score: ${score}