Skip to content

事件对象就是一个对象,每次触发事件都会产生一个事件对象,这个事件对象记录了事件发生时候的一些信息,比如点击的坐标,比如是否按下了 shift 按键,比如这次事件的事件源是哪个元素,等等这些信息都会保存下来,放到一个事件对象里面,我们可以在事件处理函数中拿到这些信息。

在事件处理函数中,实际上会传入一个参数,这个参数其实就是事件对象,我们可以从这个事件对象里面拿到关于本次事件的信息

html
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        #container {
            width: 200px;
            height: 200px;
            background-color: red;
        }
    </style>
    <div id="container">
    </div>
    <script>
        var container = document.getElementById("container");
        function handleClick(e) {//事件处理函数的参数就是事件对象
            console.log(e);//输出这个事件对象
        }
        container.addEventListener("click", handleClick);
    </script>

此时当我们点击这个元素的时候,就会输出这个事件对象

![Pasted image 20240412042417](/notes-assets/02.JavaScript/7.DOM事件/assets/Pasted image 20240412042417.png)

可以看到,这个事件对象里面有很多数据,这些数据都和本次点击事件都关。

如果是老版本的 IE 浏览器,通过 window.event 的方式获取事件对象。

事件对象里有很多我们可以使用的数据,不同事件的事件对象中的属性都不太一样,比如键盘按下事件就必须保存键盘按下了哪一个按键,而鼠标点击事件则不需要保存这些信息。正是由于每一个事件的事件对象多多少少都不太一样,所以这里只介绍通用的,即每一个类型的事件的事件对象中都存在的属性。

target 属性用于获取事件源

js
        var container = document.getElementById("container");
        function handleClick(e) {
            console.log(e.target);//此时就会输出触发这个事件的事件源(DOM元素)
        }
        container.addEventListener("click", handleClick);

这里有读者可能会有疑惑,看下面的代码

js
        var container = document.getElementById("container");
        function handleClick(e) {
            console.log(e.target === this);//true
        }
        container.addEventListener("click", handleClick);

既然 this 已经可以指代当前 DOM 元素了,为什么还要有这个 target 属性呢?其实很简单,如果我们是通过点击 container 触发的这个事件,那么事件源本身就是他自己,所以这个表达式为 true。但是这个事件不一定是他被点击触发的,有可能是他的一个子元素被点击后被触发的,此时这个 this 依然是指自己,但是事件源是他的子元素。

html
    <div id="container">
        <button>click</button>
    </div>
    <script>
        var container = document.getElementById("container");
        function containerHandleClick(e) {
            console.log("container被点击了", e.target, this);
        }
        container.addEventListener("click", containerHandleClick);
    </script>

在这种情况下,点击 button 元素,此时 e.target 和 this 就不一样了

![Pasted image 20240412052521](/notes-assets/02.JavaScript/7.DOM事件/assets/Pasted image 20240412052521.png)

显然,this 始终指向的是绑定事件的元素,也就是 container,但是此时 target 属性指向的是事件源,也就是内部的 button 元素,因为这个事件虽然是绑定在 container 上的,但是却是由于点击 button 按钮被触发的,希望读者好好理解这一点。

在旧版 IE 浏览器中,事件源属性名为 srcElement。

这个属性有时候非常有用,我们看下面的例子

html
   <div id="container">
        <ul>
            <li>list1<button>删除</button></li>
            <li>list2<button>删除</button></li>
            <li>list3<button>删除</button></li>
            <li>list4<button>删除</button></li>
            <li>list5<button>删除</button></li>
            <li>list6<button>删除</button></li>
            <li>list7<button>删除</button></li>
            <li>list8<button>删除</button></li>
            <li>list9<button>删除</button></li>
            <li>list10<button>删除</button></li>
        </ul>
    </div>
    <script>
        /**
         * 给指定ID的容器添加点击事件监听器,当点击事件发生在按钮元素上时,移除该按钮的父元素。
         * 
         * @param {string} 'container' - 容器的ID,用于获取对应的HTML元素。
         */
        var container = document.getElementById('container');
        container.addEventListener("click", function (event) {
            // 判断事件的目标元素是否为按钮
            if (event.target.nodeName == "BUTTON")
                // 如果是按钮,移除按钮的父元素
                event.target.parentElement.remove();
        });
    </script>

![recording 3](/notes-assets/02.JavaScript/7.DOM事件/assets/recording 3.gif)

在以前,我们要实现这个效果,需要循环给每一个 button 绑定点击事件,但是当我们有了 target 属性之后,我可以只在容器上绑定事件,因为点击按钮的时候是会触发容器上的点击事件的,而容器上的点击事件又可以获取到事件源,也就是点击了哪个 button,然后再将其父元素删掉,就实现了删除效果。我们可以看到,这个删除元素的操作,本来应该绑定在按钮上,当点击按钮的时候删除其父元素,但是我们绑定在容器上也可以实习同样的效果,这种将自己的事件交给别人做的行为,我们称为事件委托。这样写有一个最大的好处,如果我要给一百万个列表绑定删除事件,如果不用事件委托,就要写一个循环一百万次的循环,每次循环注册一个删除函数,但是如果我们有了事件委托,就可以只在容器上绑定一个事件即可,不管是写法还是性能都会更好。

这里还有一个很类似的属性,currentTarget,这个属性等价于 this,注意区分。

type 属性用于得到事件的类型,如下所示

js
        var container = document.getElementById('container');
        container.addEventListener("click", function (event) {
            console.log(event.type);
        });

当点击 container 的时候,就会输出事件的类型。

这个属性看似毫无卵用,因为在绑定事件的时候我们其实已经明确指出了要绑定的事件类型,但在部分场景下依旧有用处,例如我鼠标移入的事件和鼠标点击的事件基本差不多,只有一些细微的差别,我就不需要再去写两个函数,只需要写一个函数即可,在内部使用 type 属性判断一下单独处理即可。

接下来想一个问题,a 元素,作为一个元素,他也可以有点击事件对吧,但是如果当我点击 a 元素的时候,它有一个很神奇的功能,会自动跳转网页,哪怕不写超链接,也会自动刷新一次,这是我们很不喜欢的,我们可以使用事件对象中 preventDefault() 方法来阻止这个默认行为

html
    <a href="https://www.baidu.com">a元素</a>
    <script>
        var a = document.querySelector("a[href='https://www.baidu.com']");
        a.addEventListener("click", function (e) {
            e.preventDefault();
        });
    </script>

当我们在事件处理函数中调用了 preventDefault 之后,浏览器就不会再触发这个默认行为,包括像表单内的 button 按钮,点击之后会自动跳转到某个页面,这些其实都是浏览器的默认行为,当我们发现我们的事件和浏览器的默认行为发生冲突的时候,我们都可以使用这种方式来取消掉浏览器默认行为。

如果使用的是 dom.onclick = function(){} 这种方式绑定的事件,可以在最后返回 false 来阻止浏览器默认行为。

如果将 a 元素的 href 属性设置为 javascript:void(0),也可以阻止其跳转的默认行为。这种方式也很常见。

事件冒泡在一些时候很有用,特别是事件委托的应用,但是有时候恰恰相反,比如我就不希望父元素监听到子元素的点击事件,就需要调用 stopPropagation() 函数阻止事件的传播。

html
    <div id="Father">
        <div id="Son"></div>
    </div>
    <script>
        var Father = document.getElementById("Father");
        var Son = document.getElementById("Son");
        Son.addEventListener("click", function (event) {
            console.log("Son被点击了", "冒泡阶段");
            event.stopPropagation();//阻止事件传播,此时事件不会再传播
        });
        Father.addEventListener("click", function () {
            console.log("Father被点击了", "冒泡阶段");//阻止事件传播后,该事件不会执行
        });
        Son.addEventListener("click", function () {
            console.log("Son被点击了", "捕获阶段");
        }, true);
        Father.addEventListener("click", function () {
            console.log("Father被点击了", "捕获阶段");
        }, true);
    </script>

同样,也可以在捕获阶段就直接阻止事件传播

html
    <div id="Father">
        <div id="Son"></div>
    </div>
    <script>
        var Father = document.getElementById("Father");
        var Son = document.getElementById("Son");
        Son.addEventListener("click", function () {
            console.log("Son被点击了", "冒泡阶段");
            
        });
        Father.addEventListener("click", function () {
            console.log("Father被点击了", "冒泡阶段");
        });
        Son.addEventListener("click", function () {
            console.log("Son被点击了", "捕获阶段");
        }, true);
        Father.addEventListener("click", function (event) {
            console.log("Father被点击了", "捕获阶段");
            event.stopPropagation();//阻止事件传播,此时事件不会再传播
        }, true);
    </script>

上面这样写了之后,当执行了 Father 捕获阶段的函数之后,由于阻止了事件的传播,所以后面应该执行的所有函数都不会再正常执行,到此结束。

还有一个不太常用的属性,但也有可能会有点用,所以这里也介绍一下,即 eventPhase,可以得到目前事件发生的阶段,一个事件可能发生在事件捕获,事件冒泡,事件目标,三个阶段。1 表示捕获阶段,2 表示目标阶段,3 表示冒泡阶段。

以下是一些练习:

html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <p>
        <input type="text" id="txt">
        <button id="btnAdd">添加</button>
    </p>
    <ul>
        <li>
            <span>列表1</span>
            <button>删除</button>
        </li>
        <li>
            <span>列表2</span>
            <button>删除</button>
        </li>
        <li>
            <span>列表3</span>
            <button>删除</button>
        </li>
    </ul>
           <script>
            // 获取页面上的按钮和输入框元素以及第一个无序列表元素
            var btnAdd = document.getElementById('btnAdd');
            var input = document.getElementById('txt');
            var ul = document.getElementsByTagName('ul')[0];

            // 给添加按钮添加点击事件监听器
            btnAdd.addEventListener("click", function () {
                // 获取输入框中的值
                var value = input.value;
                // 如果输入值为空,则清空输入框并提示用户输入内容
                if (!(value.trim())) {
                    input.value = "";
                    alert('请输入内容');
                    return;
                }
                // 清空输入框,准备下一次输入
                input.value = "";
                // 创建新的列表项模板,包含用户输入的内容和一个删除按钮
                var template = `<li>
                <span>${value}</span>
                <button>删除</button>
            </li>`;
                // 将新的列表项模板添加到无序列表中
                ul.innerHTML += template;
            });
            // 给无序列表添加点击事件监听器,用于处理删除操作
            ul.addEventListener("click", function (e) {
                // 如果点击的目标元素不是按钮,则不进行任何操作
                if (e.target.nodeName !== "BUTTON") {
                    return;
                }
                // 如果点击的目标元素是按钮,则移除其所在的列表项
                e.target.parentElement.remove();
            });
        </script>
        
</body>

</html>
html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        .container {
            width: 300px;
            border: 1px solid;

            text-align: center;
        }

        .title {
            height: 40px;
            line-height: 40px;
            font-size: 25px;
            background-color: #f40;
            color: white;
            cursor: pointer;
        }

        .list li {
            height: 30px;
            line-height: 30px;
            font-size: 20px;
        }

        .hidden {
            display: none;
        }
    </style>
</head>

<body>
    <div class="container">

    </div>

    <script>
        var data = [
            {
                title: "项目1",
                items: [
                    "test1",
                    "test2",
                    "test3",
                    "test4",
                    "test5",
                    "test6",
                ],
            },
            {
                title: "项目2",
                items: [
                    "test1",
                    "test2",
                    "test3",
                    "test4",
                    "test5",
                    "test6",
                ],
            }, {
                title: "项目3",
                items: [
                    "test1",
                    "test2",
                    "test3",
                    "test4",
                    "test5",
                    "test6",
                ],
            }
        ];
        var container = document.querySelector(".container");
        // 这里提供两种思路
        //思路1
        // 遍历data数组中的每个元素,创建并添加相应的DOM元素到容器中
        data.forEach(function (item) {
            // 创建一个包含所有项目的div容器
            var thisItem = document.createElement("div");
            thisItem.classList.add("item");

            // 创建一个显示项目标题的div
            var title = document.createElement("div");
            title.classList.add("title");
            title.innerHTML = item.title;
            // 为标题添加点击事件,切换子元素的可见性
            title.addEventListener("click", function () {
                this.nextElementSibling.classList.toggle("hidden");
            });
            // 创建一个用于包含子项目的ul元素
            var ul = document.createElement("ul");
            ul.classList.add("list");

            // 遍历当前item的子项目,创建并添加到ul中
            item.items.forEach(function (subItem) {
                var li = document.createElement("li");
                li.innerHTML = subItem;
                ul.append(li);
            });
            // 将标题和子项目列表添加到主项目容器中
            thisItem.append(title, ul);
            // 将主项目容器添加到外部容器
            container.append(thisItem);
        });

        //思路2
        /**
         * 根据提供的数据,生成一个字符串,该字符串包含一个由数据动态生成的HTML结构。
         * 数据的每个元素被转换成一个带有标题和项目列表的<div>,
         * 每个项目列表中的每个项被转换成一个<li>。
         * @param {Array} data - 包含数据项的数组,每个数据项应包含一个title和一个items数组。
         * @returns {String} 生成的HTML字符串。
         */
        var str = data.reduce(function (str, item) {
            // 对当前item的items数组进行处理,生成包含所有<li>元素的字符串
            var lis = item.items.reduce(function (str, item) {
                return str += `<li>${item}</li>` // 为每个item生成一个<li>元素并追加到字符串中
            }, "");
            // 为当前item生成包含标题和项目列表的<div>元素,并追加到之前的字符串中
            return str += `<div class="item">
                <div class="title">${item.title}</div>
                <ul class="list">
                    ${lis}
                </ul>
            </div>`;
        }, "");
        // 将生成的HTML字符串设置为container元素的innerHTML
        container.innerHTML = str;

        // 为container元素添加点击事件监听器,用于展开和收起项目列表
        container.addEventListener("click", function (event) {
            // 如果点击的目标元素没有"class"为"title",则不执行任何操作
            if (!event.target.classList.contains("title"))
                return;
            var elem = event.target;
            // 点击标题元素时,切换其下一个兄弟元素(即项目列表)的"hidden"类状态,实现展开/收起效果
            elem.nextElementSibling.classList.toggle("hidden");
        });
    </script>

</body>

</html>

此作业希望读者掌握第一种方法,第二种方式读者也需要理解,尤其是复习数组的 reduce 函数

读者可自行完成选项卡甚至轮播图的效果,思路完全类似。

以笔记为镜,可以知得失