一、事件
1. 事件绑定
两种方法:
element.onEventType = func
element.addEventListener( 'eventType', func)
区别 :
- addEventListener 在同一元素上的同一事件类型添加多个事件,不会被覆盖,而 onEventType 会覆盖;
- addEventListener 可以设置元素在捕获阶段触发事件,而 onEventType 不能。
示例1:在同一元素上的同一事件类型添加多个事件。
let btn = document.querySelector("#btn");
btn.onclick = () => {
console.log('hello world');
}
btn.onclick = () => {
console.log('hello javascript');
}
控制台打印效果:
只打印了后面的事件执行程序结果,再看下 addEventListener 的执行效果。
let btn = document.querySelector("#btn");
btn.addEventListener('click', () => {
console.log('hello world');
})
btn.addEventListener('click', () => {
console.log('hello javascript');
})
添加的两个事件处理程序全部被执行了。
示例2:addEventListener 可以通过第三个参数来设置元素是在冒泡阶段还是在捕获阶段触发。
// 冒泡阶段触发:默认为false
btn.addEventListener('click', () => {
console.log('hello world');
}, false)
// 捕获阶段触发
btn.addEventListener('click', () => {
console.log('hello javascript');
}, true)
2.事件流
问题:三个div嵌套,都绑定click事件,点击最内层的元素,事件如何执行?
a : 只执行最内层
b: 从内到外都执行
c: 从外到内都执行
2.1 事件捕获与事件冒泡
默认情况下,事件会在冒泡阶段执行。
addEventListener(eventType, func, boolean);
默认 false:冒泡阶段触发,true:捕获阶段触发。
示例:三个 div 嵌套,都绑定 click 事件,点击最内层的元素。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>事件</title>
</head>
<style>
.big {
width: 500px;
height: 500px;
background-color: red;
}
.medium {
width: 300px;
height: 300px;
background-color: yellow;
}
.small {
width: 100px;
height: 100px;
background-color: blue;
}
</style>
<body>
<div class="big">
<div class="medium">
<div class="small"></div>
</div>
</div>
<script>
let bigBox = document.querySelector(".big");
let mediumBox = document.querySelector(".medium");
let smallBox = document.querySelector(".small");
bigBox.addEventListener('click', () => {
console.log('Hi,I am big');
})
mediumBox.addEventListener('click', () => {
console.log('Hi,I am medium');
})
smallBox.addEventListener('click', () => {
console.log('Hi,I am small');
})
</script>
</body>
</html>
点击最里层蓝色box,看下控制台的打印结果:
可以看到,事件是按照冒泡顺序从内到外执行的。
如果我们将 addEventListener 的第三个参数改为 true,再来看下页面效果:
bigBox.addEventListener('click', () => {
console.log('Hi,I am big');
}, true)
mediumBox.addEventListener('click', () => {
console.log('Hi,I am medium');
}, true)
smallBox.addEventListener('click', () => {
console.log('Hi,I am small');
}, true)
可以看到,这次事件是按照捕获顺序从外到内执行的。
3.事件对象扩展
3.1 阻止事件冒泡
阻止事件冒泡执行,让外层的事件不被执行:
e.stopPropagation();
上面的冒泡示例中,在小盒子的事件处理程序中添加阻止事件冒泡:
bigBox.addEventListener('click', () => {
console.log('Hi,I am big');
})
mediumBox.addEventListener('click', () => {
console.log('Hi,I am medium');
})
smallBox.addEventListener('click', (e) => {
console.log('Hi,I am small');
e.stopPropagation();
})
我们点击小盒子,发现这次只触发了小盒子的事件处理程序,并没有触发中盒子和大盒子的事件处理程序:
3.2 事件默认行为
去掉事件默认行为:
e.preventDefault();
或者
return false;
示例:为一个可以跳转到百度的 a 标签设置点击事件,点击事件触发时,a 标签不进行跳转。
<a href="https://www.baidu.com" class="alink">hello world</a>
let alink = document.querySelector('.alink');
alink.addEventListener('click', (e) => {
console.log('hello world');
e.preventDefault()
// return false;
})
4.事件委托
通过 e.target 将子元素的事件委托给父级处理。
示例:实现一个水果列表,可以添加和移除每项水果。
<div>
<input type="text">
<button class="add">添加</button>
</div>
<ul class="list">
<li>香蕉</li>
<li>西瓜</li>
<li>鸭梨</li>
</ul>
添加水果:
// 添加水果
btn.addEventListener('click', () => {
let value = input.value;
let li = document.createElement('li');
let txt = document.createTextNode(value);
li.appendChild(txt);
list.appendChild(li);
input.value = '';
})
点击水果进行移除:
如果在页面初始化对每个 li 添加点击事件,则后面通过输入框动态添加的水果不具有这个点击事件;
所以需要直接对 ul 元素添加,再通过 e.target 获取到当前点击的元素,再进行移除。
list.addEventListener('click', (e) => {
// e.target => 当前 li
list.removeChild(e.target)
})
5.事件类型
5.1 鼠标事件
- click:按下鼠标时触发;
- dblclick:在同一个元素上双击鼠标时触发;
- mousedown:按下鼠标键时触发;
- mouseup:释放按下的鼠标键时触发;
- mousemove:当鼠标在一个节点内部移动时触发;
- mouseenter:鼠标进入一个节点时触发,进入子节点不会触发这个事件;
- mouseover:鼠标进入一个节点时触发,进入子节点会再一次触发这个事件;
- mouseout:鼠标离开一个节点时触发,离开父节点也会触发这个事件;
- mouseleave:鼠标离开一个节点时触发,离开父节点不会触发这个事件;
5.2 键盘事件
js中的键盘事件分为三种:keydown 、keypress 和 keyup。
- keydown:按下键盘时触发;
- keypress:按下有值的键时触发,即按下 Ctrl、Alt、Shift、Meta这样无值的键,这个事件不会触发;
- keyup:松开键盘时触发;
注意:对于有值的键,按下时先触发 keydown 事件,再触发 keypress 事件。
键盘的每个按键都有绑定的键码,可以通过e.keyCode 获取。
document.onkeydown = function (e) {
console.log(e.keyCode);
}
示例:通过上下左右键控制红色方块移动。
首先,我们在控制台分别打印下左、上、右、下这四个方向键的键码:
然后,我们再来了解下 偏移量offset 常用属性:
左偏移量 | 上偏移量 | 右偏移量 | 下偏移量 |
---|---|---|---|
offsetLeft | offsetTop | offsetRight | offsetDown |
下面我们开始进行编写我们的代码了。
html 和 css 代码:
.box {
width: 100px;
height: 100px;
background-color: red;
position: absolute;
left: 100px;
top: 100px;
}
<div class="box"></div>
js 代码:
let box = document.querySelector('.box');
document.onkeydown = (e) => {
let code = e.keyCode;
switch (code) {
// 左键
case 37:
box.style.left = (box.offsetLeft - 5) + 'px';
break;
// 上键
case 38:
box.style.top = (box.offsetTop - 5) + 'px';
break;
// 右键
case 39:
box.style.left = (box.offsetLeft + 5) + 'px';
break;
// 下键
case 40:
box.style.top = (box.offsetTop + 5) + 'px';
break;
}
}
最终效果:
5.3 触屏事件
- touchstart:手指触摸到一个DOM元素时触发;
- touchend:手指从一个DOM元素上移开时触发;
- touchmove:手指在一个DOM元素上滑动时触发。
let box = document.querySelector('.box');
box.ontouchstart = () => {
console.log('start');
}
box.ontouchend = () => {
console.log('end');
}
box.ontouchmove = () => {
console.log('move');
}
二、计时器方法
1. setInterval 与 clearInterval
语法:
// 设置定时器:
setInterval(fn, seconds)
// 清除定时器:
clearInterval(定时器名)
例 1 - 每隔两秒钟输出一次hello world。
setInterval(() => {
console.log("hello world");
}, 2000);
例 2 - 点击暂停按钮可以结束打印输出。
let btn = document.querySelector('button');
// 每隔两秒钟输出一次
let timer = setInterval(() => {
console.log("hello world");
}, 2000);
// 点击事件
btn.addEventListener('click', () => {
// 暂停结束计时器 timer
clearInterval(timer);
}, false) ;
例 3 - 在网页中显示当前时间。
<h1></h1>
<script>
let h1 = document.querySelector('h1');
let timer = setInterval(() => {
// 获取当前时间
let time = new Date();
let h = time.getHours();
let m = time.getMinutes();
let s = time.getSeconds();
let result = `${addZero(h)}:${addZero(m)}:${addZero(s)}`;
h1.innerHTML = result;
}, 1000)
// 用零补位
function addZero(num) {
if (num < 10) {
return `0${num}`
} else {
return num;
}
}
</script>
例 4 - 在网页中制作一个秒表(开始功能、停止功能、重置)。
<h1>0:0</h1>
<button id="start">开始</button>
<button id="stop">停止</button>
<button id="reset">重置</button>
<script>
let h1 = document.querySelector('h1');
let startBtn = document.querySelector('#start');
let stopBtn = document.querySelector('#stop');
let resetBtn = document.querySelector('#reset');
// 声明定时器
let timer = null;
// 初始化时间
let s = 0, ms = 0;
// 开始
startBtn.addEventListener('click', () => {
if (timer) {
clearInterval(timer);
}
timer = setInterval(() => {
if (ms === 9) {
s++;
ms = 0;
}
ms++;
let timeStr = `${s}:${ms}`;
h1.innerHTML = timeStr;
}, 100)
})
// 停止
stopBtn.addEventListener('click', () => {
clearInterval(timer);
})
// 重置
resetBtn.addEventListener('click', () => {
s = 0;
ms = 0;
})
</script>
2. setTimeout 与 clearTimeout
语法:
// 设置定时器:
setTimeout(fn, seconds)
// 清除定时器:
clearTimeout(定时器名)
例 1 - 3 秒后跳转到百度。
setTimeout(() => {
window.location.href = "https://www.baidu.com";
}, 3000);
例 2 - 点击暂停按钮,可以停止计时器。
let btn = document.querySelector('button');
// 定时器初始化:
let timer = null;
// 设置定时器:
timer = setTimeout(() => {
console.log('hello world');
},2000)
btn.addEventListener('click', () => {
// 清除定时器
clearTimeout(timer);
}) ;
3. 防抖与节流
解决性能问题,开发中常会遇到。
- 防抖:对于短时间内多次触发事件的情况,可以使用防抖停止事件持续触发;
- 节流:防止短时间内多次触发事件的情况,但是间隔事件内,还是需要不断触发;
例如:window.onscroll 事件,input 输入搜索内容(当你一直输入的时候不会自动搜索内容,当你停止输入一会后才会自动搜索)。
3.1 防抖 (debounce)
多次触发事件时,它只执行事件的最后一次。
例如:鼠标滚轮滚动事件不会连续触发。
let timer = null;
window.onscroll = function() {
if (timer !== null) {
clearTimeout(timer);
}
timer = setTimeout(() => {
console.log("hello world");
timer = null;
}, 1000);
}
3.2 节流(throttle)
多次触发事件时,每隔 x 秒会执行一次。
例如:鼠标滚轮滚动的事件按时间间隔触发。
let flag = true;
window.onscroll = function() {
if (flag) {
setTimeout(() => {
console.log("hello world");
flag = true;
}, 1000)
}
flag = false;
}
3.3 防抖与节流的案例
下面有这样一个案例,鼠标滚轮滚动过程中,底部按钮出现,点击底部按钮,则会返回网页顶部。
body {
height: 2000px;
overflow: scroll;
}
.top {
position: fixed;
right: 100px;
bottom: 100px;
display: none;
}
...
<h1>hello world</h1>
<button class="top">↑</button>
let btn = document.querySelector('.top');
// 滚动条滚动
window.onscroll = () => {
// 页面滚动位置距离顶部距离大于0 时,返回顶部按钮出现
if (document.documentElement.scrollTop > 0) {
btn.style.display = 'block';
}else {
btn.style.display = 'none';
}
}
// 点击返回顶部按钮,让页面滚动条返回至顶部
btn.onclick = () => {
// 让滚动条滚动到某个位置:window.scrollTo(x, y)
window.scrollTo(0, 0);
}
页面初始化效果:
向下滚动后,按钮出现效果:
点击按钮,返回顶部后的效果:
现在功能已经完成了,但是我们发现性能似乎有些问题,我们在事件执行代码添加一句测试代码:
window.onscroll = () => {
if (document.documentElement.scrollTop > 0) {
btn.style.display = 'block';
}else {
btn.style.display = 'none';
}
// test code
console.log('计数器');
}
我们会发现随着鼠标滚轮的滚动,事件处理程序会一直不停的被触发:
这对我们的性能的影响是非常的大。
我们用 防抖 来优化下上述代码:
window.onscroll = () => {
if (timer !== null) {
clearTimeout(timer);
}
timer = setTimeout(() => {
// 业务逻辑
if (document.documentElement.scrollTop > 0) {
btn.style.display = 'block';
} else {
btn.style.display = 'none';
}
console.log('计数器');
timer = null;
}, 1000)
}
在控制台上观察下效果:
功能虽然实现了,但是上面的代码把防抖的算法和真正事件的业务逻辑混到了一起,在项目开发中其实是非常不好的。
所以我们利用 闭包 来封装一下上述代码,把防抖算法和业务逻辑做区分:
// 利用闭包封装防抖算法:
// fn:真正事件的业务逻辑函数
function debounce(fn) {
let timer = null;
// 利用闭包返回要执行的函数
function eventFun() {
if (timer !== null) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn()
timer = null;
}, 1000)
}
return eventFun;
}
window.onscroll = debounce(() => {
if (document.documentElement.scrollTop > 0) {
btn.style.display = 'block';
} else {
btn.style.display = 'none';
}
console.log('计数器');
})
如果使用 节流 来优化这个案例呢?
// 利用节流封装防抖算法:
function throttle(fn) {
let flag = true;
return function() {
if (flag) {
setTimeout(() => {
fn()
flag = true;
}, 1000)
}
flag = false;
}
}
window.onscroll = throttle(() => {
if (document.documentElement.scrollTop > 0) {
btn.style.display = 'block';
} else {
btn.style.display = 'none';
}
console.log('计数器');
})
三、BOM
1. 基本概念
《JavaScript高级程序设计》这本书讲过:
JavaScript = ECMAScript + DOM + BOM
- DOM:文档对象模型( document对象);
- BOM: 浏览器对象模型。
2. BOM对象
- window对象(全局对象);
- screen对象包含有关用户屏幕的信息;
- location对象用于获得当前页面的地址(URL),并把浏览器重定向到新的页面;
- history对象包含浏览器的历史;
- navigator对象包含有关访问者浏览器的信息;
3. window对象
window对象是全局对象,所有在浏览器可以直接使用的方法,都是window对象的方法。
- 计时器方法;
- 弹出框方法;
3.1 计时器方法
window.setTimeout(() => {
console.log('hello world');
}, 1000)
3.2 弹出框方法
- alert
- prompt
- confirm
注意:在开发应用当中,一般不使用系统自带的弹出框,移动端可能会被屏蔽。
window.alert('hello world')
let msg = prompt();
// 返回值是输入的内容
console.log(msg);
confirm('确认要删除该信息吗?')
例 1 - 猜数字
- 随机生成一个 1~100 的数字;
- 在 prompt 弹出框中猜数字;
- 提示【大于目标结果】【小于目标结果】【恭喜你回答正确】。
// 1. 随机生成一个1-100的数字
let num = Math.floor(Math.random() * 100 + 1);
// 2. 递归函数
function guess() {
let guessNum = prompt('请输入你猜想的数字');
if (guessNum < num) {
confirm('小于目标结果')
guess()
} else if (guessNum > num) {
confirm('大于目标结果')
guess()
} else if (guessNum == num) {
confirm('恭喜你回答正确')
}
}
guess();
例 2 - 水果列表删除提示功能
- 制作一个水果列表;
- 删除选中的水果;
- 删除时,使用 confirm 提示是否确认删除。
let fruitList = document.querySelector('.fruit-list');
fruitList.addEventListener('click', (e) => {
let flag = confirm('确定要删除嘛?');
if (flag) {
fruitList.removeChild(e.target)
}
})
4. location对象
- location.href :属性返回当前页面的url,比如:https://www.baidu.com;
- location.hostname:主机的域名,比如:www.baidu.com;
- location.pathname :当前页面的路径和文件名,比如:/s;
- location.port:端口,比如:8080;
- location.protocol:协议,比如:https。
页面跳转: location.href = ‘http://baidu.com’
5. navigator对象
navigator.userAgent:检查当前设备,并在控制台输出。
判断手机品牌类型:
function judgeBrand(sUserAgent) {
var isIphone = sUserAgent.match(/iphone/i) == "iphone";
var isHuawei = sUserAgent.match(/huawei/i) == "huawei";
var isHonor = sUserAgent.match(/honor/i) == "honor";
var isOppo = sUserAgent.match(/oppo/i) == "oppo";
var isOppoR15 = sUserAgent.match(/pacm00/i) == "pacm00";
var isVivo = sUserAgent.match(/vivo/i) == "vivo";
var isXiaomi = sUserAgent.match(/mi\s/i) == "mi";
var isXiaomi2s = sUserAgent.match(/mix\s/i) == "mix";
var isRedmi = sUserAgent.match(/redmi/i) == "redmi";
var isSamsung = sUserAgent.match(/sm-/i) == "sm-";
if (isIphone) {
return 'iphone';
} else if (isHuawei || isHonor) {
return 'huawei';
} else if (isOppo || isOppoR15) {
return 'oppo';
} else if (isVivo) {
return 'vivo';
} else if (isXiaomi || isRedmi || isXiaomi2s) {
return 'xiaomi';
} else if (isSamsung) {
return 'samsung';
} else {
return 'default';
}
}
let brand = judgeBrand(navigator.userAgent.toLowerCase());
console.log(brand)
四、原始类型与引用类型
1. JS数据类型
每种编程语言都具有内建的数据类型,根据使用数据的方式可以从两个不同的维度将语言进行分类。
1.1 动态/静态:
- 动态类型:运行过程中需要检查数据类型;
- 静态类型:使用之前就需要确认其变量数据类型。
1.2 强/弱:
- 强类型:不支持隐式类型转换;
- 弱类型:支持隐式类型转换。
隐式类型转换 : 在赋值过程中, 编译器会把 int 型的变量转换为 bool 型的变量,每个变量只不过是一个用于保存任意值的命名占位符。
2. 数据类型分类
2.1 六种数据类型:
数值 (Number)、字符串 (String)、布尔 (Boolean)、空 (Null)、未定义 (Undefined)、对象 (Object)。
数据类型分为 原始类型 与 引用类型 两大类,除了对象以外其他五个基础数据类型都是原始类型。
2.2 ECMAScript 有 8 种数据类型:
- Undefined
- Null
- Boolean
- String
- Number
- Symbol (ES6新增)
- BigInt (ES2020新增)
- Object (基本引用类型)
根据数据存储位置的不同,我们将JS数据类型分为两大类:
- 基本数据类型 存放在栈内存中,类型1-7;
- 复杂数据类型/引用类型 存放在堆内存中, 类型8。
注意: null 值表示一个空对象指针,所以针对 typeof null 会返回 object。
console.log(typeof null);
3. 原始类型 与 引用类型
原始类型:
数值 (Number)、字符串 (String)、布尔 (Boolean)、空 (Null)、未定义 (Undefined)。
引用类型:
对象 (Object),其中包括 Array、Date、Math 等等。
3.1 原始类型与引用类型的区别
- 赋值的区别 :原始类型赋的是值,引用类型赋的是引用;
- 比较的区别 :原始类型比较的是值,引用类型比较的是是否指向同一个对象;
- 传参的区别 :原始类型的传参不管怎样传都不会影响到外面的值的变化,引用类型的传参不管在内部还是外部,它指向都是同一个内存空间同一个对象。
3.2 原始类型与引用类型的类型检测
- 原始数据类型检测:typeof(值)
经常用来检测一个变量是不是最基本的数据类型 - 引用数据类型检测:值 instanceof(类型)
用来判断该数据是否为某个构造函数的 prototype,即属性所指向的对象是否存在于另外一个要检测对象的原型链上,简单地说就是判断一个引用类型的变量具体是不是某种类型的对象。
五、异步编程
1. 同步与异步
例子: 打电话与发短信。
同步编程:
console.log("给1打电话"); // 1
console.log("给2打电话"); // 2
console.log("给3打电话"); // 3
console.log("给4打电话"); // 4
console.log("给5打电话"); // 5
异步编程:
// 给4发短信
// 给1发短信
// 给2发短信
// 给5发短信
// 给3发短信
setTimeout(() => {
console.log("给1发短信");
}, 200);
setTimeout(() => {
console.log("给2发短信");
}, 500);
setTimeout(() => {
console.log("给3发短信");
}, 2000);
setTimeout(() => {
console.log("给4发短信");
}, 100);
setTimeout(() => {
console.log("给5发短信");
}, 900);
异步可以多条任务线去执行程序,一条任务卡顿不影响其他任务。
2. 异步获取数据的方法
AJAX 与服务器通信,异步获取数据。现阶段还没接触到 AJAX,试一下其他方法。 首先先声明 target ,假设只能通过 getData 来获取数据 target。
- 方案 1:回调函数。
先回顾一下回调函数。函数可以作为一个参数在传递到另一个函数中。setTimeout、防抖和节流经常用到。函数体在完成某种操作后由内向外调用某个外部函数。
function fun(fn) {
fn();
}
fun(() => {
console.log("我是回调函数") // 我是回调函数
});
回归本题:
let target = "hello world";
function getData() {
// // 同步的写法
// return target;
// // 异步写法测试 这样写返回的undefined,因为return只能返回同步的数据
// setTimeout(() => {
// return target
// }, 500);
// 通过回调函数输出hello world
setTimeout(() => {
fn(target);
}, 500);
}
// getData()穿递的参数是fn(),然后fn()传递的参数是d,d就是target.
getData((d) => {
console.log(d); // hello world
});
为了解决 JS 的异步执行回调地狱问题,人们又发明了一些其他解决方案,比如说 Promises、Async functions 等等。
- 方案 2:Promise 对象获取数据。
Promise 是 ES2015 提供的一个内置对象,Promise 就是用来解决异步问题。
let target = "hello promise"
// 创建一个promise对象, resolve是一个函数
let p = new Promise((resolve) =>{
setTimeout(() => {
resolve(target); // 把target作为参数传递给resolve函数给传递出来,并把promise对象赋给p
}, 500)
});
// 利用p获取target
// promise对象有个then方法,then方法有个参数是一个函数,这个函数里面有个形参,这个形参就是resolve方法传出来的值。
p.then((d)=> {
console.log(d);
})
回归本题:
let target = "hello world";
// promise版本的getData,这里是Promise原理
function getData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(target);
}, 500)
});
}
let p = getData(); // getData()的返回值是一个Promise对象
p.then((data) => {
console.log(data);
})
方案 3:async 函数 + await
使用 async函数 + await 可以成功拿到异步函数的返回值,await 后面一般跟着是一个 Promise 对象 ,等待 promise 对象获取到返回值后才会进行下一个。
注意:await 要在 async 函数中用才有用。
let target = "hello world";
function getData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(target);
}, 500)
});
}
async function fun() {
let data = await getData(); // getData()的返回值是一个Promise对象
console.log(data);
}
fun(); // hello world
3. 异步编程的解决方案
- 回调函数;
- Promise;
- async 函数;
总结
以上就是我总结的Javascript所有基本的入门知识。