DAY_10 JavaScript 深度解析:原型链 · 引用类型 · 内置对象 · 数组方法全攻略(下)

5.6 经典应用场景

js 复制代码
// 场景一:驼峰命名转换
function toCamelCase(str) {
    return str.split('-').map(function(word, index) {
        if (index === 0) return word;
        return word.charAt(0).toUpperCase() + word.slice(1);
    }).join('');
}
console.log(toCamelCase('get-element-by-id'));      // getElementById
console.log(toCamelCase('background-color'));        // backgroundColor

// 场景二:翻转字符串(利用数组 reverse 方法)
function reverseString(str) {
    return str.split('').reverse().join('');
}
console.log(reverseString('Hello'));      // olleH

// 场景三:格式化日期输出(padStart 补零)
function formatDate(date) {
    var y   = date.getFullYear();
    var m   = String(date.getMonth() + 1).padStart(2, '0');
    var d   = String(date.getDate()).padStart(2, '0');
    var h   = String(date.getHours()).padStart(2, '0');
    var min = String(date.getMinutes()).padStart(2, '0');
    var s   = String(date.getSeconds()).padStart(2, '0');
    return `${y}-${m}-${d} ${h}:${min}:${s}`;
}

// 场景四:简单邮箱格式验证
function isValidEmail(email) {
    return email.includes('@') &&
           email.indexOf('@') > 0 &&
           email.lastIndexOf('.') > email.indexOf('@') &&
           email.lastIndexOf('.') < email.length - 1;
}

// 场景五:URL 参数解析
function parseQueryString(query) {
    if (!query) return {};
    return query.replace(/^\?/, '').split('&').reduce(function(acc, pair) {
        var parts = pair.split('=');
        acc[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1] || '');
        return acc;
    }, {});
}
console.log(parseQueryString('?name=Alice&age=25&city=Shanghai'));
// { name: 'Alice', age: '25', city: 'Shanghai' }

💡 代码解析

代码片段 含义
str.split('-').map(fn).join('') 三步驼峰转换:① split('-') 按连字符切割,② map 对每个单词首字母大写(跳过第一个),③ join('') 无分隔符拼合
word.charAt(0).toUpperCase() + word.slice(1) 取首字母大写,拼接剩余部分。slice(1) 从索引1开始截取,即去掉首字母的其余部分
String(n).padStart(2, '0') 先将数字转字符串,再用 '0' 填充到最少 2 位宽。数字 9'09'14 已经两位不变
s.split('').reverse().join('') split('') 将字符串炸开为单字符数组,借用数组的 reverse() 翻转,join('') 重新合并为字符串
query.replace(/^\?/, '').split('&') 先用正则去掉开头的 ?,再按 & 分割出各个参数对,decodeURIComponent 解码 URL 编码的字符

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

场景 核心方法 业务价值
前后端字段名映射 toCamelCase(下划线→驼峰) 后端返回 user_name 自动转为前端 userName,消除命名风格差异
日志时间戳格式化 formatDate(new Date()) 统一所有日志的时间格式 YYYY-MM-DD HH:mm:ss,便于排序和检索
前端路由参数解析 parseQueryString(location.search) 不依赖第三方库解析 URL 查询参数,减少打包体积
字符串回文检测 split('') + reverse() + join('') 输入验证、算法面试题,理解数组与字符串转换的互操作
邮箱快速校验 includes('@') + lastIndexOf('.') 前端提交前的轻量级格式校验,提升用户体验(复杂校验由后端完成)

5.7 完整可运行示例

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>String 方法演示</title>
  <style>
    * { box-sizing: border-box; }
    body { font-family: 'Segoe UI', sans-serif; max-width: 900px; margin: 0 auto; padding: 24px; background: #f0f4f8; }
    .card { background: #fff; border-radius: 12px; padding: 20px; margin: 16px 0; box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
    h2 { color: #7b1fa2; margin: 0 0 16px; }
    input[type=text] { width: 100%; padding: 10px; border: 2px solid #ce93d8; border-radius: 8px; font-size: 15px; margin-bottom: 10px; }
    .btn-group { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
    button { background: #7b1fa2; color: #fff; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; }
    button:hover { background: #6a1b9a; }
    .result { background: #f3e5f5; padding: 12px 16px; border-radius: 8px; font-family: monospace; font-size: 14px; min-height: 36px; white-space: pre-wrap; color: #4a148c; }
  </style>
</head>
<body>
  <h1 style="color:#4a148c">String 内置构造函数全解析</h1>
  <div class="card">
    <h2>字符串操作实验室</h2>
    <input type="text" id="strInput" value="Hello,开发者,欢迎学习 JavaScript!">
    <div class="btn-group">
      <button onclick="run('length')">长度</button>
      <button onclick="run('upper')">大写</button>
      <button onclick="run('lower')">小写</button>
      <button onclick="run('trim')">去空格</button>
      <button onclick="run('reverse')">翻转</button>
      <button onclick="run('split')">分割(,)</button>
      <button onclick="run('slice')">截取(0,5)</button>
      <button onclick="run('includes')">includes查找</button>
      <button onclick="run('padStart')">padStart补零</button>
    </div>
    <div class="result" id="strResult">输入字符串并点击操作按钮</div>
  </div>
  <div class="card">
    <h2>驼峰命名转换器</h2>
    <input type="text" id="camelInput" value="get-element-by-id">
    <button onclick="camelConvert()">转为驼峰命名</button>
    <div class="result" id="camelResult">点击转换</div>
  </div>
  <div class="card">
    <h2>Unicode 字符查询</h2>
    <input type="text" id="unicodeInput" value="A" maxlength="2">
    <button onclick="getUnicode()">获取 Unicode 编码</button>
    <div class="result" id="unicodeResult">点击查询</div>
  </div>
  <script>
    function run(op) {
      var s = document.getElementById('strInput').value;
      var r = '';
      switch(op) {
        case 'length':   r = '字符长度(UTF-16代码单元数): ' + s.length; break;
        case 'upper':    r = s.toUpperCase(); break;
        case 'lower':    r = s.toLowerCase(); break;
        case 'trim':     r = '"' + ('  ' + s + '  ').trim() + '"(两端空格已去除)'; break;
        case 'reverse':  r = s.split('').reverse().join(''); break;
        case 'split':    r = JSON.stringify(s.split(',')); break;
        case 'slice':    r = '"' + s.slice(0, 5) + '"(slice(0,5))'; break;
        case 'includes': r = 'includes("JavaScript") → ' + s.includes('JavaScript'); break;
        case 'padStart': r = '"' + String(42).padStart(6, '0') + '"(42 补零到6位)'; break;
      }
      document.getElementById('strResult').textContent = r;
    }
    function camelConvert() {
      var s = document.getElementById('camelInput').value;
      var result = s.split('-').map(function(word, idx) {
        return idx === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1);
      }).join('');
      document.getElementById('camelResult').textContent = s + '  →  ' + result;
    }
    function getUnicode() {
      var ch = document.getElementById('unicodeInput').value;
      if (!ch) return;
      var code = ch.codePointAt(0);
      document.getElementById('unicodeResult').textContent =
        '字符: "' + ch + '"\n' +
        'charCodeAt(0) = ' + ch.charCodeAt(0) + '\n' +
        'codePointAt(0) = ' + code + '\n' +
        '十六进制: U+' + code.toString(16).toUpperCase().padStart(4,'0') + '\n' +
        'fromCodePoint(' + code + ') = "' + String.fromCodePoint(code) + '"';
    }
  </script>
</body>
</html>

📌 String --- 知识特点总结

特点 描述
不可变性 字符串是值类型,所有方法都返回新字符串,原字符串不变;不能通过索引赋值修改
UTF-16 编码 length 属性返回 UTF-16 代码单元数,emoji 等字符长度可能为 2,不等于视觉字符数
自动包装 原始字符串调用方法时,引擎自动创建临时 String 包装对象,调用后立即销毁
slice 最灵活 三种截取方法中,slice 支持负数索引且行为最一致,是首选
indexOf 返回 -1 未找到时返回 -1,可用 !== -1 判断存在性;ES6+ 推荐 includes() 更语义化
split + join 组合 字符串与数组的双向转换,是很多字符串处理(翻转、替换、格式化)的核心模式
padStart 补零技巧 日期格式化中 String(n).padStart(2, '0') 是最简洁的补零方式
模板字面量(ES6) 多行字符串和插值,彻底替代了字符串拼接,大幅提升可读性

6 内置对象 Math

6.1 理论基础:Math 不是构造函数

Math 是 JavaScript 中一个特殊的内置对象(Built-in Object) ,而非内置构造函数。它是一个普通对象(Object 的实例),所有属性和方法都直接挂载在 Math 对象上,无需也无法实例化(调用 new Math() 会报错)。

js 复制代码
console.log(typeof Math);      // "object"(不是 function)
console.log(Math instanceof Object); // true
console.log(Math.constructor); // Object(Math 是 Object 的实例)
// Math.__proto__ === Object.prototype

// 不能实例化
try { new Math(); } catch(e) { console.log(e.message); } // Math is not a constructor

💡 代码解析

代码片段 含义
typeof Math"object" Math 是普通对象实例(Object 的实例),不是函数,因此 typeof 返回 "object"
Math instanceof Objecttrue Math 的原型链:Math → Object.prototype → nullinstanceof Objecttrue
Math.constructorObject 说明 Math 对象是由 Object 构造的,而非自定义类
new Math() 抛出 TypeError Math 对象不具有 [[Construct]] 内部方法,尝试 new 调用会抛出 TypeError: Math is not a constructor

Math 对象的设计体现了**命名空间(Namespace)**模式------将相关的常量和函数组织在一个对象下,避免全局变量污染。

深层理论:伪随机数生成器(PRNG)原理

Math.random() 并非真正的随机数------计算机是确定性系统,无法凭空产生真正的随机数(除非借助硬件熵源)。它返回的是伪随机数(Pseudo-Random Number),由确定性算法基于初始"种子"计算。

① 随机数的分类

类型 原理 特点 适用场景
TRNG(真随机数) 物理随机过程(量子涨落、热噪声、放射性衰变) 不可预测,不可重现 密钥生成、彩票
PRNG(伪随机数) 确定性数学算法(线性同余、MT、xorshift) 速度极快,可重现(给定相同种子) 游戏、模拟、动画
CSPRNG(密码学安全 PRNG) 基于密码学原语(如 AES-CTR、ChaCha20) 输出不可预测,满足安全要求 密码学、安全令牌

② V8 的 xorshift128+ 算法

Chrome 49+ 起,V8 使用 xorshift128+ 算法实现 Math.random(),它是 xorshift 算法的改进版,具有极短的代码长度和良好的随机性质量:

复制代码
xorshift128+ 状态:两个 64 位无符号整数 (s0, s1)

每次调用生成一个随机数:
  t = s0
  s0 = s1
  t ^= t << 23        // 左移并异或
  t ^= t >> 17        // 右移并异或
  t ^= s1 ^ (s1 >> 26)
  s1 = t
  return s0 + t       // 返回 (s0 + t) 的低 64 位
js 复制代码
// xorshift128+ 的 JavaScript 模拟(仅用于理解原理)
function PRNG() {
    var s = [Date.now() >>> 0, (Math.random() * 0xFFFFFFFF) >>> 0];
    return function() {
        var t = s[0];
        s[0] = s[1];
        t ^= t << 23;
        t ^= t >>> 17;
        t ^= s[1] ^ (s[1] >>> 26);
        s[1] = t;
        return (s[0] + t) >>> 0;
    };
}
var rand = PRNG();
console.log(rand()); // 每次调用返回不同的 32 位整数

💡 代码解析

代码片段 含义
var s = [Date.now() >>> 0, ...] 用当前时间戳作为初始种子(state0),再用另一个随机值作为 state1;>>> 0 将值截断为 32 位无符号整数
t ^= t << 23 XOR-shift 核心操作:将 t 左移 23 位后与自身异或,使低位和高位产生混沌关联,增强随机性
t ^= t >>> 17 右移异或:逆方向混合比特,消除前一步左移操作引入的模式
t ^= s[1] ^ (s[1] >>> 26) 引入另一个状态 s[1] 的影响,使两个 64 位状态字互相影响,输出的统计特性通过了 BigCrush 测试
(s[0] + t) >>> 0 对两个状态求和作为输出,>>> 0 确保结果是 32 位无符号整数;真实 V8 实现中会除以 2^32 归一化到 [0,1)

③ 为什么 Math.random() 不能用于密码学?

PRNG 有两个关键弱点:

  1. 状态可还原性:xorshift128+ 的内部状态只有 128 位,理论上观察足够多的输出后可以重建状态,从而预测未来输出
  2. 种子可预测性:浏览器初始化种子时使用的是当前时间等有限熵源,攻击者在特定条件下可能猜测种子
js 复制代码
// ❌ 不安全:用 Math.random() 生成密码
var password = Math.random().toString(36).slice(2); // 可预测!

// ✅ 安全:用 Web Crypto API(基于操作系统的 CSPRNG)
var array = new Uint8Array(16);
crypto.getRandomValues(array); // 操作系统提供的密码学安全随机数
var secureToken = Array.from(array, b => b.toString(16).padStart(2, '0')).join('');

💡 代码解析

代码片段 含义
Math.random().toString(36).slice(2) 不安全 toString(36) 将小数转为 36 进制字符串,slice(2) 去掉 "0." 前缀;但 PRNG 内部状态有限,输出可预测
new Uint8Array(16) 创建 16 字节(128位)的类型化数组,准备接收随机字节
crypto.getRandomValues(array) Web Crypto API,底层调用操作系统的 /dev/urandomCryptGenRandom,获取真正密码学安全的随机字节
b.toString(16).padStart(2, '0') 将每个字节(0~255)转为两位十六进制字符串,不足两位补零,最终拼接成 32 个字符的十六进制令牌

④ 随机数的统计分布

Math.random() 返回 [0, 1) 上的均匀分布(Uniform Distribution)。但很多实际场景需要其他分布:

js 复制代码
// 均匀分布 → 正态分布(Box-Muller 变换)
function randomGaussian(mean, stdDev) {
    var u1 = Math.random(), u2 = Math.random();
    var z  = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
    return mean + z * stdDev;
}
// 应用:模拟身高、体重、测量误差等自然现象(中心极限定理)

// 指数分布(模拟等待时间)
function randomExponential(rate) {
    return -Math.log(1 - Math.random()) / rate;
}
// 应用:模拟用户到达间隔、网络请求响应时间

💡 代码解析

代码片段 含义
Math.sqrt(-2 * Math.log(u1)) Box-Muller 变换第一步:将均匀分布的 u1 通过 -2ln(u1) 的平方根映射,生成正态分布的幅度分量
Math.cos(2 * Math.PI * u2) 第二步:用 u2 生成均匀分布的相位角,与幅度分量相乘得到标准正态随机数 z(均值0,标准差1)
mean + z * stdDev 将标准正态分布变换为指定均值和标准差的正态分布(线性变换)
-Math.log(1 - Math.random()) / rate 指数分布的逆变换采样(Inverse CDF):对均匀分布取对数变换,rate 是期望到达率(λ),结果是平均等待时间 1/λ

6.2 名词解释

术语 定义
Math.PI 圆周率 π ≈ 3.141592653589793,精确到 15 位有效数字
Math.E 自然常数 e ≈ 2.718281828459045,自然对数的底数
Math.SQRT2 √2 ≈ 1.4142135623730951
Math.LN2 ln(2) ≈ 0.6931471805599453
向下取整(floor) 不大于给定数字的最大整数,沿负无穷方向取整
向上取整(ceil) 不小于给定数字的最小整数,沿正无穷方向取整
截断取整(trunc) ES6,截去小数部分,向零方向取整(与 floor 对负数行为不同)
Math.random() 返回 [0, 1) 范围内的伪随机浮点数,基于 PRNG(伪随机数生成器)算法
Math.hypot() 返回各参数平方和的平方根(勾股定理),Math.hypot(3, 4) === 5
Math.log2() / Math.log10() ES6,以 2/10 为底的对数,比 Math.log(x)/Math.log(2) 更精确
Math.clz32() ES6,返回 32 位整数表示中前导零的个数,底层位运算优化用
Math.sign() ES6,返回数字符号:正数→1,负数→-1,零→0

6.3 方法速查表

方法 说明 示例 结果
Math.abs(x) 绝对值 Math.abs(-5) 5
Math.pow(x,y) x 的 y 次方(等价 x**y Math.pow(2,10) 1024
Math.sqrt(x) 平方根 Math.sqrt(16) 4
Math.cbrt(x) 立方根(ES6) Math.cbrt(27) 3
Math.floor(x) 向下取整 Math.floor(-4.1) -5
Math.ceil(x) 向上取整 Math.ceil(-4.9) -4
Math.round(x) 四舍五入 Math.round(4.5) 5
Math.trunc(x) 截断小数(ES6) Math.trunc(-4.9) -4
Math.max(...args) 最大值 Math.max(1,5,3) 5
Math.min(...args) 最小值 Math.min(1,5,3) 1
Math.random() 伪随机浮点 [0,1) Math.random() 0.xxx
Math.hypot(...args) 各参数平方和的平方根 Math.hypot(3,4) 5
Math.sign(x) 符号函数(ES6) Math.sign(-3) -1
Math.log(x) 自然对数 ln(x) Math.log(Math.E) 1
Math.log2(x) 以 2 为底的对数(ES6) Math.log2(8) 3
Math.log10(x) 以 10 为底的对数(ES6) Math.log10(1000) 3
Math.sin/cos/tan(x) 三角函数(参数为弧度) Math.sin(Math.PI/2) 1

6.4 取随机数公式推导

js 复制代码
// ════════ 公式一:0 到 n 的随机整数(含 0 含 n)════════
// Math.random() 取值 [0, 1)
// * (n+1) 得到 [0, n+1)
// floor 得到 [0, n] 的整数
function randomInt0toN(n) {
    return Math.floor(Math.random() * (n + 1));
}
console.log(randomInt0toN(9));  // 0 ~ 9

// ════════ 公式二:m 到 n 的随机整数(含 m 含 n)════════
// 先在 [0, n-m] 范围内取随机数,再加偏移量 m
function randomIntMtoN(m, n) {
    return Math.floor(Math.random() * (n - m + 1)) + m;
}
console.log(randomIntMtoN(5, 10));  // 5 ~ 10

// ════════ 从数组中随机取一个元素 ════════
function randomItem(arr) {
    return arr[Math.floor(Math.random() * arr.length)];
}

// ════════ 生成随机十六进制颜色 ════════
function randomHexColor() {
    return '#' + Math.floor(Math.random() * 0xFFFFFF)
                     .toString(16).padStart(6, '0');
}

// ════════ 随机洗牌(Fisher-Yates 算法)════════
function shuffle(arr) {
    var result = arr.slice(); // 不修改原数组
    for (var i = result.length - 1; i > 0; i--) {
        var j = Math.floor(Math.random() * (i + 1));
        var temp = result[i];
        result[i] = result[j];
        result[j] = temp;
    }
    return result;
}
console.log(shuffle([1, 2, 3, 4, 5])); // 随机顺序

💡 代码解析

代码片段 含义
Math.floor(Math.random() * (n + 1)) random() 返回 [0,1),乘以 n+1 得到 [0, n+1)floor 向下取整得到 [0, n] 的整数
Math.floor(Math.random() * (n - m + 1)) + m 先在 [0, n-m] 范围取整数,再整体平移 +m,得到 [m, n] 的随机整数
Math.floor(Math.random() * 0xFFFFFF) 0xFFFFFF = 16777215,在此范围随机取整,再用 toString(16) 转十六进制,padStart(6,'0') 确保6位
arr.slice() 先拷贝 Fisher-Yates 在原地洗牌,不拷贝会修改原数组;先浅拷贝确保原数组不变
for (i = length-1; i > 0; i--) 从最后一个元素向前遍历,每次将当前位置与随机选取的更前位置交换,保证每个元素出现在任意位置的概率相等

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

场景 方法 业务价值
随机颜色主题 randomHexColor() 数据可视化图表自动分配颜色,头像背景色随机生成
抽奖/随机推荐 randomItem(list) 随机推荐商品、随机题库抽题、活动抽奖
问卷/考题乱序 shuffle(questions) 防止考试作弊,每个人看到不同题目顺序(Fisher-Yates 保证均匀分布)
游戏地图生成 randomIntMtoN(m, n) 随机生成地图障碍物位置、道具位置、NPC 坐标
AB 测试分组 Math.random() < 0.5 ? 'A' : 'B' 随机将用户分入实验组/对照组,评估功能改动的效果
验证码干扰点 randomIntMtoN(0, width) 生成坐标 在验证码图片上随机添加干扰点,增加 OCR 识别难度

6.5 取整方法对比(负数行为差异)

方法 含义 4.7 -4.7 助记
Math.floor() 地板 4 -5(向负∞) 往下
Math.ceil() 天花板 5 -4(向正∞) 往上
Math.round() 四舍五入 5 -5(≥0.5 向正∞) 最近
Math.trunc() 截断 4 -4(向 0) 去掉小数

关键区别floor(-4.7) = -5(向更小的整数),而 trunc(-4.7) = -4(向零,只去掉小数部分)。

6.6 完整可运行示例(随机颜色生成器)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Math 对象演示</title>
  <style>
    * { box-sizing: border-box; }
    body { font-family: sans-serif; max-width: 900px; margin: 0 auto; padding: 24px; background: #f5f5f5; }
    .card { background: #fff; border-radius: 12px; padding: 20px; margin: 16px 0; box-shadow: 0 4px 16px rgba(0,0,0,0.1); }
    h2 { color: #e65100; margin: 0 0 16px; }
    .color-grid { display: flex; flex-wrap: wrap; gap: 12px; margin: 16px 0; }
    .color-item { text-align: center; }
    .color-box { width: 120px; height: 70px; border-radius: 8px; display: block; margin-bottom: 4px; }
    .color-item span { font-size: 11px; font-family: monospace; color: #555; }
    button { background: #e65100; color: #fff; border: none; padding: 10px 24px; border-radius: 8px; cursor: pointer; font-size: 14px; margin: 4px; }
    button:hover { background: #bf360c; }
    .formula { background: #fff3e0; padding: 12px; border-radius: 8px; font-family: monospace; font-size: 13px; margin: 12px 0; }
    .math-result { background: #fce4ec; padding: 12px; border-radius: 8px; font-family: monospace; white-space: pre; }
    .grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
  </style>
</head>
<body>
  <h1 style="color:#bf360c">Math 内置对象全解析</h1>
  <div class="card">
    <h2>随机颜色生成器</h2>
    <div class="formula">
      Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, '0')
    </div>
    <button onclick="generateColors()">生成随机颜色</button>
    <button onclick="generateGradient()">生成渐变色</button>
    <div class="color-grid" id="colorGrid"></div>
  </div>
  <div class="card">
    <h2>随机整数生成器</h2>
    <div class="formula">
      [0, n]:Math.floor(Math.random() * (n + 1))<br>
      [m, n]:Math.floor(Math.random() * (n - m + 1)) + m
    </div>
    <label>m:<input type="number" id="minVal" value="1" style="width:70px;padding:4px"></label>
    <label>n:<input type="number" id="maxVal" value="100" style="width:70px;padding:4px"></label>
    <button onclick="generateRandom()">生成 10 个随机整数</button>
    <div class="math-result" id="randomResult">点击生成</div>
  </div>
  <div class="card">
    <h2>取整方法对比(输入任意小数)</h2>
    <div class="grid2">
      <div>
        <input type="number" id="mathInput" value="-4.7" step="0.1" style="width:100%;padding:8px;border:1px solid #ffcc80;border-radius:6px;margin-bottom:8px">
        <button onclick="runMath()" style="width:100%">计算各种取整</button>
      </div>
      <div class="math-result" id="mathResult">点击计算</div>
    </div>
  </div>
  <script>
    function generateColors() {
      var grid = document.getElementById('colorGrid');
      grid.innerHTML = '';
      for (var i = 0; i < 8; i++) {
        var color = '#' + Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, '0');
        var item = document.createElement('div');
        item.className = 'color-item';
        item.innerHTML = '<div class="color-box" style="background:' + color + '"></div><span>' + color + '</span>';
        grid.appendChild(item);
      }
    }
    function generateGradient() {
      var grid = document.getElementById('colorGrid');
      grid.innerHTML = '';
      for (var i = 0; i < 4; i++) {
        var c1 = '#' + Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, '0');
        var c2 = '#' + Math.floor(Math.random() * 0xFFFFFF).toString(16).padStart(6, '0');
        var item = document.createElement('div');
        item.className = 'color-item';
        item.innerHTML = '<div class="color-box" style="background:linear-gradient(135deg,' + c1 + ',' + c2 + ')"></div><span>' + c1 + ' → ' + c2 + '</span>';
        grid.appendChild(item);
      }
    }
    function generateRandom() {
      var m = parseInt(document.getElementById('minVal').value);
      var n = parseInt(document.getElementById('maxVal').value);
      var results = Array.from({length: 10}, function() {
        return Math.floor(Math.random() * (n - m + 1)) + m;
      });
      document.getElementById('randomResult').textContent = '[' + m + ', ' + n + '] 的随机整数:\n' + results.join('  ');
    }
    function runMath() {
      var x = parseFloat(document.getElementById('mathInput').value);
      document.getElementById('mathResult').textContent = [
        'x = ' + x,
        'floor(' + x + ')  = ' + Math.floor(x) + '(向 -∞)',
        'ceil('  + x + ')   = ' + Math.ceil(x)  + '(向 +∞)',
        'round(' + x + ')  = ' + Math.round(x)  + '(最近整数)',
        'trunc(' + x + ')  = ' + Math.trunc(x)  + '(向 0)',
        'abs('   + x + ')   = ' + Math.abs(x),
        'sign('  + x + ')  = ' + Math.sign(x),
      ].join('\n');
    }
    generateColors();
  </script>
</body>
</html>

📌 Math --- 知识特点总结

特点 描述
不是构造函数 Math 是普通对象(命名空间),不能用 new 实例化,所有成员直接通过 Math.xxx 访问
所有方法都是纯函数 Math 的方法不修改任何外部状态,相同输入永远得到相同输出(引用透明性)
random() 是伪随机 基于 PRNG 算法,不适合加密安全场景;加密场景应使用 crypto.getRandomValues()
取整方法的负数差异 floortrunc 对正数结果相同,对负数行为不同,务必区分
随机整数公式 Math.floor(Math.random() * (n - m + 1)) + m 是取 [m, n] 整数的标准公式,需要记忆
三角函数参数单位 sin/cos/tan 等的参数是弧度(radian) ,不是角度;角度转弧度:angle * Math.PI / 180
max/min 的数组用法 对数组求最大值:Math.max(...arr)Math.max.apply(null, arr)
性能 Math 方法是引擎原生实现,通常比手写 JS 循环快得多

7 内置构造函数 Date

7.1 理论基础:时间的表示与时区

Unix 时间戳 起源于 1970 年 1 月 1 日 00:00:00 UTC(协调世界时)这一历史约定。JavaScript 的时间戳以毫秒为单位(区别于 Unix 传统的秒),这提供了更高的时间精度。

时区复杂性

  • UTC(协调世界时):全球标准时间,不随地区/季节变化
  • 本地时间:用户设备所在时区的时间
  • 时区偏移:本地时间与 UTC 的差值(如 UTC+8 偏移 480 分钟)
js 复制代码
// 获取当前时区偏移(分钟)
var offset = new Date().getTimezoneOffset();
console.log(offset); // 中国:-480(UTC+8 偏移 -480 分钟)
// 注意:偏移值是 UTC-本地,所以 UTC+8 得到的是 -480

💡 代码解析

代码片段 含义
new Date().getTimezoneOffset() 返回 UTC 与本地时间的差值(分钟),公式为:UTC时间 - 本地时间(分钟)
UTC+8 返回 -480 东八区本地时间比 UTC 快 8 小时(480分钟),所以差值为 UTC - 本地 = -480
注意符号方向 西方时区(UTC-5)返回正数 +300,东方时区(UTC+8)返回负数 -480,与通常的 "UTC+8" 表示方向相反

Date 对象的内部表示 :Date 对象内部存储的是一个整数------UTC 时间的毫秒数(时间戳)。所有 get/set 方法都是对这个时间戳的格式化解读,区别在于是否应用本地时区偏移。

深层理论:ISO 8601 标准、闰秒与时区数据库

① ISO 8601 标准解析

ISO 8601 是国际标准化组织(ISO)于 1988 年发布的日期时间表示标准,JavaScript 的 Date.toISOString() 严格遵循此标准:

复制代码
完整格式:YYYY-MM-DDTHH:mm:ss.sssZ

各字段含义:
YYYY    --- 四位年份(公历)
MM      --- 两位月份(01-12)
DD      --- 两位日期(01-31)
T       --- 日期与时间的分隔符(Temperature, 历史原因)
HH      --- 两位小时(00-23,24小时制)
mm      --- 两位分钟(00-59)
ss      --- 两位秒数(00-60,60 保留用于闰秒)
.sss    --- 三位毫秒(可选)
Z       --- 时区偏移:Z 表示 UTC,+08:00 表示东八区,-05:00 表示西五区
js 复制代码
var d = new Date('2023-10-15T14:30:05.123Z');
console.log(d.toISOString()); // "2023-10-15T14:30:05.123Z"(始终 UTC)
console.log(d.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }));
// "2023/10/15 22:30:05"(转为东八区时间)

// 实际项目中,推荐始终以 ISO 8601 UTC 格式存储和传输时间
// 在展示层再转为用户本地时间

💡 代码解析

代码片段 含义
new Date('2023-10-15T14:30:05.123Z') Z 后缀明确指定 UTC 时区,不受运行环境时区影响,跨平台行为一致
.toISOString() 始终输出 UTC 时间,格式严格遵循 ISO 8601,适合存储到数据库或通过 API 传输
toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) 使用 Intl API 将 UTC 时间转换为东八区(中国标准时间)并按中文格式化;UTC 14:30CST 22:30

② 时区数据库(tz database / IANA Time Zone Database)

计算机系统中的时区信息来自 IANA 时区数据库(又称 Olson 数据库),它维护了全球约 600+ 个时区的历史夏令时规则。

js 复制代码
// Intl.DateTimeFormat 背后依赖 IANA 数据库
var formatter = new Intl.DateTimeFormat('en-US', {
    timeZone: 'America/New_York',   // IANA 时区标识符
    year: 'numeric', month: '2-digit', day: '2-digit',
    hour: '2-digit', minute: '2-digit', second: '2-digit',
    hour12: false
});
console.log(formatter.format(new Date()));
// 自动处理美国东部时间的夏令时(EDT/EST 之间切换)

💡 代码解析

代码片段 含义
new Intl.DateTimeFormat('en-US', {...}) 创建一个国际化日期格式化器,'en-US' 指定语言环境(影响数字分隔符、月份名称等格式),options 对象控制各字段的显示格式
timeZone: 'America/New_York' 使用 IANA 时区标识符,而非固定偏移量(如 -05:00);IANA 标识符包含历史 DST 规则,能正确处理美东时间在 EST(UTC-5)和 EDT(UTC-4)之间的自动切换
hour12: false 使用 24 小时制,避免 AM/PM 歧义

时区处理的三大陷阱

陷阱 例子 正确做法
夏令时(DST)跳变 美国 DST 切换时,2:00 AM 直接跳到 3:00 AM,2:30 AM 不存在 使用 IANA 标识符而非固定偏移量,让库处理 DST
月末日期溢出 new Date(2023, 0, 31); setMonth(1) → 2 月没有 31 日 先 setDate(1) 再 setMonth,或用专业日期库
不同浏览器的解析差异 new Date('2023-10-15') 有些浏览器视为 UTC,有些视为本地时间 统一使用 new Date('2023-10-15T00:00:00Z') 明确指定时区

③ 闰秒(Leap Second)

地球自转速度略有变化,导致 UTC 时间与天文时间(UT1)存在微小偏差。国际地球自转服务(IERS)偶尔会在 UTC 时钟上插入一个"闰秒"(23:59:60),保持两者同步。

复制代码
自 1972 年至今,已累计插入 27 个闰秒
最近一次:2016-12-31 23:59:60 UTC

对 JavaScript 的影响:
- ECMAScript 规范明确规定:Date 对象**忽略闰秒**
- 规范中的一天永远是 86400000 毫秒(不考虑闰秒)
- Unix 时间戳同样不考虑闰秒("闰秒涂抹"技术)
- 因此 JavaScript 的时间计算在闰秒发生的那一秒会与真实 UTC 有 1 秒误差

④ JavaScript 时间系统的设计局限

js 复制代码
// Date 对象的已知问题(等待 Temporal API 修复):
// 1. 月份从 0 开始(API 设计不一致)
// 2. 没有不可变的日期类型(类似字符串那样)
// 3. DST 处理不一致
// 4. 解析行为依赖实现

// ES2025 候选 API:Temporal(更现代、更完整的日期时间 API)
// Temporal.PlainDate, Temporal.ZonedDateTime 等(目前需要 polyfill)

7.2 名词解释

术语 定义
Unix 时间戳 从 1970-01-01 00:00:00 UTC 至今的毫秒数(JS)或秒数(传统 Unix)
UTC 协调世界时(Universal Time Coordinated),国际标准时间基准
本地时间 根据用户设备时区设置显示的时间
getTime() 获取 Date 对象对应的 Unix 时间戳(毫秒)
Date.now() 静态方法,当前时刻的时间戳,比 new Date().getTime() 更简洁高效
月份从 0 开始 JavaScript 月份值 0~11 对应 1~12 月,这是 C 语言 tm_mon 的历史遗留设计
toISOString() 返回符合 ISO 8601 标准的字符串(YYYY-MM-DDTHH:mm:ss.sssZ),始终是 UTC 时间
toLocaleString() 根据系统本地化设置格式化日期时间
Intl.DateTimeFormat 国际化 API,提供更灵活的日期格式化能力

7.3 实例化方式全解

js 复制代码
// ① 当前时间(无参数)
var now = new Date();

// ② ISO 8601 字符串(最推荐,跨平台一致)
var d1 = new Date('2023-10-15T14:30:05.000Z');  // UTC 时间
var d2 = new Date('2023-10-15T14:30:05');        // 本地时间(行为因浏览器而异)
var d3 = new Date('2023-10-15');                 // 只有日期(视为 UTC 午夜)

// ③ 数字参数(年, 月[0-11], 日, 时, 分, 秒, 毫秒)全部本地时间
var d4 = new Date(2023, 9, 1);        // 2023年10月1日(月份 9 = 10月!)
var d5 = new Date(2023, 9, 1, 8, 0, 0); // 2023年10月1日 08:00:00

// ④ 时间戳(毫秒,UTC)
var d6 = new Date(0);              // 1970-01-01T00:00:00.000Z
var d7 = new Date(Date.now());     // 等同于 new Date()

// ⑤ 复制 Date 对象
var d8 = new Date(now.getTime());  // 复制一个 Date 对象

💡 代码解析

代码片段 含义
new Date() 无参数 创建表示当前时刻的 Date 对象,内部时间戳等于 Date.now()
new Date('2023-10-15T14:30:05.000Z') ISO 8601 格式,末尾 Z 明确为 UTC,最推荐的字符串创建方式,跨浏览器行为一致
new Date('2023-10-15T14:30:05') 无 Z 无时区后缀时行为因浏览器而异:现代浏览器视为本地时间,ES5 规范视为 UTC;应避免
new Date(2023, 9, 1) 第二参数月份是 9(实际 10 月),月份从 0 开始是 JavaScript 的历史遗留设计(源自 Java 的 Calendar 类)
new Date(0) 时间戳为 0,即 Unix 纪元起点:1970-01-01T00:00:00.000Z
new Date(now.getTime()) 通过 getTime() 获取时间戳再创建新 Date,实现日期对象的深拷贝(直接赋值只复制引用)

7.4 获取与设置方法

js 复制代码
var d = new Date('2023-10-15T14:30:05.123');

// 获取(本地时间)
console.log(d.getFullYear());     // 2023
console.log(d.getMonth());        // 9(实际是 10 月,月份从 0 开始!)
console.log(d.getMonth() + 1);    // 10(正确的月份显示)
console.log(d.getDate());         // 15(月中第几天,1-31)
console.log(d.getDay());          // 0~6(0=周日,1=周一...6=周六)
console.log(d.getHours());        // 14
console.log(d.getMinutes());      // 30
console.log(d.getSeconds());      // 5
console.log(d.getMilliseconds()); // 123
console.log(d.getTime());         // 时间戳(毫秒)
console.log(d.getTimezoneOffset()); // 时区偏移(分钟),UTC+8 返回 -480

// 获取(UTC 时间)
console.log(d.getUTCFullYear()); // UTC 年
console.log(d.getUTCHours());    // UTC 小时(比 UTC+8 少 8 小时)

// 设置(直接修改 Date 对象)
d.setFullYear(2024);
d.setMonth(0);     // 1 月
d.setDate(1);
d.setHours(0, 0, 0, 0); // 时、分、秒、毫秒全设为 0(常用于日期比较)

// 静态方法
console.log(Date.now());          // 当前时间戳(毫秒)
console.log(Date.parse('2023-10-15T14:30:05.000Z')); // 将字符串解析为时间戳

💡 代码解析

代码片段 含义
d.getMonth()9 Date 内部存储 10 月,但 getMonth() 返回 9(月份从 0 计);显示时需要 +1d.getMonth() + 1
d.getDay()0~6 0 表示周日,1~6 依次为周一到周六(源自美国习惯,周日为一周第一天)
d.getHours()d.getUTCHours() getHours() 返回本地时区的小时;getUTCHours() 返回 UTC 小时;在 UTC+8 下两者相差 8 小时
setHours(0, 0, 0, 0) 四个参数依次设置:时、分、秒、毫秒,全设为 0 可将 Date 对象重置为当天 00:00:00.000,常用于日期(不含时间)比较
getTimezoneOffset()-480 返回本地时区偏移分钟数(UTC - 本地),UTC+8 区域返回 -480
Date.now() 静态方法,比 new Date().getTime() 高效,不需要创建 Date 对象,直接返回当前时间戳

7.5 日期格式化与工具函数

js 复制代码
// ════════ 标准格式化函数 ════════
function pad(n) { return String(n).padStart(2, '0'); }

function formatDateTime(d) {
    return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) +
           ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
}
console.log(formatDateTime(new Date())); // "2023-10-15 14:30:05"

// ════════ 星期名称 ════════
function getWeekdayName(date) {
    var days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
    return days[date.getDay()];
}

// ════════ 计算两日期之间的天数 ════════
function daysBetween(date1, date2) {
    var MS_PER_DAY = 1000 * 60 * 60 * 24;
    return Math.round(Math.abs(date2.getTime() - date1.getTime()) / MS_PER_DAY);
}
console.log(daysBetween(new Date('2023-01-01'), new Date('2023-12-31'))); // 364

// ════════ 判断是否为闰年 ════════
function isLeapYear(year) {
    return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0);
}
console.log(isLeapYear(2024)); // true
console.log(isLeapYear(1900)); // false(能被100整除但不能被400整除)
console.log(isLeapYear(2000)); // true(能被400整除)

// ════════ 获取本月第一天和最后一天 ════════
function getMonthRange(date) {
    var first = new Date(date.getFullYear(), date.getMonth(), 1);
    var last  = new Date(date.getFullYear(), date.getMonth() + 1, 0); // 下月第 0 天 = 本月最后一天
    return { first: first, last: last };
}

// ════════ 计时(性能测量)════════
var start = Date.now();
// ... 耗时操作 ...
var elapsed = Date.now() - start;
console.log('耗时:' + elapsed + 'ms');
// 更高精度:performance.now()(微秒级)

💡 代码解析

代码片段 含义
String(n).padStart(2, '0') 最常用的日期补零技巧:5'05'14 保持不变。padStart 只在不足指定长度时才填充
d.getDay() → 索引查 weekdays 数组 getDay() 返回 0-6(0=周日),用这个值作为数组索引取对应的中文名称
Math.abs(date2.getTime() - date1.getTime()) 时间戳相减得毫秒差,除以单日毫秒数 (1000*60*60*24) 得天数差,Math.abs 确保结果为正数
year % 4 === 0 && year % 100 !== 0 公历闰年规则一:能被4整除但不能被100整除(如 2024)
`
new Date(year, month + 1, 0) 构造"下月第0天",在 JS 中第0天会自动回退为上月最后一天,这是获取当月最后一天的经典技巧
Date.now() - start 性能计时:记录开始时间戳,操作后再取当前时间戳,差值即为耗时毫秒数

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

场景 方法组合 业务价值
订单创建时间展示 formatDateTime(new Date(timestamp)) 将后端返回的时间戳格式化为用户可读格式 2023-10-15 14:30:05
日历组件 getMonthRange + getDay 确定月历排版 获取当月的第一天是周几、共多少天,正确排列日历格子
活动倒计时 targetDate.getTime() - Date.now() 计算距离活动开始/结束的剩余毫秒数,再分解为天/时/分/秒展示
用户注册天数 daysBetween(registerDate, new Date()) 展示"您已使用本产品 X 天",增加用户留存感
前端性能监控 Date.now() 打点计时 记录页面关键路径的耗时,发现性能瓶颈(更精确用 performance.now()
日期范围校验 比较两个 Date 的 getTime() 验证用户选择的开始日期不晚于结束日期,日期选择器的核心逻辑

7.6 完整可运行示例(数字时钟)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Date 构造函数演示 ------ 数字时钟</title>
  <style>
    * { box-sizing: border-box; }
    body { font-family: 'Segoe UI', sans-serif; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; background: linear-gradient(135deg, #1a237e, #283593, #1565c0); color: #fff; margin: 0; }
    .card { background: rgba(255,255,255,0.08); backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.15); border-radius: 20px; padding: 40px 60px; margin: 20px; text-align: center; }
    .date-display { font-size: 1.4rem; opacity: 0.85; margin-bottom: 16px; letter-spacing: 2px; }
    .time-display { font-size: 5rem; font-weight: 700; letter-spacing: 8px; text-shadow: 0 0 30px rgba(100,181,246,0.8); font-variant-numeric: tabular-nums; }
    .weekday { font-size: 1.2rem; opacity: 0.75; margin-top: 8px; }
    .info-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 32px; }
    .info-item { background: rgba(255,255,255,0.1); border-radius: 12px; padding: 12px; }
    .info-item .label { font-size: 0.75rem; opacity: 0.6; text-transform: uppercase; letter-spacing: 1px; }
    .info-item .value { font-size: 1.5rem; font-weight: 600; margin-top: 4px; }
    .timestamp { font-size: 0.85rem; opacity: 0.5; margin-top: 24px; font-family: monospace; }
  </style>
</head>
<body>
  <div class="card">
    <div class="date-display" id="dateDisplay"></div>
    <div class="time-display" id="timeDisplay"></div>
    <div class="weekday" id="weekdayDisplay"></div>
    <div class="info-grid">
      <div class="info-item"><div class="label">年</div><div class="value" id="yearVal"></div></div>
      <div class="info-item"><div class="label">月</div><div class="value" id="monthVal"></div></div>
      <div class="info-item"><div class="label">日</div><div class="value" id="dayVal"></div></div>
    </div>
    <div class="timestamp" id="tsDisplay"></div>
  </div>
  <script>
    var weekdays = ['周日','周一','周二','周三','周四','周五','周六'];
    function pad(n) { return String(n).padStart(2, '0'); }
    function updateClock() {
      var now = new Date();
      document.getElementById('dateDisplay').textContent = now.getFullYear() + ' 年 ' + pad(now.getMonth()+1) + ' 月 ' + pad(now.getDate()) + ' 日';
      document.getElementById('timeDisplay').textContent = pad(now.getHours()) + ' : ' + pad(now.getMinutes()) + ' : ' + pad(now.getSeconds());
      document.getElementById('weekdayDisplay').textContent = weekdays[now.getDay()];
      document.getElementById('yearVal').textContent  = now.getFullYear();
      document.getElementById('monthVal').textContent = pad(now.getMonth()+1);
      document.getElementById('dayVal').textContent   = pad(now.getDate());
      document.getElementById('tsDisplay').textContent = 'Timestamp: ' + Date.now() + ' ms';
    }
    updateClock();
    setInterval(updateClock, 1000);
  </script>
</body>
</html>

📌 Date --- 知识特点总结

特点 描述
月份从 0 开始 这是 JavaScript 最著名的"坑"之一,月份值 0~11 对应 1~12 月,获取/设置时务必 +1/-1
内部存储时间戳 Date 对象本质是一个 UTC 毫秒数,所有方法都是对这个整数的格式化
本地时间 vs UTC getHours() 返回本地时间的小时,getUTCHours() 返回 UTC 时间;跨时区应用必须注意
ISO 8601 字符串最可靠 构造 Date 时,ISO 8601 格式(YYYY-MM-DDTHH:mm:ss.sssZ)在各浏览器中行为最一致
Date.now() 更高效 只需要时间戳时用 Date.now() 而非 new Date().getTime(),避免创建对象的开销
padStart 格式化 String(n).padStart(2, '0') 是格式化日期分量(月、日、时、分、秒)的标准技巧
日期运算靠时间戳 日期差值计算:先 getTime() 得到毫秒数,再做减法,最后除以相应毫秒数得到天/小时等
推荐使用第三方库 复杂日期操作(时区转换、相对时间等)推荐 day.js(仅 2KB)或 date-fns

8 数组 Array:修改器方法

8.1 理论基础:数组的内存模型

JavaScript 数组(Array)并非传统意义上的连续内存数组,而是一个特殊的对象,其键名为数字索引。

js 复制代码
// 数组本质是对象
var arr = [1, 2, 3];
console.log(typeof arr);           // "object"
console.log(arr instanceof Object); // true

// 可以给数组添加非数字属性(但不推荐)
arr.customProp = 'hello';
console.log(arr.customProp);       // 'hello'
console.log(arr.length);           // 3(非数字属性不计入 length)

💡 代码解析

代码片段 含义
typeof arr"object" 数组本质上是对象,typeof 无法区分数组和普通对象;应使用 Array.isArray() 来判断数组
arr instanceof Objecttrue 数组的原型链:arr → Array.prototype → Object.prototype → null,经过 Object.prototype
arr.customProp = 'hello' 有效 数组是对象,可以像普通对象一样添加任意非数字属性;但这是反模式,会干扰数组迭代方法
arr.length 仍为 3 length 属性只追踪数字索引(012...),字符串键名属性不计入 length

V8 引擎的数组优化:V8 对"密集数组"(SMI Array / PACKED Array)有特殊优化------当数组元素类型统一(如全是整数),V8 会使用高效的底层数组存储,接近 C 语言数组的性能。一旦数组出现空洞(稀疏数组)或混合类型,V8 会退化为字典模式,性能下降。

深层理论:V8 数组元素类型系统(Element Kinds)

V8 为数组维护一套**元素类型(Element Kinds)**系统,决定数组在内存中的存储方式和访问速度。这套系统是理解 JavaScript 数组性能的关键。

① 元素类型的层级(从最快到最慢)

复制代码
PACKED_SMI_ELEMENTS     ← 最快:只含小整数(Small Integer,31位)
PACKED_DOUBLE_ELEMENTS  ← 快:含浮点数(含 SMI)
PACKED_ELEMENTS         ← 中:含任意 JS 值(含 double)
HOLEY_SMI_ELEMENTS      ← 稍慢:稀疏小整数数组(有"空洞")
HOLEY_DOUBLE_ELEMENTS   ← 慢:稀疏浮点数组
HOLEY_ELEMENTS          ← 最慢:稀疏混合类型数组
DICTIONARY_ELEMENTS     ← 极慢:超大索引或超稀疏数组,退化为哈希表存储

重要原则:元素类型只能单向降级,不能自动升级!

js 复制代码
// ① PACKED_SMI_ELEMENTS(最优状态)
var arr = [1, 2, 3];               // PACKED_SMI_ELEMENTS

// ② 插入浮点数 → 降级到 PACKED_DOUBLE_ELEMENTS(不可逆!)
arr.push(4.5);                      // PACKED_DOUBLE_ELEMENTS
arr.pop();  // 移除浮点数,但类型不会回升
// arr 仍然是 PACKED_DOUBLE_ELEMENTS,即使现在全是整数

// ③ 插入字符串 → 降级到 PACKED_ELEMENTS
arr.push('hello');                  // PACKED_ELEMENTS

// ④ 创建"空洞" → 降级到 HOLEY_*
arr[100] = 'far';                   // HOLEY_ELEMENTS(索引 4-99 是空洞)

💡 代码解析

代码片段 含义
var arr = [1, 2, 3]PACKED_SMI_ELEMENTS 所有元素是连续整数,V8 用 C++ 整数数组存储,访问速度极快
arr.push(4.5) → 降级到 PACKED_DOUBLE_ELEMENTS 插入浮点数触发类型扩展:V8 将整个数组从 SMI 数组转为 Double 数组(每个元素占用更多内存),此降级不可逆
arr.pop() 后仍是 PACKED_DOUBLE_ELEMENTS 即使移除浮点数,V8 也不会回升类型(回升成本高且不安全),元素类型只会降级,从不升级
arr.push('hello')PACKED_ELEMENTS 混入字符串,V8 切换为通用对象指针数组,失去数值专用优化
arr[100] = 'far'HOLEY_ELEMENTS 跨越式赋值使索引 4~99 变为"空洞"(hole),V8 需要额外检查每个位置是否为空洞

② 稀疏数组(Sparse Array)的性能陷阱

js 复制代码
// 稀疏数组:索引不连续,V8 用哈希表存储
var sparse = new Array(1000); // 创建 1000 个空位的稀疏数组(HOLEY)
sparse[0] = 1;                // 仅第 0 位有值

// 密集数组:所有索引连续填充
var dense  = new Array(1000).fill(0); // PACKED_SMI_ELEMENTS(高效)
// 或者:
var dense2 = Array.from({ length: 1000 }, () => 0); // 同样是密集数组

// 遍历稀疏数组的性能消耗是密集数组的数倍
// forEach、map 在稀疏数组上还需要检查每个位置是否有值

💡 代码解析

代码片段 含义
new Array(1000) 创建有 1000 个空位的稀疏数组,索引 0~999 全为"空洞"(hole),V8 标记为 HOLEY_ELEMENTS
new Array(1000).fill(0) fill(0) 将所有空洞填充为整数 0,使数组变为密集的 PACKED_SMI_ELEMENTS
Array.from({ length: 1000 }, () => 0) Array.from 每个槽位执行一次工厂函数,保证每个槽位都有真实值,结果是密集数组
稀疏数组性能损耗 迭代稀疏数组时,V8 需要对每个索引执行 HasProperty 检查(比直接读取慢 2~10 倍)

③ 实践建议:保持数组类型稳定

js 复制代码
// ✅ 好的做法:预先确定类型,保持一致
var scores = [85, 92, 78, 96];    // 全整数,PACKED_SMI_ELEMENTS,最快
var prices = [9.99, 19.99, 4.50]; // 全浮点,PACKED_DOUBLE_ELEMENTS,快

// ❌ 不好的做法:混合类型
var mixed = [1, 'two', { three: 3 }, null]; // PACKED_ELEMENTS,最慢

// ✅ 预分配密集数组再赋值
var result = new Array(data.length); // 先分配空间
for (var i = 0; i < data.length; i++) {
    result[i] = process(data[i]);    // 顺序赋值,保持密集
}

// ❌ 避免用 delete 删除数组元素(会产生空洞)
var arr = [1, 2, 3, 4, 5];
delete arr[2];   // arr = [1, 2, empty, 4, 5],产生空洞,降级到 HOLEY
arr.splice(2, 1); // ✅ 正确删除方式,不产生空洞

💡 代码解析

代码片段 含义
var scores = [85, 92, 78, 96] 全为整数字面量,V8 推断为 PACKED_SMI_ELEMENTS,在所有模式中性能最优
var mixed = [1, 'two', {...}, null] 混合类型必然是 PACKED_ELEMENTS(指针数组),每次访问需要类型检查和装箱操作
new Array(data.length) + 顺序赋值 预分配空间但随即按顺序填充,V8 可能将其视为密集数组,避免多次扩容(比不断 push 性能好)
delete arr[2] 产生空洞 delete 将索引 2 变为 empty(hole),不改变 length;数组变为 HOLEY,后续所有迭代方法(map/filter)都需要跳过空洞
splice(2, 1) 正确删除 splice 删除元素并移动后续元素,保持数组连续(密集),不产生空洞

④ Array.sort() 的算法实现:TimSort

ECMAScript 2019 规范要求 sort() 必须是稳定排序(Stable Sort) 。V8 7.0+ 使用 TimSort 算法,这是 Python 的内置排序算法,同样被 Java 和 Android 使用。

TimSort 的设计思想

复制代码
TimSort = Merge Sort(归并排序)+ Insertion Sort(插入排序)的混合策略

核心概念:Run(自然有序段)
  1. 扫描数组,找到已经有序的子序列(Run)
  2. 如果 Run 长度太短(< minRun,通常 32~64),用插入排序扩展它
     (插入排序在小数组上因缓存命中率高,实际速度胜过快排)
  3. 将多个 Run 用 MergeSort 策略合并

时间复杂度:
  最坏情况:O(n log n)
  最好情况:O(n)(数组已经有序,直接找到一个 Run 就结束)
  稳定排序:相等元素的原有顺序严格保留

为什么比纯快排好?
  - 对真实数据中"部分有序"的情况有极佳的自适应性
  - 稳定排序,快排不稳定(等值元素可能换位)
  - 对已排序或近似有序的数组近乎 O(n)
js 复制代码
// 稳定排序的意义
var students = [
    { name: 'Alice', score: 90 },
    { name: 'Bob',   score: 85 },
    { name: 'Carol', score: 90 },  // 与 Alice 同分
];
students.sort((a, b) => b.score - a.score); // 按分数降序
// 稳定排序保证:Alice 和 Carol 同分时,Alice 仍然在 Carol 前面(保持原始相对顺序)
// 若用不稳定排序,Carol 可能出现在 Alice 前面

💡 代码解析

代码片段 含义
(a, b) => b.score - a.score 比较函数返回负数a 在前,返回正数b 在前;b.score - a.score 实现降序(高分在前)
Alice 在 Carol 前(稳定排序) Alice 和 Carol 同分(90),b.score - a.score = 0,稳定排序保留原始顺序,Alice(索引0)仍在 Carol(索引2)前面
TimSort 自适应 students 数组本来已按分数大致排好,TimSort 能以接近 O(n) 的速度完成排序(识别"Run"直接合并)
ES2019 稳定性保证 在 ES2019 之前,规范不要求 sort 稳定,Chrome 旧版对长度 ≤10 的数组用插入排序(稳定),长数组用快排(不稳定);现代引擎已统一为稳定排序

修改器方法(Mutator Methods) 是数组中会直接修改调用者(原数组)的方法,这与**访问器方法(Accessor Methods)**形成对比。

8.2 名词解释

术语 定义
修改器方法(Mutator Methods) 调用后会直接改变原数组本身,如 pushpopsplicesortreverse
push() 在数组末尾追加一个或多个元素,返回新数组的长度
pop() 删除并返回数组最后一个元素(栈操作:LIFO)
unshift() 在数组开头插入一个或多个元素,返回新数组的长度(性能比 push 低,需移动所有元素)
shift() 删除并返回数组第一个元素(性能比 pop 低,需移动所有元素)
splice(start, count, ...items) 万能方法:从 start 删除 count 个元素,并可插入新元素。返回被删除元素的数组
sort(compareFn) 对数组元素排序,默认按 UTF-16 字符串升序(数字排序需要比较函数)
reverse() 翻转数组元素顺序(原地操作),返回翻转后的数组
fill(value, start, end) ES6,用 value 填充数组的 start 到 end 位置
稳定排序(Stable Sort) ES2019 后规范要求 sort() 必须是稳定的,相等元素的原有相对顺序不变
toSorted() / toReversed() ES2023,非破坏性版本的 sort/reverse,返回新数组而不修改原数组

8.3 方法详解与对比

js 复制代码
var arr = ['A', 'B', 'C', 'D', 'E'];

// ════════ push / pop ------ 栈结构(LIFO)════════
arr.push('F', 'G');          // 末尾追加,返回新长度 7
var last = arr.pop();        // 删除末尾,返回 'G'
// arr: ['A','B','C','D','E','F']

// ════════ unshift / shift ------ 队列结构(FIFO)配合 push ════════
arr.unshift('Z');            // 开头插入,返回新长度
var first = arr.shift();     // 删除开头,返回 'Z'

// ════════ splice ------ 最强大的修改器方法 ════════
var arr2 = [10, 20, 30, 40, 50];

// 仅删除:从索引 1 开始删除 2 个元素
var removed = arr2.splice(1, 2);
console.log(removed); // [20, 30]
console.log(arr2);    // [10, 40, 50]

// 仅插入:从索引 1 插入,不删除(deleteCount = 0)
arr2.splice(1, 0, 'a', 'b');
console.log(arr2);    // [10, 'a', 'b', 40, 50]

// 替换:删除并插入
arr2.splice(1, 2, 'X');
console.log(arr2);    // [10, 'X', 40, 50]

// 从末尾删除(负数索引)
arr2.splice(-1, 1);
console.log(arr2);    // [10, 'X', 40]

// ════════ sort ------ 排序 ════════
var nums = [10, 1, 5, 20, 3];

// ⚠️ 默认排序按 UTF-16 字符串!数字排序结果错误
nums.sort();
console.log(nums); // [1, 10, 20, 3, 5](字符串比较:'10' < '20' < '3')

// ✅ 正确:传比较函数
nums.sort(function(a, b) { return a - b; }); // 升序(a-b < 0 时 a 在前)
nums.sort(function(a, b) { return b - a; }); // 降序

// 按对象属性排序(稳定排序,ES2019+)
var items = [
    { name: 'Cherry', price: 30 },
    { name: 'Apple',  price: 10 },
    { name: 'Banana', price: 20 },
];
items.sort(function(a, b) { return a.price - b.price; }); // 按价格升序

// ════════ reverse ════════
var letters = ['a', 'b', 'c', 'd'];
letters.reverse(); // ['d', 'c', 'b', 'a']

// ════════ ES2023 非破坏性方法 ════════
var original = [3, 1, 4, 1, 5];
var sorted = original.toSorted(function(a, b) { return a - b; }); // 新数组
var reversed = original.toReversed(); // 新数组
console.log(original); // [3, 1, 4, 1, 5](未修改!)
console.log(sorted);   // [1, 1, 3, 4, 5]

💡 代码解析

代码片段 含义
arr.push('F', 'G') 可以一次推入多个元素 ,全部追加到末尾,返回新数组的 length
arr2.splice(1, 2) 从索引 1 开始删除 2 个元素,被删除的元素作为数组返回。删除后原数组长度减少 2
arr2.splice(1, 0, 'a', 'b') deleteCount=0 表示不删除,在索引 1 处纯插入 'a''b',后面的元素自动后移
[10,1,5].sort()[1,10,5] 默认按字符串 UTF-16 排序,'10' < '5' 因为 '1' < '5',这是排序最常见的 bug 来源
sort((a,b) => a-b) 比较函数返回负数则 a 在前(升序),返回正数则 b 在前;a-b 是升序的简洁写法
original.toSorted() ES2023,返回新数组 ,原数组 original 保持不变,适合 React/Vue 等不可变数据流场景

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

场景 方法 业务价值
消息队列(先进先出) push 入队 + shift 出队 实现任务队列、消息推送队列,按顺序处理事件
浏览器历史记录栈 push 入栈 + pop 出栈 实现路由前进/后退、撤销/重做操作,pop 返回最近一条记录
动态列表项的增删 splice(index, 1) 删除 用户删除表格某一行、待办列表中删除某个任务
数组中间插入元素 splice(index, 0, newItem) 插队、在指定位置插入新评论/新数据
表格列表排序 sort((a,b) => a.field - b.field) 点击表格列头按该列升/降序排列数据
轮播图乱序 shuffle(images)(内部用 splice) 每次刷新页面呈现不同图片顺序,增加内容新鲜感

8.4 push/pop vs unshift/shift 性能差异

unshift / shift ------ O(n) 需移动所有元素
unshift('Z')
shift()

A, B, C

Z, A, B, C\] 所有元素右移 \[Z, A, B, C

A, B, C\] 所有元素左移 push / pop ------ O(1) 摊销复杂度 push('D') pop() \[A, B, C

A, B, C, D

A, B, C, D

A, B, C\] 返回 D > **性能建议** :在高频操作场景(如 10000+ 元素的数组),优先使用 `push/pop`(O(1)),而非 `unshift/shift`(O(n))。如果需要高效的队列操作,考虑使用链表或双端队列数据结构。 #### 8.5 sort 比较函数原理 \< 0(负数) = 0(零) \> 0(正数) sort(compareFn) 取两个元素 a, b 调用 compareFn(a, b) 返回值? a 排在 b 前面 保持相对位置(稳定排序 ES2019+) b 排在 a 前面 升序:return a - b 降序:return b - a 相等元素:原顺序不变 #### 8.6 完整可运行示例(待办列表) ```html 数组修改器方法 ------ 待办列表

待办列表

演示:push · pop · splice · sort · reverse

    ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/76bb99fd84474982a0a8cd915c082962.png) *** ** * ** *** > #### 📌 数组修改器方法 --- 知识特点总结 > > | 特点 | 描述 | > |------------------------|--------------------------------------------------------------------| > | **直接修改原数组** | 修改器方法是数组特有的,执行后原数组本身被改变,调用前需确认是否需要保留原数据 | > | **push/pop 是栈操作** | 末尾添加/删除,O(1) 摊销复杂度,是最高效的数组增删操作 | > | **unshift/shift 性能代价** | 开头插入/删除需要移动所有元素,O(n) 复杂度,大数组中频繁使用会有性能问题 | > | **splice 是瑞士军刀** | 一个方法实现插入、删除、替换三种操作,参数组合灵活,是面试高频考点 | > | **sort 默认是字符串排序** | 这是最常见的数字排序 bug 来源:`[10,1,5].sort()` 得到 `[1,10,5]`,必须传比较函数 | > | **sort 比较函数语义** | 返回负数→第一个参数在前,返回正数→第二个参数在前;升序:`(a,b)=>a-b` | > | **sort 的稳定性** | ES2019 后所有主流环境保证 `sort` 稳定;之前部分 V8 版本对长数组使用不稳定算法 | > | **ES2023 非破坏性方法** | `toSorted()` / `toReversed()` / `toSpliced()` 返回新数组,不修改原数组,函数式编程友好 | *** ** * ** *** ### 9 数组 Array:访问器方法与迭代方法 #### 9.1 理论基础:函数式编程思想 数组的迭代方法(`map`、`filter`、`reduce` 等)体现了\*\*函数式编程(Functional Programming)\*\*的核心思想: * **纯函数(Pure Function)**:相同输入,相同输出,无副作用 * **不可变性(Immutability)**:返回新数组,不修改原数组 * **高阶函数(Higher-Order Function)**:接受函数作为参数,或返回函数 * **声明式编程(Declarative Programming)**:描述"做什么"而非"怎么做" ```js var nums = [1, 2, 3, 4, 5]; // 命令式风格(告诉机器怎么做) var doubled1 = []; for (var i = 0; i < nums.length; i++) { doubled1.push(nums[i] * 2); } // 声明式风格(描述要什么) var doubled2 = nums.map(function(n) { return n * 2; }); // ES6 箭头函数更简洁 var doubled3 = nums.map(n => n * 2); ``` 函数式风格的代码通常**更简洁、更易读、更易测试**(因为没有副作用),是现代 JavaScript 开发的主流风格。 ##### 深层理论:范畴论基础------Functor、Monad 与函数组合 函数式编程的理论根基来自数学中的**范畴论(Category Theory)** 。虽然日常开发不需要精通范畴论,但了解其核心概念能帮助深度理解 `map`、`flatMap`、`reduce` 的本质。 **① 函子(Functor):可映射的容器** 范畴论中,\*\*函子(Functor)\*\*是一种"可以被 `map` 的结构"。直观理解:将一个函数应用到容器内的值,得到包含新值的同类容器,容器结构本身不改变。 数学定义:函子 F 满足: F.map(x => x) ≡ F(恒等律) F.map(f).map(g) ≡ F.map(x => g(f(x)))(组合律) JavaScript 中的 Functor: 数组 Array:arr.map(f).map(g) ≡ arr.map(x => g(f(x))) Promise:p.then(f).then(g) ≡ p.then(x => Promise.resolve(f(x)).then(g)) Maybe(可选值):null 安全地传播,非 null 时应用函数 ```js // Array 作为 Functor 的验证 var arr = [1, 2, 3]; var double = x => x * 2; var addOne = x => x + 1; // 组合律:两次 map 等价于一次组合后的 map var r1 = arr.map(double).map(addOne); // [3, 5, 7] var r2 = arr.map(x => addOne(double(x))); // [3, 5, 7] // r1 深度相等 r2,但 r2 只遍历一次,性能更好 // 实际应用:使用 reduce 替代链式 map,减少中间数组创建 var transform = arr.reduce((acc, x) => { acc.push(addOne(double(x))); return acc; }, []); // [3, 5, 7],只遍历一次,零中间数组 ``` > **💡 代码解析** > > | 代码片段 | 含义 | > |--------------------------------------------------------------|------------------------------------------------------------------------------------| > | `arr.map(double).map(addOne)` | 两次独立遍历:第一次 `map(double)` 创建中间数组 `[2,4,6]`,第二次 `map(addOne)` 再遍历一次,共分配两个新数组 | > | `arr.map(x => addOne(double(x)))` | 函子组合律的直接应用:在单次遍历中同时执行两个变换,只创建一个结果数组,比两次 `map` 少一次遍历和一次内存分配 | > | `arr.reduce((acc, x) => { acc.push(...); return acc; }, [])` | 利用 `reduce` 实现"transducer"风格:初始累加器为空数组 `[]`,每次迭代手动 push 变换结果,结果与组合 `map` 等价但避免中间数组 | > | 函子的恒等律 `F.map(x => x) ≡ F` | 对任意值应用恒等函数,结果与原容器等价;这是数学约束,确保 `map` 不会"改变结构" | **② 单子(Monad):可以被 flatMap 的容器** \*\*单子(Monad)\*\*是函子的扩展,增加了"展平"能力------处理嵌套的容器。`flatMap`(或 `chain`)是 Monad 的核心操作: Monad 需要满足三条定律: 1. 左单位律:M.of(a).flatMap(f) ≡ f(a) 2. 右单位律:m.flatMap(M.of) ≡ m 3. 结合律: m.flatMap(f).flatMap(g) ≡ m.flatMap(x => f(x).flatMap(g)) ```js // Array.flatMap 是 Array Monad 的实现 var sentences = ['Hello World', 'Foo Bar']; // 问题:map 后得到嵌套数组 var words1 = sentences.map(s => s.split(' ')); // [['Hello','World'], ['Foo','Bar']](嵌套) // flatMap 自动展平一层------这就是 Monad 的 flatMap 操作 var words2 = sentences.flatMap(s => s.split(' ')); // ['Hello', 'World', 'Foo', 'Bar'](展平) // Promise 是 Monad(Promise.then 自动展平嵌套 Promise) var p1 = Promise.resolve(42).then(x => Promise.resolve(x * 2)); // p1 解析为 84,而不是 Promise> // 这就是 Monad 的"展平"行为 ``` > **💡 代码解析** > > | 代码片段 | 含义 | > |---------------------------------------------------------|------------------------------------------------------------------------------------------------| > | `sentences.map(s => s.split(' '))` | `map` 将每个字符串变为字符串数组,结果是**嵌套数组** :`[['Hello','World'], ['Foo','Bar']]`,多了一层容器 | > | `sentences.flatMap(s => s.split(' '))` | `flatMap = map + flatten(1)`:先 map 得到嵌套数组,再自动展平一层,结果是一维数组 `['Hello','World','Foo','Bar']` | > | `Promise.resolve(42).then(x => Promise.resolve(x * 2))` | `.then()` 接收到 `Promise.resolve(84)` 时,自动展平(unwrap),使 `p1` 解析为值 `84` 而非 `Promise<84>` | > | Monad 的展平行为 | 若 `.then()` 不展平,链式调用 `p.then(f).then(g)` 会得到 `Promise>`,每层嵌套一个 Promise,这在实践中是灾难性的 | **③ 函数组合(Function Composition)** 函数式编程的核心原则之一是"将小函数组合成复杂函数",类似数学中的复合函数 `(f ∘ g)(x) = f(g(x))`: ```js // 手动实现 compose(从右向左执行) function compose(...fns) { return function(x) { return fns.reduceRight(function(acc, fn) { return fn(acc); }, x); }; } // 手动实现 pipe(从左向右执行,更直观) function pipe(...fns) { return function(x) { return fns.reduce(function(acc, fn) { return fn(acc); }, x); }; } // 示例:构建数据处理管道 var processPrice = pipe( price => price * 1.13, // 加 13% 税 price => Math.round(price * 100) / 100, // 精确到分 price => '¥' + price.toFixed(2) // 格式化为货币字符串 ); console.log(processPrice(99.9)); // "¥112.89" console.log(processPrice(199)); // "¥224.87" ``` > **💡 代码解析** > > | 代码片段 | 含义 | > |------------------------------------------|-----------------------------------------------------------------| > | `compose(...fns)` 使用 `reduceRight` | `compose(f, g, h)(x) = f(g(h(x)))`:从右向左执行,符合数学函数组合记法 `(f∘g)(x)` | > | `pipe(...fns)` 使用 `reduce` | `pipe(h, g, f)(x) = f(g(h(x)))`:从左向右执行,阅读顺序与数据流方向一致,更符合开发者直觉 | > | `price => price * 1.13` | 第一步:计算含税价格(加13%增值税);作为纯函数,只接受一个值,返回一个新值,无副作用 | > | `price => Math.round(price * 100) / 100` | 第二步:精确到分(两位小数),避免浮点精度问题(`1213 * 100` 后取整再除以 100) | > | `price => '¥' + price.toFixed(2)` | 第三步:格式化为货币字符串,`toFixed(2)` 确保始终显示两位小数 | > | 管道模式的优势 | 每一步都是独立的纯函数,可单独测试;添加新步骤(如折扣)只需在 `pipe` 中插入,不修改现有逻辑 | **④ 柯里化(Currying)与偏函数应用** ```js // 柯里化:将多参数函数转为一系列单参数函数 function curry(fn) { return function curried(...args) { if (args.length >= fn.length) { return fn.apply(this, args); } return function(...args2) { return curried.apply(this, args.concat(args2)); }; }; } var add = curry((a, b, c) => a + b + c); var add5 = add(2)(3); // 偏函数应用:固定前两个参数 console.log(add5(10)); // 15 console.log(add5(20)); // 25 // 在数组方法中的应用:创建可复用的过滤器 var isAbove = curry((threshold, value) => value > threshold); var isAdult = isAbove(18); var isHigh = isAbove(90); var ages = [12, 25, 17, 31, 15, 22]; console.log(ages.filter(isAdult)); // [25, 31, 22] var scores = [85, 95, 72, 91, 88]; console.log(scores.filter(isHigh)); // [95, 91] ``` > **💡 代码解析** > > | 代码片段 | 含义 | > |----------------------------------------------------------------|----------------------------------------------------------------------------------| > | `function curry(fn)` | 通用柯里化函数:检查已收集的参数数量是否达到原函数的参数数(`fn.length`),未达到则返回一个新函数继续收集参数 | > | `if (args.length >= fn.length)` | `fn.length` 是函数的形参个数;已收集参数 ≥ 形参时,立即调用原函数;否则返回等待更多参数的闭包 | > | `var add5 = add(2)(3)` | 偏函数应用(Partial Application):`add(2)` 返回等待第2个参数的函数,`add(2)(3)` 返回等待第3个参数的函数 `add5` | > | `var isAbove = curry((threshold, value) => value > threshold)` | 将双参数函数柯里化;`isAbove(18)` 固定 threshold=18,返回可直接传入 `filter` 的单参数谓词函数 | > | `ages.filter(isAdult)` | `isAdult` 等价于 `age => age > 18`,作为 `filter` 的回调,无需额外包装,体现柯里化的"创建可复用谓词"能力 | > **总结** :`map` 对应函子(Functor)的 fmap 操作;`flatMap` 对应单子(Monad)的 bind 操作;`reduce` 是代数结构中的"折叠(fold)"操作,是所有迭代方法的理论基础。掌握这些概念,你就掌握了函数式编程的核心抽象,理解 RxJS、Ramda.js、fp-ts 等函数式库的设计逻辑也会事半功倍。 #### 9.2 名词解释 | 术语 | 定义 | |-----------------------------|------------------------------------------------------| | **访问器方法(Accessor Methods)** | 不改变原数组,返回新数组或其他值,如 `concat`、`slice`、`join`、`indexOf` | | **迭代方法(Iteration Methods)** | 遍历数组每个元素并执行回调函数,如 `forEach`、`map`、`filter`、`reduce` | | **回调函数(Callback)** | 作为参数传入的函数,接收 `(item, index, array)` 三个参数 | | **`forEach`** | 遍历,无返回值(`undefined`),用于执行副作用(DOM 操作、打印日志等) | | **`map`** | 转换每个元素,返回同等长度的**新数组** | | **`filter`** | 筛选满足条件的元素,返回**新数组**(可能更短) | | **`reduce`** | 累加器,从左到右将数组折叠为单一值(数字、字符串、对象、数组均可) | | **`reduceRight`** | 与 `reduce` 相同,但从右到左遍历 | | **`every`** | 所有元素都满足条件时返回 `true`(短路:遇到第一个 false 立即停止) | | **`some`** | 任一元素满足条件时返回 `true`(短路:遇到第一个 true 立即停止) | | **`find`** | ES6,返回第一个满足条件的**元素本身** (找不到→`undefined`) | | **`findIndex`** | ES6,返回第一个满足条件元素的**索引** (找不到→`-1`) | | **`flat(depth)`** | ES2019,将嵌套数组展平到指定深度(默认深度为 1) | | **`flatMap(fn)`** | ES2019,先 `map` 再 `flat(1)`,一步完成映射和展平 | | **`Array.from()`** | ES6,将类数组对象或可迭代对象转为数组 | | **`Array.of()`** | ES6,用参数创建数组(避免 `new Array(n)` 创建空洞数组的歧义) | #### 9.3 访问器方法详解 ```js var a = [1, 2, 3]; var b = [4, 5, 6]; // ① concat ------ 合并数组(原数组不变) var c = a.concat(b, [7, 8], 9); console.log(c); // [1, 2, 3, 4, 5, 6, 7, 8, 9] console.log(a); // [1, 2, 3](原数组不变) // ES6 展开运算符等价:var c = [...a, ...b, 7, 8, 9]; // ② slice(start, end) ------ 截取(支持负数,不修改原数组) var arr = [10, 20, 30, 40, 50]; console.log(arr.slice(1, 4)); // [20, 30, 40](不含 end 索引的元素) console.log(arr.slice(-2)); // [40, 50](从倒数第2个到末尾) console.log(arr.slice()); // [10,20,30,40,50](浅拷贝整个数组) // ③ join ------ 数组转字符串 var words = ['Hello', 'World', 'JS']; console.log(words.join(' ')); // 'Hello World JS' console.log(words.join('->')); // 'Hello->World->JS' console.log(words.join('')); // 'HelloWorldJS' console.log(words.join()); // 'Hello,World,JS'(默认逗号) // ④ indexOf / lastIndexOf var nums = [1, 2, 3, 2, 1]; console.log(nums.indexOf(2)); // 1(第一次出现的索引) console.log(nums.lastIndexOf(2)); // 3(最后一次出现的索引) console.log(nums.indexOf(9)); // -1(不存在) // ⑤ includes ------ 比 indexOf 更语义化(ES6) console.log(nums.includes(3)); // true console.log(nums.includes(9)); // false // 注意:includes 能正确判断 NaN,indexOf 不行 console.log([NaN].includes(NaN)); // true console.log([NaN].indexOf(NaN)); // -1(NaN !== NaN) ``` > **💡 代码解析** > > | 代码片段 | 含义 | > |------------------------------------------------|----------------------------------------------------------------------------------------| > | `a.concat(b, [7,8], 9)` | 可以合并多个数组/值,传入数组会被展开一层(只展开一层,不递归);ES6 的 `[...a, ...b]` 是等价的现代写法 | > | `arr.slice(1, 4)` | 左闭右开区间,取索引 1、2、3 的元素,索引 4 对应的元素 `50` 不包含 | > | `arr.slice(-2)` | 负数索引从末尾计算:`-2` 等同于 `length - 2 = 3`,即从索引 3 到末尾 | > | `arr.slice()` | 无参数表示复制整个数组,是获取数组**浅拷贝**的简洁写法 | > | `words.join()` 无参数 | 默认用逗号分隔,等价于 `join(',')` | > | `indexOf(2)` → `1` | 从左开始找,返回**第一次** 出现的索引;用于判断元素是否存在时常与 `!== -1` 配合 | > | `includes(NaN)` → `true`,`indexOf(NaN)` → `-1` | `includes` 使用 **SameValueZero** 算法,可以判断 `NaN`;`indexOf` 使用 `===`,而 `NaN !== NaN`,故无法找到 | > > **🏢 经典使用场景 \& 业务价值** > > | 场景 | 方法 | 业务价值 | > |----------------|----------------------------------------------------------------|-----------------------------------| > | **合并分页数据** | `allItems.concat(newPage)` | 无限滚动加载新一页数据,合并到现有列表,原数组不变,状态管理安全 | > | **分页截取展示** | `arr.slice((page-1)*size, page*size)` | 前端分页:将大数组按页切片,不需要额外的 API 调用 | > | **表格转 CSV 导出** | `rows.map(r => r.join(',')).join('\n')` | 将二维数组拼成 CSV 格式字符串,触发文件下载 | > | **标签/权限包含检查** | `permissions.includes('admin')` | 检查用户权限列表中是否包含某权限,决定是否显示功能按钮 | > | **查找位置并高亮** | `arr.indexOf(searchValue)` | 找到匹配项的位置后,结合 DOM 操作将该行高亮显示 | > | **数据有效性校验** | `[NaN, undefined].some(v => !Number.isFinite(v))` + `includes` | 检测传感器/API 数据中的异常值,过滤 `NaN`/`null` | #### 9.4 迭代方法详解 ```js var products = [ { name: 'Laptop', price: 999, stock: 5 }, { name: 'Phone', price: 599, stock: 0 }, { name: 'Tablet', price: 399, stock: 3 }, { name: 'Monitor', price: 299, stock: 8 }, { name: 'Keyboard', price: 79, stock: 0 }, ]; // ════════ forEach ------ 遍历副作用 ════════ products.forEach(function(item, index) { // 回调函数参数:item(当前元素)、index(索引)、array(数组本身) // console.log(index, item.name); }); // 注意:forEach 返回 undefined,不能链式调用 // ════════ filter ------ 筛选 ════════ var inStock = products.filter(function(item) { return item.stock > 0; // 返回 true 的元素保留 }); console.log(inStock.length); // 3 // ════════ map ------ 转换 ════════ var names = products.map(function(item) { return item.name; // 每个元素转换为名字 }); // ['Laptop', 'Phone', 'Tablet', 'Monitor', 'Keyboard'] var discounted = products.map(function(item) { // 转换为含折扣价的新对象(不修改原对象是最佳实践) return Object.assign({}, item, { salePrice: Math.round(item.price * 0.9) }); }); // ════════ every / some ------ 短路判断 ════════ var allInStock = products.every(function(item) { return item.stock > 0; }); console.log('全部有货:', allInStock); // false(遇到第一个无货立即返回 false) var anyInStock = products.some(function(item) { return item.stock > 0; }); console.log('有货可卖:', anyInStock); // true(遇到第一个有货立即返回 true) // ════════ find / findIndex ------ 查找(ES6)════════ var laptop = products.find(function(item) { return item.name === 'Laptop'; }); console.log(laptop.price); // 999 var laptopIdx = products.findIndex(function(item) { return item.name === 'Laptop'; }); console.log(laptopIdx); // 0 > **💡 代码解析(forEach / filter / map / every / some / find)** > > | 代码片段 | 含义 | > |---------|------| > | `products.forEach(fn)` | 纯粹的遍历,用于产生**副作用**(如打印、更新 DOM、累加外部变量);返回 `undefined`,不可链式 | > | `products.filter(item => item.stock > 0)` | 回调返回 `true` 则该元素进入结果数组,返回 `false` 则跳过;结果数组长度 ≤ 原数组 | > | `products.map(item => item.name)` | 对每个元素应用转换函数,返回**等长**的新数组;不修改原数组,是函数式编程的核心方法 | > | `Object.assign({}, item, { salePrice: ... })` | 先浅拷贝 `item`(避免修改原对象),再用新属性覆盖,得到带折扣价的新对象;符合不可变数据原则 | > | `products.every(fn)` | 如同"且"关系:全部通过才返回 `true`;遇到第一个**不满足**的立即短路返回 `false` | > | `products.some(fn)` | 如同"或"关系:有一个通过就返回 `true`;遇到第一个**满足**的立即短路返回 `true` | > | `find(fn)` vs `findIndex(fn)` | `find` 返回满足条件的**第一个元素本身**(对象引用);`findIndex` 返回其**索引**;不存在时分别返回 `undefined` 和 `-1` | // ════════ reduce ------ 最强大的迭代方法 ════════ // 参数:(accumulator, currentValue, currentIndex, array) => newAccumulator // 第二个参数:初始累加值 // 计算库存总价值 var totalValue = products.reduce(function(acc, item) { return acc + item.price * item.stock; }, 0); // 初始值为 0 // 数组去重 var withDups = [1, 2, 2, 3, 3, 3, 4]; var unique = withDups.reduce(function(acc, item) { if (!acc.includes(item)) acc.push(item); return acc; }, []); // [1, 2, 3, 4] // 对象分组(Group By) var grouped = products.reduce(function(acc, item) { var key = item.price >= 500 ? '高价' : item.price >= 200 ? '中价' : '低价'; if (!acc[key]) acc[key] = []; acc[key].push(item.name); return acc; }, {}); // { '高价': ['Laptop', 'Phone'], '中价': ['Tablet', 'Monitor'], '低价': ['Keyboard'] } // 管道函数(Function Pipeline) var pipeline = [ function(arr) { return arr.filter(function(x) { return x > 0; }); }, function(arr) { return arr.map(function(x) { return x * 2; }); }, function(arr) { return arr.reduce(function(a, b) { return a + b; }, 0); } ]; var result = pipeline.reduce(function(value, fn) { return fn(value); }, [-1, 2, -3, 4, 5]); console.log(result); // (2+4+5)*2 = 22 → 先过滤正数,再翻倍,再求和 ``` > **💡 代码解析** > > | 代码片段 | 含义 | > |----------------------------------|-------------------------------------------------------------------| > | `filter(item => item.stock > 0)` | 筛选有库存的商品,回调返回 `true` 的元素保留;原数组 `products` 不被修改 | > | `map(item => item.name)` | 将每个商品对象转换为其名称字符串,返回长度相同的字符串数组 | > | `every(item => item.stock > 0)` | 遇到第一个无库存商品立即停止(短路),返回 `false`;若全部有货才返回 `true` | > | `some(item => item.stock > 0)` | 遇到第一个有库存商品立即停止(短路),返回 `true`;全无货才返回 `false` | > | `reduce(fn, 0)` 计算总价值 | `acc` 是累计结果(初始为 `0`),每轮回调将当前商品价值 `price * stock` 加入,最终 `acc` 即总价值 | > | `reduce` 对象分组(Group By) | 以价格区间为键,将商品名按组归类,最终得到分类字典;这是 `reduce` 最强大的应用之一 | > | `pipeline.reduce(fn, initValue)` | 管道模式:`reduce` 将函数数组顺序应用于初始值,每个函数的输出作为下一个函数的输入,实现函数组合 | > | `withDups.reduce` 去重 | 利用 `acc` 作为结果数组,仅在 `acc` 中不存在时才追加当前元素,实现去重(不改变元素顺序) | > > **🏢 经典使用场景 \& 业务价值** > > | 场景 | 推荐方法 | 业务价值 | > |--------------|---------------------------------------------------------|------------------------------------------------| > | **列表渲染** | `map` → HTML 字符串 / JSX | React/Vue 列表组件的核心模式,数据驱动视图,代码量比 `for` 循环减少 60% | > | **搜索筛选** | `filter` | 商品搜索、用户列表过滤,不修改原数据,用户可快速重置筛选条件 | > | **购物车总价** | `reduce((acc, item) => acc + item.price * item.qty, 0)` | 一行代码完成所有商品价格的求和,无需中间变量 | > | **按类别分组展示** | `reduce` Group By | 订单按日期分组、消息按发送者分组、统计各状态数量 | > | **全选/半选状态** | `every` + `some` | 表格全选逻辑:所有行选中则全选框勾选,有任意行选中则半选状态 | > | **无限滚动数据去重** | `reduce` 去重 | 分页加载时可能推送重复数据,去重后再渲染避免 key 冲突 | > | **数据处理管道** | `filter().map().reduce()` 链式 | 清洗原始数据(过滤无效项)→ 转换格式 → 汇总统计,替代多次循环 | #### 9.5 reduce 的执行过程图解(Mermaid) 回调函数 reduce 调用者 回调函数 reduce 调用者 \[1,2,3,4,5\].reduce(fn, 0) fn(0, 1, 0, arr) ← acc=0, item=1 返回 1 fn(1, 2, 1, arr) ← acc=1, item=2 返回 3 fn(3, 3, 2, arr) ← acc=3, item=3 返回 6 fn(6, 4, 3, arr) ← acc=6, item=4 返回 10 fn(10, 5, 4, arr) ← acc=10, item=5 返回 15 最终结果:15 #### 9.6 迭代方法选择决策树(Mermaid) 否,只做副作用 是 是 不变(转换元素) 变短(筛选元素) 展平嵌套数组 否 单一值(数字/字符串/对象) 布尔值(全部满足) 布尔值(至少一个) 第一个匹配的元素 第一个匹配的索引 我需要对数组做什么? 需要返回值吗? forEach 遍历,无返回值 返回新数组? 数组长度变化? map 一对一转换 filter 条件筛选 flat / flatMap 返回什么? reduce / reduceRight 累加归纳 every 短路,全true才true some 短路,有true就true find 返回元素本身 findIndex 返回索引 #### 9.7 完整可运行示例(商品筛选器) ```html 数组迭代方法 ------ 商品筛选器

    商品筛选器

    演示 filter · map · reduce · every · some 等数组迭代方法

    筛选用 filter · 统计用 reduce · 渲染用 map
    ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c021a823ed33453eb738d743ae7bdfdf.png) *** ** * ** *** > #### 📌 数组访问器与迭代方法 --- 知识特点总结 > > | 特点 | 描述 | > |-----------------------------|--------------------------------------------------------------------------| > | **不修改原数组** | 访问器/迭代方法均返回新数据,函数式编程核心原则,便于调试和测试 | > | **`map` 长度不变** | `map` 的返回数组长度永远与原数组相同,若需要过滤应搭配 `filter` 使用 | > | **`reduce` 无所不能** | `reduce` 是数组最通用的方法,`map`/`filter`/`every`/`some`/`find` 都可以用 `reduce` 实现 | > | **`forEach` vs `map`** | 需要副作用用 `forEach`;需要返回新数组用 `map`。`forEach` 返回 `undefined`,不可链式调用 | > | **`every` / `some` 短路** | `every` 遇到第一个 `false` 立即停止;`some` 遇到第一个 `true` 立即停止,可以提升性能 | > | **`includes` 优于 `indexOf`** | `includes()` 语义更清晰,且能正确处理 `NaN`(`[NaN].includes(NaN)` 为 `true`) | > | **回调函数三个参数** | 回调函数可接收 `(item, index, array)` 三个参数,`array` 参数可在回调中访问整个数组 | > | **链式调用是主流** | `arr.filter(...).map(...).reduce(...)` 的链式风格是现代 JS 最常见的数据处理模式 | *** ** * ** *** ### 10 经典笔试题精讲 #### 10.1 原型链经典题一:方法归属判断 ```js var F = function() {}; Object.prototype.a = function() { console.log('a'); }; Function.prototype.b = function() { console.log('b'); }; var f = new F(); ``` **原型链分析:** __proto__ __proto__ __proto__ __proto__ __proto__ f(F 的实例) F.prototype Object.prototype(含方法 a) null 构造函数 F Function.prototype(含方法 b) **结论:** * `f.a()` ✅:f → F.prototype → **Object.prototype**(有 a) * `f.b()` ❌:f → F.prototype → Object.prototype(没有 b,Function.prototype 不在此链上) * `F.a()` ✅:F → Function.prototype → **Object.prototype**(有 a) * `F.b()` ✅:F → **Function.prototype**(有 b) > **💡 答题思路解析** :判断 `x.method()` 是否可调用,先确定 `x` 是**实例** 还是**函数(构造函数)** 。实例的原型链不经过 `Function.prototype`,只经过其构造函数的 `prototype` 和 `Object.prototype`;函数(构造函数)的原型链经过 `Function.prototype`,再到 `Object.prototype`。抓住这条核心规律,所有同类题迎刃而解。 *** ** * ** *** #### 10.2 原型链经典题二:原型替换 ```js function User() {} User.prototype = { name: 'aaaa' }; // 整体替换原型 var u = new User(); // u 的 [[Prototype]] 固定指向此时的 User.prototype,即 { name: 'aaaa' } console.log(u.name); // 'aaaa' User.prototype.name = 'bbb'; // 修改当前原型对象的属性 // u 的 [[Prototype]] 仍是这个对象,所以看到修改 console.log(u.name); // 'bbb' User.prototype = { name: 'ccc' }; // 用新对象替换 User.prototype // u 的 [[Prototype]] 不变,仍指向原来那个对象 console.log(u.name); // 'bbb'(不受新原型影响) ``` **核心理解** :`new User()` 时,`u.[[Prototype]]` 已经固定指向当时的 `User.prototype` 对象。之后给 `User.prototype` 重新赋值,改变的是 `User.prototype` 变量的指向,但已有实例的 `[[Prototype]]` 引用不会随之改变。 > **💡 答题思路解析**: > > * 第二次输出 `'bbb'`:`User.prototype.name = 'bbb'` 是在**原对象上修改属性** ,`u` 的原型链仍指向这个对象,所以能看到修改。 > * 第三次输出 `'bbb'`:`User.prototype = { name: 'ccc' }` 是让 `User.prototype` 这个**变量** 指向新对象,不影响已存在实例 `u` 的 `[[Prototype]]`。记忆口诀:**修改属性影响已有实例,替换原型不影响已有实例**。 *** ** * ** *** #### 10.3 原型链经典题三:f 和 F 的原型链(综合) ```js Object.prototype.a = function() { console.log('a'); }; Function.prototype.b = function() { console.log('b'); }; var F = function() {}; // F 是 Function 的实例 var f = []; // f 是 Array 的实例 f.a(); // a ✅(f → Array.prototype → Object.prototype,有 a) F.a(); // a ✅(F → Function.prototype → Object.prototype,有 a) F.b(); // b ✅(F → Function.prototype,有 b) f.b(); // ❌ 报错(f → Array.prototype → Object.prototype,没有 b) ``` > **💡 答题思路解析** :关键在于识别 `f = []` 是**数组实例** ,它的原型链是 `[] → Array.prototype → Object.prototype → null`,完全不经过 `Function.prototype`。而 `F` 是**函数** ,其原型链为 `F → Function.prototype → Object.prototype → null`,因此能访问 `b`。在笔试中,看到类似题目,先画出两条原型链,再逐一对照查找。 *** ** * ** *** #### 10.4 引用类型经典题:函数参数传递综合 ```js function Person(name, age) { this.name = name; this.age = age; } function modify(pp) { pp.name = 'Bob'; // 通过地址修改属性 → 影响外部 pp = new Person('New', 0); // 局部变量 pp 重新指向新对象 → 不影响外部 } var p = new Person('Alice', 25); console.log(p.name); // 'Alice' modify(p); console.log(p.name); // 'Bob'(属性修改生效) // pp 是函数的局部变量,函数外部无法访问 ``` > **💡 答题思路解析**: > > * `pp.name = 'Bob'`:`pp` 持有与 `p` **相同的堆地址** ,通过地址直接修改堆上对象的属性,外部 `p` 能感知。 > * `pp = new Person('New', 0)`:`pp` 是函数**局部变量** ,重新赋值只是让局部变量改变指向,外部 `p` 的指向毫不知情。 > * 解题口诀:**改属性,外部知;换地址,外部不知**。 **内存状态变化序列图:** 堆内存 modify() 内部 外部作用域 堆内存 modify() 内部 外部作用域 p 仍然指向 0x100,不受影响 p → 地址 0x100 { name:'Alice', age:25 } 调用 modify(p),传入 0x100 pp 指向 0x100,修改 name → 'Bob' p.name 现在是 'Bob'(共享同一对象) pp = new Person('New',0),pp 指向新地址 0x200 函数结束,pp 销毁 console.log(p.name) → 'Bob' *** ** * ** *** #### 10.5 值类型经典练习集 ```js // 练习 1:基本赋值 var num1 = 10; var num2 = num1; num1 = 20; console.log(num1); // 20 console.log(num2); // 10(复制的是值,互不影响) // 练习 2:函数内修改形参(值类型) var num = 50; function f1(num) { num = 60; // 修改的是函数局部变量 console.log(num); // 60 } f1(num); console.log(num); // 50(外部不受影响) // 练习 3:全局变量 vs 局部变量 var num1 = 55; var num2 = 66; function f1(num, num1) { // 形参 num 和 num1 是局部变量,遮蔽全局 num = 100; // 局部变量 num1 = 100; // 局部变量(遮蔽全局 num1) num2 = 100; // 全局变量(无局部声明) } f1(num1, num2); console.log(num1); // 55(全局 num1 没被修改,局部 num1 不影响全局) console.log(num2); // 100(全局 num2 被修改) // 练习 4:引用类型函数参数 function f2(arr) { for (var i = 0; i < arr.length; i++) { arr[i] += 2; // 通过地址修改数组元素 } } var arr = [1, 2]; f2(arr); console.log(arr); // [3, 4](受影响) // 练习 5:同一对象的两个变量 var a = [1, 2]; var b = a; a[0] = 20; // 通过 a 修改数组元素 console.log(b); // [20, 2](b 受影响,同一对象) a = [20, 2]; // a 重新赋值,指向新数组 console.log(b); // [20, 2](b 不受影响,仍指向原数组) ``` *** ** * ** *** ### 11 综合实战案例 #### 11.1 驼峰命名转换(作业一) ```html 驼峰命名转换

    连字符 → 驼峰命名转换器

    算法:split('-') → map(首词保留,其余首字母大写)→ join('')
    结果
    ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c6a7dcbda80346568ee1ab0cb81e2918.png) > **💡 代码解析** > > | 代码片段 | 含义 | > |------------------------------------------------|--------------------------------------------------------------------------| > | `str.split('-')` | 将连字符分隔的字符串拆分为数组,例如 `'get-element-by-id'` → `['get','element','by','id']` | > | `.map(function(word, index){...})` | 第二个参数 `index` 用来区分"首词不大写"的特殊情况,`index === 0` 时原样返回 | > | `word.charAt(0).toUpperCase() + word.slice(1)` | 首字母大写:取首字母 → 转大写 → 拼接剩余部分,是字符串处理的经典组合 | > | `.join('')` | 以空字符串为分隔符重新拼合,消除数组形式,还原为字符串 | > > **🏢 业务价值** :前端与后端通常约定不同的字段命名风格(后端 Python/Java 用 `snake_case`,JS 用 `camelCase`)。`toCamelCase` 工具函数可批量转换 API 返回的字段名,让代码风格统一,减少手动映射的工作量。 #### 11.2 字符串翻转(作业二) ```html 字符串翻转

    字符串翻转(Array reverse 方法)


    翻转结果

    核心原理:split('') 将字符串分割为字符数组 → reverse()(数组修改器方法)→ join('') 拼回字符串

    ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/5d6b8f7b1c784c27b608a9b444a63991.png) > **💡 代码解析** > > | 代码片段 | 含义 | > |---------------------------------------------------------|----------------------------------------------------------| > | `s.split('').reverse().join('')` | 字符串翻转三部曲:单字符数组化 → 数组反转 → 重新拼合。字符串本身无 `reverse` 方法,借用数组完成 | > | `s.split(' ').reverse().join(' ')` | 翻转的是**单词**:按空格分割,反转单词顺序,再用空格拼合;适合英文句子单词级翻转 | > | `.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]/g, '')` | 回文检测预处理:统一小写,去除标点/空格等非字母数字字符,`\u4e00-\u9fa5` 保留汉字范围 | > | `s === reversed` | 预处理后的字符串与翻转版本直接比较,相等则为回文;值类型 `===` 比较字符串内容 | > > **🏢 业务价值** :字符串翻转是算法面试高频题,核心模式(`split/reverse/join`)同样用于:① 实现撤销/重做(反转操作列表);② 翻转 UI 中的排序方向;③ 编码/解密简单字符串(配合其他手段)。回文检测用于:输入校验(身份证号部分场景)、文本分析。 #### 11.3 随机抽取(作业三) ```html 随机抽取

    随机抽取工具

    核心公式:arr[Math.floor(Math.random() * arr.length)]
    抽取结果:
    点击按钮抽取
    ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/22b35eca431542caa63093399f97abf4.png) > **💡 代码解析** > > | 代码片段 | 含义 | > |-----------------------------------------------|-----------------------------------------------------------| > | `var indices = [];` 然后 `splice(idx, 1)` | 先用 `randomIntMtoN` 随机取一个索引,从候选索引列表中移除(防止重复抽到同一个名字),再取对应名字 | > | `picked.join(' · ')` | 多个中奖者名字用 `·` 连接为字符串,直观展示给用户 | > | `historyLog.unshift(...)` | 将本次结果**插入历史记录开头** (`unshift` 在数组最前面添加),最新的记录总在最前 | > | `historyLog.slice(0, 5)` | 只展示最近 5 条历史,避免记录无限增长撑满界面 | > | `Math.floor(Math.random() * (n - m + 1)) + m` | 核心随机抽取算法,确保 `[m, n]` 范围内每个整数被选中的概率相等 | > > **🏢 业务价值** :随机抽取是电商/直播场景的高频需求(抽奖、红包、随机优惠券)。核心技术点:① 用索引数组配合 `splice` 避免重复抽取;② `Fisher-Yates 洗牌算法`(Math 章节)保证分布均匀性;③ 历史记录用数组管理,`unshift+slice` 实现"最近N条"的固定窗口展示。 #### 11.4 日期格式化输出(作业四) ```html 日期格式化输出

    日期时间格式化演示

    核心技巧:padStart(2, '0') 补零 · getMonth()+1 修正月份

      ``` ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/35214f34f5484f8ca08f177c023283a1.png) > **💡 代码解析** > > | 代码片段 | 含义 | > |----------------------------------------------------------|-------------------------------------------------------------| > | `function pad(n) { return String(n).padStart(2, '0'); }` | 封装补零函数:所有日期分量(月、日、时、分、秒)都需要补零,提取为工具函数避免重复 | > | `d.getMonth() + 1` | `getMonth()` 返回 0-11,月份必须手动 `+1`,这是 Date API 最知名的"陷阱"设计 | > | `weekdays[d.getDay()]` | `getDay()` 返回 0-6(0=周日),以此为索引查询预定义的星期名称数组,比 `if/switch` 更简洁 | > | `formats.map(f => ...).join('')` | 将格式化数据数组映射为 HTML 字符串数组,再拼合为完整 HTML 片段,赋值给 `innerHTML` | > | `setInterval(updateTime, 1000)` | 每 1000ms(1秒)调用一次 `updateTime`,实现实时时钟更新显示 | > > **🏢 业务价值** :日期格式化是几乎所有 Web 应用的基础需求。核心价值在于:① 将后端时间戳转为用户友好格式;② `setInterval` 驱动实时时钟/倒计时,用于展示活动剩余时间;③ 多格式支持(ISO 用于 API,本地字符串用于展示,时间戳用于计算),一套工具函数满足所有场景。 *** ** * ** *** ### 12 知识点总结与速查表 JavaScript 核心 原型链 设计哲学 委托模型 **proto** 内部原型槽 prototype 构造函数属性 Object.prototype 顶端 null 终点 new 的四步执行过程 hasOwnProperty 区分来源 Object.create 精确控制 instanceof 原型链检查 class 是语法糖 值 vs 引用 值类型 7种原始类型 number string boolean null undefined symbol bigint 栈存储 值传递 不可变 引用类型 Object Array Function Date 堆存储 地址传递 可变 判等靠地址 浅拷贝 vs 深拷贝 GC 垃圾回收 内置对象 Boolean Falsy 8种 Truthy 其余 短路求值 与或空值合并 包装对象陷阱 双重取反转布尔 Number IEEE 754 双精度 toFixed toString toPrecision MAX_SAFE_INTEGER EPSILON isNaN isInteger isFinite 浮点精度解决方案 String UTF-16 编码 不可变性 slice substring substr split join 互相转换 padStart padEnd trim includes startsWith endsWith 模板字面量 ES6 Math 命名空间对象 floor ceil round trunc abs pow sqrt random 随机整数公式 纯函数设计 Date Unix时间戳 月份从0开始 getFullYear Month Date Day 格式化 padStart补零 daysBetween 计算差值 数组方法 修改器 改变原数组 push pop 末尾 常数时间 unshift shift 开头 线性时间 splice 万能增删改 sort 需比较函数 reverse 翻转 ES2023 toSorted toReversed 访问器 不改变原数组 concat 合并 slice 截取 join 转字符串 indexOf lastIndexOf includes ES6 迭代器 函数式编程 forEach 副作用 map 转换 长度不变 filter 筛选 变短 reduce 归纳 最强大 every some 短路判断 find findIndex ES6 flat flatMap ES2019 #### 12.1 JavaScript 对象模型总览(Mermaid 思维导图) #### 12.2 各类型 typeof / instanceof 速查 ```js // typeof 速查 typeof 42 // "number" typeof 42n // "bigint" typeof 'hello' // "string" typeof true // "boolean" typeof undefined // "undefined" typeof null // "object" ← 历史 bug,永久保留 typeof {} // "object" typeof [] // "object" typeof function(){} // "function" ← 特殊:函数返回 "function" typeof Symbol() // "symbol" // 判断数组的正确方式(三种等价) Array.isArray([]); // true(推荐) [] instanceof Array; // true Object.prototype.toString.call([]); // "[object Array]"(最严格) // 判断 null 的正确方式 var x = null; x === null; // true(唯一正确方式,typeof null === "object" 不可靠) ``` > **💡 代码解析** > > | 代码片段 | 含义 | > |-----------------------------------------------------------|---------------------------------------------------------------------------------------------------| > | `typeof null` → `"object"` | JavaScript 最著名的历史 bug:`null` 的内部二进制标签以 `000` 开头(与对象相同),导致 `typeof` 错误返回 `"object"`;已永久保留以避免破坏现有代码 | > | `typeof function(){}` → `"function"` | 函数在 `typeof` 中特殊处理:返回 `"function"` 而非 `"object"`,方便区分可调用对象 | > | `Array.isArray([])` | ES5 引入的最可靠的数组判断方法,能正确处理跨 iframe 的数组(`instanceof` 在跨 iframe 时失效,因为不同框架的 `Array` 是不同对象) | > | `[] instanceof Array` | 检查 `Array.prototype` 是否在 `[]` 的原型链上;在同一个全局作用域下可靠,但跨 iframe 时失效 | > | `Object.prototype.toString.call([])` → `"[object Array]"` | 最严格的类型检查:通过 `Symbol.toStringTag` 读取对象的内部 `[[Class]]`,可区分 `Array`、`Map`、`Set`、`RegExp` 等各种对象类型 | #### 12.3 数组方法是否改变原数组速查 | 方法 | 改变原数组 | 返回值 | ES版本 | |---------------|-------|---------------|--------| | `push` | ✅ | 新长度 | ES1 | | `pop` | ✅ | 被删除元素 | ES1 | | `unshift` | ✅ | 新长度 | ES1 | | `shift` | ✅ | 被删除元素 | ES1 | | `splice` | ✅ | 被删除元素数组 | ES1 | | `sort` | ✅ | 排好序的数组 | ES1 | | `reverse` | ✅ | 翻转后的数组 | ES1 | | `fill` | ✅ | 修改后的数组 | ES6 | | `copyWithin` | ✅ | 修改后的数组 | ES6 | | `concat` | ❌ | 新数组 | ES1 | | `slice` | ❌ | 新数组 | ES1 | | `join` | ❌ | 字符串 | ES1 | | `indexOf` | ❌ | 索引或 -1 | ES5 | | `lastIndexOf` | ❌ | 索引或 -1 | ES5 | | `forEach` | ❌ | `undefined` | ES5 | | `map` | ❌ | 新数组(同长) | ES5 | | `filter` | ❌ | 新数组(可能更短) | ES5 | | `reduce` | ❌ | 累计结果 | ES5 | | `every` | ❌ | 布尔值 | ES5 | | `some` | ❌ | 布尔值 | ES5 | | `find` | ❌ | 元素或 undefined | ES6 | | `findIndex` | ❌ | 索引或 -1 | ES6 | | `includes` | ❌ | 布尔值 | ES7 | | `flat` | ❌ | 新数组 | ES2019 | | `flatMap` | ❌ | 新数组 | ES2019 | | `toSorted` | ❌ | 新数组 | ES2023 | | `toReversed` | ❌ | 新数组 | ES2023 | | `toSpliced` | ❌ | 新数组 | ES2023 | #### 12.4 常见反模式与最佳实践 ```js // ❌ 反模式一:在循环中使用 innerHTML += (触发多次重排) items.forEach(function(item) { document.getElementById('list').innerHTML += '
    • ' + item + '
    • '; }); // ✅ 最佳实践:map 后一次性设置 document.getElementById('list').innerHTML = items.map(function(item) { return '
    • ' + item + '
    • '; }).join(''); // ❌ 反模式二:数字排序忘记比较函数 [10, 1, 5].sort(); // [1, 10, 5](错误!) // ✅ 最佳实践 [10, 1, 5].sort(function(a, b) { return a - b; }); // [1, 5, 10] // ❌ 反模式三:用 indexOf 判断存在性(不能处理 NaN) if (arr.indexOf(val) !== -1) { ... } // ✅ 最佳实践(ES6+) if (arr.includes(val)) { ... } // ❌ 反模式四:用 new Boolean() 做条件判断 if (new Boolean(false)) { } // 永远执行! // ✅ 最佳实践:直接量或 Boolean() 函数 if (Boolean(value)) { } if (!!value) { } // ❌ 反模式五:不考虑月份偏移 var month = new Date().getMonth(); // 0-11,直接显示是错的 // ✅ 最佳实践 var month = new Date().getMonth() + 1; // 1-12,正确 // ❌ 反模式六:给数组赋空洞(稀疏数组,V8 退出优化) var arr = new Array(3); // [ , , ](稀疏数组) // ✅ 最佳实践 var arr = [undefined, undefined, undefined]; // 密集数组 // 或 ES6: var arr = Array.from({ length: 3 }); // [undefined, undefined, undefined] ``` > **💡 代码解析** > > | 代码片段 | 含义 | > |------------------------------------------|----------------------------------------------------------------------------------------| > | `innerHTML += '
    • ...'` 在循环中 | 每次 `+=` 都会:① 读取 DOM;② 解析旧 HTML;③ 追加新 HTML;④ 写回 DOM,触发一次重排(Reflow),N 次循环触发 N 次重排,页面严重卡顿 | > | `.map(...).join('')` 一次性写入 | 在 JS 层拼接好完整 HTML 字符串,只做**一次** DOM 写入,只触发一次重排,性能提升数倍至数十倍 | > | `[10,1,5].sort()` 默认按字符串 | `sort()` 默认比较函数将元素转为字符串,`'10' < '5'`(字典序),结果 `[1, 10, 5]` 而非期望的 `[1, 5, 10]` | > | `sort(function(a, b) { return a - b; })` | 数值差作为比较函数:负数时 a 在前(升序),正确实现数值排序 | > | `arr.indexOf(NaN) !== -1` 失效 | `indexOf` 使用 `===` 严格相等,`NaN === NaN` 为 `false`,永远无法找到 NaN | > | `arr.includes(NaN)` | `includes` 使用 `SameValueZero` 算法,能正确匹配 NaN,是安全的存在性检查方式 | > | `new Boolean(false)` 在 `if` 中 | 包装对象是**对象** ,ToBoolean(object) 永远为 `true`,无论包装的原始值是什么 | > | `new Array(3)` 创建稀疏数组 | 仅设置 `length = 3`,不创建实际属性,三个槽位全是"空洞",V8 退出数组优化路径 | > | `[undefined, undefined, undefined]` 密集 | 显式赋值 `undefined`,每个槽位有真实属性值,V8 保持密集数组优化 | #### 12.5 经典使用场景总结 | 场景 | 推荐方法 | 示例 | |---------------|------------------------------|-----------------------| | 列表渲染(数据→HTML) | `map` | 商品列表、表格行 | | 搜索/筛选 | `filter` | 按条件筛选商品、用户 | | 统计/汇总 | `reduce` | 购物车总价、词频统计、对象分组 | | 遍历+DOM操作 | `forEach` | 批量添加事件监听、打印 | | 权限检查(全部满足) | `every` | 表单全字段验证 | | 存在性检查(至少一个) | `some` | 是否有选中项、是否有错误 | | 查找元素 | `find` | 根据 ID 在列表中查找对象 | | 格式化数字 | `toFixed` | 价格、百分比显示 | | 日期补零 | `padStart` | `2023-01-05 09:30:00` | | 进制转换 | `toString(16)` | CSS 颜色值处理 | | 取随机项 | `Math.random` + `floor` | 轮播图、随机推荐 | | 字符串分割处理 | `split` | CSV 解析、URL 参数解析 | | 数组元素合并 | `join` | 面包屑导航、标签展示 | | 复制数组(浅拷贝) | `slice()` | 数据备份(避免修改原数组) | | 数组去重 | `reduce` + `includes` | 标签去重、数据清洗 | | 驼峰转换 | `split` + `map` + `join` | CSS 属性名处理 | | 翻转字符串 | `split` + `reverse` + `join` | 字符串处理 | | 对象深比较 | `JSON.stringify` | 简单的结构比较(有局限性) | | 类型安全判断 | `Array.isArray` | 区分数组和普通对象 | *** ** * ** *** ### 附录:参考资料 * [MDN - JavaScript 内置对象](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects) * [MDN - Array 参考](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array) * [MDN - String 参考](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String) * [MDN - Number 参考](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Number) * [MDN - Math 参考](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Math) * [MDN - Date 参考](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Date) * [MDN - 继承与原型链](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain) * [ECMAScript 2023 规范](https://tc39.es/ecma262/) * [IEEE 754-2008 双精度浮点标准](https://ieeexplore.ieee.org/document/4610935) * [V8 引擎博客 - Array 优化](https://v8.dev/blog/elements-kinds) * [TC39 提案追踪](https://github.com/tc39/proposals) *** ** * ** *** *本博客系统覆盖了 JavaScript 面向对象阶段的全部核心知识:从 JavaScript 设计哲学与历史演进出发,深入剖析原型链委托模型、值/引用类型内存模型、七大内置对象(Boolean / Number / String / Math / Date / Array 修改器 / Array 访问器与迭代器)的理论基础、方法全解、知识特点与经典应用场景。每个章节均包含 Mermaid 图解、规范说明、完整可运行示例及反模式警示,力求在严谨的理论深度与实用的工程视角之间取得平衡。*

    • 相关推荐
      risc1234561 小时前
      python 的字符串前缀
      开发语言·python
      小程故事多_801 小时前
      Agent Loop 核心突破,上下文压缩四大流派,重新定义窗口资源利用率
      java·开发语言·人工智能
      如竟没有火炬2 小时前
      字符串相乘——int数组转字符串
      开发语言·数据结构·python·算法·leetcode·深度优先
      吃好睡好便好2 小时前
      在Matlab中绘制三维等高线图
      开发语言·python·学习·算法·matlab·信息可视化
      天若有情6732 小时前
      自制C++万能字符串流式库 formort.h|对标标准库endl,零拷贝链式拼接神器
      开发语言·c++
      行星飞行2 小时前
      从 cursor 、 Claude code 迁移到 codex,30 分钟快速上手 codex 常用技巧
      前端
      njsgcs2 小时前
      制作solidworks插件 装配体导出展开耗时分析
      开发语言·c#·solidworks
      C137的本贾尼2 小时前
      别怕异步:`async` 和 `await` 的简单理解
      开发语言·python
      __log2 小时前
      ComfyUI 集成技术方案分析报告
      javascript·python·django