Day16_JavaScript Event 对象深度解析(上篇)

系列上篇 :在掌握 Day15 各类 DOM 事件之后,本篇聚焦 Event 对象本身冒泡与默认行为事件委托 ,并延伸到 DOM / 事件原型链HTMLCollection vs NodeList 。示例均为完整可运行 HTML。
下篇预告 :用轮播图串联事件能力,并覆盖 CustomEvent、触摸手势、拖拽排序等工程场景 → 下篇链接

权威参考


目录

系列下篇JavaScript 轮播图与事件工程实战(下篇) --- 轮播图、CustomEvent、触摸/拖拽/表单等工程专题。


零、导读与学习价值

0.1 案例覆盖清单

本篇与下列实践主题一一对应(均保留并扩展为完整示例):

模块 主题
事件对象 获取 Event、通用属性、MouseEvent、KeyboardEvent
传播与控制 阻止冒泡、stopImmediatePropagation、阻止默认行为
事件委托 TodoList 动态列表、HTMLCollection 与委托结合
DOM 深入 元素/事件原型链、HTMLCollection vs NodeList

轮播图与工程专题见 下篇
Event 对象
冒泡 / 默认行为
事件委托
原型链 / 集合

0.2 核心名词速查

名词 解析
Event 事件触发时传入回调的参数对象,含 typetargettimeStamp
target 最初触发事件的元素(事件源)
currentTarget 正在执行监听器的元素(委托时在父级)
冒泡 事件从目标向 document 传播;委托依赖此机制
preventDefault 阻止浏览器默认行为(跳转、提交、右键菜单等)
stopPropagation 阻止事件继续向祖先传播
事件委托 在祖先上统一监听,用 event.target 区分子元素
HTMLCollection 动态元素集合,无 forEachgetElementsByClassName 返回
NodeList querySelectorAll 返回的静态集合,有 forEach

0.3 为什么要学本篇?

维度 价值
调试 分清 targetcurrentTarget,快速定位冒泡误触、点不中
性能 事件委托减少监听器,适合长列表与动态 DOM
交互 自定义链接、表单、右键菜单必须会 preventDefault
组件 轮播图是 CSS 布局 + 事件 + 定时器的经典综合题
进阶 理解原型链后,更易读懂框架与 DOM API 设计

一、Event 对象详解

1.1 什么是 Event 对象?

Event 对象(事件对象)是事件触发时自动创建的对象,包含了与当前事件相关的所有信息。
用户交互
事件触发
创建Event对象
传递给回调函数
处理事件逻辑

1.2 获取 Event 对象

在事件处理函数中,通过声明参数即可获取 Event 对象:

javascript 复制代码
// ===== 核心逻辑(详见下方【代码注释】) =====
// 方式一:通过参数获取
element.onclick = function(event) {
    console.log(event); // Event 对象
};

// 方式二:addEventListener 方式
element.addEventListener('click', function(event) {
    console.log(event); // Event 对象
});

// 注意:箭头函数中也可以获取
element.addEventListener('click', (event) => {
    console.log(event);
});

【代码注释】

核心逻辑

  • 浏览器在事件触发时自动创建 Event 实例,作为监听器回调的第一个参数传入。
  • onclick = function(event)addEventListener('click', function(event)) 拿到的对象是同一类,只是绑定方式不同。

关键 API / 写法对比

写法 特点
onclick = fn 覆盖式,同一元素只能绑一个;event 仍为第一参
addEventListener('click', fn) 可绑多个监听器,推荐
箭头函数 (event) => {} 能拿 event无自己的 this ,操作 DOM 用 event.currentTarget

注意点

  • 参数名随意(e/event),必须是第一个参数。
  • 现代浏览器勿用 window.event(IE 兜底已过时)。

实战场景

  • 埋点上报、快捷键、拖拽、轮播切换,都依赖回调里的 event 对象。

1.3 Event 对象通用属性

属性 类型 描述 示例值
type String 事件类型 'click', 'keydown'
timeStamp Number 事件触发时间戳(毫秒) 1234567890
target Element 触发事件的原始元素 <button>
currentTarget Element 绑定事件处理的元素 <div>
bubbles Boolean 事件是否冒泡 true/false
eventPhase Number 事件所处阶段 1(捕获)/2(目标)/3(冒泡)
defaultPrevented Boolean 是否已阻止默认行为 true/false
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Event 对象属性演示</title>
    <style>
        .demo-box {
            width: 400px;
            margin: 50px auto;
            padding: 30px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border-radius: 10px;
            color: white;
            text-align: center;
            cursor: pointer;
        }

        .info-panel {
            width: 600px;
            margin: 20px auto;
            padding: 20px;
            background: #f5f5f5;
            border-radius: 10px;
            font-family: monospace;
        }

        .info-item {
            margin: 10px 0;
            padding: 10px;
            background: white;
            border-radius: 5px;
            display: flex;
            justify-content: space-between;
        }

        .info-label {
            font-weight: bold;
            color: #667eea;
        }

        .info-value {
            color: #333;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">Event 对象属性演示</h1>

    <div class="demo-box" id="demoBox">
        <h2>点击此区域</h2>
        <p>观察下方 Event 对象属性</p>
    </div>

    <div class="info-panel" id="infoPanel">
        <div class="info-item">
            <span class="info-label">等待点击...</span>
        </div>
    </div>

    <script>
        // ===== 核心逻辑(详见下方【代码注释】) =====
        (function() {
            const demoBox = document.getElementById('demoBox');
            const infoPanel = document.getElementById('infoPanel');

            demoBox.addEventListener('click', function(event) {
                // 清空面板
                infoPanel.innerHTML = '';

                // 创建信息项
                const properties = [
                    { label: '事件类型 (type)', value: event.type },
                    { label: '时间戳 (timeStamp)', value: event.timeStamp + ' ms' },
                    { label: '目标元素 (target)', value: event.target.tagName },
                    { label: '当前元素 (currentTarget)', value: event.currentTarget.tagName },
                    { label: '是否冒泡 (bubbles)', value: event.bubbles },
                    { label: '事件阶段 (eventPhase)', value: getEventPhaseName(event.eventPhase) },
                    { label: '默认行为已阻止 (defaultPrevented)', value: event.defaultPrevented }
                ];

                properties.forEach(prop => {
                    const item = document.createElement('div');
                    item.className = 'info-item';
                    item.innerHTML = `
                        <span class="info-label">${prop.label}</span>
                        <span class="info-value">${prop.value}</span>
                    `;
                    infoPanel.appendChild(item);
                });
            });

            function getEventPhaseName(phase) {
                const phases = {
                    1: '捕获阶段 (CAPTURING_PHASE)',
                    2: '目标阶段 (AT_TARGET)',
                    3: '冒泡阶段 (BUBBLING_PHASE)'
                };
                return phases[phase] || phase;
            }
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

  1. IIFE 避免全局污染 → 在 #demoBox 注册 click → 用 properties 数组驱动面板渲染 → getEventPhaseName 翻译 eventPhase 数字。

关键 API / 属性说明

属性 本示例中的值 含义与用途
event.type 'click' 事件类型字符串;委托里常用 if (event.type === 'click')
event.timeStamp 毫秒数 相对页面加载的时间;性能分析、双击间隔判断
event.target 被点中的最内层节点 事件源 ;委托时用 target.closest('.btn') 找业务节点
event.currentTarget #demoBox 当前正在执行监听器的元素 ;此处与 target 可能不同(点在子元素上时)
event.bubbles true/false 是否参与冒泡;focus 等部分事件为 false
event.eventPhase 1 / 2 / 3 1=捕获、2=目标、3=冒泡;调试传播顺序时用
event.defaultPrevented 布尔值 是否已调用过 preventDefault()

实现细节

  • infoPanel.innerHTML = '':每次点击清空,避免面板堆叠。
  • 点击子元素时对比 target.tagNamecurrentTarget.tagName(委托基础)。

注意点

  • target = 事件源;currentTarget = 正在执行监听器的元素,勿混淆。
  • timeStamp 为相对页面加载的毫秒,非 Unix 时间。

实战场景

  • 埋点 SDK 用 event.target + data-* 上报点击元素;DevTools Event Listeners 面板即调试这些属性。

1.4 MouseEvent 专属属性

属性 描述 值说明
button 鼠标按键 0=左键, 1=滚轮, 2=右键
buttons 鼠标按键组合状态 位掩码值
offsetX/Y 相对于目标元素的坐标 像素值
clientX/Y 相对于视口的坐标 像素值
pageX/Y 相对于页面的坐标 像素值
screenX/Y 相对于屏幕的坐标 像素值
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>MouseEvent 属性演示</title>
    <style>
        .mouse-area {
            width: 600px;
            height: 400px;
            margin: 50px auto;
            background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
            border-radius: 10px;
            position: relative;
            cursor: crosshair;
            color: white;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .mouse-point {
            position: absolute;
            width: 10px;
            height: 10px;
            background: #ff6b6b;
            border-radius: 50%;
            pointer-events: none;
            transform: translate(-50%, -50%);
            box-shadow: 0 0 10px rgba(255, 107, 107, 0.5);
        }

        .coords-display {
            position: fixed;
            top: 10px;
            right: 10px;
            padding: 15px;
            background: rgba(0, 0, 0, 0.8);
            color: #00ff00;
            border-radius: 5px;
            font-family: monospace;
            font-size: 14px;
            z-index: 1000;
        }

        .coords-display div {
            margin: 5px 0;
        }

        .button-indicator {
            position: fixed;
            bottom: 10px;
            right: 10px;
            padding: 15px;
            background: rgba(0, 0, 0, 0.8);
            color: white;
            border-radius: 5px;
            font-family: monospace;
        }

        .mouse-buttons {
            display: inline-flex;
            gap: 10px;
        }

        .mouse-btn {
            padding: 10px 20px;
            background: #333;
            border-radius: 5px;
            transition: background 0.2s;
        }

        .mouse-btn.active {
            background: #00ff00;
            color: #000;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">MouseEvent 属性演示</h1>

    <div class="coords-display" id="coordsDisplay">
        <div>offsetX: <span id="offsetX">0</span> | offsetY: <span id="offsetY">0</span></div>
        <div>clientX: <span id="clientX">0</span> | clientY: <span id="clientY">0</span></div>
        <div>pageX: <span id="pageX">0</span> | pageY: <span id="pageY">0</span></div>
        <div>screenX: <span id="screenX">0</span> | screenY: <span id="screenY">0</span></div>
    </div>

    <div class="button-indicator">
        <div>鼠标按键状态:</div>
        <div class="mouse-buttons">
            <div class="mouse-btn" id="btnLeft">左键 (0)</div>
            <div class="mouse-btn" id="btnWheel">滚轮 (1)</div>
            <div class="mouse-btn" id="btnRight">右键 (2)</div>
        </div>
    </div>

    <div class="mouse-area" id="mouseArea">
        <div>
            <h2>在此区域移动鼠标</h2>
            <p>点击查看鼠标按键状态</p>
        </div>
    </div>

    <div style="height: 1000px;"></div>

    <script>
        // ===== 核心逻辑(详见下方【代码注释】) =====
        (function() {
            const mouseArea = document.getElementById('mouseArea');
            const point = document.createElement('div');
            point.className = 'mouse-point';
            mouseArea.appendChild(point);

            const buttons = {
                0: document.getElementById('btnLeft'),
                1: document.getElementById('btnWheel'),
                2: document.getElementById('btnRight')
            };

            // 鼠标移动事件
            mouseArea.addEventListener('mousemove', function(event) {
                // 更新坐标显示
                document.getElementById('offsetX').textContent = event.offsetX;
                document.getElementById('offsetY').textContent = event.offsetY;
                document.getElementById('clientX').textContent = event.clientX;
                document.getElementById('clientY').textContent = event.clientY;
                document.getElementById('pageX').textContent = event.pageX;
                document.getElementById('pageY').textContent = event.pageY;
                document.getElementById('screenX').textContent = event.screenX;
                document.getElementById('screenY').textContent = event.screenY;

                // 更新红点位置
                point.style.left = event.offsetX + 'px';
                point.style.top = event.offsetY + 'px';
            });

            // 鼠标按键事件
            mouseArea.addEventListener('mousedown', function(event) {
                if (buttons[event.button]) {
                    buttons[event.button].classList.add('active');
                }
            });

            mouseArea.addEventListener('mouseup', function(event) {
                if (buttons[event.button]) {
                    buttons[event.button].classList.remove('active');
                }
            });
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

  • mousemove:实时读 offsetX/Y 更新面板并移动红点。
  • mousedown / mouseup:用 event.button(0/1/2)高亮对应鼠标键指示器。

关键 API --- 四套坐标(必记)

属性 参照系 典型用途
offsetX / offsetY 目标元素 padding 边缘内 画板涂鸦、元素内拖拽、本示例红点位置
clientX / clientY 浏览器可视区域左上角 悬浮提示跟随鼠标、判断是否在视口内
pageX / pageY 整个文档左上角(含滚动) 长页面拖拽、与滚动距离相关的计算
screenX / screenY 用户屏幕 多屏协作、极少在前端业务中使用

关系式(无缩放时):pageX ≈ clientX + 页面水平滚动距离

buttonbuttons

  • button本次 按下的是哪一颗键 → 0 左、1 中(滚轮)、2 右。
  • buttons:当前按住的键位掩码(位运算),拖拽时判断是否仍按住左键。

注意点

  • 右键用 contextmenubutton === 2,勿用废弃的 which
  • mousemove 极高频,生产环境必须节流(本 Demo 未加)。

实战场景

  • 图片查看器拖拽用 clientX;画板涂鸦用 offsetX/Y(Figma/Excalidraw 同源)。

1.5 KeyboardEvent 专属属性

属性 描述 示例值
key 按键名称 'a', 'Enter', 'ArrowUp'
code 按键物理位置 'KeyA', 'Enter', 'ArrowUp'
keyCode ASCII 码(已废弃) 65
which 同 keyCode(已废弃) 65
ctrlKey 是否按住 Ctrl true/false
shiftKey 是否按住 Shift true/false
altKey 是否按住 Alt true/false
metaKey 是否按住 Meta(Win/Cmd) true/false
html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>KeyboardEvent 演示</title>
    <style>
        .kbd-demo { max-width: 640px; margin: 40px auto; padding: 24px; background: #f5f5f5; border-radius: 10px; }
        #keyInput {
            width: 100%; padding: 14px; font-size: 16px; border: 2px solid #667eea;
            border-radius: 8px; outline: none;
        }
        #keyInput:focus { box-shadow: 0 0 0 3px rgba(102,126,234,.2); }
        .info-panel { margin-top: 16px; padding: 16px; background: #2d2d2d; color: #0f0;
            font-family: monospace; font-size: 13px; border-radius: 8px; min-height: 140px; }
        .hint { color: #666; font-size: 14px; margin-top: 12px; line-height: 1.6; }
    </style>
</head>
<body>
    <h1 style="text-align: center;">KeyboardEvent 属性演示</h1>
    <div class="kbd-demo">
        <p>在输入框中按键(可试 Ctrl+Shift+A、方向键、Enter):</p>
        <input type="text" id="keyInput" placeholder="聚焦后按键..." autocomplete="off">
        <div class="info-panel" id="infoPanel">等待按键...</div>
        <p class="hint">提示:<code>key</code> 随输入法/语言变化;<code>code</code> 表示物理键位,适合做游戏/快捷键。</p>
    </div>
    <script>
        (function() {
            const input = document.getElementById('keyInput');
            const panel = document.getElementById('infoPanel');

            input.addEventListener('keydown', function(event) {
                const mods = [];
                if (event.ctrlKey)  mods.push('Ctrl');
                if (event.shiftKey) mods.push('Shift');
                if (event.altKey)   mods.push('Alt');
                if (event.metaKey)  mods.push('Meta(⌘)');

                panel.textContent = [
                    `type: ${event.type}`,
                    `key: "${event.key}"  ← 字符/功能键名(随语言变)`,
                    `code: "${event.code}"  ← 物理键位(布局无关)`,
                    `修饰键: ${mods.length ? mods.join(' + ') : '无'}`,
                    `repeat: ${event.repeat}  ← 长按连续触发`,
                    `defaultPrevented: ${event.defaultPrevented}`,
                    '',
                    '示例:Ctrl+S → key 可能为 "s",code 为 "KeyS"'
                ].join('\n');

                // 演示:阻止 Tab 切走焦点(教学用,可按需注释)
                if (event.key === 'Tab') {
                    event.preventDefault();
                    panel.textContent += '\n\n⚠️ 已 preventDefault Tab 默认行为';
                }
            });
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

  • keydown 触发时,回调收到 KeyboardEvent 实例(继承 UIEventEvent)。
  • 每次按键先 keydown,松开后 keyup;长按会连续触发 keydownevent.repeat === true

关键 API

属性 含义 使用建议
key 按键语义'a''Enter''ArrowLeft' 判断用户按了什么字符/功能键
code 物理键位'KeyA''Digit1' 游戏 WASD、不受输入法影响的快捷键
ctrlKey / shiftKey / altKey / metaKey 修饰键是否按下 组合键 Ctrl+S 需同时判断
repeat 是否长按连发 输入框防抖、游戏连发限制

key vs code(面试高频)

javascript 复制代码
// 法语键盘按 A:key 可能是 'q',code 仍是 'KeyQ'(物理 Q 键位)
// 快捷键管理器用 code;输入框校验字符用 key

注意点

  • keyCode / which 已废弃,勿在新代码中使用。
  • keydownpreventDefault() 可阻止浏览器默认(如 Tab 切焦点、Ctrl+S 保存页);keypress 已不推荐。
  • input 里监听时,需考虑与输入法的交互(中文拼写阶段 key 可能是 Process 等)。

实战场景

  • VS Code / Notion 快捷键(§10.3)、表单 Enter 提交、Esc 关闭弹窗、轮播左右方向键切换。

【本章小结】Event 对象

  • 回调参数 eventtarget(事件源)与 currentTarget(监听器绑定元素)。
  • 设备事件:MouseEvent 看坐标与 buttonKeyboardEventkey/code,勿用废弃的 keyCode

二、事件冒泡与捕获

2.1 事件传播机制

Child Parent Body Document Window Child Parent Body Document Window 捕获阶段(从上到下) 目标阶段 冒泡阶段(从下到上) 1. 触发捕获事件 2. 触发捕获事件 3. 触发捕获事件 4. 触发捕获事件 5. 触发目标事件 6. 触发冒泡事件 7. 触发冒泡事件 8. 触发冒泡事件 9. 触发冒泡事件

2.2 阻止事件冒泡

使用 event.stopPropagation() 方法可以阻止事件继续传播:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>阻止事件冒泡演示</title>
    <style>
        .bubble-demo {
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
        }

        .box {
            padding: 40px;
            margin: 20px;
            border-radius: 10px;
            cursor: pointer;
            transition: all 0.3s;
        }

        .box.outer {
            background: #ffeaa7;
        }

        .box.middle {
            background: #74b9ff;
        }

        .box.inner {
            background: #ff7675;
            color: white;
            padding: 30px;
            text-align: center;
        }

        .box:hover {
            transform: scale(1.02);
        }

        .log-panel {
            background: #2d2d2d;
            color: #00ff00;
            padding: 15px;
            border-radius: 10px;
            height: 200px;
            overflow-y: auto;
            font-family: monospace;
            font-size: 12px;
        }

        .log-item {
            margin: 3px 0;
            padding: 3px 5px;
            border-left: 3px solid;
            padding-left: 8px;
        }

        .log-item.outer { border-left-color: #ffeaa7; }
        .log-item.middle { border-left-color: #74b9ff; }
        .log-item.inner { border-left-color: #ff7675; }

        .controls {
            padding: 20px;
            background: #f5f5f5;
            border-radius: 10px;
            margin-bottom: 20px;
        }

        .toggle-switch {
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .toggle-switch input {
            width: 20px;
            height: 20px;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">阻止事件冒泡演示</h1>

    <div class="bubble-demo">
        <div class="controls">
            <label class="toggle-switch">
                <input type="checkbox" id="stopBubble">
                <span>在内层元素阻止事件冒泡</span>
            </label>
        </div>

        <div class="box outer" id="outerBox">
            <h3>外层元素</h3>
            <div class="box middle" id="middleBox">
                <h3>中层元素</h3>
                <div class="box inner" id="innerBox">
                    <h3>内层元素</h3>
                    <p>点击此区域观察事件传播</p>
                </div>
            </div>
        </div>

        <div class="log-panel" id="logPanel">
            <div>事件日志:</div>
        </div>
    </div>

    <script>
        // ===== 核心逻辑(详见下方【代码注释】) =====
        (function() {
            const boxes = {
                outer: document.getElementById('outerBox'),
                middle: document.getElementById('middleBox'),
                inner: document.getElementById('innerBox')
            };
            const stopBubble = document.getElementById('stopBubble');
            const logPanel = document.getElementById('logPanel');

            function log(message, className) {
                const time = new Date().toLocaleTimeString();
                const div = document.createElement('div');
                div.className = `log-item ${className}`;
                div.textContent = `[${time}] ${message}`;
                logPanel.appendChild(div);
                logPanel.scrollTop = logPanel.scrollHeight;
            }

            // 为每个盒子添加点击事件
            Object.keys(boxes).forEach(key => {
                boxes[key].addEventListener('click', function(event) {
                    const boxName = { outer: '外层', middle: '中层', inner: '内层' }[key];
                    log(`${boxName}元素被点击`, key);

                    // 如果是内层且勾选了阻止冒泡
                    if (key === 'inner' && stopBubble.checked) {
                        event.stopPropagation();
                        log('⚠️ 事件冒泡已被阻止!', 'inner');
                    }
                });
            });
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

  • 三层嵌套各绑 click;点击内层默认 内 → 中 → 外 冒泡打印日志。
  • 勾选后内层执行 event.stopPropagation(),外层/中层不再收到事件。

执行顺序(未阻止时)

复制代码
点击 inner → inner 监听器 → middle 监听器 → outer 监听器 → document ...

stopPropagation() 精确语义

  • ✅ 阻止事件继续向祖先节点传播。
  • 不阻止 同一元素上已注册的其他 监听器(那是 stopImmediatePropagation 的事)。
  • 不阻止 默认行为(链接跳转仍需 preventDefault())。

与业务场景的对应

场景 为什么要停冒泡
弹窗内按钮 避免点击按钮时冒泡到蒙层,触发「点击蒙层关闭」
表格行内操作钮 避免触发行选中 / 展开
下拉菜单项 避免触发外层容器的 click 关闭逻辑

注意点

  • 捕获阶段 stopPropagation 会截断整条传播链,比冒泡阶段调用影响更大。
  • React 合成事件需 e.nativeEvent.stopPropagation() 才能阻止原生冒泡。

实战场景

  • 弹窗内按钮、表格行内操作钮、下拉菜单项------防止误触父级 click 关闭逻辑。

2.3 stopPropagation 与 stopImmediatePropagation 的区别

方法 描述
stopPropagation() 阻止事件继续传播,但允许当前元素的其他监听器执行
stopImmediatePropagation() 阻止事件传播,并阻止当前元素的其他监听器执行
javascript 复制代码
// ===== 核心逻辑(详见下方【代码注释】) =====
element.addEventListener('click', function(event) {
    console.log('第一个监听器');
    event.stopImmediatePropagation(); // 后续监听器不会执行
});

element.addEventListener('click', function(event) {
    console.log('第二个监听器'); // 不会执行
});

【代码注释】

对比表(面试必背)

方法 阻止向祖先传播 阻止同元素后续监听器
stopPropagation()
stopImmediatePropagation()

本段代码执行结果

  1. 第一个监听器执行,打印「第一个监听器」,并调用 stopImmediatePropagation()
  2. 第二个监听器不会执行(同元素、后注册的被跳过)。
  3. 若父元素也绑了 click,父元素监听器同样不会执行(传播链被截断)。

注册顺序为什么重要

  • addEventListener注册先后顺序在同阶段内执行。
  • 需要「优先拦截」时,可用 { capture: true } 在捕获阶段先执行,不必抢注册顺序。

实战场景

  • 支付按钮防重复:第一个监听器里 stopImmediatePropagation + 置 disabled,防止同元素第二个监听器再次提交。
  • 全局点击埋点 vs 局部业务:局部在捕获阶段 stopImmediatePropagation,避免埋点重复上报。

易错点

  • 两者都不影响 默认行为;阻止跳转仍要 preventDefault()
  • 不要滥用 stopImmediatePropagation,会破坏同元素上合理的多监听器协作(如插件 + 业务代码)。

2.4 捕获阶段的 currentTarget 对比实验

核心结论currentTarget 始终等于当前正在执行监听器的那个元素 ,与传播方向无关;target 则始终是最初触发事件的元素。
inner(target) outer document inner(target) outer document 捕获阶段(useCapture=true) 冒泡阶段(useCapture=false) currentTarget=document → currentTarget=outer currentTarget=inner(目标阶段) currentTarget=outer currentTarget=document

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>currentTarget 捕获 vs 冒泡对比</title>
    <style>
        body { font-family: monospace; padding: 30px; }
        #outer {
            padding: 40px;
            background: #e3f2fd;
            border: 2px solid #1976d2;
            border-radius: 8px;
        }
        #inner {
            padding: 20px;
            background: #fff3e0;
            border: 2px solid #f57c00;
            border-radius: 6px;
            cursor: pointer;
            text-align: center;
        }
        #log {
            margin-top: 20px;
            padding: 12px;
            background: #1e1e1e;
            color: #d4d4d4;
            border-radius: 6px;
            min-height: 120px;
            font-size: 13px;
            white-space: pre-wrap;
        }
        .phase-cap { color: #4fc3f7; }
        .phase-bub { color: #a5d6a7; }
    </style>
</head>
<body>
    <h2>点击橙色内层,观察各阶段 currentTarget</h2>
    <div id="outer">
        外层 #outer
        <div id="inner">内层 #inner(点我)</div>
    </div>
    <div id="log">等待点击...</div>

    <script>
        // ===== 核心逻辑(详见下方【代码注释】) =====
        (function () {
            const log = document.getElementById('log');
            let lines = [];

            function record(phase, listener, e) {
                lines.push(
                    `[${phase}] 监听器在 #${listener.id}` +
                    `  target=${e.target.id}` +
                    `  currentTarget=${e.currentTarget.id}`
                );
                log.textContent = lines.join('\n');
            }

            const outer = document.getElementById('outer');
            const inner = document.getElementById('inner');

            // 捕获阶段监听(第三参数 true)
            document.addEventListener('click', e => record('捕获', document.documentElement, e), true);
            outer.addEventListener('click',    e => record('捕获', outer, e), true);
            inner.addEventListener('click',    e => record('目标', inner, e), true);

            // 冒泡阶段监听(默认 false)
            inner.addEventListener('click',    e => record('目标', inner, e));
            outer.addEventListener('click',    e => record('冒泡', outer, e));
            document.addEventListener('click', e => { record('冒泡', document.documentElement, e); lines.push('---'); }, false);
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

  • document / #outer / #inner 分别注册捕获(addEventListener(..., true))与冒泡监听。
  • 点击 #inner 后按真实顺序打印:target 始终为 innercurrentTarget正在执行的监听器变化。

捕获阶段日志(自上而下)

顺序 currentTarget target 说明
1 document #inner 从根向内,currentTarget 是当前执行监听的节点
2 #outer #inner target 始终是事件源,不变
3 #inner #inner 目标阶段,target === currentTarget

冒泡阶段日志(自下而上)

顺序 currentTarget target
1 #inner #inner
2 #outer #inner
3 document #inner

record(phase, listener, e) 函数

  • 第三个参数传「注册监听的元素引用」,便于和 e.currentTarget 对照验证。
  • 用数组 lines 累积字符串,比反复 console.log 更适合页面展示。

注意点

  • 委托时:currentTarget = 绑定监听的父元素,target = 实际被点的子节点。
  • 捕获监听适合「先于子组件执行」的全局守卫(权限、埋点)。

实战场景

  • 捕获阶段权限校验;ul 上委托 click 管理动态 li 列表(淘宝/京东商品行操作)。

【本章补充小结】冒泡 vs 捕获

对比项 冒泡(默认) 捕获(true
传播方向 内 → 外 外 → 内
currentTarget 依次为各祖先 依次为各祖先(顺序相反)
target 始终是事件源 始终是事件源
常见用途 事件委托 全局拦截、权限守卫

三、阻止默认行为

3.1 常见的浏览器默认行为

行为 触发元素 默认效果
跳转页面 <a> 标签 导航到 href 指定的 URL
提交表单 <form> submit 事件 将数据发送到服务器
右键菜单 任何元素 contextmenu 事件 显示浏览器上下文菜单
文本选择 部分元素 允许选择文本
表单重置 <input type="reset"> 清空表单内容

3.2 阻止默认行为的方法

javascript 复制代码
// ===== 核心逻辑(详见下方【代码注释】) =====
// 方法一:使用 preventDefault()
element.addEventListener('click', function(event) {
    event.preventDefault();
    // 自定义行为
});

// 方法二:在事件属性中返回 false(仅限传统绑定方式)
element.onclick = function() {
    // 自定义行为
    return false; // 阻止默认行为
};

【代码注释】

两种写法对比

写法 阻止默认行为 阻止冒泡 适用
event.preventDefault() ❌(需另调 stopPropagation addEventListener 推荐
return false ✅(部分元素) ✅(部分元素) onclickonsubmitHTML/DOM 属性
addEventListenerreturn false ❌ 无效 ❌ 无效 常见误区

preventDefault() 做了什么

  • 通知浏览器:不要执行该事件类型对应的内置行为 (如 <a> 跳转、表单提交、右键菜单、文本拖选等)。
  • 调用后 event.defaultPrevented === true,可用来判断是否已拦截。

典型对应关系

元素/事件 默认行为 拦截后要自己实现
<a href> + click 导航 history.pushState / 路由
<form> + submit 提交并刷新 fetch + JSON
contextmenu 系统右键菜单 自定义菜单 DOM
touchmove(部分场景) 页面滚动 轮播横向滑动

易错点

  • preventDefault() 不会 阻止冒泡;弹窗里阻止链接跳转,若还怕触达蒙层,需再 stopPropagation()
  • passive: true 的监听器里调用 preventDefault() 会被忽略并报警(见 §8.5)。

3.3 完整示例

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>阻止默认行为演示</title>
    <style>
        .prevent-demo {
            max-width: 800px;
            margin: 50px auto;
            padding: 30px;
        }

        .demo-section {
            margin: 30px 0;
            padding: 20px;
            background: #f5f5f5;
            border-radius: 10px;
        }

        .demo-link {
            color: #667eea;
            text-decoration: none;
            font-weight: bold;
            font-size: 18px;
        }

        .demo-link:hover {
            text-decoration: underline;
        }

        .context-area {
            padding: 40px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border-radius: 10px;
            text-align: center;
        }

        .custom-menu {
            position: fixed;
            background: white;
            border-radius: 5px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.2);
            padding: 10px;
            display: none;
            z-index: 1000;
        }

        .custom-menu.show {
            display: block;
        }

        .custom-menu-item {
            padding: 10px 20px;
            cursor: pointer;
            border-radius: 3px;
        }

        .custom-menu-item:hover {
            background: #f0f0f0;
        }

        .status-message {
            position: fixed;
            top: 20px;
            right: 20px;
            padding: 15px 25px;
            background: #28a745;
            color: white;
            border-radius: 5px;
            opacity: 0;
            transform: translateY(-20px);
            transition: all 0.3s;
        }

        .status-message.show {
            opacity: 1;
            transform: translateY(0);
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">阻止默认行为演示</h1>

    <div class="prevent-demo">
        <div class="demo-section">
            <h3>1. 阻止链接跳转</h3>
            <p>
                <label>
                    <input type="checkbox" id="preventLink"> 启用阻止
                </label>
            </p>
            <a href="https://example.com" class="demo-link" id="demoLink">点击这个链接</a>
            <p id="linkStatus">当前状态:正常跳转</p>
        </div>

        <div class="demo-section">
            <h3>2. 自定义右键菜单</h3>
            <div class="context-area" id="contextArea">
                <h3>在此区域右键点击</h3>
                <p>查看自定义菜单</p>
            </div>
        </div>

        <div class="demo-section">
            <h3>3. 阻止表单提交</h3>
            <form id="demoForm">
                <input type="text" placeholder="输入内容" required style="padding: 10px; width: 200px;">
                <button type="submit" style="padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px;">提交</button>
            </form>
        </div>
    </div>

    <div class="custom-menu" id="customMenu">
        <div class="custom-menu-item" data-action="copy">复制</div>
        <div class="custom-menu-item" data-action="paste">粘贴</div>
        <div class="custom-menu-item" data-action="delete">删除</div>
    </div>

    <div class="status-message" id="statusMessage"></div>

    <script>
        // ===== 核心逻辑(详见下方【代码注释】) =====
        (function() {
            const preventLink = document.getElementById('preventLink');
            const demoLink = document.getElementById('demoLink');
            const linkStatus = document.getElementById('linkStatus');
            const contextArea = document.getElementById('contextArea');
            const customMenu = document.getElementById('customMenu');
            const demoForm = document.getElementById('demoForm');
            const statusMessage = document.getElementById('statusMessage');

            // 显示状态消息
            function showMessage(message) {
                statusMessage.textContent = message;
                statusMessage.classList.add('show');
                setTimeout(() => {
                    statusMessage.classList.remove('show');
                }, 2000);
            }

            // 1. 阻止链接跳转
            preventLink.addEventListener('change', function() {
                linkStatus.textContent = this.checked ? '当前状态:已阻止跳转' : '当前状态:正常跳转';
            });

            demoLink.addEventListener('click', function(event) {
                if (preventLink.checked) {
                    event.preventDefault();
                    showMessage('链接跳转已被阻止!');
                }
            });

            // 2. 自定义右键菜单
            contextArea.addEventListener('contextmenu', function(event) {
                event.preventDefault();

                // 显示自定义菜单
                customMenu.style.left = event.pageX + 'px';
                customMenu.style.top = event.pageY + 'px';
                customMenu.classList.add('show');
            });

            // 点击其他地方隐藏菜单
            document.addEventListener('click', function() {
                customMenu.classList.remove('show');
            });

            // 自定义菜单项点击
            customMenu.addEventListener('click', function(event) {
                const action = event.target.dataset.action;
                showMessage(`执行操作:${action}`);
                customMenu.classList.remove('show');
            });

            // 3. 阻止表单提交
            demoForm.addEventListener('submit', function(event) {
                event.preventDefault();
                showMessage('表单提交已被阻止!可以在这里添加验证逻辑...');
            });
        })();
    </script>
</body>
</html>

【代码注释】

页面模块与事件对应

模块 监听事件 默认行为 拦截方式
演示链接 click 跳转 href preventDefault() + 改状态文案
演示表单 submit GET/POST 提交并刷新 阻止提交,用 JS 模拟成功提示
右键区域 contextmenu 系统菜单 preventDefault() + 显示 #customMenu
滚轮区域 wheel 页面滚动 可选阻止,演示缩放/日志

交互设计要点

  • 「是否阻止」用复选框控制,方便对比实验:同一链接,勾选前后感受差异。
  • 自定义菜单用 clientX/clientY 定位(MouseEvent 坐标),菜单需 stopPropagation,避免立即被 document 点击关闭逻辑误伤。

contextmenu 完整链路

  1. contextmenupreventDefault() 关掉系统菜单。
  2. 显示自定义 divposition: fixed; left: clientX; top: clientY
  3. document 再监听 click 关闭菜单(注意与冒泡配合)。

易错点

  • 只阻止 click 无法阻止键盘 Enter 激活链接,需同时处理 keydown 或保证焦点逻辑。
  • 表单验证失败时也应 preventDefault(),否则仍会提交。

市面应用

  • SPA(Vue Router / React Router):<a href="/about"> + preventDefault + 路由跳转。
  • 富文本编辑器:统一拦截 contextmenu 出格式化菜单。
  • 登录页:submit + fetch,避免整页刷新。

【本章小结】阻止默认行为

知识点 核心要点
preventDefault() 标准方法,仅阻止默认行为,事件继续传播
return false HTML 属性绑定可用;addEventListener 中无效
event.defaultPrevented 可读属性,检查是否已阻止
不可阻止的事件 scroll(已发生)、部分浏览器安全限制
常见场景 路由拦截、表单校验、右键菜单、文件拖放

记忆口诀stopPropagation 管"传播",preventDefault 管"行为",二者互不影响。


四、事件委托实战

4.1 事件委托的原理

事件委托 (Event Delegation)利用了事件冒泡机制,将事件处理函数绑定在父元素上,通过 event.target 判断具体触发事件的子元素。
冒泡
冒泡
冒泡
父元素绑定事件
事件监听器
子元素1点击
子元素2点击
子元素3点击
检查target
执行对应操作

4.2 事件委托的优势

优势 说明
性能优化 减少事件监听器数量,节省内存
动态元素 新增子元素自动具有事件处理能力
代码简洁 集中管理事件处理逻辑
维护方便 事件处理逻辑统一,易于维护

4.3 TodoList 事件委托示例

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>TodoList - 事件委托示例</title>
    <style>
        .todo-app {
            width: 600px;
            margin: 50px auto;
            background: white;
            border-radius: 15px;
            box-shadow: 0 10px 40px rgba(0,0,0,0.1);
            overflow: hidden;
        }

        .todo-header {
            padding: 30px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
        }

        .todo-header h1 {
            margin: 0 0 20px 0;
            text-align: center;
        }

        .input-group {
            display: flex;
            gap: 10px;
        }

        .input-group input {
            flex: 1;
            padding: 12px 15px;
            border: none;
            border-radius: 5px;
            font-size: 16px;
        }

        .input-group button {
            padding: 12px 25px;
            background: #28a745;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            transition: background 0.3s;
        }

        .input-group button:hover {
            background: #218838;
        }

        .todo-list {
            list-style: none;
            padding: 0;
            margin: 0;
        }

        .todo-item {
            display: flex;
            align-items: center;
            padding: 15px 30px;
            border-bottom: 1px solid #f0f0f0;
            transition: background 0.3s;
        }

        .todo-item:hover {
            background: #f9f9f9;
        }

        .todo-item.completed {
            opacity: 0.6;
        }

        .todo-item.completed .todo-text {
            text-decoration: line-through;
            color: #999;
        }

        .todo-checkbox {
            width: 20px;
            height: 20px;
            margin-right: 15px;
            cursor: pointer;
        }

        .todo-text {
            flex: 1;
            font-size: 16px;
        }

        .todo-delete {
            padding: 5px 15px;
            background: #ff6b6b;
            color: white;
            border: none;
            border-radius: 3px;
            cursor: pointer;
            opacity: 0.8;
            transition: opacity 0.3s;
        }

        .todo-delete:hover {
            opacity: 1;
        }

        .todo-stats {
            padding: 20px 30px;
            background: #f8f9fa;
            display: flex;
            justify-content: space-between;
            color: #666;
        }

        .empty-state {
            padding: 40px;
            text-align: center;
            color: #999;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">TodoList - 事件委托实战</h1>

    <div class="todo-app">
        <div class="todo-header">
            <h1>待办事项</h1>
            <div class="input-group">
                <input type="text" id="todoInput" placeholder="添加新的待办事项...">
                <button id="addBtn">添加</button>
            </div>
        </div>

        <ul class="todo-list" id="todoList">
            <li class="todo-item" data-id="1">
                <input type="checkbox" class="todo-checkbox">
                <span class="todo-text">学习 JavaScript 事件委托</span>
                <button class="todo-delete">删除</button>
            </li>
            <li class="todo-item" data-id="2">
                <input type="checkbox" class="todo-checkbox">
                <span class="todo-text">掌握 DOM 操作技巧</span>
                <button class="todo-delete">删除</button>
            </li>
            <li class="todo-item completed" data-id="3">
                <input type="checkbox" class="todo-checkbox" checked>
                <span class="todo-text">实践前端项目开发</span>
                <button class="todo-delete">删除</button>
            </li>
        </ul>

        <div class="todo-stats">
            <span>总计: <strong id="totalCount">3</strong></span>
            <span>已完成: <strong id="completedCount">1</strong></span>
            <span>未完成: <strong id="activeCount">2</strong></span>
        </div>
    </div>

    <script>
        // ===== 核心逻辑(详见下方【代码注释】) =====
        (function() {
            const todoList = document.getElementById('todoList');
            const todoInput = document.getElementById('todoInput');
            const addBtn = document.getElementById('addBtn');
            let todoId = 4;

            // 更新统计数据
            function updateStats() {
                const items = todoList.querySelectorAll('.todo-item');
                const completed = todoList.querySelectorAll('.todo-item.completed');

                document.getElementById('totalCount').textContent = items.length;
                document.getElementById('completedCount').textContent = completed.length;
                document.getElementById('activeCount').textContent = items.length - completed.length;
            }

            // 添加新任务
            function addTodo() {
                const text = todoInput.value.trim();
                if (!text) return;

                const li = document.createElement('li');
                li.className = 'todo-item';
                li.dataset.id = todoId++;
                li.innerHTML = `
                    <input type="checkbox" class="todo-checkbox">
                    <span class="todo-text">${text}</span>
                    <button class="todo-delete">删除</button>
                `;

                todoList.appendChild(li);
                todoInput.value = '';
                updateStats();
            }

            // 点击添加按钮
            addBtn.addEventListener('click', addTodo);

            // 输入框回车添加
            todoInput.addEventListener('keypress', function(e) {
                if (e.key === 'Enter') addTodo();
            });

            // 使用事件委托处理所有点击事件
            todoList.addEventListener('click', function(event) {
                const target = event.target;
                const todoItem = target.closest('.todo-item');

                if (!todoItem) return;

                // 处理复选框点击
                if (target.classList.contains('todo-checkbox')) {
                    todoItem.classList.toggle('completed', target.checked);
                    updateStats();
                }

                // 处理删除按钮点击
                if (target.classList.contains('todo-delete')) {
                    todoItem.style.opacity = '0';
                    todoItem.style.transform = 'translateX(100px)';
                    setTimeout(() => {
                        todoItem.remove();
                        updateStats();
                    }, 300);
                }
            });

            // 初始化统计
            updateStats();
        })();
    </script>
</body>
</html>

【代码注释】

核心逻辑

复制代码
用户点击 → 冒泡到 #todoList → 唯一监听器
         → event.target / closest 识别按钮 → 增删改 DOM

关键步骤

步骤 代码思路 原因
绑定 todoList.addEventListener('click', handler) 父节点稳定,子节点可增删
识别 event.target.closest('.delete-btn')matches('.delete-btn') 点到图标/文字时 target 可能是子节点
新增任务 appendChild(li) 不必 给新 liaddEventListener
统计 updateStats() 在增删后重算 保持 UI 与数据一致

为什么不用给每个按钮绑监听

  • 1000 条任务 = 1000 个监听器 → 内存与注册成本线性增长。
  • 委托 = 1 个监听器 + 冒泡,复杂度 O(1) 注册。

target vs currentTarget 在本例中

  • currentTarget:始终是 #todoList(绑定监听的 ul)。
  • target:实际被点击的按钮或 checkbox。

注意点

  • 点在子节点上时 target 可能不是按钮 → 用 event.target.closest('.todo-delete')
  • 输入框 clickstopPropagation,避免误触列表逻辑。

实战场景

  • Ant Design Table 操作列、微信消息列表、评论区点赞/回复,均为父级委托 + closest

4.4 事件委托适用场景

场景 说明
列表项操作 如待办事项、商品列表等
动态内容 内容通过 AJAX 动态加载
大量相似元素 如表格行、图片网格等
性能敏感 需要减少事件监听器数量

【本章小结】事件委托

知识点 核心要点
原理 利用冒泡,把子元素的事件交由父元素统一处理
event.target 实际触发事件的元素(委托的目标)
event.currentTarget 绑定监听器的元素(委托的父元素)
matches(selector) 精确过滤,避免点击父级空白误触
适用场景 动态列表、大量同类元素、AJAX 动态内容
不适用场景 focus/blur 不冒泡;mouseenter/mouseleave 不冒泡

最佳实践 :在委托处理中始终通过 closest()matches() 做类型校验,防御性编程。


五、DOM 对象原型链

5.1 元素对象原型链

div元素对象
HTMLDivElement.prototype
HTMLElement.prototype
Element.prototype
Node.prototype
EventTarget.prototype
Object.prototype
null

5.2 原型链层级功能

原型对象 提供的功能
Object.prototype 对象基础方法:toString(), valueOf(), hasOwnProperty()
EventTarget.prototype 事件相关:addEventListener(), removeEventListener(), dispatchEvent()
Node.prototype 节点操作:appendChild(), removeChild(), cloneNode(), hasChildNodes()
Element.prototype 元素操作:querySelector(), getAttribute(), setAttribute(), classList
HTMLElement.prototype HTML元素通用属性:innerHTML, style, title, dataset
HTMLXXXElement.prototype 特定元素特有属性(很少)

5.3 原型链探索示例

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>DOM 原型链探索</title>
    <style>
        .prototype-demo {
            max-width: 800px;
            margin: 50px auto;
            padding: 30px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
        }

        .prototype-chain {
            display: flex;
            flex-direction: column;
            gap: 10px;
            margin: 20px 0;
        }

        .chain-item {
            padding: 15px;
            background: #f5f5f5;
            border-radius: 5px;
            font-family: monospace;
            border-left: 4px solid #667eea;
        }

        .chain-item .constructor {
            color: #667eea;
            font-weight: bold;
        }

        .chain-item .methods {
            color: #28a745;
            margin-top: 5px;
        }

        .test-element {
            padding: 40px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border-radius: 10px;
            text-align: center;
            margin-bottom: 20px;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">DOM 原型链探索</h1>

    <div class="prototype-demo">
        <div class="test-element" id="testElement">
            <h2>测试元素</h2>
            <p>这是一个 div 元素</p>
        </div>

        <h3>原型链结构</h3>
        <div class="prototype-chain" id="chainDisplay">
            <!-- 原型链信息将通过JS动态生成 -->
        </div>
    </div>

    <script>
        // ===== 核心逻辑(详见下方【代码注释】) =====
        (function() {
            const testElement = document.getElementById('testElement');
            const chainDisplay = document.getElementById('chainDisplay');

            // 定义原型链信息
            const prototypeInfo = [
                {
                    name: 'HTMLDivElement.prototype',
                    constructor: 'HTMLDivElement',
                    methods: ['特定于 div 元素的方法(很少)'],
                    color: '#ff6b6b'
                },
                {
                    name: 'HTMLElement.prototype',
                    constructor: 'HTMLElement',
                    methods: ['innerHTML', 'style', 'title', 'dataset', 'click()', 'focus()'],
                    color: '#feca57'
                },
                {
                    name: 'Element.prototype',
                    constructor: 'Element',
                    methods: ['querySelector()', 'getAttribute()', 'setAttribute()', 'classList', 'id'],
                    color: '#48dbfb'
                },
                {
                    name: 'Node.prototype',
                    constructor: 'Node',
                    methods: ['appendChild()', 'removeChild()', 'cloneNode()', 'hasChildNodes()', 'parentNode'],
                    color: '#1dd1a1'
                },
                {
                    name: 'EventTarget.prototype',
                    constructor: 'EventTarget',
                    methods: ['addEventListener()', 'removeEventListener()', 'dispatchEvent()'],
                    color: '#5f27cd'
                },
                {
                    name: 'Object.prototype',
                    constructor: 'Object',
                    methods: ['toString()', 'valueOf()', 'hasOwnProperty()'],
                    color: '#00d2d3'
                }
            ];

            // 显示原型链
            prototypeInfo.forEach(info => {
                const item = document.createElement('div');
                item.className = 'chain-item';
                item.style.borderLeftColor = info.color;
                item.innerHTML = `
                    <div class="constructor">${info.name}</div>
                    <div class="methods">构造函数: ${info.constructor}</div>
                    <div class="methods">常用方法/属性: ${info.methods.join(', ')}</div>
                `;
                chainDisplay.appendChild(item);
            });

            // 验证原型链
            console.log('验证原型链:');
            console.log('testElement instanceof HTMLDivElement:', testElement instanceof HTMLDivElement);
            console.log('testElement instanceof HTMLElement:', testElement instanceof HTMLElement);
            console.log('testElement instanceof Element:', testElement instanceof Element);
            console.log('testElement instanceof Node:', testElement instanceof Node);
            console.log('testElement instanceof EventTarget:', testElement instanceof EventTarget);
            console.log('testElement instanceof Object:', testElement instanceof Object);
        })();
    </script>
</body>
</html>

【代码注释】

代码在做什么

  • 给定 #testElement(一个 div),沿 __proto__ 向上遍历,把每一层原型上的「代表性能力」列出来。
  • 页面用卡片展示链:HTMLDivElementHTMLElementElementNodeEventTarget

为什么事件能挂在任意元素上

  • 原型链终点附近一定有 EventTarget.prototype.addEventListener
  • 因此不仅是 divdocumentwindow 也能监听------它们都混入了 EventTarget 接口。

层级与常见 API 对应

原型层 与本篇相关的方法/属性
HTMLDivElement 无额外事件 API,继承上层
HTMLElement clickfocusdataset
Element querySelectorclassList
Node appendChildchildNodes
EventTarget addEventListenerremoveEventListenerdispatchEvent

__proto__ 说明

  • 教学用 obj.__proto__ 直观;规范写法是 Object.getPrototypeOf(obj)
  • 读取原型不等于 修改原型;修改 HTMLElement.prototype 会影响所有元素(极少这样做)。

面试延伸

  • 「DOM 元素为什么能 addEventListener?」→ 继承自 EventTarget
  • event 是什么类型?」→ 点击时是 MouseEvent,继承链 MouseEventUIEventEvent

5.4 事件对象原型链

MouseEvent对象
MouseEvent.prototype
UIEvent.prototype
Event.prototype
Object.prototype
null

javascript 复制代码
// ===== 验证事件对象类型与原型链 =====
document.addEventListener('click', function(event) {
    console.log(event);                              // MouseEvent { ... }
    console.log(event instanceof MouseEvent);        // true
    console.log(event instanceof UIEvent);           // true
    console.log(event instanceof Event);             // true
    console.log(Object.getPrototypeOf(event) === MouseEvent.prototype); // true
});

// 键盘事件同理
input.addEventListener('keydown', function(event) {
    console.log(event instanceof KeyboardEvent);     // true
});

【代码注释】

核心逻辑

  • 用户点击时,浏览器创建的是 MouseEvent 实例 ,不是「裸」Event
  • 继承链:MouseEventUIEventEventObject;键盘为 KeyboardEventUIEventEventObject
  • 因此 event.preventDefaultevent.targetEvent.prototype 上;clientXMouseEvent 上。

关键 API

事件类型 构造函数 特有属性示例
鼠标 MouseEvent clientXbuttonoffsetX
键盘 KeyboardEvent keycodectrlKey
通用 Event typetargetbubbles

instanceof 与调试

  • DevTools 打印 event 显示 PointerEvent 时,仍可用 MouseEvent 方法(现代点击常走 Pointer 事件,再兼容为 Mouse)。
  • Object.getPrototypeOf(event)event.__proto__ 更规范。

注意点

  • 不要假设所有事件都是 MouseEventfocusFocusEventsubmitSubmitEvent(部分浏览器)。
  • 自定义事件用 CustomEvent,继承 Event,没有坐标属性。

实战场景

  • 类型收窄:if (event instanceof KeyboardEvent) { ... } 在 TS/复杂处理器中区分设备事件。
  • 理解框架合成事件:React 17+ 仍基于原生事件池化前的模型,底层仍是 DOM Event 子类。

【本章小结】DOM 对象原型链

知识点 核心要点
继承链方向 实例 → 具体类.prototype → 父类.prototype → ... → Object.prototypenull
HTMLButtonElement HTMLElementElementNodeEventTargetObject
MouseEvent UIEventEventObject
实用意义 理解为什么所有 DOM 元素都有 addEventListener(来自 EventTarget
查看方法 Object.getPrototypeOf(el);浏览器 DevTools Console 直接展开 __proto__

面试考点 :为什么 document.getElementById() 返回的元素可以直接调用 addEventListener?------因为 HTMLElement 原型链上继承了 EventTarget


六、HTMLCollection 与 NodeList

6.1 核心区别对比

特性 HTMLCollection NodeList
返回方法 getElementsByTagName(), getElementsByClassName(), children querySelectorAll(), getElementsByName(), childNodes
成员类型 仅元素节点 任意节点类型(元素、文本、注释等)
forEach ❌ 不支持 ✅ 支持
动态性 动态集合(DOM变化自动更新) 静态集合(querySelectorAll返回)
length ✅ 有 ✅ 有
item() ✅ 有 ✅ 有

6.2 动态与静态对比

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMLCollection 与 NodeList 对比</title>
    <style>
        .collection-demo {
            max-width: 800px;
            margin: 50px auto;
            padding: 30px;
            background: white;
            border-radius: 10px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
        }

        .demo-area {
            padding: 20px;
            background: #f5f5f5;
            border-radius: 10px;
            margin: 20px 0;
        }

        .demo-item {
            padding: 10px 15px;
            margin: 5px 0;
            background: white;
            border-radius: 5px;
            border-left: 3px solid #667eea;
        }

        .comparison-table {
            width: 100%;
            border-collapse: collapse;
            margin: 20px 0;
        }

        .comparison-table th,
        .comparison-table td {
            padding: 12px;
            text-align: left;
            border-bottom: 1px solid #e0e0e0;
        }

        .comparison-table th {
            background: #667eea;
            color: white;
        }

        .btn-group {
            display: flex;
            gap: 10px;
            margin: 20px 0;
        }

        .btn {
            padding: 10px 20px;
            border: none;
            border-radius: 5px;
            background: #667eea;
            color: white;
            cursor: pointer;
        }

        .btn:hover {
            background: #764ba2;
        }

        .output-panel {
            background: #2d2d2d;
            color: #00ff00;
            padding: 15px;
            border-radius: 5px;
            font-family: monospace;
            margin-top: 20px;
            min-height: 100px;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">HTMLCollection 与 NodeList 对比</h1>

    <div class="collection-demo">
        <table class="comparison-table">
            <thead>
                <tr>
                    <th>特性</th>
                    <th>HTMLCollection</th>
                    <th>NodeList</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td><strong>获取方式</strong></td>
                    <td>getElementsByTagName()<br>getElementsByClassName()<br>children</td>
                    <td>querySelectorAll()<br>getElementsByName()<br>childNodes</td>
                </tr>
                <tr>
                    <td><strong>成员类型</strong></td>
                    <td>仅元素节点</td>
                    <td>任意节点类型</td>
                </tr>
                <tr>
                    <td><strong>forEach</strong></td>
                    <td>❌ 不支持</td>
                    <td>✅ 支持</td>
                </tr>
                <tr>
                    <td><strong>动态性</strong></td>
                    <td>动态集合</td>
                    <td>静态集合</td>
                </tr>
            </tbody>
        </table>

        <div class="demo-area">
            <h3>测试区域</h3>
            <div id="testContainer">
                <div class="demo-item">项目 1</div>
                <div class="demo-item">项目 2</div>
                <div class="demo-item">项目 3</div>
            </div>

            <div class="btn-group">
                <button class="btn" id="testCollection">测试 HTMLCollection</button>
                <button class="btn" id="testList">测试 NodeList</button>
                <button class="btn" id="addItem">添加项目</button>
                <button class="btn" id="reset">重置</button>
            </div>
        </div>

        <div class="output-panel" id="outputPanel">
            点击上方按钮查看结果...
        </div>
    </div>

    <script>
        // ===== 核心逻辑(详见下方【代码注释】) =====
        (function() {
            const testContainer = document.getElementById('testContainer');
            const outputPanel = document.getElementById('outputPanel');

            function output(message) {
                outputPanel.textContent = message;
            }

            // 测试 HTMLCollection
            document.getElementById('testCollection').addEventListener('click', function() {
                const collection = testContainer.getElementsByClassName('demo-item');
                let message = 'HTMLCollection (动态集合):\n';
                message += `初始长度: ${collection.length}\n\n`;

                // 尝试 forEach
                message += '尝试 forEach:\n';
                try {
                    collection.forEach(item => message += item.textContent);
                } catch (e) {
                    message += `错误: ${e.message}\n`;
                    message += '\n解决方法: 使用 Array.from(collection).forEach() 或 for 循环\n\n';

                    // 使用 for 循环遍历
                    message += '使用 for 循环遍历:\n';
                    for (let i = 0; i < collection.length; i++) {
                        message += `${collection[i].textContent}\n`;
                    }
                }

                output(message);
            });

            // 测试 NodeList
            document.getElementById('testList').addEventListener('click', function() {
                const list = testContainer.querySelectorAll('.demo-item');
                let message = 'NodeList (静态集合):\n';
                message += `初始长度: ${list.length}\n\n`;

                // 尝试 forEach
                message += '尝试 forEach:\n';
                try {
                    const items = [];
                    list.forEach(item => items.push(item.textContent));
                    message += `成功! 内容:\n${items.join('\n')}\n\n`;
                } catch (e) {
                    message += `错误: ${e.message}`;
                }

                output(message);
            });

            // 添加项目
            document.getElementById('addItem').addEventListener('click', function() {
                const newItem = document.createElement('div');
                newItem.className = 'demo-item';
                newItem.textContent = `项目 ${testContainer.children.length + 1}`;
                testContainer.appendChild(newItem);
                output(`已添加项目,当前有 ${testContainer.children.length} 个项目`);
            });

            // 重置
            document.getElementById('reset').addEventListener('click', function() {
                testContainer.innerHTML = `
                    <div class="demo-item">项目 1</div>
                    <div class="demo-item">项目 2</div>
                    <div class="demo-item">项目 3</div>
                `;
                output('已重置');
            });
        })();
    </script>
</body>
</html>

【代码注释】

示例在验证什么

  • getElementsByClassName('item') 拿到 HTMLCollection ,删除第一个子节点后,不重新查询,再次打印长度------长度会自动减 1(动态)。
  • querySelectorAll('.item') 拿到 NodeList ,同样删除后,旧 NodeList 长度不变(静态快照)。

动态 vs 静态(核心)

类型 返回 API 是否随 DOM 变化 是否有 forEach
HTMLCollection getElementsByTagName/ClassName ✅ 实时反映 ❌(需 Array.from
NodeList(静态) querySelectorAll ❌ 快照
NodeList(动态) childNodes

经典 Bug:边遍历边删

javascript 复制代码
const items = document.getElementsByClassName('item'); // 动态
for (let i = 0; i < items.length; i++) {
  items[i].remove(); // 每删一个,length 变短,索引错位 → 漏删
}

正确做法 :倒序删、Array.from 固定副本、或委托。

与事件的关系

  • 课堂强调:即使用 HTMLCollection 拿到很多按钮,也应用父元素委托 ,而不是 forEach + addEventListener
  • 动态列表(聊天、Feed)增删节点时,委托监听器无需重新绑定。

易错点

  • 把 HTMLCollection 当数组直接 [...collection] 可以,但 collection.forEach 不存在。
  • querySelectorAll 结果是静态的,DOM 变了要重新查询

6.3 使用建议

javascript 复制代码
// ===== 核心逻辑(详见下方【代码注释】) =====
// ✅ 推荐:使用 querySelectorAll 获取静态集合
const items = document.querySelectorAll('.item');
items.forEach(item => {
    // 处理每个项目
});

// ✅ 推荐:需要动态更新时使用 HTMLCollection
const items = document.getElementsByClassName('item');
// 当 DOM 变化时,items 会自动更新

// ✅ 转换为数组进行数组操作
const itemsArray = Array.from(document.getElementsByClassName('item'));
itemsArray.forEach(item => {
    // 处理每个项目
});

【代码注释】

推荐写法解读

javascript 复制代码
const items = document.querySelectorAll('.item');
items.forEach(item => { /* 处理 */ });
  • querySelectorAll:一次查询,静态 NodeList,自带 forEach(现代浏览器)。
  • 适合:页面加载后不再增删的节点批量初始化(如图标绑定一次)。
javascript 复制代码
const items = document.getElementsByClassName('item');
Array.from(items).forEach(item => { /* 处理 */ });
  • Array.from 把 HTMLCollection 拷贝成数组,避免遍历过程中集合长度变化导致漏项。
  • 适合:必须沿用旧 API、或需要兼容极老环境时。

事件监听场景(更重要)

需求 推荐
列表项会动态增删 父元素 事件委托(一个监听器)
固定 10 个按钮各绑一次 querySelectorAll + forEach 可接受
上千节点各绑一次 ❌ 性能差,必须委托

与 Day15 / Day16 的衔接

  • Day15:学会绑事件;Day16:集合类型决定「怎么拿元素」,委托决定「怎么绑更省」。

面试一句话

  • HTMLCollection 动态、NodeList 多数静态;动态列表 + 事件 = 委托。

【本章小结】HTMLCollection 与 NodeList

对比维度 HTMLCollection NodeList
动/静 动态(DOM 变更自动更新) 大多数静态querySelectorAll 返回快照)
来源 getElementsByTagName / children querySelectorAll / childNodes
包含内容 仅元素节点 元素 + 文本 + 注释节点
遍历 需先转 Array.from() forEach(IE 不支持需转换)
索引访问 collection[0] list[0]

常见坑 :在 for 循环中直接遍历 HTMLCollection 同时修改 DOM,会导致集合长度变化造成死循环或跳项。务必先 Array.from() 再操作。



本篇小结

模块 关键 API / 概念
Event 对象 typetargetcurrentTargeteventPhase
传播控制 stopPropagationstopImmediatePropagation、捕获 capture: true
默认行为 preventDefault()(优先于 return false
事件委托 父级监听 + event.target 匹配
DOM 深入 EventTargetNodeElement 原型链
集合类型 HTMLCollection 动态 / NodeList 静态

学习建议

  1. 在回调里打印 event.targetevent.currentTarget,对比委托与捕获阶段差异。
  2. 与 Day15 对照:Day15 讲「有哪些事件」,上篇讲「事件对象与如何用事件控制页面行为」。
  3. 完成 TodoList 委托示例后,继续阅读 下篇 轮播图与工程专题实战。

上篇为 Day16 技术博客拆分版(第 1/2 篇)。完整原版见同目录 JavaScript_Event对象与轮播图实战.md

相关推荐
聆风吟º16 小时前
深入理解C语言 islower 函数详解:判断字符是否为小写字母
c语言·开发语言·库函数·字符处理·islower
Zhang~Ling16 小时前
C++继承机制详解上:概念、语法、作用域与转换规则
开发语言·c++
wengqidaifeng16 小时前
C++从菜鸟到强手:2.类和对象(中)—— 拷贝、赋值与运算符重载
开发语言·c++
0x000716 小时前
Git Bash 中无法启动 Claude Code ?
开发语言·git·bash
冴羽yayujs16 小时前
前端周报:Google I/O 发布 Agentic Web、TypeScript 6.0 正式版、npm 安全新策略
前端·javascript·前端开发·前端学习·前端周报
彦为君16 小时前
Spring定时任务开发指南(动态实现)
java·开发语言·后端·python·spring·wpf
不绝19116 小时前
AB包相关知识
开发语言·lua
Cobyte16 小时前
13.响应式系统演进:版本化动态依赖管理机制解析(Vue3.4)
前端·javascript·vue.js
lly20240616 小时前
WebPages 发布
开发语言