Python Day 33 JavaScript BOM 与 DOM 核心笔记整合

一、函数基础:参数默认值处理

1. 空值合并运算符(??)实现参数默认值

  • 核心作用:仅当参数为 null 或 undefined 时启用默认值(区别于 ||,不会覆盖 0'' 等 falsy 值)
  • 示例代码

    function test(a, b) {
        // 若 a 是 null/undefined,a 取默认值 10
        a = a ?? 10;
        // 建议补充 b 的默认值逻辑,避免返回 NaN
        b = b ?? 0;
        return a + b;
    }
    console.log(test()); // 输出 10(a=10,b=0)
    console.log(test(5)); // 输出 5(a=5,b=0)
    console.log(test(null, 8)); // 输出 18(a=10,b=8)
    

二、BOM(浏览器对象模型)核心内容

BOM 是操作浏览器窗口的 API,顶层对象为 window(可省略不写),核心子对象包括 navigatorlocationhistory 等。

1. window 对象(BOM 顶层核心)

(1)全局变量与 window 属性的关联

全局变量 / 函数会自动成为 window 的属性 / 方法,两种访问方式等价:

var globalNum = 20; // 全局变量
console.log(globalNum); // 20(直接访问)
console.log(window.globalNum); // 20(通过 window 访问)

function globalFn() {
    console.log('全局函数');
}
globalFn(); // 全局函数
window.globalFn(); // 全局函数
(2)窗口对话框(均阻塞代码,需用户操作后继续)
对话框类型语法功能与返回值
提示框alert(message)仅展示信息,无返回值(点击 “确认” 后执行后续代码)
确认框confirm(tips)展示 “确定 / 取消”,返回 true(确定)或 false(取消)
输入框prompt(tips, [def])支持用户输入,返回输入内容(确定)或 null(取消),def 为可选默认输入值

  • 示例代码

    // 1. 提示框
    alert('请完成表单填写');
    
    // 2. 确认框(删除逻辑示例)
    if (confirm('确定要删除这条数据吗?')) {
        console.log('数据删除成功');
    } else {
        console.log('删除操作已取消');
    }
    
    // 3. 输入框(获取用户名示例)
    let userName = prompt('请输入您的用户名', '默认用户');
    console.log(userName); // 输入内容或 null
    
(3)定时器任务(延迟 / 轮询)
任务类型创建方法取消方法核心说明
延迟任务setTimeout(fn, delay)clearTimeout(taskId)延迟 delay 毫秒(1 秒 = 1000ms)执行 fn,仅执行 1 次,返回任务 ID
轮询任务setInterval(fn, duration)clearInterval(taskId)每隔 duration 毫秒重复执行 fn,返回任务 ID,需手动取消否则持续执行

  • 示例 1:3 秒后输出内容(延迟任务)

    const delayTask = setTimeout(() => {
        console.log('3秒后执行');
    }, 3000);
    // (可选)3秒内取消任务:clearTimeout(delayTask);
    
  • 示例 2:10 秒倒计时(轮询任务)

    // 需页面提前添加 <h1>10</h1>
    let count = 10;
    const timer = setInterval(() => {
        const h1 = document.querySelector('h1');
        count--;
        h1.innerText = count;
        // 倒计时结束,停止轮询
        if (count <= 0) {
            clearInterval(timer);
            h1.innerText = '时间到!';
        }
    }, 1000);
    
(4)窗口操作(打开 / 关闭 / 父子关联)
  • window.open(url, target, features):打开新窗口,返回新窗口对象

    • url:新窗口地址(如 https://www.baidu.com
    • target:打开方式(_blank 新窗口,_self 当前窗口)
    • features:窗口属性(如 width=600,height=400,top=100,left=100
  • window.close():关闭当前窗口(或通过新窗口对象关闭指定窗口)

  • window.opener:获取当前窗口的 “打开者”(父窗口)

  • 示例代码

    // 打开新窗口,3秒后自动关闭
    const newWin = window.open(
        'https://www.baidu.com',
        '_blank',
        'width=600,height=400,top=100,left=100'
    );
    setTimeout(() => {
        newWin.close(); // 关闭新窗口
    }, 3000);
    
(5)其他常用方法 / 属性
方法 / 属性功能
window.btoa(str)Base64 编码(仅支持 Latin-1 字符,中文需先 encodeURI
window.atob(encodedStr)Base64 解码(解码后需 decodeURI 还原中文)
window.scrollX/scrollY获取水平 / 垂直滚动条滚动距离(单位:像素)
window.scrollTo(x, y)滚动到指定位置(如 scrollTo({top: 0, left: 0}) 回到页面顶部)
window.scrollBy(x, y)相对当前位置偏移滚动(如 scrollBy({top: 200}) 向下滚 200 像素)

  • 中文 Base64 编解码示例

    // 中文编码:先转义再编码
    const encoded = btoa(encodeURI('前端笔记'));
    console.log(encoded); // 输出 "5Yqh5ouG5pWw5o2u"
    
    // 中文解码:先解码再还原
    const decoded = decodeURI(atob(encoded));
    console.log(decoded); // 输出 "前端笔记"
    

2. navigator 对象(浏览器信息)

用于获取浏览器及设备相关信息,核心属性:

  • navigator.userAgent:获取浏览器 “用户代理” 字符串(用于判断浏览器类型 / 版本)

    console.log(navigator.userAgent);
    // 示例输出:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36
    
  • navigator.cookieEnabled:判断浏览器是否启用 Cookie(返回 true/false

    console.log(navigator.cookieEnabled); // true(若启用 Cookie)
    
  • navigator.geolocation.getCurrentPosition():获取设备地理位置(需用户授权)

    // 成功回调:获取经纬度并解析地址(示例用高德地图API)
    navigator.geolocation.getCurrentPosition(
        (position) => {
            const lat = position.coords.latitude; // 纬度
            const lng = position.coords.longitude; // 经度
            const key = '你的高德地图APIKey'; // 需替换为自己的APIKey
            // 调用高德地图逆地理编码API
            fetch(`https://restapi.amap.com/v3/geocode/regeo?key=${key}&location=${lng},${lat}`)
                .then(res => res.json())
                .then(data => {
                    if (data.status === '1') {
                        document.querySelector('#addr').innerText = `当前地址:${data.regeocode.formatted_address}`;
                    }
                });
        },
        (error) => {
            console.log('定位失败:', error.message);
        }
    );
    // 页面需添加 <div id="addr"></div> 用于展示地址
    

3. location 对象(当前网址操作)

(1)获取网址各组成部分
属性名功能说明
location.protocol获取协议(如 http:https:
location.hostname获取主机 / 域名(如 www.baidu.com127.0.0.1
location.port获取端口号(http 默认 80、https 默认 443,无显式端口则返回空字符串)
location.pathname获取请求路径(如 /index.html/user/profile
location.search获取查询参数(如 ?name=张三&age=20
location.hash获取锚点(如 #section1,用于页面内定位)
location.href获取完整网址(如 https://www.baidu.com/s?wd=js),也可用于跳转

  • 示例代码

    console.log('协议:', location.protocol);
    console.log('域名:', location.hostname);
    console.log('查询参数:', location.search);
    console.log('完整网址:', location.href);
    
(2)页面跳转方法
方法名功能说明
location.href = url跳转到指定 url,会生成历史记录(可通过 history.back() 后退)
location.assign(url)与 href 效果一致,跳转到指定 url 并生成历史记录
location.replace(url)跳转到指定 url不生成历史记录(无法通过 history.back() 后退)

  • 示例代码

    // 1秒后跳转到百度(生成历史记录)
    setTimeout(() => {
        location.href = 'https://www.baidu.com';
        // 或 location.assign('https://www.baidu.com');
    }, 1000);
    
    // 跳转到谷歌(不生成历史记录)
    // location.replace('https://www.google.com');
    

4. history 对象(历史记录操作)

用于操作浏览器的访问历史(前进 / 后退),核心方法:

方法名功能说明
history.back()后退到上一页(等价于浏览器 “后退” 按钮)
history.forward()前进到下一页(等价于浏览器 “前进” 按钮)
history.go(n)跳转到历史记录中第 n 页(n=1 前进 1 页,n=-1 后退 1 页,n=0 刷新当前页)

  • 示例代码

    // 后退1页
    document.querySelector('#backBtn').addEventListener('click', () => {
        history.back();
    });
    
    // 前进1页
    document.querySelector('#forwardBtn').addEventListener('click', () => {
        history.forward();
    });
    
    // 刷新当前页
    history.go(0);
    

三、DOM(文档对象模型)核心操作

DOM 是操作 HTML 文档结构的 API,核心流程:选中元素 → 操作元素

1. 元素选择器(选中元素的核心方法)

(1)常用选择器对比
选择器方法语法示例返回值类型核心特点
querySelector(cssSelector)document.querySelector('#box')单个元素对象(Element)支持所有 CSS 选择器,返回第一个匹配元素,无匹配则返回 null
querySelectorAll(cssSelector)document.querySelectorAll('.item')NodeList(类数组)支持所有 CSS 选择器,返回所有匹配元素,无匹配则返回空 NodeList
getElementById(id)document.getElementById('username')单个元素对象按 id 选择,id 需页面唯一,性能优,无匹配则返回 null
getElementsByTagName(tag)document.getElementsByTagName('p')HTMLCollection(类数组)按标签名选择,实时更新(DOM 变化时自动同步)
getElementsByClassName(className)document.getElementsByClassName('active')HTMLCollection按 class 选择,实时更新
getElementsByName(name)document.getElementsByName('hobby')NodeList按 name 属性选择(常用于表单元素,如单选框、复选框)

  • 示例代码

    // 1. querySelector(选第一个 .link 类元素)
    const firstLink = document.querySelector('.link');
    
    // 2. querySelectorAll(选所有 p 标签下的 a 标签)
    const allLinks = document.querySelectorAll('p a');
    allLinks.forEach(link => {
        console.log(link.innerText);
    });
    
    // 3. getElementById(选 id 为 header 的元素)
    const header = document.getElementById('header');
    
    // 4. getElementsByTagName(选所有 div 标签)
    const allDivs = document.getElementsByTagName('div');
    
(2)基础 CSS 选择器参考(配合 querySelector 使用)
选择器类型语法示例功能说明
通用选择器*选所有元素
标签选择器pa选指定标签元素
类选择器.active选 class="active" 的元素
ID 选择器#box选 id="box" 的元素
属性选择器[href][type="text"]选有指定属性或属性值匹配的元素
后代选择器div p选 div 内部所有 p 元素(包括子、孙等后代)
子选择器div > p选 div 直接子元素中的 p(仅一级子元素)
相邻兄弟选择器p + a选 p 元素后紧邻的第一个 a 元素
通用兄弟选择器p ~ a选 p 元素后所有同级的 a 元素
分组选择器p, a, div同时选 p、a、div 元素
伪类选择器a:hoverli:first-child按状态(hover)或位置(first-child)选择
伪元素选择器p::beforediv::after选择元素的伪元素(需配合样式操作,JS 无法直接选中伪元素节点)

2. 元素标签操作(选中元素后的核心操作)

(1)获取标签基础信息
属性名功能说明
tagName获取标签名(全大写,如 PA),Element 节点特有
nodeName获取节点名(标签节点返回全大写标签名,文本节点返回 #text

  • 示例代码

    const pTag = document.querySelector('p');
    console.log('标签名(tagName):', pTag.tagName); // P
    console.log('节点名(nodeName):', pTag.nodeName); // P
    
(2)操作标签体内容(文本 / 超文本)
属性名功能说明特点
innerText获取 / 设置标签体纯文本内容自动过滤 HTML 标签,设置时会将 < 转义为 &lt;(仅显示文本)
textContent与 innerText 类似,获取 / 设置纯文本内容更规范(W3C 标准),会保留空格和换行,兼容性稍好
innerHTML获取 / 设置标签体超文本内容保留 HTML 标签,设置时可解析标签(如设置 <a href="#">链接</a> 会生成链接)
outerHTML获取标签自身 + 内部的超文本内容(设置时会替换整个标签)包含标签本身,如 <p><a>hello</a></p>,慎用设置功能

  • 示例代码(页面需提前添加 <p><a href="">hello</a></p>):

    const pTag = document.querySelector('p');
    
    // 1. 获取内容
    console.log('innerText(纯文本):', pTag.innerText); // hello
    console.log('innerHTML(超文本):', pTag.innerHTML); // <a href="">hello</a>
    console.log('outerHTML(自身+内部):', pTag.outerHTML); // <p><a href="">hello</a></p>
    
    // 2. 设置内容
    pTag.innerText = '<a href="#">world</a>'; // 页面显示:<a href="#">world</a>(纯文本)
    pTag.innerHTML = '<a href="#">world</a>'; // 页面显示:world(可点击的链接)
    

(3)操作标签特有属性(如表单、链接、图片)

标签特有的属性(如 a 标签 hrefimg 标签 src、表单 value)支持直接通过 . 访问和修改:

一、DOM 交互实战案例

1. 复选框全选 / 反选与选中内容展示

功能需求
  • 实现 “全选” 复选框控制所有子复选框(爱好)的选中状态
  • 子复选框选中状态变化时,同步 “全选” 框状态
  • 实时展示当前选中的爱好名称
核心代码与解析
<!-- HTML 结构 -->
<input type="checkbox" name="" id="all" onchange="checkHobby(this)">全选
<hr>
<input type="checkbox" name="" id="" onchange="ckHobby()">游泳
<input type="checkbox" name="" id="" onchange="ckHobby()">爬山
<input type="checkbox" name="" id="" onchange="ckHobby()">看书
<input type="checkbox" name="" id="" onchange="ckHobby()">听音乐
<p>当前选中的爱好有 :  [<span></span>]</p>

<script>
// 1. 提前获取所有子复选框(排除全选框)和展示选中内容的 span
let checkList = document.querySelectorAll("input[type=checkbox]:not(#all)");
let spanIs = document.querySelector('p span');

// 2. 全选框改变事件:控制所有子复选框
function checkHobby(tag) {
    const isChecked = tag.checked; // 获取全选框当前状态
    // 遍历子复选框,同步状态
    for (let item of checkList) {
        item.checked = isChecked;
    }
    // 调用更新选中内容的逻辑(复用代码)
    updateSelectedHobbies();
}

// 3. 子复选框改变事件:同步全选框状态
function ckHobby() {
    let isAllChecked = true; // 假设初始为全选状态
    // 遍历子复选框,判断是否有未选中的
    for (let item of checkList) {
        if (!item.checked) {
            isAllChecked = false;
            break; // 有一个未选中则跳出循环
        }
    }
    // 同步全选框状态
    document.querySelector('#all').checked = isAllChecked;
    // 更新选中内容
    updateSelectedHobbies();
}

// 4. 复用函数:更新当前选中的爱好文本
function updateSelectedHobbies() {
    const selected = [];
    for (let item of checkList) {
        if (item.checked) {
            // 获取复选框后的文本(需去除空格)
            selected.push(item.nextSibling.textContent.trim());
        }
    }
    // 拼接选中内容并展示
    spanIs.innerText = selected.join('、');
}
</script>
关键知识点
  • querySelectorAll("input[type=checkbox]:not(#all)"):使用 CSS 伪类 :not() 排除指定元素
  • nextSibling.textContent:获取元素的下一个兄弟节点(文本节点)的内容
  • 逻辑复用:将 “更新选中内容” 抽取为独立函数,避免代码冗余

2. 星级评分(点击星星高亮)

功能需求
  • 点击某个星星时,该星星及左侧所有星星高亮(红色),右侧星星恢复默认色(米色)
核心代码与解析
<style>
    span {
        color: beige; /* 默认色:米色 */
        cursor: pointer; /* 鼠标悬浮为手型,提示可点击 */
        font-size: 24px; /* 放大星星,增强交互感 */
    }
</style>

<p><span>❤</span><span>❤</span><span>❤</span><span>❤</span><span>❤</span></p>

<script>
    // 1. 获取所有星星元素
    let spanList = document.querySelectorAll('span');
    
    // 2. 为每个星星绑定点击事件
    for (let star of spanList) {
        star.addEventListener('click', function() {
            // 步骤1:先将所有星星恢复默认色
            spanList.forEach(s => s.style.color = 'beige');
            // 步骤2:高亮当前星星及左侧所有星星
            for (let s of spanList) {
                s.style.color = 'red'; // 高亮为红色
                if (s === this) break; // 遇到当前点击的星星则停止
            }
        });
    }
</script>
关键知识点
  • addEventListener:事件绑定(相比 onclick 更灵活,支持多事件绑定)
  • 遍历逻辑:先 “重置” 所有元素状态,再 “高亮” 目标及左侧元素,逻辑清晰

3. 用户名 / 密码框切换

功能需求
  • 点击按钮实现 “用户名输入框” 与 “密码输入框” 的切换(含文本提示和输入类型切换)
  • 切换时清空输入框内容,避免敏感信息残留
核心代码与解析
<!-- HTML 结构 -->
<p>用户名:<input class="input-box" type="text"></p>
<input type="button" value="切换" class="switch-btn">

<script>
    // 1. 获取按钮、文本提示父容器、输入框
    let switchBtn = document.querySelector('.switch-btn');
    let textContainer = document.querySelector('p');
    let inputBox = document.querySelector('.input-box');
    
    // 2. 状态标记:true 表示当前是用户名框,false 表示密码框
    let isUsername = true;
    
    // 3. 按钮点击事件
    switchBtn.addEventListener('click', function() {
        inputBox.value = ''; // 切换时清空输入内容
        if (isUsername) {
            // 切换为密码框:修改文本提示和输入类型
            textContainer.firstChild.textContent = "密码:";
            inputBox.type = 'password';
        } else {
            // 切换为用户名框:恢复文本提示和输入类型
            textContainer.firstChild.textContent = "用户名:";
            inputBox.type = 'text';
        }
        // 切换状态标记
        isUsername = !isUsername;
    });
</script>
关键知识点
  • input.type:动态修改输入框类型(text 或 password
  • firstChild.textContent:修改父容器中第一个文本节点的内容(实现 “用户名 / 密码” 提示切换)

4. 2 小时倒计时(含开始 / 暂停 / 继续 / 结束功能)

功能需求
  • 初始显示 02:00:00,点击 “开始” 启动倒计时
  • 支持 “暂停”(停止倒计时但保留剩余时间)、“继续”(恢复倒计时)、“结束”(重置为初始状态)
核心代码与解析
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>2小时倒计时</title>
    <script>
        // 1. 初始化变量:总秒数(2小时 = 2*60*60 = 7200秒)、定时器ID、倒计时状态
        let totalSeconds = 2 * 60 * 60;
        let timerId = null; // 存储定时器ID,用于暂停/结束
        let isRunning = false; // 标记是否正在倒计时

        // 2. 格式化时间:将秒数转为 "HH:MM:SS" 格式(补前导0)
        function formatTime(seconds) {
            const hours = Math.floor(seconds / 3600).toString().padStart(2, '0');
            const minutes = Math.floor((seconds % 3600) / 60).toString().padStart(2, '0');
            const secs = (seconds % 60).toString().padStart(2, '0');
            return `${hours}:${minutes}:${secs}`;
        }

        // 3. 开始倒计时
        function startCountdown() {
            if (isRunning) return; // 避免重复启动
            isRunning = true;
            timerId = setInterval(() => {
                totalSeconds--;
                // 倒计时结束:清除定时器,重置状态
                if (totalSeconds <= 0) {
                    clearInterval(timerId);
                    isRunning = false;
                    totalSeconds = 2 * 60 * 60; // 重置为初始值
                }
                // 更新页面显示
                document.querySelector('.time').innerText = formatTime(totalSeconds);
            }, 1000); // 每秒执行一次
        }

        // 4. 暂停倒计时
        function pauseCountdown() {
            if (!isRunning) return;
            clearInterval(timerId);
            isRunning = false;
        }

        // 5. 结束倒计时(重置为初始状态)
        function endCountdown() {
            clearInterval(timerId);
            isRunning = false;
            totalSeconds = 2 * 60 * 60;
            document.querySelector('.time').innerText = formatTime(totalSeconds);
        }
    </script>
</head>
<body>
    <h1 class="time">02:00:00</h1>
    <button onclick="startCountdown()">开始</button>
    <button onclick="pauseCountdown()">暂停</button>
    <button onclick="startCountdown()">继续</button> <!-- 继续复用开始逻辑 -->
    <button onclick="endCountdown()">结束</button>
</body>
</html>
关键知识点
  • padStart(2, '0'):字符串补全方法,确保时间格式为两位数(如 1 → 01
  • 定时器控制:通过 timerId 管理定时器,实现 “暂停”(clearInterval)和 “继续”(重新启动定时器)
  • 状态标记 isRunning:避免重复启动定时器导致倒计时加速

5. 表单验证(用户名 / 密码 / 邮箱)

功能需求
  • 用户名:仅允许字母、数字、下划线
  • 密码:长度 6~20 位
  • 确认密码:与密码一致
  • 邮箱:符合标准邮箱格式(含 @ 和域名后缀)
  • 点击 “提交” 时触发所有校验,有错误则提示并终止提交
核心代码与解析
<!-- HTML 结构 -->
<p>用户名: <input class="username" type="text" placeholder="仅字母、数字、下划线"></p>
<p>密码: <input class="password" type="password" placeholder="6-20位"></p>
<p>确认密码: <input class="repassword" type="password"></p>
<p>邮箱: <input class="email" type="text" placeholder="如 xxx@xxx.com"></p>
<p><input class="submit-btn" type="button" value="提交"></p>

<script>
    const submitBtn = document.querySelector('.submit-btn');
    
    // 提交按钮点击事件
    submitBtn.addEventListener('click', function() {
        // 1. 获取所有输入值(trim() 去除首尾空格)
        const username = document.querySelector('.username').value.trim();
        const password = document.querySelector('.password').value;
        const repassword = document.querySelector('.repassword').value;
        const email = document.querySelector('.email').value.trim();

        // 2. 用户名校验:正则 /^[0-9a-zA-Z_]+$/ 匹配字母、数字、下划线
        if (!username) {
            alert('用户名不能为空!');
            return; // 有错误则终止后续校验
        } else if (!/^[0-9a-zA-Z_]+$/.test(username)) {
            alert('用户名仅允许字母、数字、下划线!');
            return;
        }

        // 3. 密码长度校验
        if (password.length < 6 || password.length > 20) {
            alert('密码长度必须为 6~20 位!');
            return;
        }

        // 4. 确认密码校验
        if (password !== repassword) {
            alert('两次输入的密码不一致!');
            return;
        }

        // 5. 邮箱格式校验:标准邮箱正则(含 @、域名后缀)
        const emailReg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
        if (!emailReg.test(email)) {
            alert('请输入正确的邮箱格式(如 xxx@xxx.com)!');
            return;
        }

        // 6. 所有校验通过
        alert('表单验证通过,提交成功!');
    });
</script>
关键知识点
  • 正则表达式应用:
    • 用户名:/^[0-9a-zA-Z_]+$/^ 开头,$ 结尾,确保整个字符串符合规则)
    • 邮箱:/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/(匹配用户名、@、域名、后缀)
  • 校验逻辑顺序:先校验非空,再校验格式 / 长度,最后校验一致性,逻辑清晰

6. 猜灯谜小游戏(调用第三方 API)

功能需求
  • 页面加载时自动获取灯谜(调用第三方 API)
  • 用户有 3 次答题机会,答错提示剩余次数,3 次答错显示正确答案和解析
  • 答对可切换下一题,支持回车键提交答案
核心代码与解析
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>猜灯谜</title>
    <script>
        // 全局变量:当前灯谜数据、错误次数、最大错误次数、API密钥
        let currentRiddle = null;
        let errorCount = 0;
        const MAX_ERRORS = 3;
        const API_KEY = 'd9ea9ddca4f048b487a70dd8e29f3547'; // 第三方API密钥(需自行申请)

        // 页面加载完成后自动获取灯谜
        window.onload = fetchRiddle;

        // 1. 获取灯谜(调用第三方API)
        function fetchRiddle() {
            // 重置状态:错误次数、提示文本、输入框状态
            errorCount = 0;
            document.getElementById('errorMessage').textContent = '';
            document.getElementById('answerInput').value = '';
            document.getElementById('correctAnswer').style.display = 'none';
            document.getElementById('answerInput').disabled = false;
            document.getElementById('submitBtn').disabled = false;
            document.getElementById('nextRiddleBtn').style.display = 'none';

            // 调用API获取随机灯谜
            fetch(`https://api.tianapi.com/lantern/random?key=${API_KEY}`)
                .then(response => response.json())
                .then(data => {
                    currentRiddle = data.newslist[0]; // 提取灯谜数据
                    document.getElementById('riddleQuestion').textContent = currentRiddle.question;
                })
                .catch(error => {
                    alert('获取灯谜失败,请刷新页面重试!');
                    console.error('API请求错误:', error);
                });
        }

        // 2. 验证答案
        function checkAnswer() {
            if (!currentRiddle) return; // 无灯谜数据时不执行

            const userAnswer = document.getElementById('answerInput').value.trim();
            const correctAnswer = currentRiddle.answer.trim();

            // 答案不区分大小写
            if (userAnswer.toLowerCase() === correctAnswer.toLowerCase()) {
                document.getElementById('errorMessage').textContent = '恭喜你,答对了!';
                document.getElementById('errorMessage').style.color = 'green';
                document.getElementById('nextRiddleBtn').style.display = 'inline-block';
            } else {
                errorCount++;
                // 提示剩余次数
                document.getElementById('errorMessage').textContent = `答错了!还有${MAX_ERRORS - errorCount}次机会`;
                document.getElementById('errorMessage').style.color = 'red';

                // 3次答错:显示正确答案和解析,禁用输入框
                if (errorCount >= MAX_ERRORS) {
                    document.getElementById('correctAnswer').innerHTML = `
                        <p>正确答案:${currentRiddle.answer}</p>
                        <p>解析:${currentRiddle.explanation || '暂无解析'}</p>
                    `;
                    document.getElementById('correctAnswer').style.display = 'block';
                    document.getElementById('nextRiddleBtn').style.display = 'inline-block';
                    document.getElementById('answerInput').disabled = true;
                    document.getElementById('submitBtn').disabled = true;
                }
            }
        }

        // 3. 下一题(复用获取灯谜逻辑)
        function nextRiddle() {
            fetchRiddle();
        }

        // 4. 回车键提交答案
        function handleKeyPress(event) {
            if (event.key === 'Enter') {
                checkAnswer();
            }
        }
    </script>
</head>
<body>
    <h1>猜灯谜</h1>
    <div>
        <h3 id="riddleQuestion">加载中...</h3>
        <p>请输入答案:</p>
        <input type="text" id="answerInput" onkeypress="handleKeyPress(event)">
        <button id="submitBtn" onclick="checkAnswer()">提交答案</button>
        <button id="nextRiddleBtn" onclick="nextRiddle()" style="display: none;">下一个灯谜</button>
        <p id="errorMessage"></p>
        <div id="correctAnswer" style="display: none; margin-top: 10px; padding: 10px; border: 1px solid #ccc;"></div>
    </div>
    <p>提示:您有3次答题机会,3次错误后将显示正确答案和解析</p>
</body>
</html>
关键知识点
  • fetch API:用于调用第三方 API 获取数据,需处理异步逻辑(then/catch
  • 状态管理:通过全局变量 currentRiddleerrorCount 管理游戏状态
  • 键盘事件:onkeypress 监听回车键,提升用户体验

二、正则表达式实战

1. 提取字符串中的所有数字并求和

需求:从混合字符串中提取所有连续数字,转为数字后求和
const str = '46546asdafa87gfg654dg';
// 1. 正则 /\d+/g:匹配1个及以上连续数字,g表示全局匹配
const numArr = str.match(/\d+/g); 
// 2. 求和(先判断是否有匹配结果,避免报错)
const sum = numArr ? numArr.reduce((total, numStr) => total + parseInt(numStr), 0) : 0;
console.log(sum); // 46546 + 87 + 654 = 47287

2. 将字符串中的所有数字替换为 **

需求:将字符串中每个数字字符替换为 **

const str = '1sdad45s64ad';
// 正则 /\d/g:匹配单个数字,g表示全局替换
const newStr = str.replaceAll(/\d/g, '**'); 
console.log(newStr); // **sdad****s****ad

3. 手机号中间 4 位脱敏

需求:将手机号 15455367889 处理为 154****7889

const phone = '15455367889';
// 正则分组:(\d{3}) 匹配前3位,(\d{4}) 匹配中间4位,(\d{4}) 匹配后4位
const desensitizedPhone = phone.replace(/(\d{3})(\d{4})(\d{4})/, '$1****$3'); 
console.log(desensitizedPhone); // 154****7889

4. 邮箱用户名脱敏

需求:将邮箱 16544987465654@qq.com 处理为 1***@qq.com

const email = '16544987465654@qq.com';
// 正则分组:(.) 匹配第一个字符,.* 匹配中间任意字符,(@.*$) 匹配@及后面所有内容
const desensitizedEmail = email.replace(/(.).*(@.*$)/, '$1***$2'); 
console.log(desensitizedEmail); // 1***@qq.com

5. 提取文件路径中的文件名和扩展名

需求:从路径 /home/user/docs/report.pdf 中提取文件名 report 和扩展名 .pdf

const path = '/home/user/docs/report.pdf';
// 正则解析:
// ([^/]*?):匹配最后一个/后的非/字符(非贪婪,避免包含.)
// (\.[^.]*)$:匹配.及后面非.字符(直到字符串结束,即扩展名)
const matchResult = path.match(/([^/]*?)(\.[^.]*)$/);
if (matchResult) {
    const fileName = matchResult[1]; // report
    const extName = matchResult[2]; // .pdf
    console.log(`文件名:${fileName},扩展名:${extName}`);
}

6. 查找字符串中最长的字母单词

需求:从混合字符串中提取所有字母组成的单词,找到最长的一个

const str = '11465sdada55dasdwasd789522adsadada';
// 正则 /[a-z]+/ig:匹配所有字母(不区分大小写),g全局匹配
const words = str.match(/[a-z]+/ig); 
// 找到最长单词(reduce 遍历比较长度)
const longestWord = words.reduce((max, word) => word.length > max.length ? word : max, '');
console.log(longestWord); // adsadada(长度8)

7. 驼峰命名转脊柱命名

需求:将 fontSizeKiss 转为 font-size-kiss

const camelCase = 'fontSizeKiss';
// 正则 /[A-Z]/g:匹配大写字母,替换为 -+小写字母
const kebabCase = camelCase.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`);
console.log(kebabCase); // font-size-kiss

三、数组与对象实战

1. 多数组交叉组合

需求:将 [1, 2] 和 [3, 4] 组合为 [[1,3], [1,4], [2,3], [2,4]]

function combineArrays(...arrays) {
    // 边界处理:无输入数组返回空数组
    if (arrays.length === 0) return [];
    // 初始值:将第一个数组的每个元素转为数组(如 [1,2] → [[1], [2]])
    let result = arrays[0].map(item => [item]);
    
    // 遍历后续数组,与现有结果交叉组合
    for (let i = 1; i < arrays.length; i++) {
        const temp = [];
        // 现有结果中的每个组合,与当前数组的每个元素拼接
        result.forEach(combo => {
            arrays[i].forEach(item => {
                temp.push([...combo, item]); // 展开现有组合,添加新元素
            });
        });
        result = temp; // 更新结果
    }
    return result;
}

// 测试
console.log(combineArrays([1,2], [3,4])); // [[1,3],[1,4],[2,3],[2,4]]
console.log(combineArrays([1], [2,3], [4])); // [[1,2,4], [1,3,4]]

2. 员工数据多维度处理

需求:基于员工数组,实现筛选、排序、求和、分组等操作

const employees = [
    { name: 'Alice', age: 30, department: 'Engineering' },
    { name: 'Bob', age: 25, department: 'Marketing' },
    { name: 'Charlie', age: 35, department: 'Engineering' },
    { name: 'David', age: 40, department: 'Sales' }
];

// 1. 获取 Engineering 部门员工
const engineeringEmp = employees.filter(emp => emp.department === 'Engineering');

// 2. 按年龄升序排序(注意:sort 会修改原数组,需先复制)
const sortedByAge = [...employees].sort((a, b) => a.age - b.age);

// 3. 计算所有员工年龄和
const totalAge = employees.reduce((sum, emp) => sum + emp.age, 0);

// 4. 统计各部门人数
const deptCount = employees.reduce((acc, emp) => {
    acc[emp.department] = (acc[emp.department] || 0) + 1;
    return acc;
}, {});

// 5. 按部门分组(值为员工数组)
const deptGroup = employees.reduce((acc, emp) => {
    if (!acc[emp.department]) acc[emp.department] = [];
    acc[emp.department].push(emp);
    return acc;
}, {});

// 6. 查找年龄最大的员工
const oldestEmp = employees.reduce((oldest, current) => 
    current.age > oldest.age ? current : oldest, employees[0]
);

// 输出结果
console.log('1. Engineering部门员工:', engineeringEmp);
console.log('2. 按年龄升序:', sortedByAge);
console.log('3. 总年龄:', totalAge);
console.log('4. 各部门人数:', deptCount);
console.log('5. 按部门分组:', deptGroup);
console.log('6. 年龄最大员工:', oldestEmp);

3. 实现可遍历的自定义对象(for...of 支持)

需求:自定义对象支持 push/unshift/pop/shift 等数组方法,并可通过 for...of 遍历

let obj = {
    values: [], // 存储数据的内部数组
    
    // 1. 向尾部添加元素
    push(...val) {
        this.values.push(...val);
        return this.values.length;
    },
    
    // 2. 向头部添加元素
    unshift(...val) {
        this.values.unshift(...val);
        return this.values.length;
    },
    
    // 3. 移除尾部元素
    pop() {
        return this.values.pop();
    },
    
    // 4. 移除头部元素
    shift() {
        return this.values.shift();
    },
    
    // 5. 获取指定索引元素
    get(index) {
        return index >= 0 && index < this.values.length ? this.values[index] : undefined;
    },
    
    // 6. 修改指定索引元素
    set(index, value) {
        if (index >= 0 && index < this.values.length) {
            this.values[index] = value;
        }
    },
    
    // 7. 转为字符串
    toString() {
        return `[${this.values.join(', ')}]`;
    },
    
    // 8. 实现迭代器(支持 for...of 遍历)
    [Symbol.iterator]() {
        let index = 0;
        const values = this.values;
        return {
            next() {
                return index < values.length 
                    ? { value: values[index++], done: false } 
                    : { done: true };
            }
        };
    }
};

// 测试
obj.push(1, 2, 3);
for (let val of obj) {
    console.log(val); // 1, 2, 3
}

四、解构赋值实战

1. 数组混合解构(提取部门员工)

需求:从部门数组中提取 HR 部门第一个员工、IT 部门最后一个员工

const departments = [
    { name: 'HR', employees: ['Alice', 'Bob', 'Charlie'] },
    { name: 'IT', employees: ['Dave', 'Eve', 'Frank'] }
];

// 解构:HR 部门取第一个员工,IT 部门取第三个(最后一个)员工
const [{ employees: [firstHREmployee] }, { employees: [, , lastITEmployee] }] = departments;
console.log(firstHREmployee); // Alice
console.log(lastITEmployee); // Frank

2. 对象复杂解构(提取配置项)

需求:从配置对象中提取服务器地址(重命名)、超时时间(默认值)、重试次数(默认值)

javascript

运行

const config = {
    server: { host: 'localhost', port: 8080 },
    timeout: 0,
    retries: undefined
};

// 解构:重命名 host/port,timeout 为0时用默认值5000,retries 未定义时用默认值3
const {
    server: { host: serverHost, port: serverPort },
    timeout: timeout = 5000,
    retries: retries = 3
} = config;

console.log(serverHost); // localhost
console.log(serverPort); // 8080
console.log(timeout); // 0(原值为0,不触发默认值)
console.log(retries); // 3(原值为undefined,触发默认值)

3. 解构函数返回值

需求:从函数返回的嵌套对象中提取 id、name、country

function getUserProfile() {
    return {
        id: 123,
        info: {
            name: 'Sarah',
            location: { city: 'New York', country: 'USA' }
        }
    };
}

// 嵌套解构:提取 id、info.name、info.location.country
const {
    id: userId,
    info: {
        name: userName,
        location: { country: userCountry }
    }
} = getUserProfile();

console.log(userId); // 123
console.log(userName); // Sarah
console.log(userCountry); // USA

五、工具函数实战

1. 随机生成 RGB 颜色

需求:编写函数 randomColor,返回 rgb(r, g, b) 格式的随机颜色(r/g/b 范围 0~255)

function randomColor() {
    // Math.random() 生成 0~1 随机数,*256 后取整(0~255)
    const r = Math.floor(Math.random() * 256);
    const g = Math.floor(Math.random() * 256);
    const b = Math.floor(Math.random() * 256);
    return `rgb(${r}, ${g}, ${b})`;
}

// 测试:生成随机颜色并设置页面背景
document.body.style.backgroundColor = randomColor();

2. 查找所有三位数水仙花数

需求:水仙花数是指一个 3 位数,其各位数字的立方和等于该数本身(如 153 = 1³ + 5³ + 3³)

const narcissisticNumbers = [];
// 遍历所有三位数(100~999)
for (let i = 100; i <= 999; i++) {
    const hundreds = Math.floor(i / 100); // 百位
    const tens = Math.floor((i % 100) / 10); // 十位
    const units = i % 10; // 个位
    // 判断是否为水仙花数
    if (Math.pow(hundreds, 3) + Math.pow(tens, 3) + Math.pow(units, 3) === i) {
        narcissisticNumbers.push(i);
    }
}
console.log('三位数水仙花数:', narcissisticNumbers); // [153, 370, 371, 407]

3. 数字转中文大写(12 位以内)

需求:将数字 1234 转为 壹仟贰佰叁拾肆,处理中间含 0 的情况(如 1001 → 壹仟零壹

function numberToChinese(numStr) {
    // 边界校验:1~12位数字
    if (!/^\d{1,12}$/.test(numStr)) return "请输入1-12位数字";
    
    // 中文数字和单位(对应 个、十、百、千、万、十万、百万、千万、亿、十亿、百亿、千亿)
    const digits = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'];
    const units = ['', '拾', '佰', '仟', '万', '拾', '佰', '仟', '亿', '拾', '佰', '仟'];
    
    let result = '';
    // 遍历数字的每一位,拼接“数字+单位”
    for (let i = 0; i < numStr.length; i++) {
        const digit = parseInt(numStr[i]);
        const unitIndex = numStr.length - 1 - i; // 计算当前位的单位索引
        result += digits[digit] + units[unitIndex];
    }
    
    // 处理冗余的“零”(关键步骤)
    result = result
        .replace(/零[拾佰仟]/g, '零') // 去掉“零十”“零百”等
        .replace(/零+/g, '零') // 多个零合并为一个
        .replace(/零(万|亿)/g, '$1') // 去掉“零万”“零亿”中的零
        .replace(/零$/, ''); // 去掉末尾的零
    
    // 特殊情况:结果为空时返回“零”
    return result || '零';
}

// 测试
console.log(numberToChinese('1234')); // 壹仟贰佰叁拾肆
console.log(numberToChinese('1001')); // 壹仟零壹
console.log(numberToChinese('105')); // 壹佰零伍
console.log(numberToChinese('0')); // 零

4. BOM 实战:自动填充当前网址信息

需求:页面加载时,将当前网址的协议、域名、端口、请求地址、锚点自动填充到对应输入框

<!-- HTML 结构 -->
协议 : <input class="protocol" type="text"><br>
域名 : <input class="hostname" type="text"><br>
端口号: <input class="port" type="text"><br>
请求地址 : <input class="pathname" type="text"><br>
锚点 : <input class="hash" type="text"><br>

<script>
// 页面加载完成后填充
window.onload = function() {
    // 从 location 对象中获取各部分信息
    document.querySelector('.protocol').value = location.protocol.slice(0, -1); // 去掉末尾的冒号(如 http: → http)
    document.querySelector('.hostname').value = location.hostname;
    document.querySelector('.port').value = location.port || '80(http默认)/443(https默认)'; // 无端口时提示默认值
    document.querySelector('.pathname').value = location.pathname;
    document.querySelector('.hash').value = location.hash.slice(1); // 去掉开头的#(如 #section1 → section1)
};
</script>

5. 扩展 RegExp 原型:实现指定次数替换

需求:给 RegExp 类添加 sub 方法,支持替换指定次数的匹配内容(默认替换所有)

RegExp.prototype.sub = function(string, newStr, count = -1) {
    const regex = this;
    let match;
    let result = string;
    let replaceCount = 0;

    // 情况1:count=-1(默认),替换所有
    if (count === -1) {
        // 若原正则无 g 标志,添加 g 后替换
        const globalRegex = new RegExp(regex.source, regex.flags.includes('g') ? regex.flags : regex.flags + 'g');
        return result.replace(globalRegex, newStr);
    }

    // 情况2:count>0,替换指定次数
    // 若原正则有 g 标志,先去掉(避免全局匹配导致计数问题)
    const nonGlobalRegex = new RegExp(regex.source, regex.flags.replace('g', ''));
    while ((match = nonGlobalRegex.exec(result)) !== null && replaceCount < count) {
        // 替换当前匹配项
        result = result.slice(0, match.index) + newStr + result.slice(match.index + match[0].length);
        // 更新下一次匹配的起始位置(避免重复匹配同一位置)
        nonGlobalRegex.lastIndex = match.index + newStr.length;
        replaceCount++;
    }

    return result;
};

// 测试
const str = 'xyz12345adsda1113';
const regex = /\d+/;
console.log(regex.sub(str, '*****', 2)); // xyz*****adsda*****3(替换前2个数字组合)
console.log(regex.sub(str, '*****')); // xyz*****adsda*****(替换所有数字组合)
    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值