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 的关键。
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' 步骤④:若构造函数显式返回一个对象,则以该对象为结果;否则返回我们创建的 obj
p1.toString() 能调用 p1[[Prototype]] 指向 Point.prototype,查找 toString 时从原型上找到

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

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

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

1.4 三角关系图解

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

1.5 原型链结构(Mermaid)

proto
proto
proto
prototype
proto
proto
prototype
proto
实例对象 f

new F()
F.prototype

(F的原型对象)
Object.prototype

(顶层原型)
null ← 链的终点
构造函数 F
Function.prototype
Object 构造函数

1.6 核心代码演示

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') → true name 是构造函数体内用 this.name= 赋值的,属于实例自身属性
dog.hasOwnProperty('speak') → false speak 在原型上,不属于实例自身,for...in 会遍历到它但 hasOwnProperty 返回 false
Object.create(null) 创建无原型链 的纯净对象,没有 toStringvalueOf 等继承方法,常用于创建安全的字典/哈希表,避免原型污染攻击

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

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

1.7 Function 与 Object 的特殊关系

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

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

鸡与蛋的哲学问题:
Object 是函数,所以 Object.__proto__ === Function.prototype
Function.prototype 是对象,所以 Function.prototype.__proto__ === Object.prototype

这两者互为依存,是 JS 引擎在初始化时通过"特殊引导"建立的循环引用,无法用纯粹的 JS 代码描述。

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



否,已到达 null



访问 obj.prop
obj 自身

\[OwnProperty\]\] 有 prop? ✅ 返回 obj.prop obj.\[\[Prototype\]

不为 null?
❌ 返回 undefined
obj.[[Prototype]]

有 prop?
✅ 返回该原型上的 prop
继续向上:obj = obj.[[Prototype]]

1.9 原型链的性能注意事项

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 明确指定 thisdog,确保方法内部 this.name 取到正确值
高频循环中的 dog.speak() 每次调用都触发原型链查找(尽管 V8 有内联缓存优化,但动态属性结构仍会致缓存失效)

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


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

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

2 值类型与引用类型

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

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

栈内存(Stack Memory)

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

堆内存(Heap Memory)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

是,晋升
Major GC (Mark-Sweep)
Major GC (Mark-Compact)
JS 代码执行
新生代 From Space
存活 2 次?
老年代
释放无引用对象
整理碎片(选择性)

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

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

2.2 名词解释

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

2.3 内存结构精解(Mermaid)

堆内存(Heap)
调用栈(Call Stack)
引用
初始引用
obj2 重新赋值后
函数帧 main()
a = 100(直接存储值)
b = 200(直接存储值)
obj1 → 0x4A2F(存储堆地址)
obj2 → 0x4A2F(同一地址!)
地址 0x4A2F

{ age: 100 }
地址 0x6B31

{ age: 400 }(新对象)

2.4 四大区别对照表

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

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

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

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

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

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 不会影响外部变量 x
user.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.boriginal.b 指向同一个堆对象
JSON.parse(JSON.stringify(...)) 先序列化为 JSON 字符串(值类型),再解析为全新对象,实现深拷贝;但无法处理 undefinedFunctionSymbolDate(会变字符串)、循环引用
deepClone 递归函数 对每个属性值递归判断,若是对象则继续克隆,若是原始值则直接返回,实现真正意义的深层独立副本
structuredClone(original) ES2023 标准 API,基于结构化克隆算法,支持 DateMapSetArrayBuffer 等,但不支持函数和 Symbol

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

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

2.9 完整可运行示例

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 引擎会临时创建一个对应的包装对象(NumberStringBoolean 的实例),执行属性访问或方法调用后,立即销毁该临时对象。

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.customundefined 每次访问属性都会创建全新 的临时包装对象,上次赋值的属性早已随前一个临时对象销毁,因此永远是 undefined
深层理论:ECMAScript 规范的类型转换算法

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

① ToBoolean 算法(规范 §7.1.2)

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

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

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

② ToNumber 算法(规范 §7.1.4)

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

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

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

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


两者都是 null/undefined
一个是 null,另一个不是 null/undefined









x == y
类型相同?
使用严格相等 === 比较
其中一个是 null 或 undefined?
✅ true
❌ false
其中一个是数字?
将另一个 ToNumber 后再比较
其中一个是字符串?
将字符串 ToNumber 后再比较
其中一个是布尔值?
将布尔值 ToNumber(1/0) 后再比较
一个是对象,另一个是原始值?
对象 ToPrimitive 后再比较
❌ false

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

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

3.2 名词解释

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

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 空对象是引用类型,是一个有效的内存地址,任何对象(含空对象、空数组)转布尔都是 true
Boolean([])true 空数组同理,这是初学者最常见的误判:if ([]) 的条件永远成立
typeof b3'object' new Boolean(true) 创建的是包装对象 ,不是原始布尔值,类型是 object
if (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 && 22 && 遇到第一个 truthy 值不停止,继续向右求值,返回最后一个 被求值的结果 2
0 && 'hello'0 && 遇到第一个 falsy 值立即返回它,后续不再求值(短路),所以 'hello' 根本不被执行
user && user.name && console.log(user.name) 链式安全访问模式:先确认 user 存在,再确认 user.name 存在,最后执行操作,等价于 user?.name(ES2020 可选链)
`''
0 ?? 'default'0 ?? 只对 null/undefined 触发,0 是有效值被保留;这是 `
!!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.name ES2020 可选链+空值合并,极大简化深层属性访问
配置项默认值合并 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 内置构造函数演示

Number 构造函数全解析

IEEE 754 精度演示

⚠️ 理论:0.1 和 0.2 在二进制中是无限循环小数,存储时被截断,累加产生精度误差
点击查看

toFixed ------ 格式化小数

结果将在此显示

toString ------ 进制转换

结果将在此显示

Number 静态属性

点击查看
``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/71170bfd73be45e5970a1b5956a7b014.png) *** ** * ** *** > #### 📌 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)。 这导致了一个有趣的问题: ```js 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` → `2` | emoji `😀` 的 Unicode 码点 U+1F600 超过 U+FFFF,需要两个 UTF-16 代码单元(代理对)表示,所以 `length` 为 2 | > | `charCodeAt(0)` → `55357` | 读取高代理(High Surrogate)的编码,范围 `0xD800~0xDBFF`,单独使用无意义 | > | `charCodeAt(1)` → `56832` | 读取低代理(Low Surrogate)的编码,范围 `0xDC00~0xDFFF`,单独使用无意义 | > | `codePointAt(0)` → `128512` | ES6 正确处理代理对,将高低代理合并计算出真实的 Unicode 码点 `U+1F600`(即十进制 128512) | **字符串不可变性(Immutability)**:字符串一旦创建,其内容不能被修改。所有字符串方法都返回新字符串,原始字符串保持不变。这是字符串作为值类型的本质体现,也使字符串可以安全地共享(字符串驻留/intern 优化)。 > **字符串驻留(String Interning)** :相同的字符串字面量在引擎内部可能指向同一块内存,这是引擎的优化手段,解释了为什么 `'hello' === 'hello'` 是 `true`(值类型按值比较,且引擎可能复用同一对象)。 ##### 深层理论:V8 引擎的字符串内部表示 V8 并非用单一结构存储所有字符串,而是根据字符串的创建方式和使用场景,选择最高效的内部表示。理解这些机制有助于编写高性能字符串处理代码。 **① V8 字符串的五种内部类型** | 类型 | 中文名 | 适用场景 | 内存特点 | |--------------------|----------|----------------------------|-------------------| | `SeqOneByteString` | 顺序单字节字符串 | 纯 ASCII 字符串(每个字符一字节) | 内存最紧凑,访问最快 | | `SeqTwoByteString` | 顺序双字节字符串 | 含非 ASCII 字符(每个字符两字节) | 标准 UTF-16 | | `ConsString` | 拼接字符串 | `a + b + c` 字符串连接 | 树形结构,延迟展开 | | `SlicedString` | 切片字符串 | `str.slice(start, end)` 截取 | 保存原字符串引用+偏移,无内存拷贝 | | `ExternalString` | 外部字符串 | 从 C++ 层传入的字符串 | 内存由宿主环境管理 | **② ConsString(拼接字符串树)的关键影响** ```js // 字符串拼接不会立即复制内存,V8 创建一个"树节点" var a = 'Hello'; var b = ', '; var c = 'World'; var result = a + b + c; // V8 内部:ConsString { left: 'Hello', right: ConsString { left: ', ', right: 'World' } } // 实际字符串内容是懒求值的------只有真正需要遍历字符时才展开(如 console.log) // 影响:大量字符串拼接时,V8 不会每次都复制内存(避免 O(n²) 的内存操作) // 但最终遍历时需要展开树,若树过深会有额外开销 ``` > **💡 代码解析** > > | 代码片段 | 含义 | > |--------------------------|--------------------------------------------------------------------------------------------| > | `var result = a + b + c` | V8 不立即分配连续内存,而是创建 `ConsString` 树节点:`{left:'Hello', right:{left:', ', right:'World'}}`,延迟展开 | > | 懒求值的时机 | 当需要逐字符遍历(如 `console.log`、正则匹配、`slice`)时,V8 才会将树"展开"(flatten)为连续内存的 `SeqString` | > | O(n²) 问题 | 若用旧引擎或不支持 ConsString 的环境,每次 `+=` 都复制整个字符串,10000次拼接总操作量为 1+2+...+10000 = O(n²) | **③ SlicedString(切片字符串)的零拷贝特性** ```js 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)的本质** ```js // 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 字符串的 `===` 比较的是**字符串值(内容)**,不是引用地址,与是否驻留无关;驻留只影响内存使用,不影响语义 | **⑤ 高性能字符串拼接的最佳实践** ```js // 不推荐:循环中 += 会产生 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 实例方法全解 ```js var msg = 'Hello,开发者,欢迎学习 JavaScript!'; // ════════ 属性 ════════ console.log(msg.length); // 字符个数(准确是 UTF-16 代码单元数) // ════════ 访问字符 ════════ console.log(msg.charAt(0)); // 'H' console.log(msg[0]); // 'H'(ES5+) console.log(msg.at(-1)); // '!'(ES2022,负数索引) // ════════ 查找位置 ════════ console.log(msg.indexOf(',')); // 5(第一次出现) console.log(msg.lastIndexOf(',')); // 9(最后一次出现) console.log(msg.indexOf('Python')); // -1(不存在返回 -1) console.log(msg.includes('JavaScript')); // true(ES6) console.log(msg.startsWith('Hello')); // true(ES6) console.log(msg.endsWith('!')); // true(ES6) // ════════ 截取字符串 ════════ // slice(start, end):支持负数索引,推荐使用 console.log(msg.slice(0, 5)); // 'Hello' console.log(msg.slice(-12)); // 从倒数第12个到末尾 // substring(start, end):不支持负数(负数转0),参数顺序可互换 console.log(msg.substring(0, 5)); // 'Hello' // substr(start, length):已废弃,不推荐 console.log(msg.substr(0, 5)); // 'Hello' // ════════ 分割 ════════ var parts = 'apple,banana,cherry'; console.log(parts.split(',')); // ['apple', 'banana', 'cherry'] console.log(parts.split('', 3)); // ['a', 'p', 'p'](限制数量) console.log(parts.split()); // ['apple,banana,cherry'] // ════════ 大小写 ════════ console.log('Hello World'.toUpperCase()); // 'HELLO WORLD' console.log('Hello World'.toLowerCase()); // 'hello world' // ════════ 去空格 ════════ console.log(' hello '.trim()); // 'hello' console.log(' hello '.trimStart()); // 'hello ' console.log(' hello '.trimEnd()); // ' hello' // ════════ 填充(ES2017)════════ console.log('5'.padStart(3, '0')); // '005'(数字补零) console.log('hi'.padEnd(6, '.')); // 'hi....' // ════════ 重复 ════════ console.log('abc'.repeat(3)); // 'abcabcabc' // ════════ 替换 ════════ console.log('hello world'.replace('o', '0')); // 'hell0 world'(只替换第一个) console.log('hello world'.replace(/o/g, '0')); // 'hell0 w0rld'(全部,正则) console.log('hello world'.replaceAll('o', '0')); // 'hell0 w0rld'(ES2021,全部) > **💡 代码解析(String 扩展方法)** > > | 代码片段 | 含义 | > |---------|------| > | `trim()` | 去除首尾空格(含 `\t`、`\n`、`\r`);`trimStart/trimEnd` 只去一侧,ES2019 | > | `'5'.padStart(3, '0')` → `'005'` | 在字符串**左侧**填充,达到指定长度;常用于数字/日期补零、对齐文本 | > | `'hi'.padEnd(6, '.')` → `'hi....'` | 在字符串**右侧**填充;常用于格式化对齐输出(如进度条)| > | `'abc'.repeat(3)` → `'abcabcabc'` | 将字符串重复 n 次并拼接,`repeat(0)` 返回空字符串 | > | `replace('o', '0')` | 字符串参数只替换**第一个**匹配项;需替换全部须用正则加 `g` 标志 | > | `replace(/o/g, '0')` | 正则 `/o/g` 的 `g` 标志(global)匹配**全部**,这是 ES2021 `replaceAll` 出现前的标准做法 | > | `replaceAll('o', '0')` | ES2021,直接替换全部字符串匹配,比正则更直观(不需要记忆 `g` 标志)| > > **🏢 业务价值** > > | 场景 | 方法 | 收益 | > |------|------|------| > | **表单输入预处理** | `input.trim()` | 去除用户无意输入的首尾空格,防止空格导致的校验失败或数据存储不一致 | > | **进度条/表格对齐** | `padEnd(width, ' ')` | 纯文本终端/等宽字体环境中对齐输出,如 CLI 工具的状态信息 | > | **数字/编号格式化** | `padStart(6, '0')` | 订单号 `42` → `000042`,保证所有编号等宽,数据库排序和展示更规范 | > | **模板批量生成** | `baseStr.repeat(n)` | 生成重复的占位内容(如 Skeleton 屏幕的 HTML 结构、密码强度条的填充色块)| > | **敏感词过滤** | `replaceAll(badWord, '***')` | 批量替换用户输入中的违规词汇,比循环更简洁 | // ════════ Unicode 操作 ════════ console.log('A'.charCodeAt(0)); // 65 console.log('a'.charCodeAt(0)); // 97 console.log('0'.charCodeAt(0)); // 48 console.log(String.fromCharCode(65)); // 'A' console.log(String.fromCharCode(20013)); // '中' console.log('😀'.codePointAt(0)); // 128512(ES6,正确获取码点) console.log(String.fromCodePoint(128512)); // '😀'(ES6,正确还原字符) ``` > **💡 代码解析** > > | 代码片段 | 含义 | > |--------------------------------------|-------------------------------------------------------------------------| > | `msg.charAt(2)` / `msg[2]` | 两种方式等价,均返回索引 2 处的字符;`charAt` 兼容性更好,`[]` 语法更简洁 | > | `indexOf(',')` vs `lastIndexOf(',')` | 前者从左找第一个,后者从右找最后一个,两者返回**索引值** ,不存在返回 `-1` | > | `includes('JavaScript')` | ES6 语法,直接返回布尔值,比 `indexOf(...) !== -1` 更语义化 | > | `slice(0, 5)` 与 `substring(0, 5)` | 两者结果相同,区别在于 `slice` 支持负数索引(`slice(-3)` 取最后3个),`substring` 不支持 | > | `split(',')` | 按逗号分割为数组,`split('')` 分割为单个字符数组,无参数则返回含整个字符串的数组 | > | `'A'.charCodeAt(0)` → `65` | 获取 ASCII/Unicode 编码,常用于密码强度校验(大写 65-90,小写 97-122,数字 48-57) | > | `'😀'.codePointAt(0)` → `128512` | emoji 占2个 UTF-16 单元,`charCodeAt` 只读到高代理(55357),`codePointAt` 才能正确读取完整码点 | > > **🏢 经典使用场景 \& 业务价值** > > | 场景 | 方法组合 | 业务价值 | > |------------------|--------------------------------------------------|-----------------------------------------------| > | **搜索高亮** | `indexOf` + `slice` 提取匹配位置前后文本 | 在搜索结果中高亮关键词,提升用户体验 | > | **文件类型检测** | `filename.slice(filename.lastIndexOf('.'))` 获取后缀 | 前端上传文件时验证扩展名,拒绝不允许的文件类型 | > | **密码强度校验** | `charCodeAt` 判断字符属于大写/小写/数字/特殊字符 | 实时反馈密码强度,引导用户设置安全密码 | > | **国际化截断(...省略)** | `slice(0, maxLen) + '...'` | 列表页标题超长时截断,确保布局不被撑破 | > | **手机号脱敏** | `tel.slice(0,3) + '****' + tel.slice(-4)` | 展示 `138****5678`,保护用户隐私 | > | **URL 参数解析** | `split('&').map(...)` | 将 `?a=1&b=2` 解析为对象,替代 URLSearchParams 在旧环境中使用 | #### 5.4 截取方法对比表 | 方法 | 第二参数含义 | 负数支持 | 参数互换 | 状态 | |-------------------------|----------|-----------|--------|------| | `slice(start, end)` | 结束位置(不含) | ✅ 两个参数均支持 | ❌ | 推荐使用 | | `substring(start, end)` | 结束位置(不含) | ❌ 负数视为 0 | ✅ 自动处理 | 可用 | | `substr(start, length)` | 截取长度 | ✅ 第一参数支持 | --- | 已废弃 | #### 5.5 模板字面量(ES6) ```js // 基本用法:插值表达式 var name = 'Alice'; var score = 95; var msg = `你好,${name}!你的得分是 ${score} 分,等级为 ${score >= 90 ? 'A' : 'B'}。`; // 多行字符串(无需 \n) var html = `

${name}

Score: ${score}

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

相关推荐
zmzb01031 小时前
Python课后习题训练记录Day122
开发语言·python
陳土2 小时前
R语言jiebaR包使用摘要
开发语言·r语言
Evand J2 小时前
【MATLAB】多无人机编队协同控制与三维航迹规划仿真。障碍物斥力避障,输出编队误差、控制输入、三维轨迹等
开发语言·matlab·无人机
froginwe112 小时前
jQuery UI 小部件方法调用
开发语言
信奥胡老师2 小时前
B3930 [GESP202312 五级] 烹饪问题
开发语言·数据结构·c++·学习·算法
JAVA学习通2 小时前
安脉盛 软件后端开发实习面经
java·开发语言
sycmancia2 小时前
Qt——Qt中的事件处理(一)
开发语言·qt
Halo_tjn2 小时前
Java IO流文件操作
java·开发语言
折哥的程序人生 · 物流技术专研2 小时前
《Java 100 天进阶之路》第23篇:缓冲区数据结构 ByteBuffer
java·开发语言·数据结构·后端·面试·求职招聘