事件对象就是一个对象,每次触发事件都会产生一个事件对象,这个事件对象记录了事件发生时候的一些信息,比如点击的坐标,比如是否按下了 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>此时当我们点击这个元素的时候,就会输出这个事件对象

可以看到,这个事件对象里面有很多数据,这些数据都和本次点击事件都关。
如果是老版本的 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 就不一样了

显然,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>
在以前,我们要实现这个效果,需要循环给每一个 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 函数
读者可自行完成选项卡甚至轮播图的效果,思路完全类似。