六、DOM
了解 DOM 的结构并掌握其基本的操作,体验 DOM 的在开发中的作用
知道 ECMAScript 与 JavaScript 的关系,Web APIs 是浏览器扩展的功能。
- 知道 ECMAScript 与 JavaScript 的关系
- 了解 DOM 的相关概念及DOM 的本质是一个对象
- 掌握查找节点的基本方法
- 掌握节点属性和文本的操作
- 能够使用间歇函数创建定时任务
严格意义上讲,我们在 JavaScript 阶段学习的知识绝大部分属于 ECMAScript 的知识体系,ECMAScript 简称 ES 它提供了一套语言标准规范,如变量、数据类型、表达式、语句、函数等语法规则都是由 ECMAScript 规定的。浏览器将 ECMAScript 大部分的规范加以实现,并且在此基础上又扩展一些实用的功能,这些被扩展出来的内容我们称为 Web APIs。
ECMAScript 运行在浏览器中然后再结合 Web APIs 才是真正的 JavaScript,Web APIs 的核心是 DOM 和 BOM。
扩展阅读:ECMAScript 规范在不断的更新中,存在多个不同的版本,早期的版本号采用数字顺序编号如 ECMAScript3、ECMAScript5,后来由于更新速度较快便采用年份做为版本号,如 ECMAScript2017、ECMAScript2018 这种格式,ECMAScript6 是 2015 年发布的,常叫做 EMCAScript2015。
关于 JavaScript 历史的扩展阅读。
知道 DOM 相关的概念,建立对 DOM 的初步认识,学习 DOM 的基本操作,体会 DOM 的作用
DOM(Document Object Model)是将整个 HTML 文档的每一个标签元素视为一个对象,这个对象下包含了许多的属性和方法,通过操作这些属性或者调用这些方法实现对 HTML 的动态更新,为实现网页特效以及用户交互提供技术支撑。
简言之 DOM 是用来动态修改 HTML 的,其目的是开发网页特效及用户交互。
观察一个小例子:
上述的例子中当用户分分别点击【开始】或【结束】按钮后,通过右侧调试窗口可以观察到 html 标签的内容在不断的发生改变,这便是通过 DOM 实现的。
概念
DOM 树
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>标题</title>
</head>
<body>
文本
<a href="">链接名</a>
<div id="" class="">文本</div>
</body>
</html>
如下图所示,将 HTML 文档以树状结构直观的表现出来,我们称之为文档树或 DOM 树,文档树直观的体现了标签与标签之间的关系。
DOM 节点
节点是文档树的组成部分,每一个节点都是一个 DOM 对象,主要分为元素节点、属性节点、文本节点等。
- 【元素节点】其实就是 HTML 标签,如上图中
head、div、body等都属于元素节点。 - 【属性节点】是指 HTML 标签中的属性,如上图中
a标签的href属性、div标签的class属性。 - 【文本节点】是指 HTML 标签的文字内容,如
title标签中的文字。 - 【根节点】特指
html标签。 - 其它...
document
document 是 JavaScript 内置的专门用于 DOM 的对象,该对象包含了若干的属性和方法,document 是学习 DOM 的核心。
<script>
// document 是内置的对象
// console.log(typeof document);
// 1. 通过 document 获取根节点
console.log(document.documentElement); // 对应 html 标签
// 2. 通过 document 节取 body 节点
console.log(document.body); // 对应 body 标签
// 3. 通过 document.write 方法向网页输出内容
document.write('Hello World!'); // Hello World!
</script>
上述列举了 document 对象的部分属性和方法,我们先对 document 有一个整体的认识。
DOM 的增查改删
查询
获取元素节点
getElementById():通过 id 属性获取一个元素节点对象getElementsByClassName():通过 class 属性获取一个元素节点对象getElementsByTagName():通过标签名获取一组元素节点对象getElementsByTagName("body")[0]:获取 body 标签,等价于:document.bodygetElementsByTagName("*"):获取页面所有元素querySelector(): 满足条件的第一个元素querySelectorAll(): 满足条件的元素集合 返回伪数组
<body>
<h3>查找元素类型节点</h3>
<p>从整个 DOM 树中查找 DOM 节点是学习 DOM 的第一个步骤。</p>
<ul>
<li class="b1">元素1</li>
<li class="b2">元素2</li>
<li id="a1">元素3</li>
<li id="a2">元素4</li>
</ul>
<script>
const a1 = document.getElementById('a1');
console.log(a1) // 里面放了a1节点的很多属性,如下左图所示,后面则不再写打印语句,直接看结果
document.getElementsByClassName('b1') // HTMLCollection [li.b1]
document.getElementsByTagName('li') // 如下右图所示
document.getElementsByTagName('body')[0] // 获取 body 标签
document.getElementsByTagName("*") // HTMLCollection(13) [html, head, meta, title, body, h3, p, ul#AA, li.b1, li.b2, li#a1, li#a2, script, AA: ul#AA, a1: li#a1, a2: li#a2]
document.querySelector('#a1') // <li id="a1">元素3</li>
document.querySelector('li') // li.b1(里面包含了跟下图一样很多属性)
document.querySelectorAll('li')// NodeList(4) [li.b1, li.b2, li#a1, li#a2]
</script>
</body>
操作元素内容
通过修改 DOM 的文本内容,动态改变网页的内容。
innerText: 将文本内容添加/更新到任意标签位置,文本中包含的标签不会被解析。innerHTML:将文本内容添加/更新到任意标签位置,文本中包含的标签会被解析。
<ul>
<li class="b1">AAA</li>
<li class="b2">元素2</li>
<li id="a1">元素3</li>
<li id="a2">元素4</li>
</ul>
<script>
const ul = document.querySelector('ul');
console.log(ul.innerText); // 如下图所示(上)
console.log(ul.innerHTML); // 如下图所示(下)
</script>
总结:如果文本内容中包含 html 标签时推荐使用 innerHTML,否则建议使用 innerText 属性。
获取元素子节点
getElementsByTagName():返回当前节点的指定标签名后代节点childNodes:表示当前节点的所有子节点,如果子节点的话可以用:当前节点.childrenfirstChild:表示当前节点的第一个子节点lastChild:表示当前节点的最后一个子节点nextElementSibling:表示当前节点的下一个兄弟节点
<ul id="AA">
<li class="b1">元素1</li>
<li class="b2">元素2</li>
<li id="a1">元素3</li>
<li id="a2">元素4</li>
</ul>
<script>
let b = document.getElementById("AA")
let c = b.getElementsByTagName("li")
console.log(c) // HTMLCollection(4)[li.b1, li.b2, li#a1, li#a2, a1: li#a1, a2: li#a2]
const a = document.querySelector('ul')
console.log(a.children) // HTMLCollection(4)[li.b1, li.b2, li#a1, li#a2, a1: li#a1, a2: li#a2]
console.log(a.children.length) // 4
console.log(a.childNodes) // NodeList(9)[text, li.b1, text, li.b2, text, li#a1, text, li#a2, text]
console.log(a.childNodes[3]) // <li class="b2">元素2</li>
console.log(a.firstChild) // #text
console.log(a.lastChild) // #text
console.log(a.lastChild.previousSibling.innerText) // 元素4
let a1 = document.querySelector('#a1')
console.log(a1.nextElementSibling.innerText) // 元素4
</script>
增加
createElement:创建元素节点对象createTextnode:创建文本节点对象appendChild:把新的子节点添加到指定节点
<ul id="AA">
<li class="b1">元素1</li>
<li class="b2">元素2</li>
<li id="a1">元素3</li>
<li id="a2">元素4</li>
</ul>
<script>
let li = document.createElement("li")
let gz = document.createTextNode("广州")
li.appendChild(gz) // 先创建新节点“广州”
let AA = document.getElementById("AA")
AA.appendChild(li) // 效果如下
let AA = document.getElementById("AA")
let li = document.createElement("li")
li.innerHTML = "广州" // 效果同上一致,但前面的方法只会局部修改,innerHTML会修改整体
AA.appendChild(li)
</script>
父.insertBefore(新,旧):把新的节点插入到指定节点
<ul id="AA">
<li class="b1">元素1</li>
<li class="b2">元素2</li>
<li id="a1">元素3</li>
<li id="a2">元素4</li>
</ul>
<script>
let li = document.createElement("li")
let gz = document.createTextNode("广州")
li.appendChild(gz)
let a1 = document.getElementById("a1")
let AA = document.getElementById("AA")
// 将 “广州” 插到 “a1” 前面
AA.insertBefore(li,a1) // 效果如下,注意这里的a1参数如果用的getElementsByClassName则获取不到
</script>
- cloneNode(true):复制,参数为true时表示深克隆,false时表示浅克隆
<ul id="AA">
<li class="b1">元素1</li>
<li class="b2">元素2</li>
<li id="a1">元素3</li>
<li id="a2">元素4</li>
</ul>
<script>
const ul = document.querySelector('#AA')
const copy = ul.children[3].cloneNode(true)
ul.appendChild(copy)
</script>
修改
父.replaceChild(新,旧):将旧节点替换为新节点
<ul id="AA">
<li class="b1">元素1</li>
<li class="b2">元素2</li>
<li id="a1">元素3</li>
<li id="a2">元素4</li>
</ul>
<script>
let li = document.createElement("li")
let ss = document.createTextNode("深圳")
li.appendChild(ss)
let AA = document.getElementById("AA")
let a1 = document.getElementById("a1")
AA.replaceChild(li,a1) // 如下图所示
</script>
删除
父.removeChild(子):删除子节点,但这种得找父节点,不方便,下面这种比较常用
子.parentNode.removeChild(子)
<ul id="AA">
<li class="b1">元素1</li>
<li class="b2">元素2</li>
<li id="a1">元素3</li>
<li id="a2">元素4</li>
</ul>
<script>
let a1 = document.getElementById("a1")
a1.parentNode.removeChild(a1)
</script>
三种动态创建元素的区别
document.write 是直接将内容写入页面的内容流,但是文档流执行完毕,则它会导致页面全部重绘
innerHTML
是将内容写入某个 DOM 节点,不会导致页面全部重绘
创建多个元素效率更高(不要拼按字符串,采取数组形式拼接)结构稍微复杂
createElement()创建多个元素效率稍低一点点,但是结构更清晰
DOM 对 CSS 的增删改查
元素.属性 = 新值:直接能过属性名修改,最简洁的语法
<script>
// 1. 获取 img 对应的 DOM 元素
const pic = document.querySelector('.pic')
// 2. 修改属性
pic.src = './images/lion.webp'
pic.width = 400;
pic.alt = '图片不见了...'
</script>
查询
- 方式一:
元素.style.样式名 - 方式二:
getComputedStyle(元素).样式名(该方法不支持IE8以下浏览器) - 方法三:
元素.currentStyle.样式名(仅支持IE8浏览器)
<body>
<!-- 内联样式 -->
<div id="box" style="color: red;">随便添加一些文字</div>
<script>
let box = document.getElementById("box")
/*通过 style 属性设置和读取都是内联样式,但无法读取内部样式、外部样式*/
console.log(box.style.color); // red
console.log(getComputedStyle(box).color) // red rgb(255,165,0)
console.log(box.currentStyle.color) // 报错,仅IE8支持
</script>
</body>
<style>
#box {
color: green; // 内部样式
}
</style>
</html>
增加/修改
元素.style.样式名 = 新样式值:增加/修改元素样式。
通过元素节点获得的 style 属性本身的数据类型也是对象,如 box.style.color、box.style.width 分别用来获取元素节点 CSS 样式的 color 和 width 的值。
<body>
<! --这里的内联样式有没有都不影响,只是为了方便理解 -->
<div class="box" style="color: red;width: 150px;">随便一些文本内容</div>
<script>
// 获取 DOM 节点
const box = document.querySelector('.intro')
box.style.border = '1px solid #000' // 增加样式
box.style.color = 'orange' // 修改样式
// css 属性的 - 连接符与 JavaScript 的 减运算符冲突,所以要改成驼峰命名法
box.style.backgroundColor = 'yellow' // 效果如下
</script>
</body>
</html>
DOM 对 类 的增删改查
- 增加:
元素.className += "类名" - 覆盖:
元素.className = "类名" - 删除:元素.className.replace("类名",“”)
<body>
<div id="xx">xxxxxxxxx</div>
<script>
let box = document.querySelector('div')
box.className += "one" // 增加
box.className = "two" // 覆盖
box.className.replace(one, "") // 删除
</script>
</body>
<style>
#xx {
background-color: pink;
}
.one {
color: yellow;
}
.two {
color: green;
}
</style>
为了解决className 容易覆盖以前的类名,也可以通过classList方式追加和删除类名
元素.classList.add: 添加类元素.classList.remove:删除类- 元素.classList.toggle:替换类
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div {
width: 200px;
height: 200px;
background-color: pink;
}
.one {
width: 300px;
height: 300px;
background-color: hotpink;
margin-left: 100px;
}
</style>
</head>
<body>
<div class="one"></div>
<script>
// 1.获取元素
// let box = document.querySelector('css选择器')
let box = document.querySelector('div')
// add是个方法 添加 追加
// box.classList.add('one')
// remove() 移除 类
// box.classList.remove('one')
// 切换类
box.classList.toggle('one')
</script>
</body>
</html>
自定义属性
标准属性: 标签天生自带的属性 比如class id title等, 可以直接使用点语法操作比如: disabled、checked、selected
自定义属性:
在html5中推出来了专门的data-自定义属性
在标签上一律以data-开头
在DOM对象上一律以dataset对象方式获取
容易区分出哪个是自定义属性,但兼容性较差,仅支持ie11以上的浏览器
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div data-id="1"> 自定义属性 </div>
<script>
// 1. 获取元素
let div = document.querySelector('div')
// 2. 获取自定义属性值
console.log(div.dataset.id)
</script>
</body>
</html>
操作自定义属性:getAttribute()、setAttribute()
注意:这种方式不容易区别操作的属性是否为自定义属性,但兼容性较好
<body>
<div id="demo" data-index="1" class="nav"></div>
<script>
let div = document.querySelector('div')
// 1.获取元素的属性值
// (1) element.属性
console.log(div.id) // demo
// (2) element.getAttribute(属性') get得到获取 attribute 属性的意思 我们程序员自己添加的属性,我们称为自定义属性 index
console.log(div.getAttribute('id')) // demo
console.log(div.getAttribute('index')) // 1
// 2. 修改元素属性值
// (1) element.属性= '值'
div.id = 'test'
div.className = 'navs'
// (2) element.setAttribute('属性’,这值'); 主要针对于自定义属性
div.setAttribute('data-index', 2)
div.setAttribute('class', 'footer') // class 特殊 这里面写的就是class 不是 cllassName
// 3 移除属性 removeAttribute(属性)
div.removeAttribute('data-index') // <div id="test" class="footer">
</script>
</body>
间歇函数
setInterval 是 JavaScript 中内置的函数,它的作用是间隔固定的时间自动重复执行另一个函数,也叫定时器函数。
<script>
// 1. 定义一个普通函数
function repeat() {
console.log('不知疲倦的执行下去....')
}
// 2. 使用 setInterval 调用 repeat 函数
// 间隔 1000 毫秒,重复调用 repeat
setInterval(repeat, 1000)
</script>
七、事件
学习会为 DOM 注册事件,实现简单可交互的网页特交
事件是编程语言中的术语,它是用来描述程序的行为或状态的,一旦行为或状态发生改变,便立即调用一个函数。
例如:用户使用【鼠标点击】网页中的一个按钮、用户使用【鼠标拖拽】网页中的一张图片
事件监听
结合 DOM 使用事件时,需要为 DOM 对象添加事件监听,等待事件发生(触发)时,便立即调用一个函数。
addEventListener 是 DOM 对象专门用来添加事件监听的方法,它的两个参数分别为【事件类型】和【事件回调】。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件监听</title>
</head>
<body>
<h3>事件监听</h3>
<p id="text">为 DOM 元素添加事件监听,等待事件发生,便立即执行一个函数。</p>
<button id="btn">点击改变文字颜色</button>
<script>
// 1. 获取 button 对应的 DOM 对象
const btn = document.querySelector('#btn')
// 2. 添加事件监听
btn.addEventListener('click', function () {
console.log('等待事件被触发...')
// 改变 p 标签的文字颜色
let text = document.getElementById('text')
text.style.color = 'red'
})
// 3. 只要用户点击了按钮,事件便触发了!!!
</script>
</body>
</html>
完成事件监听分成3个步骤:
- 获取 DOM 元素
- 通过
addEventListener方法为 DOM 节点添加事件监听 - 等待事件触发,如用户点击了某个按钮时便会触发
click事件类型 - 事件触发后,相对应的回调函数会被执行
大白话描述:所谓的事件无非就是找个机会(事件触发)调用一个函数(回调函数)。
事件类型
click 译成中文是【点击】的意思,它的含义是监听(等着)用户鼠标的单击操作,除了【单击】还有【双击】dblclick
<script>
// 双击事件类型
btn.addEventListener('dblclick', function () {
console.log('等待事件被触发...');
// 改变 p 标签的文字颜色
const text = document.querySelector('.text')
text.style.color = 'red'
})
// 只要用户双击击了按钮,事件便触发了!!!
</script>
结论:【事件类型】决定了事件被触发的方式,如 click 代表鼠标单击,dblclick 代表鼠标双击。
将众多的事件类型分类可分为:鼠标事件、键盘事件、表单事件、焦点事件等,我们逐一展开学习。
鼠标事件
鼠标事件是指跟鼠标操作相关的事件,如单击、双击、移动等。
mouseenter 监听鼠标是否移入 DOM 元素
<body>
<h3>鼠标事件</h3>
<p>监听与鼠标相关的操作</p>
<hr>
<div class="box"></div>
<script>
// 需要事件监听的 DOM 元素
const box = document.querySelector('.box');
// 监听鼠标是移入当前 DOM 元素
box.addEventListener('mouseenter', function () {
// 修改文本内容
this.innerText = '鼠标移入了...';
// 修改光标的风格
this.style.cursor = 'move';
})
// 禁用鼠标右键菜单
document.addEventListener('contextmenu', function(e){
e.preventDefault()
})
// 禁用选中文字
document.addEventListener('selectstart', function(e){
e.preventDefault()
})
</script>
</body>
mouseleave 监听鼠标是否移出 DOM 元素
<body>
<h3>鼠标事件</h3>
<p>监听与鼠标相关的操作</p>
<hr>
<div class="box"></div>
<script>
// 需要事件监听的 DOM 元素
const box = document.querySelector('.box');
// 监听鼠标是移出当前 DOM 元素
box.addEventListener('mouseleave', function () {
// 修改文本内容
this.innerText = '鼠标移出了...';
})
</script>
</body>
键盘事件
keydown 键盘按下触发 keyup 键盘抬起触发
焦点事件
focus 获得焦点
blur 失去焦点
文本框输入事件
input
事件处理程序
addEventListener 的第2个参数是函数,这个函数会在事件被触发时立即被调用,在这个函数中可以编写任意逻辑的代码,如改变 DOM 文本颜色、文本内容等。
<script>
// 双击事件类型
btn.addEventListener('dblclick', function () {
console.log('等待事件被触发...')
const text = document.querySelector('.text')
// 改变 p 标签的文字颜色
text.style.color = 'red'
// 改变 p 标签的文本内容
text.style.fontSize = '20px'
})
</script>
结论:【事件处理程序】决定了事件触发后应该执行的逻辑。
事件对象
任意事件类型被触发时与事件相关的信息会被以对象的形式记录下来,我们称这个对象为事件对象。
<body>
<h3>事件对象</h3>
<p>任意事件类型被触发时与事件相关的信息会被以对象的形式记录下来,我们称这个对象为事件对象。</p>
<hr>
<div class="box"></div>
<script>
// 获取 .box 元素
const box = document.querySelector('.box')
// 添加事件监听
box.addEventListener('click', function (e) {
console.log('任意事件类型被触发后,相关信息会以对象形式被记录下来...');
// 事件回调函数的第1个参数即所谓的事件对象
console.log(e)
})
</script>
</body>
事件回调函数的【第1个参数】即所谓的事件对象,通常习惯性的将这个对数命名为 event、ev 、ev 。
接下来简单看一下事件对象中包含了哪些有用的信息:
ev.type当前事件的类型ev.clientX/Y光标相对浏览器窗口的位置ev.offsetX/Y光标相于当前 DOM 元素的位置
注:在事件回调函数内部通过 window.event 同样可以获取事件对象。
环境对象
能够分析判断函数运行在不同环境中 this 所指代的对象。
环境对象指的是函数内部特殊的变量 this ,它代表着当前函数运行时所处的环境。
<script>
// 声明函数
function sayHi() {
// this 是一个变量
console.log(this);
}
// 声明一个对象
let user = {
name: '张三',
sayHi: sayHi // 此处把 sayHi 函数,赋值给 sayHi 属性
}
let person = {
name: '李四',
sayHi: sayHi
}
// 直接调用
sayHi() // window
window.sayHi() // window
// 做为对象方法调用
user.sayHi()// user
person.sayHi()// person
</script>
结论:
this本质上是一个变量,数据类型为对象- 函数的调用方式不同
this变量的值也不同 - 【谁调用
this就是谁】是判断this值的粗略规则 - 函数直接调用时实际上
window.sayHi()所以this的值为window
回调函数
如果将函数 A 做为参数传递给函数 B 时,我们称函数 A 为回调函数。
<script>
// 声明 foo 函数
function foo(arg) {
console.log(arg);
}
// 普通的值做为参数
foo(10);
foo('hello world!');
foo(['html', 'css', 'javascript']);
function bar() {
console.log('函数也能当参数...');
}
// 函数也可以做为参数!!!!
foo(bar);
</script>
函数 bar 做参数传给了 foo 函数,bar 就是所谓的回调函数了!!!
我们回顾一下间歇函数 setInterval
<script>
function fn() {
console.log('我是回调函数...');
}
// 调用定时器
setInterval(fn, 1000);
</script>
fn 函数做为参数传给了 setInterval ,这便是回调函数的实际应用了,结合刚刚学习的函数表达式上述代码还有另一种更常见写法。
<script>
// 调用定时器,匿名函数做为参数
setInterval(function () {
console.log('我是回调函数...');
}, 1000);
</script>
结论:
- 回调函数本质还是函数,只不过把它当成参数使用
- 使用匿名函数做为回调函数比较常见
事件流
事件流是对事件执行过程的描述,了解事件的执行过程有助于加深对事件的理解,提升开发实践中对事件运用的灵活度。
如上图所示,任意事件被触发时总会经历两个阶段:【捕获阶段】和【冒泡阶段】。
简言之,捕获阶段是【从父到子】的传导过程,冒泡阶段是【从子向父】的传导过程。
捕获和冒泡
了解了什么是事件流之后,我们来看事件流是如何影响事件执行的:
<body>
<h3>事件流</h3>
<p>事件流是事件在执行时的底层机制,主要体现在父子盒子之间事件的执行上。</p>
<div class="outer">
<div class="inner">
<div class="child"></div>
</div>
</div>
<script>
// 获取嵌套的3个节点
const outer = document.querySelector('.outer');
const inner = document.querySelector('.inner');
const child = document.querySelector('.child');
// html 元素添加事件
document.documentElement.addEventListener('click', function () {
console.log('html...')
})
// body 元素添加事件
document.body.addEventListener('click', function () {
console.log('body...')
})
// 外层的盒子添加事件
outer.addEventListener('click', function () {
console.log('outer...')
})
// 中间的盒子添加事件
outer.addEventListener('click', function () {
console.log('inner...')
})
// 内层的盒子添加事件
outer.addEventListener('click', function () {
console.log('child...')
})
</script>
</body>
执行上述代码后发现,当单击事件触发时,其祖先元素的单击事件也【相继触发】,这是为什么呢?
结合事件流的特征,我们知道当某个元素的事件被触发时,事件总是会先经过其祖先才能到达当前元素,然后再由当前元素向祖先传递,事件在流动的过程中遇到相同的事件便会被触发。
再来关注一个细节就是事件相继触发的【执行顺序】,事件的执行顺序是可控制的,即可以在捕获阶段被执行,也可以在冒泡阶段被执行。
如果事件是在冒泡阶段执行的,我们称为冒泡模式,它会先执行子盒子事件再去执行父盒子事件,默认是冒泡模式。
如果事件是在捕获阶段执行的,我们称为捕获模式,它会先执行父盒子事件再去执行子盒子事件。
<body>
<h3>事件流</h3>
<p>事件流是事件在执行时的底层机制,主要体现在父子盒子之间事件的执行上。</p>
<div class="outer">
<div class="inner"></div>
</div>
<script>
// 获取嵌套的3个节点
const outer = document.querySelector('.outer')
const inner = document.querySelector('.inner')
// 外层的盒子
outer.addEventListener('click', function () {
console.log('outer...')
}, true) // true 表示在捕获阶段执行事件
// 中间的盒子
outer.addEventListener('click', function () {
console.log('inner...')
}, true)
</script>
</body>
结论:
addEventListener第3个参数决定了事件是在捕获阶段触发还是在冒泡阶段触发addEventListener第3个参数为true表示捕获阶段触发,false表示冒泡阶段触发,默认值为false- 事件流只会在父子元素具有相同事件类型时才会产生影响
- 绝大部分场景都采用默认的冒泡模式(其中一个原因是早期 IE 不支持捕获)
阻止冒泡
ev.stopPropagation()
阻止冒泡是指阻断事件的流动,保证事件只在当前元素被执行,而不再去影响到其对应的祖先元素。
<body>
<h3>阻止冒泡</h3>
<p>阻止冒泡是指阻断事件的流动,保证事件只在当前元素被执行,而不再去影响到其对应的祖先元素。</p>
<div class="outer">
<div class="inner">
<div class="child"></div>
</div>
</div>
<script>
// 获取嵌套的3个节点
const outer = document.querySelector('.outer')
const inner = document.querySelector('.inner')
const child = document.querySelector('.child')
// 外层的盒子
outer.addEventListener('click', function () {
console.log('outer...')
})
// 中间的盒子
inner.addEventListener('click', function (ev) {
console.log('inner...')
// 阻止事件冒泡
ev.stopPropagation()
})
// 内层的盒子
child.addEventListener('click', function (ev) {
console.log('child...')
// 借助事件对象,阻止事件向上冒泡
ev.stopPropagation()
})
</script>
</body>
结论:事件对象中的 ev.stopPropagation 方法,专门用来阻止事件冒泡。
鼠标经过事件:
mouseover 和 mouseout 会有冒泡效果
mouseenter 和 mouseleave 没有冒泡效果 (推荐)
阻止默认行为
我们某些情况下需要阻止默认行为的发生,比如阻止链接的跳转,表单域的跳转等
<form action="http://www.baidu.com>
<input type="submit” value="提交">
</form>
<script>
const form = document .querySelector( form')
form.addEventListener('click',function (e) {
// 阻止表单默认提交行为
e.preventDefault()
})
</script>
事件委托
事件委托是利用事件流的特征解决一些现实开发需求的知识技巧,主要的作用是提升程序效率。
大量的事件监听是比较耗费性能的,如下代码所示
<script>
// 假设页面中有 10000 个 button 元素
const buttons = document.querySelectorAll('table button');
for(let i = 0; i <= buttons.length; i++) {
// 为 10000 个 button 元素添加了事件
buttons.addEventListener('click', function () {
// 省略具体执行逻辑...
})
}
</script>
利用事件流的特征,可以对上述的代码进行优化,事件的的冒泡模式总是会将事件流向其父元素的,如果父元素监听了相同的事件类型,那么父元素的事件就会被触发并执行,正是利用这一特征对上述代码进行优化,如下代码所示:
<script>
// 假设页面中有 10000 个 button 元素
let buttons = document.querySelectorAll('table button');
// 假设上述的 10000 个 buttom 元素共同的祖先元素是 table
let parents = document.querySelector('table');
parents.addEventListener('click', function () {
console.log('点击任意子元素都会触发事件...');
})
</script>
我们的最终目的是保证只有点击 button 子元素才去执行事件的回调函数,如何判断用户点击是哪一个子元素呢?
事件对象中的属性 target 或 srcElement属性表示真正触发事件的元素,它是一个元素类型的节点。
<script>
// 假设页面中有 10000 个 button 元素
const buttons = document.querySelectorAll('table button')
// 假设上述的 10000 个 buttom 元素共同的祖先元素是 table
const parents = document.querySelector('table')
parents.addEventListener('click', function (ev) {
// console.log(ev.target);
// 只有 button 元素才会真正去执行逻辑(注意这里判断的标签需要全大写)
if(ev.target.tagName === 'BUTTON') {
// 执行的逻辑
}
})
</script>
优化过的代码只对祖先元素添加事件监听,相比对 10000 个元素添加事件监听执行效率要高许多!!!
总结:
- 优点:减少注册次数,可以提高程序性能
- 原理:事件委托其实是利用事件冒泡的特点
- 给父元素注册事件,当我们触发子元素的时候,会冒泡到父元素身上,从而触发父元素的事件
- 实现:事件对象.target.tagName 可以获得真正触发事件的元素
例:
其他事件
页面事件
// 页面加载事件: load,监听页面所有资源加载完毕
// 加载外部资源(如图片、外联CSS和JavaScript等)加载完毕时触发的事件, 有些时候需要等页面资源全部处理完了做一些事情
window.addEventListener('load', function() {
// xxxxx
})
// 调整窗口大小事件: resize,常用于响应式页面布局
window.addEventListener('resize', function() {
// xxxxx
})
元素滚动事件
滚动条在滚动的时候持续触发的事件:scroll
获取滚动的位置:
scrollHeight:获取元素整个滚动区的高度,当满足 scrollHeight - scrollTop == clientHeight 时,说明垂直滚动条滚动到底了scrollWidth:获取元素整个滚动区的宽度,当满足 scrollWidth - scrollLeft == clientWidth 时,说明水平滚动条滚动到底了- scrollTo(x,y):把内容滚动到指定位置
window.addEventListener('scroll', function() {
// 页面到底滚动了多少像素,被卷去了多少:scrollTop
console.log(document.documentElement.scrollTop);
})
例:
页面尺寸事件
会在窗口尺寸改变的时候触发事件:
window.addEventListener('resize', function() {
// xxxxx
})
获取元素尺寸位置:
clientWidth:获取元素(内容+内边距)宽度,clientHeight 高度同理offsetParent:获取当前元素的父元素offsetWidth:获取元素(内容+内边距+边框)宽度,clientHeight 高度同理offsetLeft:当前元素相对于其定位父元素的水平偏移量,offsetTop:垂直偏移量
<body>
<div id="box" style="width: 150px;border: 1px solid #000;">随便添加一些文字</div>
<script>
let box = document.getElementById("box")
console.log(box.clientWidth) // 150
console.log(box.clientHeight) // 21
console.log(box.offsetParent) // body
console.log(box.offsetWidth) // 152
console.log(box.offsetHeight) // 23
</script>
</body>
注意: 获取的是可视宽高, 如果盒子是隐藏的,获取的结果是0
想获取页面滚动位置,可查看后面的元素滚动事件
例:以下例子可实现tab导航栏切换时实现下划线移动的效果
offset与style的区别
offset、client、scroll总结
主要用法:
- offset 系列经常用于获得元素位置 offsetLeft offsetTop
- client 经常用于获取元素大小 clientWidth clientHeight
- scroll 经常用于获取滚动距离 scrollTop scrollLeft
页面触摸事件(M端事件)
会在触摸屏幕时触发事件
- touchstart:开始触摸时
- touchend:结束触摸时
- touchmove:一直摸时
window.addEventListener('touchstart', function() {
// xxxxx
})
移动端click延迟解决方案
移动端click事件会有300ms的延时,原因是移动端屏幕双击会缩放(double tap to zoom)
页面解决方案:
方法一:禁用缩放。浏览器禁用默认的双击缩放行为并且去掉300ms的点击延迟
<meta name="viewport" content="user-scalable=no"
方法二:利用touch事件自己封装这个事件解决300ms延迟
原理就是
- 当我们手指触摸屏幕,记录当前触摸时间
- 当我们手指离开屏幕,用离开的时间减去触摸的时间
- 如果时间小于150ms,并且没有滑动过屏幕,那么我们就定义为点击
<script>
//封装tap,解决click 300ms 延时
function tap (obj, callback) {
var isMove = false;
var startTime = 0;// 记录触摸时候的时间变量
obj.addEventListener('touchstart', function (e) {
startTime = Date.now(); // 记录触摸时间
});
obj.addEventListener('touchmove', function (e) {
isMove = true; // 看看是否有滑动,有滑动算拖拽,不算点击
});
obj.addEventListener('touchend', function (e) {
if(!isMove && (Date.now() - startTime) < 150) { // 如果手指摸和离开时间小于150ms 算点击
callback && callback(); // 执行回调函数
}
isMove = false; // 取反 重置
startTime = 0;
});
}
//调用
tap(div,function(){ // 执行代码 });
</script>
方法三:用第三方插件(github.com/ftlabs/fast…
<script src="fastclick.js"></script>
<body>
<div></div>
<script>
if ('addEventListener' in document) {
document.addEventListener( 'DOMContentLoaded', function() {
(FastClick.attach(document.body);
},false);
}
</script>
</body>
JS插件
swiper
演示:www.swiper.com.cn/demo/index.…
教程:www.swiper.com.cn/usage/index…
API:www.swiper.com.cn/api/index.h…
注意!使用多个swiper时,注意区分类名
bootstrap
使用步骤:
- 上官网下载并引入相关的js文件
- 将需要的html、css、js复制并修改参数即可(swiper、bootstrap这俩插件都一样)
八、正则表达式
正则表达式(Regular Expression)是一种字符串匹配的模式(规则)
使用场景:
- 例如验证表单:手机号表单要求用户只能输入11位的数字 (匹配)
- 过滤掉页面内容中的一些敏感词(替换),或从字符串中获取我们想要的特定部分(提取)等
正则基本使用
-
定义规则
const reg = /表达式/- 其中
/ /是正则表达式字面量 - 正则表达式也是
对象
- 其中
-
作用
- 表单验证(匹配)
- 过滤敏感词(替换)
- 字符串中提取我们想要的部分(提取)
-
使用正则
test()方法用来查看正则表达式与指定的字符串是否匹配- 如果正则表达式与指定的字符串匹配 ,返回
true,否则false
<body>
<script>
// 正则表达式的基本使用
const str = 'web前端开发'
// 1. 定义规则
const reg = /web/
// 2. 使用正则 test()
console.log(reg.test(str)) // true 如果符合规则匹配上则返回true
console.log(reg.test('java开发')) // false 如果不符合规则匹配上则返回 false
</script>
</body>
元字符
- 普通字符:
- 大多数的字符仅能够描述它们本身,这些字符称作普通字符,例如所有的字母和数字。
- 普通字符只能够匹配字符串中与它们相同的字符。
- 比如,规定用户只能输入英文26个英文字母,普通字符的话 /[abcdefghijklmnopqrstuvwxyz]/
- 元字符(特殊字符)
- 是一些具有特殊含义的字符,可以极大提高了灵活性和强大的匹配功能。
- 比如,规定用户只能输入英文26个英文字母,换成元字符写法: /[a-z]/
边界符
正则表达式中的边界符(位置符)用来提示字符所处的位置,主要有两个字符
如果 ^ 和 $ 在一起,表示必须是精确匹配
<body>
<script>
// 元字符之边界符
// 1. 匹配开头的位置 ^
const reg = /^web/
console.log(reg.test('web前端')) // true
console.log(reg.test('前端web')) // false
console.log(reg.test('前端web学习')) // false
console.log(reg.test('we')) // false
// 2. 匹配结束的位置 $
const reg1 = /web$/
console.log(reg1.test('web前端')) // false
console.log(reg1.test('前端web')) // true
console.log(reg1.test('前端web学习')) // false
console.log(reg1.test('we')) // false
// 3. 精确匹配 ^ $
const reg2 = /^web$/
console.log(reg2.test('web前端')) // false
console.log(reg2.test('前端web')) // false
console.log(reg2.test('前端web学习')) // false
console.log(reg2.test('we')) // false
console.log(reg2.test('web')) // true
console.log(reg2.test('webweb')) // flase
</script>
</body>
量词
量词用来设定某个模式重复次数
注意: 逗号左右两侧千万不要出现空格
<body>
<script>
// 元字符之量词
// 1. * 重复次数 >= 0 次
const reg1 = /^w*$/
console.log(reg1.test('')) // true
console.log(reg1.test('w')) // true
console.log(reg1.test('ww')) // true
console.log('-----------------------')
// 2. + 重复次数 >= 1 次
const reg2 = /^w+$/
console.log(reg2.test('')) // false
console.log(reg2.test('w')) // true
console.log(reg2.test('ww')) // true
console.log('-----------------------')
// 3. ? 重复次数 0 || 1
const reg3 = /^w?$/
console.log(reg3.test('')) // true
console.log(reg3.test('w')) // true
console.log(reg3.test('ww')) // false
console.log('-----------------------')
// 4. {n} 重复 n 次
const reg4 = /^w{3}$/
console.log(reg4.test('')) // false
console.log(reg4.test('w')) // flase
console.log(reg4.test('ww')) // false
console.log(reg4.test('www')) // true
console.log(reg4.test('wwww')) // false
console.log('-----------------------')
// 5. {n,} 重复次数 >= n
const reg5 = /^w{2,}$/
console.log(reg5.test('')) // false
console.log(reg5.test('w')) // false
console.log(reg5.test('ww')) // true
console.log(reg5.test('www')) // true
console.log('-----------------------')
// 6. {n,m} n =< 重复次数 <= m
const reg6 = /^w{2,4}$/
console.log(reg6.test('w')) // false
console.log(reg6.test('ww')) // true
console.log(reg6.test('www')) // true
console.log(reg6.test('wwww')) // true
console.log(reg6.test('wwwww')) // false
// 7. 注意事项: 逗号两侧千万不要加空格否则会匹配失败
</script>
范围
表示字符的范围,定义的规则限定在某个范围,比如只能是英文字母,或者数字等等,用表示范围
<body>
<script>
// 元字符之范围 []
// 1. [abc] 匹配包含的单个字符, 多选1
const reg1 = /^[abc]$/
console.log(reg1.test('a')) // true
console.log(reg1.test('b')) // true
console.log(reg1.test('c')) // true
console.log(reg1.test('d')) // false
console.log(reg1.test('ab')) // false
// 2. [a-z] 连字符 单个
const reg2 = /^[a-z]$/
console.log(reg2.test('a')) // true
console.log(reg2.test('p')) // true
console.log(reg2.test('0')) // false
console.log(reg2.test('A')) // false
// 想要包含小写字母,大写字母 ,数字
const reg3 = /^[a-zA-Z0-9]$/
console.log(reg3.test('B')) // true
console.log(reg3.test('b')) // true
console.log(reg3.test(9)) // true
console.log(reg3.test(',')) // flase
// 用户名可以输入英文字母,数字,可以加下划线,要求 6~16位
const reg4 = /^[a-zA-Z0-9_]{6,16}$/
console.log(reg4.test('abcd1')) // false
console.log(reg4.test('abcd12')) // true
console.log(reg4.test('ABcd12')) // true
console.log(reg4.test('ABcd12_')) // true
// 3. [^a-z] 取反符
const reg5 = /^[^a-z]$/
console.log(reg5.test('a')) // false
console.log(reg5.test('A')) // true
console.log(reg5.test(8)) // true
</script>
</body>
字符类
某些常见模式的简写方式,区分字母和数字
替换和修饰符
replace 替换方法,可以完成字符的替换
<body>
<script>
// 替换和修饰符
const str = '欢迎大家学习前端,相信大家一定能学好前端,都成为前端大神'
// 1. 替换 replace 需求:把前端替换为 web
// 1.1 replace 返回值是替换完毕的字符串
// const strEnd = str.replace(/前端/, 'web') 只能替换一个
</script>
</body>
修饰符约束正则执行的某些细节行为,如是否区分大小写、是否支持多行匹配等
- i 是单词 ignore 的缩写,正则匹配时字母不区分大小写
- g 是单词 global 的缩写,匹配所有满足正则表达式的结果
<body>
<script>
// 替换和修饰符
const str = '欢迎大家学习前端,相信大家一定能学好前端,都成为前端大神'
// 1. 替换 replace 需求:把前端替换为 web
// 1.1 replace 返回值是替换完毕的字符串
// const strEnd = str.replace(/前端/, 'web') 只能替换一个
// 2. 修饰符 g 全部替换
const strEnd = str.replace(/前端/g, 'web')
console.log(strEnd)
</script>
</body>
正则插件
change 事件
给input注册 change 事件,值被修改并且失去焦点后触发
判断是否有类
元素.classList.contains() 看看有没有包含某个类,如果有则返回true,么有则返回false
九、数据处理
深浅拷贝
浅拷贝
首先浅拷贝和深拷贝只针对引用类型
浅拷贝:拷贝的是地址
常见方法:
- 拷贝对象:Object.assgin() / 展开运算符 {...obj} 拷贝对象
- 拷贝数组:Array.prototype.concat() 或者 [...arr]
拷贝对象之后,里面的属性值:如果是简单数据类型则直接拷贝值,如果是引用数据类型则拷贝地址
直接赋值和浅拷贝的区别?
- 直接赋值的方法,只要是对象,都会相互影响,因为是直接拷贝对象栈里的地址
- 浅拷贝如果是一层对象,不互相影响,如果出现多层对象拷贝还是会互相影响
<script>
const obj = {
name: 'pink',
age: 18
}
// 直接赋值会影响原对象
const y = obj
y.age = 20
console.log(obj) // {name: 'pink', age: 20}
console.log(y) // {name: 'pink', age: 20}
// 采用浅拷贝
// 方式一
const o = {...obj}
o.age = 22
console.log(obj) // {name: 'pink', age: 18}
console.log(o) // {name: 'pink', age: 22}
// 方式二
const x = Object.assign(x, obj)
x.age = 3
console.log(obj) // {name: 'pink', age: 18}
console.log(x) // {name: 'pink', age: 3}
// 多层对象的浅拷贝就出现问题了,它只会拷贝最外面一层的地址,而里面的一层还是会跟直接赋值一样影响原对象
const obj2 = {
name: 'pink',
age: 18
family: {
baby: '小pink'
}
}
const n = Object.assign(n, obj2)
n.age = 30
n.family.baby = '老登'
console.log(obj2) // {name: 'pink', age: 18, family: {baby: '老登'}}
console.log(n) // {name: 'pink', age: 30, family: {baby: '老登'}}
</script>
深拷贝
首先浅拷贝和深拷贝只针对引用类型
深拷贝:拷贝的是对象,不是地址
常见方法:
- 通过递归实现深拷贝
- lodash/cloneDeep
- 通过JSON.stringify()实现
递归实现深拷贝
函数递归:
如果一个函数在内部可以调用其本身,那么这个函数就是递归函数
- 简单理解:函数内部自己调用自己, 这个函数就是递归函数
- 递归函数的作用和循环效果类似
- 由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return
<body>
<script>
const obj = {
uname: 'pink',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: '小pink'
}
}
const o = {}
// 拷贝函数
function deepCopy(newObj, oldObj) {
for (let k in oldObj) {
// 处理数组的问题 一定先写数组 在写 对象 不能颠倒
if (oldObj[k] instanceof Array) {
newObj[k] = []
// newObj[k] 接收 [] hobby
// oldObj[k] ['乒乓球', '足球']
deepCopy(newObj[k], oldObj[k])
} else if (oldObj[k] instanceof Object) {
newObj[k] = {}
deepCopy(newObj[k], oldObj[k])
}
else {
// k 属性名 uname age oldObj[k] 属性值 18
// newObj[k] === o.uname 给新对象添加属性
newObj[k] = oldObj[k]
}
}
}
deepCopy(o, obj) // 函数调用 两个参数 o 新对象 obj 旧对象
console.log(o)
o.age = 20
o.hobby[0] = '篮球'
o.family.baby = '老pink'
console.log(obj)
console.log([1, 23] instanceof Object)
// 复习
// const obj = {
// uname: 'pink',
// age: 18,
// hobby: ['乒乓球', '足球']
// }
// function deepCopy({ }, oldObj) {
// // k 属性名 oldObj[k] 属性值
// for (let k in oldObj) {
// // 处理数组的问题 k 变量
// newObj[k] = oldObj[k]
// // o.uname = 'pink'
// // newObj.k = 'pink'
// }
// }
</script>
</body>
总结:
- 深拷贝需要用到函数递归
- 当普通拷贝时直接赋值就好,遇到复杂对象的话,如果里面有数组时则调用这个递归函数,如果遇到对象形式则再次利用递归解决
- 先处理数组再处理对象,顺序不能颠倒
lodash/cloneDeep
引用第三方js库:库的 lodash 里面 cloneDeep() 内部实现了深拷贝
<body>
<!-- 先引用 -->
<script src="./lodash.min.js"></script>
<script>
const obj = {
uname: 'pink',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: '小pink'
}
}
const o = _.cloneDeep(obj)
o.family.baby = '老pink'
console.log(obj) // {uname: 'pink', age: 18, hobby: ['乒乓球', '足球'], family: {baby: '小pink'}}
console.log(o) // {uname: 'pink', age: 18, hobby: ['乒乓球', '足球'], family: {baby: '老pink'}}
</script>
</body>
JSON.stringify()
<body>
<script>
const obj = {
uname: 'pink',
age: 18,
hobby: ['乒乓球', '足球'],
family: {
baby: '小pink'
}
}
// 通过 JSON.stringify() 的JSON序列化,把对象转换为 JSON 字符串
// console.log(JSON.stringify(obj))
const o = JSON.parse(JSON.stringify(obj))
console.log(o)
o.family.baby = '123'
console.log(obj)
</script>
</body>
异常处理
了解 JavaScript 中程序异常处理的方法,提升代码运行的健壮性。
throw
异常处理是指预估代码执行过程中可能发生的错误,然后最大程度的避免错误的发生导致整个程序无法继续运行
总结:
- throw 抛出异常信息,程序也会终止执行
- throw 后面跟的是错误提示信息
- Error 对象配合 throw 使用,能够设置更详细的错误信息
<script>
function counter(x, y) {
if(!x || !y) {
// throw '参数不能为空!';
throw new Error('参数不能为空!')
}
return x + y
}
counter()
</script>
try ... catch ... finally
<script>
function foo() {
try {
// 查找 DOM 节点
const p = document.querySelector('.p')
p.style.color = 'red'
} catch (error) {
// try 代码段中执行有错误时,会执行 catch 代码段
// 查看错误信息
console.log(error.message)
// 终止代码继续执行
return
}
finally {
// 不管你程序对不对,一定会执行的代码
alert('执行')
}
console.log('如果出现错误,我的语句不会执行')
}
foo()
</script>
总结:
try...catch用于捕获错误信息- 将预估可能发生错误的代码写在
try代码段中 - 如果
try代码段中出现错误后,会执行catch代码段,并截获到错误信息
debugger
相当于断点调试,直接在页面写debugger后,打开控制台调试时会自动跳转到代码的断点调试页面;
也可以先写console.log("..."),在控制台点击该打印结果页面,跳转到代码页,然后点击代码页左栏打断点调试
处理this
了解函数中 this 在不同场景下的默认值,知道动态指定函数 this 值的方法。
this 是 JavaScript 最具“魅惑”的知识点,不同的应用场合 this 的取值可能会有意想不到的结果,在此我们对以往学习过的关于【 this 默认的取值】情况进行归纳和总结。
普通函数
普通函数的调用方式决定了 this 的值,即【谁调用 this 的值指向谁】,如下代码所示:
<script>
// 普通函数
function sayHi() {
console.log(this)
}
// 函数表达式
const sayHello = function () {
console.log(this)
}
// 函数的调用方式决定了 this 的值
// 全局作用域或者普通函数中 this 指向全局对象 window,注意定时器里的 this 指向 window
sayHi() // window
window.sayHi() // window
window.setTimeout(function(){
console.log(this) // window
}, 1000)
// 普通对象
const user = {
name: '小明',
walk: function () {
console.log(this)
}
}
// 动态为 user 添加方法
user.sayHi = sayHi
uesr.sayHello = sayHello
// 函数调用方式,决定了 this 的值
user.sayHi()
user.sayHello()
</script>
注: 普通函数没有明确调用者时 this 值为 window,严格模式下没有调用者时 this 的值为 undefined。
箭头函数
箭头函数中的 this 与普通函数完全不同,也不受调用方式的影响,事实上箭头函数中并不存在 this !箭头函数中访问的 this 不过是箭头函数所在作用域的 this 变量。
<script>
console.log(this) // 此处为 window
// 箭头函数
const sayHi = function() {
console.log(this) // 该箭头函数中的 this 为函数声明环境中 this 一致
}
// 普通对象
const user = {
name: '小明',
// 该箭头函数中的 this 为函数声明环境中 this 一致
walk: () => {
console.log(this)
},
sleep: function () {
let str = 'hello'
console.log(this)
let fn = () => {
console.log(str)
console.log(this) // 该箭头函数中的 this 与 sleep 中的 this 一致
}
// 调用箭头函数
fn();
}
}
// 动态添加方法
user.sayHi = sayHi
// 函数调用
user.sayHi()
user.sleep()
user.walk()
</script>
在开发中【使用箭头函数前需要考虑函数中 this 的值】,事件回调函数使用箭头函数时,this 为全局的 window,因此DOM事件回调函数不推荐使用箭头函数,如下代码所示:
<body>
<button class="btn">按钮</button>
<script>
// DOM 节点
const btn = document.querySelector('.btn')
// 箭头函数 此时 this 指向了 window
btn.addEventListener('click', () => {
console.log(this) // window
})
// 普通函数 此时 this 指向了 DOM 对象
btn.addEventListener('click', function () {
console.log(this) // <button class="btn">按钮</button>
})
</script>
</body>
同样由于箭头函数 this 的原因,基于原型的面向对象也不推荐采用箭头函数,如下代码所示:
<script>
function Person() {
}
// 原型对像上添加了箭头函数
Person.prototype.walk = () => {
console.log('人都要走路...')
console.log(this); // window
}
const p1 = new Person()
p1.walk()
</script>
总结:
- 函数内不存在this,沿用上一级的,过程:向外层作用域中,一层层查找 this,直到有 this 的定义
- 不适用:构造函数、原型函数、字面量对象中函数、dom事件函数
- 适用:需要使用上层 this 的地方
改变this指向
以上归纳了普通函数和箭头函数中关于 this 默认值的情形,不仅如此 JavaScript 中还允许指定函数中 this 的指向,有 3 个方法可以动态指定普通函数中 this 的指向:
call
使用 call 方法调用函数,同时指定函数中 this 的值,使用方法如下代码所示:
<script>
// 普通函数
function sayHi() {
console.log(this);
}
let user = {
name: '小明',
age: 18
}
let student = {
name: '小红',
age: 16
}
// 调用函数并指定 this 的值
sayHi.call(user); // this 值为 user
sayHi.call(student); // this 值为 student
// 求和函数
function counter(x, y) {
return x + y;
}
// 调用 counter 函数,并传入参数
let result = counter.call(null, 5, 10);
console.log(result);
</script>
总结:
call方法能够在调用函数的同时指定this的值- 使用
call方法调用函数时,第1个参数为this指定的值 call方法的其余参数会依次自动传入函数做为函数的参数
apply
使用 call 方法调用函数,同时指定函数中 this 的值,使用方法如下代码所示:
<script>
// 普通函数
function sayHi() {
console.log(this)
}
let user = {
name: '小明',
age: 18
}
let student = {
name: '小红',
age: 16
}
// 调用函数并指定 this 的值
sayHi.apply(user) // this 值为 user
sayHi.apply(student) // this 值为 student
// 求和函数
function counter(x, y) {
return x + y
}
// 调用 counter 函数,并传入参数
let result = counter.apply(null, [5, 10])
console.log(result)
</script>
总结:
apply方法能够在调用函数的同时指定this的值- 使用
apply方法调用函数时,第1个参数为this指定的值 apply方法第2个参数为数组,数组的单元值依次自动传入函数做为函数的参数
bind
bind 方法并不会调用函数,而是创建一个指定了 this 值的新函数,使用方法如下代码所示:
<script>
// 普通函数
function sayHi() {
console.log(this)
}
let user = {
name: '小明',
age: 18
}
// 调用 bind 指定 this 的值
let sayHello = sayHi.bind(user);
// 调用使用 bind 创建的新函数
sayHello()
// 需求:有个按钮,单击后禁用2秒再开启
const btn = document.querySelector('.btn')
btn.addEventListener('click', function() {
// 禁用按钮
this.disabled = true
window.setTimeout(function() {
// 在这个普通函数里,我们要把this由原来的指向 window 改为 btn
this.disabled = true
}.bind(btn), 2000)
// 也可以改成箭头函数,因为箭头函数没有this,所以指向上一层的 btn
// setTimeout(() => {
// this.disabled = true
// }, 2000)
})
</script>
注:bind 方法创建新的函数,与原函数的唯一的变化是改变了 this 的值。
总结:
相同点:
都可以改变函数内部的 this 指向
不同点:
call 和 apply 会调用函数,并且改变函数内部 this 指向
call 和 apply 传递的参数不一样,call 传递参数 aru1,aru2.. 形式,apply 必须数组形式 [arg]
bind 不会调用函数,可以改变函数内部的 this 指向
应用场景:
call 可以调用函数并传参
apply 经常跟数组有关,比如借助数学对象实现数组最大值最小值
bind 不调用函数,但还想改变 this 指向,比如改变定时器内部的 this 指向
防抖节流
防抖(debounce)
所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间
当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。.简单理解就是连续多次点击一个链接,最终只执行最后一次的点击,之前的点击都取消了
<body>
<div class="box">1</div>
<!--<script src="./js/lodash.min.js"></script>-->
<script>
// 利用防抖实现性能优化
// 鼠标在盒子上移动,利用防抖,实现鼠标停止500ms之后,里面的数字+1
const box = document.querySelector('.box')
let i = 1
function mouseMove () {
box.innerHTML = i++
// 如果里面存在大量消耗性能的代码,比如 dom 操作,数据处理等,可能造成卡顿
}
// 方法一:利用 lodash 库实现防抖 - 500 毫秒之后采取 + 1
// box.addEventListener('mousemove', _.debounce(mouseMove, 500))
// 方法二:手写防抖函数
// 核心是利用 setTimeout 定时器来实现
// 1.声明定时器变量
// 2. 每次鼠标移动(事件触发》的时候都要先判断是否有定时器,如果有先清除以前的定时器
// 3.如果没有定时器,则开启定时器,存入到定时器变量里面
// 4.定时器里面写函数调用
function debounce(fn, t) {
let timer
return function () {
if(timer) clearTimeout(timer)
timer = setTimeout(function (){
fn()
}, t)
}
}
box.addEventListener('mousemove', debounce(mouseMove, 500))
</script>
</body>
节流(throttle)
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数
让一个函数无法在很短的时间间隔内连续调用,当上一次执行完之后过了规定的时间间隔,才能进行下一次的函数调用。
简单理解就是点击一次链接后,再次点击就会无效,隔几秒后点击才会有效。
<body>
<div class="box">1</div>
<!--<script src="./js/lodash.min.js"></script>-->
<script>
// 利用节流实现性能优化
// 鼠标在盒子上移动,利用节流,实现每经过500ms之后,数字才+1
const box = document.querySelector('.box')
let i = 1
function mouseMove () {
box.innerHTML = i++
// 如果里面存在大量消耗性能的代码,比如 dom 操作,数据处理等,可能造成卡顿
}
// 方法一:利用 lodash 库实现节流 - 500 毫秒之后采取 + 1
// box.addEventListener('mousemove', _.throttle(mouseMove, 500))
// 方法二:手写节流函数
// 流的核心就是利用定时器(setTimeout) 来实现
// 1.声明一个定时器变量
// 2.当鼠标每次滑动都先判断是否有定时器了,如果有定时器则不开启新定时器,如果没有定时器则开启定时器,并存到变量里
// 3.1定时器里面调用执行的函数
// 3.2定时器里面要把定时器清空
function throttle(fn, t) {
let timer = null
return function () {
if (!timer) {
timer = setTimeout(function () {
fn()
// 清空定时器
timer = null
}, t)
}
}
}
box.addEventListener( 'mousemove',throttle(mouseMove, 500))
</script>
</body>
总结:
完结!