JS引入
JavaScript 程序不能独立运行,它需要被嵌入 HTML 中,然后浏览器才能执行 JavaScript 代码。
通过 script
标签将 JavaScript 代码引入到 HTML 中,有两种方式:
内部方式
通过 script
标签包裹 JavaScript 代码
- 我们将 < script>放在HTML文件的底部附近的原因是浏览器会按照代码在文件中的顺序加载HTML。
- 如果先加载的JavaScript期望修改其下方的HTML,那么它可能由于HTML尚未被加载而失效。
- 因此,将JavaScript代码放在HTML页面的底部附近通常是最好的策略。
外部形式
一般将 JavaScript 代码写在独立的以 .js 结尾的文件中,然后通过 script
标签的 src
属性引入
// demo.js
document.write('嗨,欢迎学习前端技术!')
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>JavaScript 基础 - 引入方式</title>
</head>
<body><!-- 外部形式:通过 script 的 src 属性引入独立的 .js 文件 --><script src="demo.js">//中间不要写内容</script>
</body>
</html>
如果 script 标签使用 src 属性引入了某 .js 文件,那么 标签的代码会被忽略!!!如下代码所示:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>JavaScript 基础 - 引入方式</title>
</head>
<body><!-- 外部形式:通过 script 的 src 属性引入独立的 .js 文件 --><script src="demo.js">// 此处的代码会被忽略掉!!!!alert(666); </script>
</body>
</html>
内联JavaScript
代码写在标签内部
语法:
<body>
<button οnclick="alert('逗你玩 ~~~ ')”>点击我月薪过万</button>
</body>
输出
JavaScript 可以接收用户的输入,然后再将输入的结果输出:
语法1
document.wirte()
document.write不仅可以直接写字,也可以写标签(如果输出的内容写的是标签,也会被解析成网页元素)
eg.
document.write('<h1>标题</h1>')
语法2:
alert('要出的内容')
作用:页面弹出警告对话框
语法3:
console.log('控制台打印')
作用:控制台输出语法,程序员调试使用
以数字为例,向 alert()
或 document.write()
输入任意数字,他都会以弹窗形式展示(输出)给用户。
输入
向 prompt()
输入任意内容会以弹窗形式出现在浏览器中,一般提示用户输入一些内容。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>JavaScript 基础 - 输入输出</title>
</head>
<body><script> // 1. 输入的任意数字,都会以弹窗形式展示document.write('要输出的内容')alert('要输出的内容');// 2. 以弹窗形式提示用户输入姓名,注意这里的文字使用英文的引号prompt('请输入您的姓名:')</script>
</body>
</html>
变量
变量是计算机中用来存储数据的“容器”,它可以让计算机变得有记忆,通俗的理解变量就是使用【某个符号】来代表【某个具体的数值】(数据)
声明
let 变量名
声明(定义)变量有两部分构成:声明关键字、变量名(标识)
关键字是 JavaScript 中内置的一些英文词汇(单词或缩写),它们代表某些特定的含义
关键字
JavaScript 使用专门的关键字 let
和 var
来声明(定义)变量,在使用时需要注意一些细节:
以下是使用 let
时的注意事项:
- 允许声明和赋值同时进行
- 不允许重复声明
- 允许同时声明多个变量并赋值
- JavaScript 中内置的一些关键字不能被当做变量名
以下是使用 var
时的注意事项:
- 允许声明和赋值同时进行
- 允许重复声明
- 允许同时声明多个变量并赋值
大部分情况使用 let
和 var
区别不大,但是 let
相较 var
更严谨,因此推荐使用 let
,后期会更进一步介绍二者间的区别。
变量名命名规则
关于变量的名称(标识符)有一系列的规则需要遵守:
- 只能是字母、数字、下划线、$,且不能能数字开头
- 字母区**分大小写**,如 Age 和 age 是不同的变量
- JavaScript 内部已占用于单词(关键字或保留字)不允许使用
- 尽量保证变量具有一定的语义,见字知义
注:所谓关键字是指 JavaScript 内部使用的词语,如 let
和var
,保留字是指 JavaScript 内部目前没有使用的词语,但是将来可能会使用词语。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>JavaScript 基础 - 变量名命名规则</title>
</head>
<body><script> let age = 18 // 正确let age1 = 18 // 正确let _age = 18 // 正确// let 1age = 18; // 错误,不可以数字开头let $age = 18 // 正确let Age = 24 // 正确,它与小写的 age 是不同的变量// let let = 18; // 错误,let 是关键字let int = 123 // 不推荐,int 是保留字</script>
</body>
</html>
变量的本质
内存:计算机中存储数据的地方,相当于一个空间
变量本质:是程序在内存中申请的一块用来存放数据的小空间
常量
概念:使用 const 声明的变量称为“常量”。
使用场景:当某个变量永远不会改变的时候,就可以使用 const 来声明,而不是let。
注意: 常量不允许重新赋值,声明的时候必须赋值(初始化)
数据类型
计算机世界中的万事成物都是数据。
- JS 弱数据类型的语言 , 只有当我们赋值了,才知道是什么数据类型
8种数据类型:
Number, String, Boolean, Object, Null, undefined, BigInt(谷歌67版本), symbol(ES6新增)
计算机程序可以处理大量的数据,为了方便数据的管理,将数据分成了不同的类型:基本数据类型,引用数据类型
JS判断数据类型的6种方法
-
最常见的判断方法:typeof
-
已知对象类型: instanceof
-
对象原型链判断方法: prototype
-
根据对象的构造器constructor进行判断
-
jQuery方法: jquery.type()
-
严格运算符:
===
NaN代表一个计算错误。
它是一个不正确的或者一个未定义的数学操作所得到的结果
console.log('老师’-2)//NaN
NaN是粘性的。任何对NaN的操作都会返回NaN
console.log(NaN + 2) // NaN
数值类型(Number)
即我们数学中学习到的数字,可以是整数、小数、正数、负数
JavaScript 中的数值类型与数学中的数字是一样的,分为正数、负数、小数等。
字符串类型
通过单引号( ''
) 、双引号( ""
)或反引号包裹的数据都叫字符串,单引号和双引号没有本质上的区别,推荐使用单引号。
注意事项:
- 无论单引号或是双引号必须成对使用
- 单引号/双引号可以互相嵌套,但是不以自已嵌套自已(外双内单,或者,外单内双)
- 必要时可以使用转义符
\
,输出单引号或双引号
字符串拼接
- 场景:+运算符 可以实现字符串的拼接。
模板字符串
使用场景
拼接字符串和变量
在没有它之前,要拼接变量比较麻烦
语法
-
``(反引号)
-
在英文输入模式下按键盘的tab键上方那个键(1左边那个键)
-
内容拼接变量时,用${}包住变量
document.write(`大家好我叫${name},好好好`)
布尔类型
表示肯定或否定时在计算机中对应的是布尔类型数据,它有两个固定的值 true
和 false
,表示肯定的数据用 true
,表示否定的数据用 false
。
undefined
未定义是比较特殊的类型,只有一个值 undefined,只声明变量,不赋值的情况下,变量的默认值为 undefined,一般很少【直接】为某个变量赋值为 undefined。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>JavaScript 基础 - 数据类型</title>
</head>
<body><script> // 只声明了变量,并末赋值let tmp;document.write(typeof tmp) // 结果为 undefined</script>
</body>
</html>
注:JavaScript 中变量的值决定了变量的数据类型。
null(空类型)
JavaScript中的null仅仅是一个代表“无”、“空”或“值未知”的特殊值
let obj = null
console.log(obj) // null
null 和 undefined 区别:
-
undefined 表示没有赋值
-
null表示赋值了,但是内容为空
null 开发中的使用场景:
官方解释:把null作为尚未创建的对象
大白话:将来有个变量里面存放的是一个对象,但是对象还没创建好,可以先给个null
// 计算有区别
console.log(undefined + 1) // NaN
console.log(null +1) // 1
通过typeof 关键字检测数据类型
typeof 运算符可以返回被检测的数据类型。它支持两种语法形式:
-
作为运算符:typeof x(常用的写法)
-
函数形式:typeof(x)
eg.
let num = 10
console.log(typeof num)
let str = 'pink'
console.log(typeof str)
类型转换
在 JavaScript 中数据不同数据类型之间存在着转换的关系。
JavaScript是弱数据类型: JavaScript也不知道变量到底属于那种数据类型,只有赋值了才清楚。
坑: 使用表单、prompt获取过来的数据默认是字符串类型的,此时就不能直接简单的进行加法运算。
隐式转换
某些运算符被执行时,系统内部自动将数据类型进行转换,这种转换称为隐式转换。
规则:
-
- +号两边只要有一个是字符串,都会把另外一个转成字符串
- 除了+以外的算术运算符 比如 -
*
/等都会把数据转成数字类型
缺点:
- 转换类型不明确,靠经验才能总结
小技巧:
- +号作为正号解析可以转换成数字型
- 任何数据和字符串相加结果都是字符串
<script> let num = 13 // 数值let num2 = '2' // 字符串// 结果为 132// 原因是将数值 num 转换成了字符串,相当于 '13'// 然后 + 将两个字符串拼接到了一起console.log(num + num2)// 结果为 11// 原因是将字符串 num2 转换成了数值,相当于 2// 然后数值 13 减去 数值 2console.log(num - num2)let a = prompt('请输入一个数字')let b = prompt('请再输入一个数字')alert(a + b);
</script>
注:数据类型的隐式转换是 JavaScript 的特征,后续学习中还会遇到,目前先需要理解什么是隐式转换。
![]]](https://i-blog.csdnimg.cn/direct/79440de564134aa18f784426aa29fced.png)
显式转换
隐式转换规律并不清晰,大多是靠经验总结的规律。为了避免因隐式转换带来的问题,通常根逻辑需要对数据进行显示转换。
Number
通过 Number
显示转换成数值类型,当转换失败时结果为 NaN
(Not a Number)即不是一个数字。
<script>let t = '12'let f = 8// 显式将字符串 12 转换成数值 12t = Number(t)// 检测转换后的类型// console.log(typeof t);console.log(t + f) // 结果为 20// 并不是所有的值都可以被转成数值类型let str = 'hello'// 将 hello 转成数值是不现实的,当无法转换成// 数值时,得到的结果为 NaN (Not a Number)console.log(Number(str))
</script>
parselnt(数据)
- 只保留整数(不四舍五入)
parseFloat(数据)
- 保留数字可以保留小数(不四舍五入)
转换为字符型:
- String(数据)
- 变量.toString(进制)
复习:
splice() 方法用于添加或删除数组中的元素。
注意这种方法会改变原始数组。
- 删除数组:
splice(起始位置, 删除的个数)
比如:
let arr = ['red', 'green', 'blue']
arr.splice(1,1) // 删除green元素
console.log(arr) // ['red, 'blue']
- 添加元素
splice(起始位置,删除个数,添加数组元素)
let arr = ['red', 'green', 'blue']
//arr.splice(1, 0, 'pink') // 在索引号是1的位置添加 pink
//console.log(arr) // ['red', 'pink', 'green', 'blue']
arr.splice(1, 0, 'pink', 'hotpink') // 在索引号是1的位置添加 pink hotpink
console.log(arr) // ['red', 'pink', 'hotpink', 'green', 'blue']
- 知道 ECAScript 与 JavaScript 的关系
- 了解 DOM 的相关概念及DOM 的本质是一个对象
- 掌握查找节点的基本方法
- 掌握节点属性和文本的操作
- 能够使用间歇函数创建定时任务
const报错
- 建议数组和对象使用const来声明,不会丢失地址找不到对象属性
//报错
const name = []
name = [1, 2, 3]
console.log('this',this.name)//不报错
const names = []
names[0] = 1
names[1] = 1
names[2] = 1
names[3] = 1
console.log('tag',names)
- 数组等引用类型变量存储的是地址,上边相当于直接改变了该变量的地址指向,就和直接重新赋值变量是一个概念,所以会报错(使用一个数组给name赋值)
- 下边改变的是数组内的元素,本质上names存储的内存地址并未改变,所以不会报错
//报错
const obj = {}
obj = {uname: `pink老师`
}//不会报错
const obj = {}
obj.uname = `pink老师`
- 空对象是一个地址
- 而后的赋值{}又是一个地址,变量obj里面存储的是地址,const限定之后不可更改变量值
ES、JS、Web APIs介绍
知道 ECMAScript 与 JavaScript 的关系,Web APIs 是浏览器扩展的功能。
严格意义上讲,我们在 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概念
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 树,文档树直观的体现了标签与标签之间的关系。
最大的对象就是document
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!');
</script>
上述列举了 document
对象的部分属性和方法,我们先对 document
有一个整体的认识。
获取DOM对象
注意:因为body是唯一标签可以不用获取,对body标签样式修改可以直接写document.body.style
-
querySelector
只会选取满足条件的第一个元素-
语法:
document.querySelector('CSS选择器') //括号里是字符串,也就是必须加引号
-
**参数:**包含一个或多个有效的CSS选择器 字符串
-
返回值:CSS选择器匹配的第一个元素,一个HTMLElement对象,如果没有匹配到返回null
-
-
querySelectorAll
满足条件的元素集合 返回伪数组-
语法:
document.querySlector(`CSS选择器`) //括号里是字符串,也就是必须加引号
-
**参数:**包含一个或多个有效的CSS选择器 字符串
-
返回值:CSS选择器匹配的NodeList 对象集合,也就是得到的是数组,但是是伪数组(有长度有索引号,但是没有pop() push()等数组方法)
-
想要得到里面的每一个对象,则需要遍历的方式获得
-
-
了解其他方式
-
getElementById
-
//根据id获取一个元素 document.getElementGyId('nav') //不用加#
-
-
getElementsByTagName
-
//根据标签获取一类元素 获取页面所有div document.getElementsByTagName('div') //获取的伪数组
-
-
getElementsByClassName
-
//根据类名获取元素 document.getElementsByClassName('w') //获取的伪数组
-
-
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>DOM - 查找节点</title>
</head>
<body><h3>查找元素类型节点</h3><p>从整个 DOM 树中查找 DOM 节点是学习 DOM 的第一个步骤。</p><ul><li>元素</li><li>元素</li><li>元素</li><li>元素</li></ul><script>const p = document.querySelector('p') // 获取第一个p元素const lis = document.querySelectorAll('li') // 获取第一个p元素</script>
</body>
</html>
总结:
- document.getElementById 专门获取元素类型节点,根据标签的
id
属性查找 - 任意 DOM 对象都包含 nodeType 属性,用来检检测节点类型
操作元素内容
通过修改 DOM 的文本内容,动态改变网页的内容。
innerText
将文本内容添加/更新到任意标签位置,文本中包含的标签不会被解析。
<script>// innerText 将文本内容添加/更新到任意标签位置const intro = document.querySelector('.intro')intro.innerText = '嗨~ 我叫李雷!'intro.innerText = '<h4>嗨~ 我叫李雷!</h4>'//不会解析<h4>
</script>
innerHTML
将文本内容添加/更新到任意标签位置,文本中包含的标签会被解析。
<script>// innerHTML 将文本内容添加/更新到任意标签位置const intro = document.querySelector('.intro')intro.innerHTML = '嗨~ 我叫韩梅梅!'intro.innerHTML = '<h4>嗨~ 我叫韩梅梅!</h4>'
</script>
总结:如果文本内容中包含 html
标签时推荐使用 innerHTML
,否则建议使用 innerText
属性。
##操作元素属性
有3种方式可以实现对属性的修改:
常用属性修改
- 直接能过属性名修改,最简洁的语法
<script>// 1. 获取 img 对应的 DOM 元素const pic = document.querySelector('.pic')// 2. 修改属性pic.src = './images/lion.webp'pic.width = 400;pic.alt = '图片不见了...'
</script>
控制样式属性
style
- 应用【修改样式】,通过修改行内样式
style
属性,实现对样式的动态修改。
通过元素节点获得的 style
属性本身的数据类型也是对象,如 box.style.color
、box.style.width
分别用来获取元素节点 CSS 样式的 color
和 width
的值。
<!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><div class="box">随便一些文本内容</div><script>// 获取 DOM 节点const box = document.querySelector('.intro')box.style.color = 'red'box.style.width = '300px'// css 属性的 - 连接符与 JavaScript 的 减运算符// 冲突,所以要改成小驼峰法box.style.backgroundColor = 'pink'box.style.borderTop = '2px solid red'</script>
</body>
</html>
任何标签都有 style
属性,通过 style
属性可以动态更改网页标签的样式,如要遇到 css
属性中包含字符 -
时,要**将 -
去掉并将其后面的字母改成大写**,如 background-color
要写成 box.style.backgroundColor
className
- 操作类名(className) 操作CSS
如果修改的样式比较多,直接通过style属性修改比较繁琐,我们可以通过借助于css类名的形式。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>练习 - 修改样式</title><style>.pink {background: pink;color: hotpink;}</style>
</head>
<body><div class="intro">随便一些文本内容</div><script>// 获取 DOM 节点const box = document.querySelector('.intro')box.className = 'pink'</script>
</body>
</html>
注意:
1.由于class是关键字, 所以使用className去代替
2.className是使用新值换旧值, 如果不保留的话新的类名会覆盖掉旧的类名,如果需要添加一个类,需要保留之前的类名
保留方式:
<body><div class = 'nav'>123</div><script>const div = document.querySelector('div')div.className = 'nav box'</script>
</body>
classList
- 通过 classList 操作类控制CSS
为了解决className 容易覆盖以前的类名,我们可以通过classList方式追加和删除类名
<head><style>div {width: 200px;height: 200px;background-color: pink;}.active {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('active')// remove() 移除 类box.classList.remove('one')// 切换类togglre(),有就删掉,没有就加上box.classList.toggle('one')</script>
</body></html>
- 追加add() 类名不加点,并且是字符串
操作表单元素属性
表单很多情况,也需要修改属性,比如点击眼睛,可以看到密码,本质是把表单类型转换为文本框
正常的有属性有取值的跟其他的标签属性没有任何区别
获取:DOM对象.属性名
设置:DOM对象.属性名= 新值
注:不能用innerHTML去得到表单的内容,innerHTML只能得到普通的元素的内容,除了表单
标准属性: 标签天生自带的属性 比如class id title等, 可以直接使用点语法操作比如: disabled、checked、selected(一律使用布尔值表示)
<body><input type="text" value="请输入"><button disabled>按钮</button><input type="checkbox" name="" id="" class="agree"><script>// 1. 获取元素let input = document.querySelector('input')// 2. 取值或者设置值 得到input里面的值可以用 value,不能用innerHTMLconsole.log(input.value)input.value = '小米手机'input.type = 'password'// 2. 启用按钮let btn = document.querySelector('button')// disabled 不可用 = false 这样可以让按钮启用btn.disabled = false// 3. 勾选复选框let checkbox = document.querySelector('.agree')checkbox.checked = truecheckbox.checked = 'true'//会选中,但只是因为字符串除了空字符串,都是true,这里发生了隐式转换,会选中但不提倡//比如说checkbox.checked = 'false'//也会选中</script>
</body></html>
注意:表单中button是特殊的,所以获取botton的内容使用innerHTML
自定义属性
自定义属性:
<!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>
间歇函数setInterval
知道间歇函数的作用,利用间歇函数创建定时任务。
setInterval
是 JavaScript 中内置的函数,它的作用是间隔固定的时间自动重复执行另一个函数,也叫定时器函数。
setInterval(函数名/匿名函数,间隔时间)
//不加小括号,加括号是调用的意思
//毫秒
<script>// 1. 定义一个普通函数function repeat() {console.log('不知疲倦的执行下去....')}// 2. 使用 setInterval 调用 repeat 函数// 间隔 1000 毫秒==1s,重复调用 repeatsetInterval(repeat, 1000)
</script>
注意:
- 函数名字不需要加括号
- 定时器返回的是一个id数字
关闭定时器clearInterval
let 变量名 = setInterval(函数,间隔时间)
clearInterval(变量名)
事件
事件是编程语言中的术语,它是用来描述程序的行为或状态的,一旦行为或状态发生改变,便立即调用一个函数。
例如:用户使用【鼠标点击】网页中的一个按钮、用户使用【鼠标拖拽】网页中的一张图片
事件监听addEventListener
结合 DOM 使用事件时,需要为 DOM 对象添加事件监听,等待事件发生(触发)时,便立即调用一个函数。
元素对象.addEventListener('事件类型', 要执行的函数)
//元素对象就是事件源
//要执行的函数时事件回调
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
代表鼠标双击。
事件处理程序
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>
结论:【事件处理程序】决定了事件触发后应该执行的逻辑。
事件监听版本
-
DOM L0
-
只能做冒泡不能做捕获
-
L0 L1相同的事件只能绑定一次
-
因为这种方式的实质是采用的一种赋值的方式
事件源.on事件=function(){}
-
-
DOM L2
-
事件源.addEventListener('事件',事件处理函数)
-
L2 L3 相同的事件类型可以绑定多次
-
-
区别:
- on方式会被覆盖,addEventListener方式可绑定多次,拥有事件更多特性,推荐使用
-
发展史:
DOM LO:是DOM的发展的第一个版本;L:level
DOM L1:DOM级别1于1998年10月1日成为W3C推荐标准
DOM L2:使用addEventListener注册事件
DOM L3:DOM3级事件模块在DOM2级事件的基础上重新定义了这些事件,也添加了一些新事件类型
事件类型
将众多的事件类型分类可分为:鼠标事件、键盘事件、表单事件、焦点事件等,我们逐一展开学习。
鼠标事件
鼠标事件是指跟鼠标操作相关的事件,如单击、双击、移动等。
click
鼠标点击
mouseenter
监听鼠标是否移入 DOM 元素
mounseleave
鼠标
<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';})</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>
鼠标经过事件的区别:
- mouseover和mouseout会有冒泡效果
- mouseenter和mouseleave没有冒泡效果(拓建)
键盘事件
-
keydown 键盘按下触发
-
keyup 键盘抬起触发
焦点事件
-
focus 获得焦点
-
blur 失去焦点
文本框输入事件
- input (输入的时候触发)
事件对象
任意事件类型被触发时与事件相关的信息会被以对象的形式记录下来,我们称这个对象为事件对象。
事件回调函数的第1个参数即所谓的事件对象
<script>// 获取 .box 元素const box = document.querySelector('.box')// 添加事件监听box.addEventListener('click', function (e) {console.log('任意事件类型被触发后,相关信息会以对象形式被记录下来...');// 事件回调函数的第1个参数即所谓的事件对象console.log(e)})
</script>
事件回调函数的【第1个参数】即所谓的事件对象,通常习惯性的将这个对数命名为 event
、e
、ev
。
接下来简单看一下事件对象中包含了哪些有用的信息:
ev.type
当前事件的类型ev.clientX/Y
光标相对浏览器窗口的位置ev.offsetX/Y
光标相于当前 DOM 元素的位置ev.key
用户按下的键盘键的值
注:在事件回调函数内部通过 window.event 同样可以获取事件对象。
环境对象
环境对象指的是函数内部特殊的变量 this
,它代表着当前函数运行时所处的环境。
<script>// 声明函数function sayHi() {// this 是一个变量console.log(this);}// 声明一个对象let user = {name: '张三',sayHi: sayHi // 此处把 sayHi 函数,赋值给 sayHi 属性}let person = {name: '李四',sayHi: sayHi}// 直接调用sayHi() // windowwindow.sayHi() // window// 做为对象方法调用user.sayHi()// userperson.sayHi()// person
</script>
结论:
this
本质上是一个变量,数据类型为对象- 每个函数里面都有环境对象this
- 函数的调用方式不同
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>
结论:
- 回调函数本质还是函数,只不过把它当成参数使用
- 使用匿名函数做为回调函数比较常见
一个const和作用域以及事件监听发生的tip
代码:
const num = 10
const btn = document.querySelector('botton')
btn.addEventListener('click',function(){const num = Math.random()console.log(num)
})
- 这个代码不会报错
- 第1个tip:全局的const为什么不与函数内的const冲突
- 由于第一个num虽然是全局作用域,但第二num在函数这个局部区域里面,只在内部有效与外面的不冲突
- 第2个tip:函数内部,被一直反复生成随机数的num为什么不会报错
- 在函数内部,执行完函数之后有一个垃圾回收机制,当里面的变量不再使用的时候会自动回收,下一次点击会产生一个新的num
处理字符串的好用方法
- 万一用户不小心输入空格过多trim()方法
- 只会去除字符串左右两边
const str = ' pi nk '
console.log(str.trim())
//实际显示pi nk
事件流
事件流是对事件执行过程的描述,了解事件的执行过程有助于加深对事件的理解,提升开发实践中对事件运用的灵活度。
如上图所示,任意事件被触发时总会经历两个阶段:【捕获阶段】和【冒泡阶段】。
简言之,捕获阶段是【从父到子】的传导过程,冒泡阶段是【从子向父】的传导过程。
捕获和冒泡
事件流是如何影响事件执行的:
<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>
执行上述代码后发现,当单击事件触发时,其祖先元素的单击事件也【相继触发】,这是为什么呢?
结合事件流的特征,我们知道当某个元素的事件被触发时,事件总是会先经过其祖先才能到达当前元素,然后再由当前元素向祖先传递,事件在流动的过程中遇到相同的事件便会被触发。
再来关注一个细节就是事件相继触发的【执行顺序】,事件的执行顺序是可控制的,即可以在捕获阶段被执行,也可以在冒泡阶段被执行。
如果事件是在冒泡阶段执行的,我们称为冒泡模式,它会先执行子盒子事件再去执行父盒子事件,默认是冒泡模式。
如果事件是在捕获阶段执行的,我们称为捕获模式,它会先执行父盒子事件再去执行子盒子事件。
事件冒泡概念:
- 当一个元素的事件被触发时,同样的事将会在该元素的所有祖先元素中依次被触发。=》当一个元素触发事件后,会依次向上调用所有腹肌元素的同名事件
- L2事件监听第三个参数是false,或者默认都是冒泡
<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 不支持捕获)
- L0事件没有捕获
阻止冒泡
阻止冒泡是指阻断事件的流动,保证事件只在当前元素被执行,而不再去影响到其对应的祖先元素。
语法:
事件对象.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 没有冒泡效果 (推荐)
阻止默认行为
某些情况下需要阻止默认行为的发生,比如阻止链接的跳转,表单域跳转
-
语法:
-
事件对象.preventDefault()
-
<form action="http://www.baidu.com"> <input type="submit" value=""> </form> <script> const form = document.querySelector('form') form.addEventListener('click', function (e) { // 阻止表单默认提交行为 e.preventDefault() })
-
解绑事件
on事件方式,直接使用null覆盖就可以实现事件的解绑
-
语法:
//绑定事件 btn.onclick = function(){alert('点击了') } //解绑事件 btn.onclick = null
addEventListener方式,必须用:
-
语法:
removeEventListener(事件类型,事件处理函数[,获取捕获或者冒泡阶段]) //中括号意味着可写可不写
-
匿名函数无法被解绑
两种注册事件的区别
- 传统or注册(L0)
- 同一个对象,后面注册的事件会覆盖前面注册(同一个事件)
- 直接使用null覆盖就可以实现事件的解绑
- 都是冒泡阶段执行的
- 事件监听注册(L2)
- 语法:addEventListener(事件类型,事件处理函数,是否使用捕获)
- 后面注册的事件不会覆盖前面注册的事件(同一个事件)
- 可以通过第三个参数去确定是在冒泡或者捕获阶段执行
- 必须使用removeEventListener(事件类型,事件处理函数,获取捕获或冒泡阶段)
- 匿名函数无法被解绑
事件委托
事件委托是利用事件流的特征解决一些现实开发需求的知识技巧,主要的作用是提升程序效率。
- 减少注册次数
原理:
- 委托给父元素
- 利用事件冒泡的特点
- 给父元素注册事件,当我们触发子元素的时候,会冒泡到父元素身上,从而触发父元素的事件
- 事件的冒泡模式总是会将事件流向其父元素的
- 如果父元素监听了相同的事件类型,那么父元素的事件就会被触发并执行,正是利用这一特征对上述代码进行优化
大量的事件监听是比较耗费性能的,如下代码所示
<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 元素共同的祖先元素是 tablelet parents = document.querySelector('table');parents.addEventListener('click', function () {console.log('点击任意子元素都会触发事件...');})
</script>
我们的最终目的是保证只有点击 button 子元素才去执行事件的回调函数,如何判断用户点击是哪一个子元素呢?
事件对象中的属性 target
或 srcElement
属性表示真正触发事件的元素,它是一个元素类型的节点。
- 通过事件对象.target操作子元素
- 但如果只想对其中某一类标签有操作配合tagName
- 实现:事件对象.target.tagName 可以获得真正触发事件的元素(注意大写)
<script>// 假设页面中有 10000 个 button 元素const buttons = document.querySelectorAll('table button')// 假设上述的 10000 个 buttom 元素共同的祖先元素是 tableconst parents = document.querySelector('table')parents.addEventListener('click', function (ev) {// console.log(ev.target);// 只有 button 元素才会真正去执行逻辑if(ev.target.tagName === 'BUTTON') {// 执行的逻辑,注意大写}})
</script>
重要例子tab栏
重要例子tab栏
其他事件
页面加载事件
加载外部资源(如图片、外联CSS和JavaScript等)加载完毕时触发的事件
有些时候需要等页面资源全部处理完了做一些事情
事件名:load
监听页面所有资源加载完毕:
- 给window 添加 load事件
- 不光可以监听整个页面加载完毕,也可以针对某个资源绑定load事件
//等待页面所有资源加载完毕,就回去执行回调函数
window.addEventListener('load', function() {// xxxxx
})
当初始的HTML文档被完全加载和解析完成之后,DOMContentLoaded事件被触发,而无需等待样式表、图像等完全加载
事件名:DOMContentLoaded
监听页面DOM加载完毕:
- 给document 添加 DOMContentLoaded
document.addEventListener('DOMContentLoaded',function(){//执行操作
})
元素滚动事件
滚动条在滚动的时候持续触发的事件
事件名:scroll
监听整个页面滚动:
window.addEventListener('scroll', function() {// xxxxx
})
- 给window 或 document 添加 scroll事件都可
- 监听某个元素的内部滚动直接给某个元素即可
获取位置
scrollLeft 和 scrollTop(属性)
- 获取被卷去的大小
- 获取元素内容往左、往上滚出去看不到的距离
- 这两个值是**可读写**的
获取被卷去的高度,宽度(可读)
... = document.documentElement.scrollTop
... = document.documentElement.scrollLeft
- 注意也可以写
滚动到指定位置
scrollTo()方法可把内容滚动到指定的坐标
语法:
window.scrollTo(x,y)
- 这个是方法
页面尺寸事件
resize 是会在窗口尺寸改变的时候触发事件:
window.addEventListener('resize', function() {// xxxxx
})
获取元素宽高
获取元素的可见部分宽高(不包含边框、margin、滚动条等)
clientWidth 和 clientHeight
元素尺寸与位置
获取宽高:
-
获取元素的自身宽高、包含元素自身设置的宽高、padding、border(内容+padding+border)
-
offsetWidth 和 offsetHeight
-
获取出来的是数值,方便计算
-
注意: 获取的是可视宽高, 如果盒子是隐藏的,获取的结果是0
获取位置:
- 获取元素自己距离带有定位的父级元素的左、上距离
- offsetLeft 和 offsetTop
- 注意:是只读属性
尺寸
获取位置:
-
语法:
element.getBoudingClientRect()
-
方法返回元素的大小及其相对于视口(可视区)的位置
属性 | 作用 | 说明 |
---|---|---|
scrollLeft 和 scrollTop | 被卷去的头部和左侧 | 配合页面滚动来用,可读写 |
clientWidth 和 clientHeight | 获得元素宽度和高度 | 不包含border,margin,滚动条;用于JS获取元素大小,只读属性 |
offsetWidth和offsetHeight | 获得元素宽度和高度 | 包含border,padding,滚动条等,只读 |
offLeft和offTop | 获取元素自己距离定位父级元素的左、上距离 | 获取元素位置的时候用,只读属性 |
综合案例-电梯导航
需求:点击不同的模块,页面可以自动跳转不同的位置
模块分析:
- 页面滚动到对应位置,导航显示,否则隐藏模块
- 点击导航对应小模块,页面 会跳到对应大模块位置
- 页面滚动到对应位置,电梯导航对应模块自动发生变化
模块分析:
- 显示隐藏电梯盒子和返回顶部已经完成,可以放到自执行函数里面,防止变量污染
- 电梯模块单独放到自执行函数里面
- 点击每个模块,页面自动滚动到对应模块,使用事件委托方法更加简单
- 页面滚动到对应位置,电梯导航对应模块自动发生变化
日期对象
用来表示时间的对象
ECMAScript 中内置了获取系统时间的对象 Date,使用 Date 时与之前学习的内置对象 console 和 Math 不同,它需要借助 new 关键字才能使用。
实例化
用四大发明中的印刷术来比喻:
定义一个类=》制造一个印刷模板
实例化=》印刷在纸张上;
内存=》纸张
每实例化一次就是印刷一次。相对应的在计算机中会在内存里面分配个空间来存储(纸张);
实例化概念:
实例化是指在面向对象的编程中,把类创建对象的过程称为实例化。是将一个抽象的概念类,具体到该类实物的过程。
const date = new Date();
// 1. 实例化// const date = new Date(); // 系统默认时间const date = new Date('2020-05-01') // 指定时间// date 变量即所谓的时间对象console.log(typeof date)
方法
// 1. 实例化,获得日期对象
const date = new Date();
// 2. 调用时间对象方法
// 通过方法分别获取年、月、日,时、分、秒
const year = date.getFullYear(); // 四位年份
const month = date.getMonth(); // 0 ~ 11
方法 | 作用 | 说明 |
---|---|---|
getFullYear() | 获得年份 | 获取四位年份 |
getMonth () | 获取月份 | 取值为 0 ~ 11 |
getDate() | 获取月份中的每一天 | 不同月份取值也不相同 |
getDay() | 获取星期 | 取值为 0 ~ 6 |
getHours() | 获取小时 | 取值为 0 ~ 23 |
getMinutes() | 获取分钟 | 取值为 0 ~ 59 |
getSeconds () | 获取秒 | 取值为 0 ~ 59 |
注意:
-
月份0~11和星期,所以需要加1
const date = new Date() date.getMonth()+1
实时时间的两种写法
但是会有一秒空白因为是间隔函数
所以可以这样写
div.innnerHTML = getMyDate()
setInterval(function(){div.innerHTML = getMyDate()
},1000)
第二种写法:
const div = document.querrySelector('div')
const date = new Date()
div.inner.HTML = date.toLocalString()//打印:2024/6/11 13:46:51
date.toLocalDateString()//打印:2024/6/11
date.toLocalTimeString()//打印:13:46:08
所以第二种写法:
const div = document.querrySelector('div')div.innnerHTML = date.toLocalString()
setInterval(function(){const date = new Date()div.innerHTML = date.toLocalString()
})
时间戳
使用场景:倒计时
算法:
- 将来的时间戳 - 现在的时间戳 = 剩余时间毫秒数
- 剩余时间毫秒数 转换为 剩余时间的年月日时分秒 就是 倒计时时间
- 1000ms = 1s
时间戳是指1970年01月01日00时00分00秒起至现在的总秒数或毫秒数,它是一种特殊的计量时间的方式。
注:ECMAScript 中时间戳是以毫秒计的。
获取时间戳的方法:
// 方法一:需要实例化,获得日期对象const date = new Date()console.log(date.getTime())
// 方法二:不需要实例化console.log(+new Date())
// 方法三:不需要实例化,但是只能得到当前的时间戳,前面两种可以返回指定时间的时间戳console.log(Date.now())
获取时间戳的方法,分别为 getTime 和 Date.now 和 **+new Date() **
- 通过时间戳得到是亳秒,需要转换为秒在计算
- 转换公式:
d=parselnt(总秒数/60/60/24);//计算天数
h=parselnt(总秒数/60/60%24)//计算小时m=parselnt(总秒数/60%60);// 计算分数
s=parselnt(总秒数%60);// 计算当前秒数
DOM 节点
回顾之前 DOM 的操作都是针对元素节点的属性或文本的,除此之外也有专门针对元素节点本身的操作,如插入、复制、删除、替换等。
- DOM树里每一个内容等称之为节点
- 节点类型:
- 元素节点:所有的标签(html是根节点)
- 属性节点:所有的属性,比如href
- 文本节点:所有的文本
- 其他
插入节点
追加节点:
-
要想在界面看到,还得插入到某个父元素中
-
插入到父元素的最后一个子元素
父元素.appendChild(要插入的元素)
-
插入到父元素的第一个元素
父元素.insertBefore(要插入的元素,放到哪个元素的前面)
在已有的 DOM 节点中插入新的 DOM 节点时,需要关注两个关键因素:首先要得到新的 DOM 节点,其次在哪个位置插入这个节点。
如下代码演示:
<body><h3>插入节点</h3><p>在现有 dom 结构基础上插入新的元素节点</p><hr><!-- 普通盒子 --><div class="box"></div><!-- 点击按钮向 box 盒子插入节点 --><button class="btn">插入节点</button><script>// 点击按钮,在网页中插入节点const btn = document.querySelector('.btn')btn.addEventListener('click', function () {// 1. 获得一个 DOM 元素节点const p = document.createElement('p')p.innerText = '创建的新的p标签'p.className = 'info'// 复制原有的 DOM 节点const p2 = document.querySelector('p').cloneNode(true)p2.style.color = 'red'// 2. 插入盒子 box 盒子document.querySelector('.box').appendChild(p)document.querySelector('.box').appendChild(p2)})</script>
</body>
结论:
-
createElement
动态创建任意 DOM 节点 -
cloneNode
复制现有的 DOM 节点,传入参数 true 会复制所有子节点 -
appendChild
在末尾(结束标签前)插入节点
克隆节点:
-
//克隆一个已有的元素节点 元素.cloneNode(布尔值)
cloneNode会克隆出一个跟原标签一样的元素,括号内传入布尔值
- 若为true,则代表克隆时会包含后代节点一起克隆(深克隆)
- 若为false,则代表克隆时不包含后代节点
- 默认为false(浅克隆)
再来看另一种情形的代码演示:
<body><h3>插入节点</h3><p>在现有 dom 结构基础上插入新的元素节点</p><hr><button class="btn1">在任意节点前插入</button><ul><li>HTML</li><li>CSS</li><li>JavaScript</li></ul><script>// 点击按钮,在已有 DOM 中插入新节点const btn1 = document.querySelector('.btn1')btn1.addEventListener('click', function () {// 第 2 个 li 元素const relative = document.querySelector('li:nth-child(2)')// 1. 动态创建新的节点const li1 = document.createElement('li')li1.style.color = 'red'li1.innerText = 'Web APIs'// 复制现有的节点const li2 = document.querySelector('li:first-child').cloneNode(true)li2.style.color = 'blue'// 2. 在 relative 节点前插入document.querySelector('ul').insertBefore(li1, relative)document.querySelector('ul').insertBefore(li2, relative)})</script>
</body>
结论:
-
createElement
动态创建任意 DOM 节点 -
cloneNode
复制现有的 DOM 节点,传入参数 true 会复制所有子节点 -
insertBefore
在父节点中任意子节点之前插入新节点
删除节点
-
在JS的原生DOM操作中,要删除元素必须通过父元素删除
-
语法
父元素.removeChild(要删除的元素)
-
注意:
- 如果不存在父子关系则删除不成功
- 删除节点和隐藏节点(display:none)是有区别的,隐藏节点还是存在的,但是删除,则从html中删除系欸点
删除现有的 DOM 节点,也需要关注两个因素:首先由父节点删除子节点,其次是要删除哪个子节点。
<body><!-- 点击按钮删除节点 --><button>删除节点</button><ul><li>HTML</li><li>CSS</li><li>Web APIs</li></ul><script>const btn = document.querySelector('button')btn.addEventListener('click', function () {// 获取 ul 父节点let ul = document.querySelector('ul')// 待删除的子节点let lis = document.querySelectorAll('li')// 删除节点ul.removeChild(lis[0])})</script>
</body>
结论:removeChild
删除节点时一定是由父子关系。
查找节点
DOM 树中的任意节点都不是孤立存在的,它们要么是父子关系,要么是兄弟关系,不仅如此,我们可以依据节点之间的关系查找节点。
父子关系
父节点查找:
-
parentNode
-
返回最近一级的父节点(返回dom对象),找不到返回null
子元素.parentNode
子节点查找:
-
childNodes
- 获得所有子节点,包括文本节点(空格,换行)、注释节点等
-
children
- 仅获得所有元素节点
- 返回的还是一个伪数组
父元素.children
<body><button class="btn1">所有的子节点</button><!-- 获取 ul 的子节点 --><ul><li>HTML</li><li>CSS</li><li>JavaScript 基础</li><li>Web APIs</li></ul><script>const btn1 = document.querySelector('.btn1')btn1.addEventListener('click', function () {// 父节点const ul = document.querySelector('ul')// 所有的子节点console.log(ul.childNodes)// 只包含元素子节点console.log(ul.children)})</script>
</body>
结论:
childNodes
获取全部的子节点,回车换行会被认为是空白文本节点children
只获取元素类型节点
<body><table><tr><td width="60">序号</td><td>课程名</td><td>难度</td><td width="80">操作</td></tr><tr><td>1</td><td><span>HTML</span></td><td>初级</td><td><button>变色</button></td></tr><tr><td>2</td><td><span>CSS</span></td><td>初级</td><td><button>变色</button></td></tr><tr><td>3</td><td><span>Web APIs</span></td><td>中级</td><td><button>变色</button></td></tr></table><script>// 获取所有 button 节点,并添加事件监听const buttons = document.querySelectorAll('table button')for(let i = 0; i < buttons.length; i++) {buttons[i].addEventListener('click', function () {// console.log(this.parentNode); // 父节点 td// console.log(this.parentNode.parentNode); // 爷爷节点 trthis.parentNode.parentNode.style.color = 'red'})}</script>
</body>
结论:parentNode
获取父节点,以相对位置查找节点,实际应用中非常灵活。
兄弟关系
下一个兄弟节点:
- nextElementSibling 属性
上一个兄弟节点:
- previousElementSibling 属性
<body><ul><li>HTML</li><li>CSS</li><li>JavaScript 基础</li><li>Web APIs</li></ul><script>// 获取所有 li 节点const lis = document.querySelectorAll('ul li')// 对所有的 li 节点添加事件监听for(let i = 0; i < lis.length; i++) {lis[i].addEventListener('click', function () {// 前一个节点console.log(this.previousSibling)// 下一下节点console.log(this.nextSibling)})}</script>
</body>
结论:
previousSibling
获取前一个节点,以相对位置查找节点,实际应用中非常灵活。nextSibling
获取后一个节点,以相对位置查找节点,实际应用中非常灵活。
M端事件
移动端也有自己独特的地方。比如触屏事件touch(也称触摸事件),Android和IOS都有。
-
触屏事件touch(也称触摸事件),Android和IOS都有。
-
touch 对象代表一个触摸点。触摸点可能是一根手指,也可能是一根触摸笔。触屏事件可响应用户手指(或触控笔)对屏幕或者触控板操作。
-
常见的触屏事件如下:
触屏touch事件 | 说明 |
---|---|
touchstart | 手指触摸到一个DOM元素时触发 |
touchmove | 手指在一个DOM元素上滑动时触发 |
touchend | 手指从一个DOM元素上移开时触发 |
插件
插件:就是别人写好的一些代码,我们只需要复制对应的代码,就可以直接实现对应的效果
-
学习插件的基本过程
-
熟悉官网,了解这个插件可以完成什么需求 https://www.swiper.com.cn/
-
看在线演示,找到符合自己需求的demo https://www.swiper.com.cn/demo/index.html
-
看在线演示,找到符合自己需求的demo https://www.swiper.com.cn/usage/index.html
-
查看APi文档,去配置自己的插件 https://www.swiper.com.cn/api/index.html
-
注意:多个swiper同时使用的时候,类名需要注意区分
学生信息表案例
核心思路:
-
声明一个空的数组
-
点击录入,根据相关数据,生成对象,追加到数组里面
-
根据数组数据渲染页面-表格的行
-
点击删除按钮,删除的是对应数组里面的数据
-
再次根据数组的数据,渲染页面
录入模块:
核心思路:
1:声明一个空的数组
2:点击录入模块
(1).首先取消表单默认提交事件
(2). 创建新的对象,里面存储 表单获取过来的数据,格式如右图
(3). 追加给数组
(4). 渲染数据。遍历数组,动态生成tr,里面填写对应td数据,并追加给tbody
(5). 重置表单
(6). 注意防止多次生成多条数据,先清空tbody
3:点击删除模块
(1).采用事件委托形式,给tbody注册点击事件
(2). 点击链接,要删除的是对应数组里面的这个数据,而不是删除dom节点,如何找到这个数据?
(3). 前面渲染数据的时候,动态给a链接添加 自定义属性data-id=“0”,这样点击当前对象就知道索引号了
(4).根据索引号,利用splice删除这条数据
(5). 重新渲染
4:点击新增需要验证表单
(1).获取所有需要填写的表单,他们共同特点都有name属性
(2).遍历这些表单,如果有一个值为空,则return返回提示输入为空中断程序
(3).注意书写的位置,应该放到新增数据的前面(因为如果不满足条件就中断程序不录入),阻止默认行为的后面(因为一点击就判断了,先阻止默认行为再判断)
模块分析2:
点击每个模块,页面自动滚动到对应模块,使用事件委托方法更加简单
- 点击小模块,当前添加active这个类
- 解决处理初次获取不到active报错的问题
解决方案:
-
不能直接获取这个类,然后移除,这样会报错
-
先获取这个类,然后加个判断
-
如果有这个类,就移除
-
如果没有这个类,返回为null,就不执行移除,就不报错了
-
让页面滑动:
//给滚动条添加滑动效果
html {scroll-behavior: smooth;
}
需求:
点击不同的模块,页面可以自动跳转不同的位置
模块分析3:页面滚动到大盒子位置,电梯导航小盒子对应模块自动处于选中状态
-
当页面滚动了,先移除所有小li的状态
-
因为页面滚动需要不断获取大盒子的位置,所以需要把所有的大盒子都获取过来
-
开始进行滚动判断
-
如果页面滚动大于 新鲜好物大盒子的offsetTop 并且小于 人气推荐盒子的offsetTop就把
-
对应的小盒子先出来添加类
-
依次类推
-
最后一个,如果大于等于最新专题模块,就选出最后一个对应小盒子(更精确)
-
js组成
JavaScript的组成
-
ECMAScript:
- 规定了js基础语法核心知识。
- 比如:变量、分支语句、循环语句、对象等等
-
Web APIs :
- DOM 文档对象模型, 定义了一套操作HTML文档的API
- BOM 浏览器对象模型,定义了一套操作浏览器窗口的API
window对象
BOM(浏览器对象模型)
BOM里面包含着DOM
(Browser Object Model ) 是浏览器对象模型
- window对象是一个全局对象,也可以说是JavaScript中的顶级对象
- 像document、alert()、console.log()这些都是window的属性,基本BOM的属性和方法都是window的
- 所有**通过var定义在全局作用域中的变量、函数都会变成window对象的属性和方法**
- window对象下的属性和方法调用的时候可以省略window
定时器-延迟函数setTimeout
JavaScript 内置的一个用来让代码延迟执行的函数,叫 setTimeout
语法:
setTimeout(回调函数, 延迟时间)
setTimeout 仅仅只执行一次,所以可以理解为就是把一段代码延迟执行, 平时省略window
间歇函数 setInterval : 每隔一段时间就执行一次, , 平时省略window
清除延时函数:
clearTimeout(timerId)
注意点
- 延时函数需要等待,所以后面的代码先执行
- 返回值是一个正整数,表示定时器的编号
<body><script>// 定时器之延迟函数// 1. 开启延迟函数let timerId = setTimeout(function () {console.log('我只执行一次')}, 3000)// 1.1 延迟函数返回的还是一个正整数数字,表示延迟函数的编号console.log(timerId)// 1.2 延迟函数需要等待时间,所以下面的代码优先执行// 2. 关闭延迟函数clearTimeout(timerId)</script>
</body>
JS执行机制
浏览器有两个引擎:1.渲染引擎 2.JS解析器
经典两道题
console.log(111)
setTimeout(function(){console.log(222)
},1000)
console.log(333)
//打印:132
注意另一道题
console.log(111)
setTimeout(function(){console.log(222)
},0)
console.log(333)
//结果仍然是132!!!
JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。
这是因为Javascript这门脚本语言诞生的使命所致 – JavaScript是为处理页面中用户的交互,以及操作
**DOM而诞生的。**比如我们对某个DOM元素进行添加和删除操作,不能同时进行。应该先进行添加,之后
再删除。
单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。这样所导致的问题是:如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。
为了解决这个问题,利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程。于是JS中出现了同步和异步。
同步
前一个任务结束后再执行后一个任务,程序的执行顺序与任务的排列顺序是一致的、同步的。比如做饭的同步做法:我们要烧水煮饭,等水开了(10分钟之后),再去切菜,炒菜。
异步
你在做一件事情时,因为这件事情会花费很长时间,在做这件事的同时,你还可以去处理其他事情。比如做饭的异步做法,我们在烧水的同时,利用这10分钟,去切菜,炒菜。
同步任务
同步任务都在主线程上执行,形成一个**执行栈**。
异步任务
JS的异步是通过回调函数实现的。
一般而言,异步任务有以下三种类型:
1、普通事件,如click、resize等
2、资源加载,如load、error等
3、定时器,包括setlnterval、setTimeout等
异步任务相关添加到**任务队列中(任务队列也称为消息队列**)。
总的执行机制(事件循环):
- 先执行执行栈中的同步任务
- 异步任务放入任务队列中
- 一旦执行栈中的所有同步任务执行完毕,系统就会按次序读取任务队列中的异步任务,于是被读取的异步任务结束等待状态,进入执行栈,开始执行
- 浏览器可以多线程
- 由于主线程不断地重复获得任务,执行任务,再获取任务,再执行,所以这种机制被称为**事件循环(event loop)**
location对象
- location的数据类型是对象
location (地址) 它拆分并保存了 URL 地址的各个组成部分, 它是一个对象
属性/方法 | 说明 |
---|---|
href | 属性,获取完整的 URL 地址,赋值时用于地址的跳转 |
search | 属性,获取地址中携带的参数,符号 ?后面部分 |
hash | 属性,获取地址中的哈希值,符号 # 后面部分 |
reload() | 方法,用来刷新当前页面=>f5,传入参数 true 时表示强制刷新=》相当于ctrl+f5 |
- location.search => 在网页中是上面那行地址
<body><form><input type="text" name="search"> <button>搜索</button></form><a href="#/music">音乐</a><a href="#/download">下载</a><button class="reload">刷新页面</button><script>// location 对象 // 1. href属性 (重点) 得到完整地址,赋值则是跳转到新地址console.log(location.href)// location.href = 'http://www.itcast.cn'// 2. search属性 得到 ? 后面的地址 console.log(location.search) // ?search=笔记本// 3. hash属性 得到 # 后面的地址console.log(location.hash)// 4. reload 方法 刷新页面const btn = document.querySelector('.reload')btn.addEventListener('click', function () {// location.reload() // 页面刷新location.reload(true) // 强制页面刷新 ctrl+f5})</script>
</body>
navigator对象
navigator是对象,该对象下记录了浏览器自身的相关信息
常用属性和方法:
- 通过 userAgent 检测浏览器的版本及平台
// 检测 userAgent(浏览器信息),放在<head></head>标签里面
(function () {const userAgent = navigator.userAgent// 验证是否为Android或iPhoneconst android = userAgent.match(/(Android);?[\s\/]+([\d.]+)?/)const iphone = userAgent.match(/(iPhone\sOS)\s([\d_]+)/)// 如果是Android或iPhone,则跳转至移动站点if (android || iphone) {location.href = 'http://m.itcast.cn'}})();
histroy对象
history (历史)是对象,主要管理历史记录, 该对象与浏览器地址栏的操作相对应,如前进、后退等
使用场景
history对象一般在实际开发中比较少用,但是会在一些OA 办公系统中见到。
常见方法:
<body><button class="back">←后退</button><button class="forward">前进→</button><script>// histroy对象// 1.前进const forward = document.querySelector('.forward')forward.addEventListener('click', function () {// history.forward() history.go(1)})// 2.后退const back = document.querySelector('.back')back.addEventListener('click', function () {// history.back()history.go(-1)})</script>
</body>
本地存储(今日重点)
常见的使用场景:
https://todomvc.com/examples/vanilla-es6/ 页面刷新数据不丢失
好处:
1、页面刷新或者关闭不丢失数据,实现数据持久化
2、容量较大,sessionStorage和 localStorage 约 5M 左右
localStorage(重点.本地存储)
存储数据语法:
- 如果原来有这个键,则是改
- 如果原来没有这个键,则是加
localStorage.setItem(key, value)
//键值对形式
获取数据语法:
localStorage.getItem(key)
//返回的就是值
删除数据语法:
localStorage.removeItem(key)
作用: 数据可以长期保留在本地浏览器中,刷新页面和关闭页面,数据也不会丢失
特性:以键值对的形式存储,并且存储的是字符串, 省略了windo
注意:
- 本地存储只能存储字符串数据类型,不管往里面存了什么都会变为字符串
- 取出来时候需要小心
<!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>本地存储-localstorage</title>
</head><body><script>// 本地存储 - localstorage 存储的是字符串// 1. 存储localStorage.setItem('age', 18)// 2. 获取console.log(typeof localStorage.getItem('age'))// 3. 删除localStorage.removeItem('age')</script>
</body></html>
sessionStorage(了解)
特性:
-
生命周期为关闭浏览器窗口
-
在同一个窗口(页面)下数据可以共享
-
用法跟localStorage基本相同
-
区别是:当页面浏览器被关闭时,存储在 sessionStorage 的数据会被清除
**存储:**sessionStorage.setItem(key,value)
**获取:**sessionStorage.getItem(key)
**删除:**sessionStorage.removeItem(key)
localStorage 存储复杂数据类型
**问题:**本地只能存储字符串,无法存储复杂数据类型
解决:需要将复杂数据类型转换成 JSON字符串,在存储到本地
语法:JSON.stringify(复杂数据类型)
JSON字符串:
- 首先是1个字符串
- 属性名使用双引号引起来,不能单引号
- 属性值如果是字符串型也必须双引号
<body><script>// 本地存储复杂数据类型const goods = {name: '小米',price: 1999}// localStorage.setItem('goods', goods)// console.log(localStorage.getItem('goods'))// 1. 把对象转换为JSON字符串 JSON.stringifylocalStorage.setItem('goods', JSON.stringify(goods))// console.log(typeof localStorage.getItem('goods'))</script>
</body>
**问题:**因为本地存储里面取出来的是字符串,不是对象,无法直接使用
**解决: **把取出来的字符串转换为对象
**语法:**JSON.parse(JSON字符串)
<body><script>// 本地存储复杂数据类型const goods = {name: '小米',price: 1999}// localStorage.setItem('goods', goods)// console.log(localStorage.getItem('goods'))// 1. 把对象转换为JSON字符串 JSON.stringifylocalStorage.setItem('goods', JSON.stringify(goods))// console.log(typeof localStorage.getItem('goods'))// 2. 把JSON字符串转换为对象 JSON.parseconsole.log(JSON.parse(localStorage.getItem('goods')))</script>
</body>
刷新和强制刷新
F5刷新
只是刷新一次当前页面的资源,使用F5刷新,浏览器仍然会重复使用之前的缓存数据
ctrl + F5强制刷新
这个时候浏览器就不会重复利用之前已经缓存的数据了,而是去清空缓存,把所有的资源进行重新下载,使网页与本机储存的网页时间标记相同
区别:
一个网页的资源都是下载到浏览器本地的,由于资源可能很大,会降低网络传输的速率,加载页面的速度可能会降低,进一步影响用户的使用感
所以浏览器就会把这些依赖的资源直接缓存到本地,后续访问的时候速度就很快,因为已经缓存了,不需要下载
刷新:
强制刷新:
加载的这些文件大小都来自于网络下载,而且显示了多大的文件。其实我们通过加载时间,也可以理解为什么浏览器会有缓存机制,使用F5刷新的时候,加载时间在1秒左右,但是使用ctrl + F5的时候,加载时间在3秒左右,这就能够理解浏览器为什么就会把这些依赖的资源直接缓存到本地,后续访问的时候速度就会很快。
总结:
- F5刷新只是请求本地资源重新加载。那么在这个时候,如果在某些业务场景的服务器资源发生了改变,那么这边的本地用户可能某些功能或产品访问不到。
- ctrl + F5 是直接请求服务器的资源,让当前页面的资源重新全部从服务器上下载下来,这样就全部更新了。
综合案例
学生就业统计表
渲染业务
根据持久化数据渲染页面
核心步骤:
1:读取 localstorage 本地数据
- 如果有数据则转换为对象放到变量里面一会使用它渲染页面
- 如果没有则用默认空数组[]
- 为了测试效果,咱们可以先把initData存入本地存储看效果
2.根据数据渲染页面。遍历数组,根据数组生成tr,里面填充数据,最后追加给tbody
- 数组中map + join方法渲染页面思路:
- map遍历数组处理数据生成tr,返回一个数组
步骤:
- 渲染业务要封装成一个函数 render
- 我们使用map方法遍历数组,里面更换数据,然后会返回 有数据的tr数组
- 通过join方法把map返回的数组转换为字符串
- 把字符串通过innerHTML赋值给tbody
新增业务
点击新增按钮,页面显示新的数据
核心步骤:
-
给form注册提交事件,要阻止默认提交事件(阻止默认行为)
事件对象.preventDefault()//阻止默认行为
-
非空判断
- 如果年龄、性别、薪资有一个值为空,则return返回”输入不能为空“中断程序
-
给arr数组追加对象,里面存储表单获取过来的数据
-
渲染页面和重置表单(reset()方法)
-
把数组数据存储到本地存储里面,利用**JSON.stringify()**存储为JSON字符串
删除业务
点击删除按钮,可以删除对应的数据
核心步骤:
- 采用事件委托形式,给tbody注册点击事件
- 得到当前点击的索引号/渲染数据的时候,动态给a链接添加自定义属性data-id="0 "
- 根据索引号,利用splice删除数组这条数据
- 重新渲染页面
- 把最新arr数组存入本地存储
关于stuId的处理
核心思路:
- 新增加序号应该是最后一条数据的stuId + 1
- 数组[数组的长度 - 1].stuId + 16
字符串拼接新思路
数组map 方法
使用场景:
map 可以遍历数组处理数据,并且返回新的数组
语法:
<body><script>const arr = ['red', 'blue', 'pink']// 1. 数组 map方法 处理数据并且 返回一个数组const newArr = arr.map(function (ele, index) {console.log(ele) // 数组元素console.log(index) // 索引号return ele + '颜色'})
console.log(newArr)//['red颜色','blue颜色','pink颜色']
</script>
</body>
- map 也称为映射。映射是个术语,指两个元素的集之间元素相互“对应”的关系。
- map重点在于有返回值,forEach没有返回值(undefined)
数组join方法
**作用:**join() 方法用于把数组中的所有元素转换一个字符串
语法:
<body><script>const arr = ['red', 'blue', 'pink']// 1. 数组 map方法 处理数据并且 返回一个数组const newArr = arr.map(function (ele, index) {// console.log(ele) // 数组元素// console.log(index) // 索引号return ele + '颜色'})console.log(newArr)// 2. 数组join方法 把数组转换为字符串// 小括号为空则逗号分割console.log(newArr.join()) // red颜色,blue颜色,pink颜色// 小括号是空字符串,则元素之间没有分隔符console.log(newArr.join('')) //red颜色blue颜色pink颜色console.log(newArr.join('|')) //red颜色|blue颜色|pink颜色</script>
</body>
正则表达式
正则表达式(Regular Expression)是一种字符串匹配的模式(规则),在JS中是一种对象,并非JS的私有
使用场景:
- 例如验证表单:手机号表单要求用户只能输入11位的数字 (匹配)
- 过滤掉页面内容中的一些敏感词(替换),或从字符串中获取我们想要的特定部分(提取)等
正则基本使用
-
定义规则
const reg = /表达式/
- 其中
/ /
是**正则表达式字面量** - 正则表达式也是
对象
- 其中
-
使用正则
test()方法
用来查看正则表达式与指定的字符串是否匹配- 如果正则表达式与指定的字符串匹配 ,返回
true
,否则false
<body><script>// 正则表达式的基本使用const str = 'web前端开发'// 1. 定义规则const reg = /web/// 2. 使用正则 test()console.log(reg.test(str)) // true 如果符合规则匹配上则返回trueconsole.log(reg.test('java开发')) // false 如果不符合规则匹配上则返回 false</script> </body>
-
检索(查找)符合规则的字符串:
-
**
exec()
**方法 在一个指定字符串中执行一个搜索匹配 -
语法:
reobj.exec(被检测字符串)
-
//要检测的字符串 const str = "IT培训,前端开发培训,web前端培训,java培训" //1.定义正则表达式,检测规则 const reg = /前端/ //2.检测方法 console.log(reg.exec(str))
-
如果匹配成功,exac()方法返回一个数组,否则返回null
-
-
第三种表达方式:
/哈/.text('哈') //true
元字符(特殊字符)
- 普通字符:
- 大多数的字符仅能够描述它们本身,这些字符称作普通字符,例如所有的字母和数字。
- 普通字符只能够匹配字符串中与它们相同的字符。
- 比如,规定用户只能输入英文26个英文字母,普通字符的话 /[abcdefghijklmnopqrstuvwxyz]/,但是换成元字符写法:[a-z]
- 元字符(特殊字符)
- 是一些具有特殊含义的字符,可以极大提高了灵活性和强大的匹配功能。
- 比如,规定用户只能输入英文26个英文字母,换成元字符写法: /[a-z]/
边界符
- 表示位置,开头和结尾,必须用声明开头,用什么结尾
正则表达式中的边界符(位置符)用来提示字符所处的位置,主要有两个字符
边界符 | 说明 |
---|---|
^ | 表示匹配行首的文本(以谁开始) |
$ | 表示匹配行尾的文本(以谁结束) |
- 如果 ^ 和 $ 在一起,表示必须是精确匹配
<body><script>// 元字符之边界符// 1. 匹配开头的位置 ^ ==>相当于必须以web开头const reg = /^web/console.log(reg.test('web前端')) // trueconsole.log(reg.test('前端web')) // falseconsole.log(reg.test('前端web学习')) // falseconsole.log(reg.test('we')) // false// 2. 匹配结束的位置 $const reg1 = /web$/console.log(reg1.test('web前端')) // falseconsole.log(reg1.test('前端web')) // trueconsole.log(reg1.test('前端web学习')) // falseconsole.log(reg1.test('we')) // false // 3. 精确匹配 ^ $const reg2 = /^web$/console.log(reg2.test('web前端')) // falseconsole.log(reg2.test('前端web')) // falseconsole.log(reg2.test('前端web学习')) // falseconsole.log(reg2.test('we')) // false console.log(reg2.test('web')) // trueconsole.log(reg2.test('webweb')) // flase </script>
</body>
量词
量词**用来设定某个模式重复次数**
量词 | 说明 |
---|---|
* | 重复零次或更多次 |
+ | 重复一次或更多次 |
? | 重复零次或一次 |
{n} | 重复n次 |
{n,} | 重复n次或更多次 |
{n,m} | 重复n到m次 |
- 注意: 逗号左右两侧千万不要出现空格
<body><script>// 元字符之量词// 1. * 重复次数 >= 0 次const reg1 = /^w*$/console.log(reg1.test('')) // trueconsole.log(reg1.test('w')) // trueconsole.log(reg1.test('ww')) // trueconsole.log(reg1.test('whw')) //false console.log('-----------------------')// 2. + 重复次数 >= 1 次const reg2 = /^w+$/console.log(reg2.test('')) // falseconsole.log(reg2.test('w')) // trueconsole.log(reg2.test('ww')) // trueconsole.log('-----------------------')// 3. ? 重复次数 0 || 1 const reg3 = /^w?$/console.log(reg3.test('')) // trueconsole.log(reg3.test('w')) // trueconsole.log(reg3.test('ww')) // falseconsole.log('-----------------------')// 4. {n} 重复 n 次const reg4 = /^w{3}$/console.log(reg4.test('')) // falseconsole.log(reg4.test('w')) // flaseconsole.log(reg4.test('ww')) // falseconsole.log(reg4.test('www')) // trueconsole.log(reg4.test('wwww')) // falseconsole.log('-----------------------')// 5. {n,} 重复次数 >= n const reg5 = /^w{2,}$/console.log(reg5.test('')) // falseconsole.log(reg5.test('w')) // falseconsole.log(reg5.test('ww')) // trueconsole.log(reg5.test('www')) // trueconsole.log('-----------------------')// 6. {n,m} n =< 重复次数 <= mconst reg6 = /^w{2,4}$/console.log(reg6.test('w')) // falseconsole.log(reg6.test('ww')) // trueconsole.log(reg6.test('www')) // trueconsole.log(reg6.test('wwww')) // trueconsole.log(reg6.test('wwwww')) // false// 7. 注意事项: 逗号两侧千万不要加空格否则会匹配失败</script>
范围
表示字符的范围,定义的规则限定在某个范围,比如只能是英文字母,或者数字等等,用表示范围
<body><script>// 元字符之范围 [] // 1. [abc] 匹配包含的单个字符, 多选1const reg1 = /^[abc]$/console.log(reg1.test('a')) // trueconsole.log(reg1.test('b')) // trueconsole.log(reg1.test('c')) // trueconsole.log(reg1.test('d')) // falseconsole.log(reg1.test('ab')) // false// 2. [a-z] 连字符 单个const reg2 = /^[a-z]$/console.log(reg2.test('a')) // trueconsole.log(reg2.test('p')) // trueconsole.log(reg2.test('0')) // falseconsole.log(reg2.test('A')) // false// 想要包含小写字母,大写字母 ,数字const reg3 = /^[a-zA-Z0-9]$/console.log(reg3.test('B')) // trueconsole.log(reg3.test('b')) // trueconsole.log(reg3.test(9)) // trueconsole.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')) // trueconsole.log(reg4.test('ABcd12')) // trueconsole.log(reg4.test('ABcd12_')) // true// 3. [^a-z] 取反符const reg5 = /^[^a-z]$/console.log(reg5.test('a')) // false console.log(reg5.test('A')) // trueconsole.log(reg5.test(8)) // true</script>
</body>
字符类
-
[]里面加上 - 连字符
- 使用连字符 - 表示一个范围
//字符类 [abc] 只选一个 console.log(/^[abc]$/.text('a'))//true console.log(/^[abc]$/.text('b'))//true console.log(/^[abc]$/.text('c'))//true console.log(/^[abc]$/.text('abc'))//falseconsole.log(/^[abc]{2}$/.text('ab'))//true //字符类 [a-z] 只选一个 console.log(/^[abc]$/.test('a')) console.log(/^[a-zA-Z0-9]$/.test(2))//true
-
一个小例子
腾讯qq号: /^[1-9][0-9]{4,}&/ //重复的是离{}最近的那个 //这样的话 console.log(/^嘻哈{4,}$/.test('嘻哈哈哈哈哈哈'))//是true还是false
-
[]里面加上^取反符号
- 比如:
- [^a-z]匹配除了小写字母以外的字符
- 注意要写到中括号里面
-
.匹配除换行符之外的任何单个字符
-
某些常见模式的简写方式,区分字母和数字(如下表)
预定类 | 说明 |
---|---|
\d | 匹配0-9之间的任一数字,相当于[0-9] |
\D | 匹配所有0-9以外的字符,相当于[ ^ 0-9] |
\w | 匹配任意的字母、数字和下划线,相当于[A-Za-z0-9_] |
\W | 匹配所有字母、数字、下划线以外的字符[ ^A-Za-z0-9_] |
\s | 匹配空格(包括换行符、制表符、空格符等),相当于[\t\r\n\v\f] |
\S | 匹配所有非空格的字符,相当于[ ^\t\r\n\v\f] |
替换和修饰符
replace 替换方法
-
可以完成字符的替换
-
语法:
/表达式/修饰符
<body><script>// 替换和修饰符const str = '欢迎大家学习前端,相信大家一定能学好前端,都成为前端大神'// 1. 替换 replace 需求:把前端替换为 web// 1.1 replace 返回值是替换完毕的字符串const strEnd = str.replace(/前端/, 'web') //只能替换一个,想要全局就用g修饰符</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>
注意:
在正则表达式中‘或’是一个竖线
也就是说
/java/i
//==>
/java|JAVA/
正则插件
change 事件
给input注册 change 事件,值被修改并且失去焦点后触发
判断是否有类
元素.classList.contains() 看看有没有包含某个类,如果有则返回true,么有则返回false
综合案例-小兔鲜页面注册
分析业务模块
- 发送验证码模块
- 各个表单验证模块
- 勾选已经阅读同意模块
- 下一步验证全部模块
- 只要上面有一个input验证不通过就不同意提交
发送验证码模块:
-
用户点击之后,显示05秒后重新获取时间到了,自动改为 重新获取
let i = 5code.addEventListener('click', function () {code.innerHTML = `0${i}秒后重新获取`const timeId = setInterval(function () {i--//this指向函数的调用者,window调用了定时器,这里的this指的是windowcode.innerHTML = `0${i}秒后重新获取`if (i === 0) {code.innerHTML = `发送验证码`clearInterval(timeId)}}, 1000)})
-
但会出现一个问题如果第一次点击后,但连续点击之后会加速不停计时
-
解决方法(节流阀):
// 1.发送短信验证码模块const code = document.querySelector('.code')let flag = true //通过一个变量来控制播放//1.1点击事件let i = 5code.addEventListener('click', function () {if (flag) {flag = false//取反了不能立马点击code.innerHTML = `0${i}秒后重新获取`const timeId = setInterval(function () {i--//this指向函数的调用者,window调用了定时器,这里的this指的是windowcode.innerHTML = `0${i}秒后重新获取`if (i === 0) {code.innerHTML = `发送验证码`flag = truei = 5clearInterval(timeId)}}, 1000)}})
用户名验证
- 注意封装函数,失去焦点触发这个函数
- 正则:/1{6,16}$/
- 如果不符合要求,则出现提示信息,并return false中断程序
- 否则,则返回return true
- 之所以返回布尔值,是为了最后的提交按钮做准备
- 侦听使用change事件,当鼠标离开了表单,并且表单值发生了变化时触发(类似京东效果 )
作用域
作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问,作用域分为全局作用域和局部作用域。
局部作用域
局部作用域分为函数作用域和块作用域。
函数作用域
在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。
<script>// 声明 counter 函数function counter(x, y) {// 函数内部声明的变量const s = x + yconsole.log(s) // 18}// 设用 counter 函数counter(10, 8)// 访问变量 sconsole.log(s)// 报错
</script>
总结:
- 函数内部声明的变量,在函数外部无法被访问
- 函数的参数也是函数内部的局部变量
- 不同函数内部声明的变量无法互相访问
- 函数执行完毕后,函数内部的变量实际被清空了
块作用域
在 JavaScript 中**使用 {}
包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】无法被访问。**
<script>{// age 只能在该代码块中被访问let age = 18;console.log(age); // 正常}// 超出了 age 的作用域console.log(age) // 报错let flag = true;if(flag) {// str 只能在该代码块中被访问let str = 'hello world!'console.log(str); // 正常}// 超出了 age 的作用域console.log(str); // 报错for(let t = 1; t <= 6; t++) {// t 只能在该代码块中被访问console.log(t); // 正常}// 超出了 t 的作用域console.log(t); // 报错
</script>
JavaScript 中除了变量外还有常量,常量与变量本质的区别是【常量必须要有值且不允许被重新赋值】,常量值为对象时其属性和方法允许重新赋值。
<script>// 必须要有值const version = '1.0.0';// 不能重新赋值// version = '1.0.1';// 常量值为对象类型const user = {name: '小明',age: 18}// 不能重新赋值user = {};// 属性和方法允许被修改user.name = '小小明';user.gender = '男';
</script>
总结:
let
声明的变量会产生块作用域,var
不会产生块作用域const
声明的常量也会产生块作用域- 不同代码块之间的变量无法互相访问
- 推荐使用
let
或const
注:开发中 let
和 const
经常不加区分的使用,如果担心某个值会不小被修改时,则只能使用 const
声明成常量。
全局作用域
<script>
标签和 .js
文件的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。
<script>// 此处是全局function sayHi() {// 此处为局部}// 此处为全局
</script>
全局作用域中声明的变量,任何其它作用域都可以被访问,如下代码所示:
<script>// 全局变量 nameconst name = '小明'// 函数作用域中访问全局function sayHi() {// 此处为局部console.log('你好' + name)}// 全局变量 flag 和 xconst flag = truelet x = 10// 块作用域中访问全局if(flag) {let y = 5console.log(x + y) // x 是全局的}
</script>
总结:
- 为
window
对象动态添加的属性默认也是全局的,不推荐! - 函数中未使用任何关键字声明的变量为全局变量,不推荐!!!
- 尽可能少的声明全局变量,防止全局变量被污染
JavaScript 中的作用域是程序被执行时的底层机制,了解这一机制有助于规范代码书写习惯,避免因作用域导致的语法错误。
作用域链
在解释什么是作用域链前先来看一段代码:
<script>// 全局作用域let a = 1let b = 2// 局部作用域function f() {let c// 局部作用域function g() {let d = 'yo'}}
</script>
函数内部允许创建新的函数,f
函数内部创建的新函数 g
,会产生新的函数作用域,由此可知作用域产生了嵌套的关系。
如下图所示,父子关系的作用域关联在一起形成了链状的结构,作用域链的名字也由此而来。
作用域链**本质上是底层的 变量查找机制 **,在函数被执行时,会优先查找当前函数作用域中查找变量,如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域,如下代码所示:
<script>// 全局作用域let a = 1let b = 2// 局部作用域function f() {let c// let a = 10;console.log(a) // 1 或 10console.log(d) // 报错// 局部作用域function g() {let d = 'yo'// let b = 20;console.log(b) // 2 或 20}// 调用 g 函数g()}console.log(c) // 报错console.log(d) // 报错f();
</script>
总结:
- 嵌套关系的作用域串联起来形成了作用域链
- 相同作用域链中按着从小到大的规则查找变量
- 子作用域能够访问父作用域,父级作用域无法访问子级作用域
垃圾回收机制
简称GC
- JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收
内存的生命周期
JS环境中分配的内存,一般有如下生命周期:
- 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
- 内存使用:即读写内存,也就是使用变量、函数等
- 内存回收:使用完毕,由垃圾回收器自动回收不再使用的内存
说明:
- 全局变量一般不会回收(关闭页面回收)
- 一般情况下局部变量的值,不用了,会被自动回收掉
**内存泄露:**程序中分配的内存由于某种原因程序未释放或无法释放叫做程序泄漏
算法说明
堆栈空间分配区别:
- 栈(操作系统):由操作系统自动分配释放函数的参数值、局部变量等,基本数据类型放到栈里面
- 堆(操作系统):一般由程序员分配释放,若程序员不是,由垃圾回收机制回收。复杂数据类型放到堆里面。
引用计数法
- 现在基本不使用
- 就是看一个对象是否有指向它的引用,没有了引用就回收对象
算法:
-
跟踪记录被引用的次数
-
如果被引用了一次,那么就记录次数1,多次引用会累加++
-
如果减少一个引用就减1 –
-
如果引用次数是0,则释放内存
}
**致命的问题:**嵌套引用(循环引用)
如果**两个对象相互引用**,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露。
function fn() {let o1 = {}let o2 = {}o1.a = 02o2.a = o1return‘引用计数无法回收'
}
fn()
因为他们的引用次数永远不会是0。这样的相互引用如果说很大量的存在就会导致大量的内存泄露
标记清除法
- 现在大多使用
核心:
-
标记清除算法将“不再使用的对象”定义为“无法达到的对象”
-
就是从根部 ( 在JS中就是全局对象) 出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。
-
那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
例子:
function fn(){let o1 = {}let o2 = {}o1.a = o2o2.a = o1return '引用计数无法回收'
}
fn()
//这里之所以标记清除可以清除掉,是因为从根本开始访问,无法直接进入到函数,无法找到这两个对象
闭包
闭包是一种比较特殊和函数,使用闭包能够访问函数作用域中的变量。从代码形式上看闭包是一个做为返回值的函数
闭包 : 内层函数 + 外层函数变量
如下代码所示:
<body><script>// 1. 闭包 : 内层函数 + 外层函数变量function outer() {const a = 1function f() {console.log(a)}f()}outer()// 2. 闭包的应用: 实现数据的私有。统计函数的调用次数let count = 1function fn() {count++console.log(`函数被调用${count}次`)}// 3. 闭包的写法 统计函数的调用次数function outer() {let count = 1function fn() {count++console.log(`函数被调用${count}次`)}return fn}const re = outer()/*outer() === fn === function fn(){}*/// const re = function fn() {// count++// console.log(`函数被调用${count}次`)// }re()//调用函数re()//调用函数const fn = function() { } 函数表达式// 4. 闭包存在的问题: 可能会造成内存泄漏</script>
</body>
总结:
1.闭包 = 内层函数 + 外层函数的变量
2.闭包的作用:
- 封闭数据,实现数据私有,外部也可以访问函数内部的变量
- 允许将函数与其所操作的某些数据(环境)关联起来
3.闭包可能引起的问题:
- 内存泄漏:因为return了一个函数,这个函数一直在被外面使用,因为接受这个返回值的是一个全局作用域,这个全局作用域变量暂时不被销毁,按照标记清除法,只要能找到就不销毁
变量提升
它允许在变量声明之前即被访问,只有var定义的变量会发生变量提升
- 把所有var声明的变量提升到当前作用域的最前面
- 只提升声明,不提升赋值
<script>//==》var str// 访问变量 strconsole.log(str + 'world!');// 声明变量 strvar str = 'hello ';
</script>
总结:
- 变量在未声明即被访问时会报语法错误
- 变量在var声明之前即被访问,变量的值为
undefined
let
声明的变量不存在变量提升,推荐使用let
- 变量提升出现在相同作用域当中
- 实际开发中推荐先声明再访问变量
注:关于变量提升的原理分析会涉及较为复杂的词法分析等知识,而开发中使用 let
可以轻松规避变量的提升,因此在此不做过多的探讨,有兴趣可查阅资料。
函数
函数提升
函数提升与变量提升比较类似,是指函数在声明之前即可被调用。
<script>// 调用函数foo()// 声明函数function foo() {console.log('声明之前即被调用...')}// 不存在提升现象bar() // 错误,var只提升声明不提升赋值,bar还不是个函数var bar = function () {console.log('函数表达式不存在提升现象...')}
</script>
总结:
- 函数提升能够使函数的声明调用更灵活
- 函数表达式不存在提升的现象
- 函数提升出现在相同作用域当中
- 函数表达式 必须先声明和赋值,后调用否则报错
函数参数
函数参数的使用细节,能够提升函数应用的灵活度。
默认值
<script>// 设置参数默认值function sayHi(name="小明", age=18) {document.write(`<p>大家好,我叫${name},我今年${age}岁了。</p>`);}// 调用函数sayHi();sayHi('小红');sayHi('小刚', 21);
</script>
总结:
- 声明函数时为形参赋值即为参数的默认值
- 如果参数未自定义默认值时,参数的默认值为
undefined
- 调用函数时没有传入对应实参时,参数的默认值被当做实参传入
动态参数
arguments
是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参。
<script>// 求生函数,计算所有参数的和function sum() {// console.log(arguments)let s = 0for(let i = 0; i < arguments.length; i++) {s += arguments[i]}console.log(s)}// 调用求和函数sum(5, 10)// 两个参数sum(1, 2, 4) // 两个参数
</script>
总结:
arguments
是一个伪数组arguments
的作用是动态获取函数的实参
剩余参数
- 函数参数使用,得到真数组
<script>function config(baseURL, ...other) {console.log(baseURL) // 得到 'http://baidu.com'console.log(other) // other 得到 ['get', 'json']}// 调用函数config('http://baidu.com', 'get', 'json');
</script>
总结:
...
是语法符号,置于最末函数形参之前,用于获取多余的实参- 借助
...
获取的剩余实参,是个真数组(伪数组不能使用pop,push这些方法)
展开运算符
展开运算符( …… ),将一个数组进行展开
const arr = [1, 5, 3, 8, 2]
console.log( ... arr) //15 3 8 2
- 不会修改原数组
使用例子
const arr = [1,2,3]
console.log(...arrr)// 1 2 3/*1.求最大值*/
console.log(Math.max(1,2,3))
//因为math接的必须是字符的形式
//==>实质上官方解释 ...arr === 1,2,3
console.log(Math.max(...arr))/*2.合并数组*/
const arr2 = [4,5]
const arr = [...arr1,...arr2]//[1,2,3,4,5]
箭头函数
箭头函数是一种声明函数的简洁语法,它与普通函数并无本质的区别,差异性更多体现在语法格式上。
- 使用场景:箭头函数更适用于那些本来需要匿名函数的地方
- 目的:为了更简短的函数写法并且不绑定this,箭头函数的语法比函数表达式更简洁
<body><script>// const fn = function () {// console.log(123)// }/*1. 箭头函数 基本语法*/const fn = () => {console.log(123)}fn()const fn = (x) => {console.log(x)}fn(1)/* 2. 只有一个形参的时候,可以省略小括号 */const fn = x => {console.log(x)}fn(1)/* 3. 只有一行代码的时候,我们可以省略大括号 */const fn = x => console.log(x)fn(1)/* 4. 只有一行代码的时候,可以省略return */const fn = x => x + xconsole.log(fn(1))/* 5. 箭头函数可以直接返回一个对象 */const fn = (uname) => ({ uname: uname })console.log(fn('刘德华'))</script>
</body>
总结:
- 箭头函数属于表达式函数,因此不存在函数提升
- 箭头函数只有一个参数时可以省略圆括号
()
- 箭头函数函数体只有一行代码时可以省略花括号
{}
,并自动做为返回值被返回
阻止表单的默认提交事件更简洁的语法:
<body><script>const form = document.querySelector('form')form.addEventlistener('click',ev => ev.preventDefault())//最初的form.addEventListener('submit', function (e) {e.preventDefault()})</script>
</body>
箭头函数参数
- 箭头函数中没有
arguments
,只能使用...
动态获取实参
<body><script>// 1. 利用箭头函数来求和const getSum = (...arr) => {let sum = 0for (let i = 0; i < arr.length; i++) {sum += arr[i]}return sum}const result = getSum(2, 3, 4)console.log(result) // 9</script>
箭头函数 this
-
箭头函数出现之前,每一个新函数根据它是如何调用的来定义这个函数的this值
-
以前this的指向:
-
<body><script>console.log(this)//window//普通函数function fn(){console.log(this)//window}fn()//实质上是window.fn()const obj = {//对象方法里面的thisname:'andy',sayHi: function(){console.log(this)//obj}}obj.sayHi()</script> </body>
-
箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this。
<script>/*箭头函数的this*/const fn = () => {console.log(this)//window/*这个箭头函数没有自己的this,往上层作用域找,来到script的this是window*/}/* 对象方法箭头函数 this */const obj = {uname: 'pink老师',sayHi: function () {console.log(this) // objlet i = 10const count = () => {console.log(this) // obj }count()}}obj.sayHi()</script>
在开发中【使用箭头函数前需要考虑函数中this的值】,事件回调函数使用箭头函数时,this为全局的window,因此
DOM事件回调函数为了简便,还是不太推荐使用箭头函数
-
<script>const btn = document.querySelector('.btn')//箭头函数此时this指向了windowbtn.addEventListener('click',()=>{console.log(this)//window})//普通函数 此时this 指向了DOM对象btn.addEventListener('click',function(){console.log(this)}) </script>
解构赋值
解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值,分为数组解构、对象解构两大类型。
数组解构
数组解构是将数组的单元值快速批量赋值给一系列变量的简洁语法,如下代码所示:
<script>// 普通的数组let arr = [1, 2, 3]// 批量声明变量 a b c // 同时将数组单元值 1 2 3 依次赋值给变量 a b clet [a, b, c] = arrconsole.log(a); // 1console.log(b); // 2console.log(c); // 3
</script>
-
交换两个变量值:
-
let a = 1 let b = 2;//这里必须加分号 [b,a] = [a,b]
-
-
剩余参数 变量少,单元值多
const [a,b, ...c] = [1,2,3,4,5,6]
-
防止undefined传递,设置默认值
const [a=0,b=0] = [1]
-
按需导入
const [a,b,,d] = [1,2,3,4]
-
多维数组结构
const [a,b,[c,d]] = [1,2,[3,4]]
总结:
- 赋值运算符
=
左侧的[]
用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量 - 变量的顺序对应数组单元值的位置依次进行赋值操作
- 变量的数量大于单元值数量时,多余的变量将被赋值为
undefined
- 变量的数量小于单元值数量时,可以通过
...
获取剩余单元值,但只能置于最末位 - 允许初始化变量的默认值,且只有单元值为
undefined
时默认值才会生效
注:支持多维解构赋值,比较复杂后续有应用需求时再进一步分析
JS必须加分号情况:
-
立即执行函数
(function(){})(); (function(){})();
-
数组解构:数组开头的,特别是前面有语句的一定注意加分号
;[a,b] = [b,a]
-
使用数组的时候
const arr = [1,2,3] const str = 'pink'; [1,2,3].map(function(item){console.log(item) })
对象解构
对象解构是将对象属性和方法快速批量赋值给一系列变量的简洁语法,如下代码所示:
<script>// 普通对象const user = {name: '小明',age: 18};// 批量声明变量 name age// 同时将数组单元值 小明 18 依次赋值给变量 name ageconst {name, age} = userconsole.log(name) // 小明console.log(age) // 18
</script>
-
对象解构的变量名 可以重新改名 旧变量名:新变量名
const name = 'LALA' /* 因为可能出现重复的变量 那么对象结构的变量名就需要重新改名 */ const{uname: username, age} = { uname: 'pink老师' age:18}
- 冒号表示“什么值: 赋值给谁 ”
总结:
- 赋值运算符
=
左侧的{}
用于批量声明变量,右侧对象的属性值将被赋值给左侧的变量 - 对象属性的值将被赋值给与属性名相同的变量
- 对象中找不到与变量名一致的属性时变量值为
undefined
- 允许初始化变量的默认值,属性不存在或单元值为
undefined
时默认值才会生效
注:支持多维解构赋值
<body><script>// 1. 这是后台传递过来的数据const msg = {"code": 200,"msg": "获取新闻列表成功","data": [{"id": 1,"title": "5G商用自己,三大运用商收入下降","count": 58},{"id": 2,"title": "国际媒体头条速览","count": 56},{"id": 3,"title": "乌克兰和俄罗斯持续冲突","count": 1669},]}/* 需求1: 请将以上msg对象 采用对象解构的方式 只选出 data 方面后面使用渲染页面*/const { data } = msgconsole.log(data)/* 需求2: 上面msg是后台传递过来的数据,我们需要把data选出当做参数传递给 函数*/// const { data } = msg// msg 虽然很多属性,但是我们利用解构只要 data值function render({ data }) {// const { data } = arr// 我们只要 data 数据// 内部处理console.log(data)}render(msg)// 需求3, 为了防止msg里面的data名字混淆,要求渲染函数里面的数据名改为 myDatafunction render({ data: myData }) {// 要求将 获取过来的 data数据 更名为 myData// 内部处理console.log(myData)}render(msg)</script>
数组对象的解构
const pig = [
{uname:'佩奇’,age: 6
]
const [{ uname, age }] = pig
console.log(uname)
console.log(age)
forEach遍历数组
forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数
注意:
1.forEach 主要是遍历数组
2.参数当前数组元素是必须要写的, 索引号可选。
3.适合遍历数组对象
<body><script>// forEach 就是遍历 加强版的for循环 适合于遍历数组对象const arr = ['red', 'green', 'pink']const result = arr.forEach(function (item, index) {console.log(item) // 数组元素 red green pinkconsole.log(index) // 索引号})// console.log(result)//undefined</script>
</body>
filter筛选数组
filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素
主要使用场景: 筛选数组符合条件的元素,并返回筛选之后元素的新数组
<body><script>const arr = [10, 20, 30]// const newArr = arr.filter(function (item, index) {// // console.log(item)// // console.log(index)// return item >= 20// })// 返回的符合条件的新数组const newArr = arr.filter(item => item >= 20)console.log(newArr)</script>
</body>
重要例子-TAP栏切换(CSS-filter):
<head>.filter a:active,.filter a:focus{backgroud:red;color:#fff}
</head>
深入对象
创建对象的三种方式
-
利用对象字面量创建对象
const o = {name: '佩奇' }
-
利用new Object创建对象
const o = new Object({ name: '佩奇' }) console.log(o)//{name: '佩奇'}
-
利用构造函数创建对象
构造函数
- 构造函数是专门用于创建对象的函数,主要用于初始化对象
- 如果一个函数使用
new
关 键字调用,那么这个函数就是构造函数。
不过有两个约定:
-
它们的命名以大写字母开头。
-
它们只能由“new"操作符来执行。
<script> function Pig(name, age, gender) {this.name = name//前面的uname是属性,后面的uname是形参this.age = agethis.gener = gender// 创建佩奇对象const Peppa = new Pig('佩奇',6,‘女’)// 创建乔治对象const George = new Pig('乔治’,3,‘男')// 创建猪妈妈对象const Mum =new Pig('豬妈妈’,30,'女’)// 创建豬爸爸对象const Dad = new Pig('豬爸爸',32,'男')console.log(Peppa)//{name:'佩奇',age:6,gener:'女'}}
</script>
总结:
- 使用
new
关键字调用函数的行为被称为实例化 - 实例化构造函数时没有参数时可以省略
()
- 构造函数的返回值即为新创建的对象
- 构造函数内部的
return
返回的值无效!
注:实践中为了从视觉上区分构造函数和普通函数,习惯将构造函数的首字母大写。
实例化执行过程
function Goods(name, price, count){this.name = namethis.price = pricethis.count = count
}
Goods()//仅仅是调用函数,如果一旦是用了new
new Goods()//就创建了一个新对象
说明:
- 创建新的空对象
- 构造函数this指向新对象
- 执行构造函数代码,修改this,添加新的属性
- 返回新对象
实例成员
通过构造函数创建的对象称为实例对象,实例对象中的属性和方法称为实例成员。
说明:
- 为构造函数传入参数,创建结构相同但值不同的对象
- 构造函数创建的实例对象彼此独立互不影响
<script>// 构造函数function Person() {// 构造函数内部的 this 就是实例对象// 实例对象中动态添加属性this.name = '小明'// 实例对象动态添加方法this.sayHi = function () {console.log('大家好~')}}// 实例化,p1 是实例对象// p1 实际就是 构造函数内部的 thisconst p1 = new Person()console.log(p1)console.log(p1.name) // 访问实例属性p1.sayHi() // 调用实例方法
</script>
总结:
- 构造函数内部
this
实际上就是实例对象,为其动态添加的属性和方法即为实例成员 - 为构造函数传入参数,动态创建结构相同但值不同的对象
注:构造函数创建的实例对象彼此独立互不影响。
静态成员
在 JavaScript 中底层函数本质上也是对象类型,因此允许直接为函数动态添加属性或方法,构造函数的属性和方法被称为静态成员。
// 2. 静态成员 : 构造函数上的属性和方法称为静态成员
function Pig(name) {
this. name = namePig.eyes=2 // 静态属性
Pig.sayHi = function(){ // 静态方法console.log(this)//Pig(name){this.name = name}
}
Pig.sayHi()
console.log(Pig.eyes) // 2}
<script>// 构造函数function Person(name, age) {// 省略实例成员}// 静态属性Person.eyes = 2Person.arms = 2// 静态方法Person.walk = function () {console.log('^_^人都会走路...')// this 指向 Personconsole.log(this.eyes)}
</script>
总结:
- 静态成员指的是添加到构造函数本身的属性和方法
- 一般公共特征的属性或方法静态成员设置为静态成员
- 静态成员方法中的
this
指向构造函数本身
内置构造函数
在 JavaScript 中最主要的数据类型有 6 种
- 分别是字符串、数值、布尔、undefined、null 和 对象
- 常见的对象类型数据包括数组和普通对象。
- **简单类型或基础类型:**其中字符串、数值、布尔、undefined、null
- **引用类型:**对象。
在 JavaScript 内置了一些构造函数,绝大部的数据处理都是基于这些构造函数实现的,JavaScript 基础阶段学习的 Date
就是内置的构造函数。
-
const str = 'pink' //实质上JS底层包装,实际执行是: const str = new String('pink') /*解释:有一个String这样的大构造函数,构造函数的目的是创建对象,所以把基本数据类型的字符串'pink'包装成了对象 */
-
在JS的底层把基础数据类型包装成复杂数据类型
<script>// 实例化let date = new Date();// date 即为实例对象console.log(date);
</script>
甚至字符串、数值、布尔、数组、普通对象也都有专门的构造函数,用于创建对应类型的数据。
引用类型
- Object,Array,RegExp,Date等
包装类型
- String,Number,Boolean等
Object-对象构造函数
Object
是内置的构造函数,用于创建普通对象。
<script>// 通过构造函数创建普通对象const user = new Object({name: '小明', age: 15})// 这种方式声明的变量称为【字面量】let student = {name: '杜子腾', age: 21}// 对象语法简写let name = '小红';let people = {// 相当于 name: namename,// 相当于 walk: function () {}walk () {console.log('人都要走路...');}}console.log(student.constructor);console.log(user.constructor);console.log(student instanceof Object);
</script>
总结:
- 推荐使用字面量方式声明对象,而不是
Object
构造函数 Object.assign
静态方法创建新的对象Object.keys
静态方法获取对象中所有属性Object.values
表态方法获取对象中所有属性值
三个常用静态方法
/* 之前获取对象的键值方法 */
// 想要获得对象里面的属性和值怎么做的?
const o= { name:'佩奇',age:6}for (let k in o) {console.log(k)//属性 name ageconsole.log(o[k])//值 佩奇 6
}
Object.keys
-
静态方法获取对象中所有属性
-
静态方法就是只有构造函数Object可以调用的
-
语法:
const obj = { name: '佩奇', age: 6} //获得对象的所有键,并且返回是一个数组 const arr = Object.keys(o) console.log(arr)//['name','age']
-
返回的是一个数组
Object.values
- 静态方法获取对象中所有值
Object.assign
-
静态方法常用于对象拷贝
-
使用:经常使用的场景给对象添加属性
-
// 给o新增属性 const o ={ name:'佩奇’,age:6} Object.assign(o,{ gender:'女'}) console.log(o)//{name:'佩奇’,age:6,gender:'女'}
Array
方法 | 作用 | 说明 |
---|---|---|
forEach | 遍历数组 | 不返回数组,经常用于查找遍历数组元素 |
filter | 过滤数组 | 返回新数组,返回的是筛选满足条件的数组元素 |
map | 迭代数组 | 返回新数组,返回的是处理之后的数组元素,想要使用返回的新数组 |
reduce | 累计器 | 返回累计处理的结果,经常用于求和等 |
Array
是内置的构造函数,用于创建数组。
<script>// 构造函数创建数组let arr = new Array(5, 7, 8);// 字面量方式创建数组let list = ['html', 'css', 'javascript']</script>
数组赋值后,无论修改哪个变量另一个对象的数据值也会相当发生改变。
reduce方法
const arr = [1, 5, 8]// 1. 没有初始值
const total = arr.reduce(function (prev, current) {return prev + current
}
console.log(total)// 2.有初始值
const total = arr.reduce(function (prev, current) {
return prev + current
}, 10)
console.log(total)//24// 3. 箭头函数的写法
const total = arr.reduce((prev, current) => prev + current, 10)
console.log(total)
reduce 执行过程:
如果没有起始值,则上一次值以数组的第一个数组元素的值
每一次循环,把返回值给做为 下一次循环的上一次值
如果有起始值,则起始值做为上一次值
总结:
-
推荐使用字面量方式声明数组,而不是
Array
构造函数 -
实例方法
forEach
用于遍历数组,替代for
循环 (重点) -
实例方法
filter
过滤数组单元值,生成新数组(重点) -
实例方法
map
迭代原数组,生成新数组(重点) -
实例方法
join
数组元素拼接为字符串,返回字符串(重点) -
实例方法
find
查找元素, 返回符合测试条件的第一个数组元素值,如果没有符合条件的则返回 undefined(重点) -
实例方法
every
检测数组所有元素是否都符合指定条件,如果所有元素都通过检测返回 true,否则返回 false( 重点) -
实例方法
some
检测数组中的元素是否满足指定条件 如果数组中有元素满足条件返回 true,否则返回 false -
实例方法
concat
合并两个数组,返回生成新数组 -
实例方法
sort
对原数组单元值排序 -
实例方法
splice
删除或替换原数组单元 -
实例方法
reverse
反转数组 -
实例方法
findIndex
查找元素的索引值
from方法
-
静态方法,把伪数组转换为真数组
-
语法:
-
Array.from()
-
const list = document.querySelector('ul li')
console.log(list)//伪数组
//转换为真数组
Array.from(list)
包装类型
在 JavaScript 中的字符串、数值、布尔具有对象的使用特征,如具有属性和方法,如下代码举例:
<script>// 字符串类型const str = 'hello world!'// 统计字符的长度(字符数量)console.log(str.length)// 数值类型const price = 12.345// 保留两位小数price.toFixed(2) // 12.34
</script>
之所以具有对象特征的原因是字符串、数值、布尔类型数据是 JavaScript 底层使用 Object 构造函数“包装”来的,被称为包装类型。
String
String
是内置的构造函数,用于创建字符串。
<script>// 使用构造函数创建字符串let str = new String('hello world!');// 字面量创建字符串let str2 = '你好,世界!';// 检测是否属于同一个构造函数console.log(str.constructor === str2.constructor); // trueconsole.log(str instanceof String); // false
</script>
总结:
- 实例属性
length
用来获取字符串的度长(重点) - 实例方法
split('分隔符')
用来将字符串拆分成数组(重点) - 实例方法
substring(需要截取的第一个字符的索引[,结束的索引号])
用于字符串截取(重点) - 实例方法
startsWith(检测字符串[, 检测位置索引号])
检测是否以某字符开头(重点) - 实例方法
includes(搜索的字符串[, 检测位置索引号])
判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false(重点) - 实例方法
toUpperCase
用于将字母转换成大写 - 实例方法
toLowerCase
用于将就转换成小写 - 实例方法
indexOf
检测是否包含某字符 - 实例方法
endsWith
检测是否以某字符结尾 - 实例方法
replace
用于替换字符串,支持正则匹配 - 实例方法
match
用于查找字符串,支持正则匹配
注:String 也可以当做普通函数使用,这时它的作用是强制转换成字符串数据类型。
Number
Number
是内置的构造函数,用于创建数值。
<script>// 使用构造函数创建数值let x = new Number('10')let y = new Number(5)// 字面量创建数值let z = 20</script>
常用方法:
**toFixed()
**设置保留小数位的长度
- 返回值:使用定点表示法表示给定数字的字符串
// 数值类型
const price = 12.345
// 保留两位外数 四舍五入
console.log(price.toFixed(2))// 12.35
总结:
- 推荐使用字面量方式声明数值,而不是
Number
构造函数 - 实例方法
toFixed
用于设置保留小数位的长度
小数计算精度问题
是js number类型运算都需要先将十进制转二进制
但小数点后的位数转二进制会出现无限循环的问题,只能舍0入1,所以会出现小数点丢失问题
比如说
0.3+0.1
=>0.3000000000004
通常将小数转换为整数
(0.3*100+0.1*100)/100
js number类型
JS 数字类型只有number类型,number类型相当于其他强类型语言中的double类型(双精度浮点型
),不区分浮点型和整数型
**Js的所有数字类型都是双精度浮点型(64位
)**采用 IEEE754 标准
64位二进制数表示一个number数字
其中 64位 = 1位符号位 + 11位指数位 + 52位小数位
- 浮点数的运算精度丢失问题就是因为,浮点数转化为该标准的二进制的过程中出现的丢失
一些浮点数在转化为二进制时,会出现无限循环 。比如, 十进制的 0.1 转化为二进制,会得到如下结果:
0.1 => 0.0001 1001 1001 1001…(无限循环)
0.2 => 0.0011 0011 0011 0011…(无限循环)
存储结构中的尾数部分最多只能表示 53 位。为了能表示 0.1,只能模仿十进制进行四舍五入了,但二进制只有 0 和 1 , 于是变为 0 舍 1 入 。
- 浮点数转换的时候小数乘二取整会有无限循环的情况,但是整数除二取余是不会的,所以整数部分不会出现精度丢失问题
编程思想
学习 JavaScript 中基于原型的面向对象编程序的语法实现,理解面向对象编程的特征。
面向过程
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次
调用就可以了。
举个栗子:蛋炒饭
面向对象(oop)
面向对象是把事务分解成为一个个对象,然后由对象之间分工与合作。
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工。
面向对象编程具有灵活、代码可复用、容易维护和开发的优点,更适合多人合作的大型软件项目。
面向对象的特性:
-
封装性
-
继承性
-
多态性
面向过程编程
-
优点:性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机就采用的面向过程编程。
-
缺点:没有面向对象易维护、易复用、易扩展
面向对象编程
- 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护
- 缺点:性能比面向过程低
构造函数
对比以下通过面向对象的构造函数实现的封装:
<script>function Person() {this.name = '佚名'// 设置名字this.setName = function (name) {this.name = name}// 读取名字this.getName = () => {console.log(this.name)}}// 实例对像,获得了构造函数中封装的所有逻辑let p1 = new Person()p1.setName('小明')console.log(p1.name)// 实例对象let p2 = new Person()console.log(p2.name)
</script>
封装是面向对象思想中比较重要的一部分,js面向对象可以通过构造函数实现的封装。
同样的将变量和函数组合到了一起并能通过 this 实现数据的共享,所不同的是借助构造函数创建出来的实例对象之间是彼此不影响的
总结:
- 构造函数体现了面向对象的封装特性
- 构造函数实例创建的对象彼此独立、互不影响
封装是面向对象思想中比较重要的一部分,js面向对象可以通过构造函数实现的封装。
前面我们学过的构造函数方法很好用,但是 存在浪费内存的问题
-
此时是只有两个,但如果创建了两百个对象并进行该赋值,那么在堆中会开辟两百多个function()造成浪费内存
-
console.log(ldh.sing === zxy.sing)//结果是false 说明两函数不一样
-
我们需要所有的对象使用同一个函数,这样就比较节省内存,就使用原型对象
原型对象
构造函数通过原型分配的函数是所有对象所 共享的。
- JavaScript 规定,每一个构造函数都有一个
prototype
属性,指向另一个对象,所以我们也称为原型对象 - 这个对象可以挂载函数,对象实例化不会多次创建原型上函数,节约内存
- 我们**可以把那些不变的方法,直接定义在 prototype 对象上,这样所有对象的实例就可以共享这些方法。**
- 构造函数和原型对象中的this 都指向 实例化的对象
- 公共属性写在构造函数,公共方法写在原型
<script>function Person() {}// 每个函数都有 prototype 属性console.log(Person.prototype)
</script>
了解了 JavaScript 中构造函数与原型对象的关系后,再来看原型对象具体的作用,如下代码所示:
<script>function Person() {// 此处未定义任何方法}// 为构造函数的原型对象添加方法Person.prototype.sayHi = function () {console.log('Hi~');}// 实例化let p1 = new Person();p1.sayHi(); // 输出结果为 Hi~
</script>
构造函数 Person
中未定义任何方法,这时实例对象调用了原型对象中的方法 sayHi
,接下来改动一下代码:
<script>function Person() {// 此处定义同名方法 sayHithis.sayHi = function () {console.log('嗨!');}}// 为构造函数的原型对象添加方法Person.prototype.sayHi = function () {console.log('Hi~');}let p1 = new Person();p1.sayHi(); // 输出结果为 嗨!
</script>
构造函数 Person
中定义与原型对象中相同名称的方法,这时实例对象调用则是构造函中的方法 sayHi
。
通过以上两个简单示例不难发现 JavaScript 中对象的工作机制:当访问对象的属性或方法时,先在当前实例对象是查找,然后再去原型对象查找,并且原型对象被所有实例共享。
<script>function Person() {// 此处定义同名方法 sayHithis.sayHi = function () {console.log('嗨!' + this.name)}}// 为构造函数的原型对象添加方法Person.prototype.sayHi = function () {console.log('Hi~' + this.name)}// 在构造函数的原型对象上添加属性Person.prototype.name = '小明'let p1 = new Person()p1.sayHi(); // 输出结果为 嗨!let p2 = new Person()p2.sayHi()
</script>
总结:结合构造函数原型的特征,实际开发重往往会将封装的功能函数添加到原型对象中。
let that
function Star(uname){that = this// console.log(this)//结果是 Star???? this.uname = uname
}
//原型对象里面的函数 this 指向的还是 实例对象ldh
Star.prototype.sing = function(){console.log('唱歌
}
//实例化对象ldh
//构造函数里面的this 就是 实例化对象ldh
const ldh = new Star('刘德华')
console.log(that === ldh)//true
constructor 属性
每个原型对象里面都有个constructor 属性(constructor 构造函数)
作用:该属性指向该原型对象的构造函数, 简单理解,就是指向我的爸爸,我是有爸爸的孩子
使用场景:
如果有多个对象的方法,我们可以给原型对象采取对象形式赋值.
但是这样就会覆盖构造函数原型对象原来的内容,这样**修改后的原型对象 constructor 就不再指向当前构造函数了**
此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。
作用解释:
<body> <script>function Star(){}/* 这时追加式 */Star.prototype.sing = function(){console.log('唱歌')}Star.prototype.dance = function(){console.log('跳舞')}console.log(Star.prototype)//{construct: f}/* 这是赋值式,换值了 */Star.prototype = {/* 重新指回这个原型对象的 构造函数 */construct: Starsing: function(){console.log('唱歌')}dance: function(){console.log('跳舞')}}console.log(Star.prototype)//{sing:f, dance:f }</script>
</body>
对象原型
对象都会有一个属性 __proto__
指向构造函数的 prototype 原型对象,所以我对象可以使用构造函数 prototype
- 注意前后各两个杠
原型对象的属性和方法,就是因为对象有 **__proto__
**原型的存在。
注意:
__proto__
是JS非标准属性- 是个只读的属性不能进行修改,一定指向原型对象
- [[prototype]]和
__proto__
意义相同 - 用来表明当前实例对象指向哪个原型对象prototype
__proto__
对象原型里面也有一个 constructor属性,指向创建该实例对象的构造函数
原型继承
继承是面向对象编程的另一个特征,通过继承进一步提升代码封装的程度,JavaScript 中大多是借助原型对象实现继承 的特性。
龙生龙、凤生凤、老鼠的儿子会打洞描述的正是继承的含义。
- 进行了原型继承会导致覆盖遗失构造函数,还需要construct指回来
<body><script>// 继续抽取 公共的部分放到原型上// const Person1 = {// eyes: 2,// head: 1// }// const Person2 = {// eyes: 2,// head: 1// }// 构造函数 new 出来的对象 结构一样,但是对象不一样function Person() {this.eyes = 2this.head = 1}// console.log(new Person)// 女人 构造函数 继承 想要 继承 Personfunction Woman() {}// Woman 通过原型来继承 Person// 父构造函数(父类) 子构造函数(子类)// 子类的原型 = new 父类 Woman.prototype = new Person() // {eyes: 2, head: 1} // 指回原来的构造函数Woman.prototype.constructor = Woman// 给女人添加一个方法 生孩子Woman.prototype.baby = function () {console.log('宝贝')}const red = new Woman()console.log(red)// console.log(Woman.prototype)// 男人 构造函数 继承 想要 继承 Personfunction Man() {}// 通过 原型继承 PersonMan.prototype = new Person()Man.prototype.constructor = Manconst pink = new Man()console.log(pink)</script>
</body>
原型链
基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对
象的链状结构关系称为原型链
<body><script>// function Objetc() {}console.log(Object.prototype)console.log(Object.prototype.__proto__)function Person() {}const ldh = new Person()// console.log(ldh.__proto__ === Person.prototype)// console.log(Person.prototype.__proto__ === Object.prototype)console.log(ldh instanceof Person)console.log(ldh instanceof Object)console.log(ldh instanceof Array)console.log([1, 2, 3] instanceof Array)console.log(Array instanceof Object)</script>
</body>
① 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
② 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)
③ 如果还没有就查找原型对象的原型(Object的原型对象)
④ 依此类推一直找到 Object 为止(null)
⑤ __proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线
⑥ 可以使用 instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
console.log(Array intanceof Object)//trus
- 万物皆对象
- 所有的对象里面都有
__proto__
对象原型 指向原型对象 - 所有的原型对象里面有constructor,指向创造该原型对象的构造函数
综合案例-模态框
分析需求:
-
多个模态框一样的,而且每次点击都会出来一个,怎么做呢?
- 构造函数。把模态框封装一个构造函数Modal,每次new都会产出一个模态框,所以点击不同的按钮就是在做new模态框,实例化。
-
模态框有什么功能呢?打开功能(显示),关闭功能,而且每个模态框都包含着2个功能
-
open功能
-
close功能
-
问:
open和close方法 写到哪里?
构造函数Modal的原型对象上,共享方法
深浅拷贝
浅拷贝
首先浅拷贝和深拷贝只针对引用类型
浅拷贝:拷贝的是地址
const obj = {uname: 'pink',age: 18
}
const o = obj//赋值的是obj的地址
console.log(0)
o.age = 20
console.log(o)//{uname: 'pink', age: 20}
console.log(obj)//{uname: 'pink', age: 20}
常见方法:
- 拷贝对象:Object.assgin() / 展开运算符 {…obj} 拷贝对象
- 拷贝数组:Array.prototype.concat() 或者 […arr]
如果是简单数据类型拷贝值,引用数据类型拷贝的是地址 (简单理解: 如果是单层对象,没问题,如果有多层就有问题)
深拷贝
首先浅拷贝和深拷贝只针对引用类型
深拷贝:拷贝的是对象,不是地址
常见方法:
- 通过递归实现深拷贝
- 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) {debuggerfor (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 = 20o.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>
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)console.log(o)o.family.baby = '老pink'console.log(obj)</script>
</body>
JSON序列化
<body><script>const obj = {uname: 'pink',age: 18,hobby: ['乒乓球', '足球'],family: {baby: '小pink'}}// 把对象转换为 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>
异常处理
throw
异常处理是指预估代码执行过程中可能发生的错误,然后最大程度的避免错误的发生导致整个程序无法继续运行
总结:
- throw 抛出异常信息,程序也会终止执行
- throw 后面跟的是错误提示信息
- Error 对象配合 throw 使用,能够设置更详细的错误信息
<script>function counter(x, y) {if(!x || !y) {// throw '参数不能为空!';throw new Error('参数不能为空!')}return x + y}counter()
</script>
总结:
throw
抛出异常信息,程序也会终止执行throw
后面跟的是错误提示信息Error
对象配合throw
使用,能够设置更详细的错误信息
try … catch
<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
相当于断点调试
处理this
- 不同的应用场合
this
的取值可能会有意想不到的结果 - 在此对以往学习过的关于【
this
默认的取值】情况进行归纳和总结。
普通函数
- 普通函数没有明确调用者时this值为window,严格模式下没有调用者时this的值为undefined
普通函数的调用方式决定了 this
的值,即【谁调用 this
的值指向谁】,如下代码所示:
<script>// 普通函数function sayHi() {console.log(this) }// 函数表达式const sayHello = function () {console.log(this)}// 函数的调用方式决定了 this 的值sayHi() // windowwindow.sayHi()// 普通对象const user = {name: '小明',walk: function () {console.log(this)}}// 动态为 user 添加方法user.sayHi = sayHiuesr.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事件回调函数不推荐使用箭头函数,如下代码所示:
<script>// DOM 节点const btn = document.querySelector('.btn')// 箭头函数 此时 this 指向了 windowbtn.addEventListener('click', () => {console.log(this)})// 普通函数 此时 this 指向了 DOM 对象btn.addEventListener('click', function () {console.log(this)})
</script>
同样由于箭头函数 this
的原因,基于原型的面向对象也不推荐采用箭头函数,如下代码所示:
<script>function Person() {}// 原型对像上添加了箭头函数Person.prototype.walk = () => {console.log('人都要走路...')console.log(this); // window}const p1 = new Person()p1.walk()
</script>
改变this指向
以上归纳了普通函数和箭头函数中关于 this
默认值的情形,不仅如此 JavaScript 中还允许指定函数中 this
的指向,有 3 个方法可以动态指定普通函数中 this
的指向:
call
-
使用
call
方法调用函数,同时指定函数中this
的值 -
语法:
-
fun.call(thisArg,...)
-
thisArg:在函数运行时指定的this值
-
使用方法如下代码所示:
<script>// 普通函数function sayHi() {console.log(this);}let user = {name: '小明',age: 18}let student = {name: '小红',age: 16}// 调用函数并指定 this 的值sayHi.call(user); // this 值为 usersayHi.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 值为 usersayHi.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个参数为数组,数组的单元值依次自动传入函数做为函数的参数
所以这里让求数组最大值除了展开运算和循环:
const max = Math.max.apply(Math, arr)
bind
bind
方法并**不会调用函数**,而是创建一个指定了 this
值的新函数,使用方法如下代码所示:
<script>// 普通函数function sayHi() {console.log(this)}let user = {name: '小明',age: 18}// 调用 bind 指定 this 的值let sayHello = sayHi.bind(user);// 调用使用 bind 创建的新函数sayHello()
</script>
注:bind
方法创建新的函数,与原函数的唯一的变化是改变了 this
的值。
改变定时器内部this例子:
/* 需求:有一个按钮,点击里面就禁用,2秒之后开启 */
const btn = document.querySelector('button')
btn.addEventListener('click',function(){//禁用按钮this.disabled = truesetTimeout(function(){//在这个普通函数里面,要this由原来的window改为btnthis.disabled = false//无法执行这个计时函数,因为这个函数的调用者是window}.blind(this),2000)
})
防抖节流
防抖(debounce)
所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间
防抖:单位时间内,频繁触发事件,只执行最后一次
举个栗子:王者荣耀回城,只要被打断就需要重新来
使用场景:
搜索框搜索输入。只需用户最后一次输入完,再发送请求
手机号、邮箱验证输入检测
- lodash库实现防抖
利用防抖来处理-鼠标滑过盒子显示文字(手写防抖函数)
要求:鼠标在盒子上移动,鼠标停止500ms之后,里面的数字才会变化+1
防抖的核心就是利用定时器(setTimeout)来实现
-
声明一个定时器变量
-
当鼠标每次滑动都先判断是否有定时器了,如果有定时器先清除以前的定时器
-
如果没有定时器则开启定时器,记得存到变量里面
-
在定时器里面调用要执行的函数
- 防抖的底层是闭包,之所以是闭包是因为要把timer提到全局变量上去,如果直接把timer写成全局变量不利于调用
节流(throttle)
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数
手写节流函数
手写一个节流函数-每隔 500ms +1
-
节流的核心就是利用定时器(setTimeout)来实现
-
声明一个定时器变量
-
当鼠标每次滑动都先判断是否有定时器了,如果有定时器则不开启新定时器
-
如果没有定时器则开启定时器,记得存到变量里面
-
定时器里面调用执行的函数
-
定时器里面要把定时器清空
-
function throttle(fn, t) {let timer = nullreturn function () {if (!timer) {timer = setTimeout(function () {fn()//清空计时器timer = null//为什么用null而不是clearTimeout//在定时器(setTimeout)中是无法删除定时器,因为定时器还在运作,所以使用timer = null//而不是clearTimeout(timer)}, t)}}}box.addEventListener('mousemove', throttle(mousemove, 3000))
- 定时器(setTimeout)中是无法删除定时器,因为定时器还在运作,所以使用timer = null
防抖和节流总结
性能优化 | 说明 | 使用场景 |
---|---|---|
防抖 | 单位时间内,频繁触发事件,只执行最后一次 | 搜索框搜索输入、手机号、邮箱验证输入检测 |
节流 | 单位时间内,频繁触发事件,只执行一次 | 高频事件:鼠标移动mousemove、页面尺寸缩放resize、滚动条滚动scroll 等等 |
页面打开,记录上一次播放位置
分析:
两个事件:
-
ontimeupdate 事件在视频/音频(audio/video)当前的播放位置发送改变时触发
-
onloadeddata 事件在当前帧的数据加载完成且还没有足够的数据播放视频/音频(audio/video)的下一帧时触发
ontimeupdate,触发频次太高了,可以设定一秒钟触发一次=》节流
思路:
-
在ontimeupdate事件触发的时候,每隔1秒钟,就记录当前时间到本地存储
-
下次打开页面,onloadeddata事件触发,就可以从本地存储取出时间,让视频从取出的时间播放,
果没有就默认为Os -
获得当前时间 video.currentTime
a-zA-Z0-9-_ ↩︎