操作系统: 类似于一个大工厂
进程: 我们可以认为,启动一个应用程序,就会默认启动一个进程,也可能是多个进程(车间)
线程: 每一个进程中,都会启动至少一个线程用来执行程序代码,这个线程被称为主线程(工人)
微任务和宏任务
- 宏任务队列:
ajax、setTimeout、setInterval、DOM监听、UI Rendering等 - 微任务队列:Promise的
then回调、Mutation Observer API、queueMicrotask()等
- 宏任务执行之前,必须保证微任务队列是空的;
- 如果不为空,那么就优先执行微任务队列中的任务(回调)
console.log('script start');
function foo() {
console.log('foo function');
}
function bar() {
console.log('bar function');
foo()
}
bar()
console.log('script end');
/*
script start
bar function
foo function
script end*/
promise会在第一次事件循环执行
then的回调会被添加到队列中
// 宏任务
setTimeout(() => {
console.log('setTimeout1111');
},0)
setTimeout(() => {
console.log('setTimeout2222');
},0)
// 微任务
console.log('1111');
new Promise((resolve, reject) => {
console.log('2222');
resolve('-------')
console.log('-----1');
}).then(res => {
console.log('then传入的回调:', res); // then的回调会被添加到队列中
})
console.log('3333');
1111
2222
-----1
3333
then传入的回调: -------
setTimeout1111
setTimeout2222
面试题一:
console.log('script start');
// 宏任务1
setTimeout(() => {
console.log("setTimeout1");
new Promise( (resolve) => {
resolve(); // then加入到任务队列中
}).then(function () {
new Promise((resolve) => {
resolve();
}).then(function () {
console.log("then4"); // 宏任务中的微任务1
});
console.log("then2");
});
});
// 同步执行
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("then1"); // 微任务队列1
});
// 宏任务2
setTimeout(function () {
console.log("setTimeout2");
});
console.log(2);
// 微任务队列2
queueMicrotask(() => {
console.log("queueMicrotask1")
});
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log("then3"); // 微任务队列3
});
console.log('script end');
/*
script start
promise1
2
script end
then1
queueMicrotask1
then3
setTimeout1
then2
then4
setTimeout2*/
面试题二:
async function async1() {
console.log('async1 start')
await async2(); // 直接执行代码
console.log('async1 end') // 所以会加入到微任务1
}
async function async2() {
console.log('async2') // 相当于 return undefined => Promise.resole(undefined)
}
console.log('script start')
// 宏任务1
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1();
new Promise((resolve) => {
console.log('promise1')
resolve();
}).then( () => {
console.log('promise2') // 微任务2
})
console.log('script end')
/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout*/
抛出异常
throw 后续代码不会执行
可以抛出一个具体的信息
function foo() {
throw new Error('抛出异常') // 后续代码不会执行了
console.log('后续代码');
}
function test() {
// 捕获处理了异常,异常就不会传递给浏览器,后续代码可以正常执行
try {
foo()
} catch (err) {
console.log(err);
}finally {
console.log('不管咋都会执行的代码');
}
}
test()
console.log('处理异常后,代码执行');
本地存储
localStorage:本地存储,是一种永久性的存储方法,在关闭掉网页重新打开存储的内容依然保留
sessionStorage:会话存储,提供的是本次会话的存储,在关闭掉会话时,存储的内容会被清除
let token = localStorage.getItem('token')
if(!token){
console.log('从服务器请求token');
token = 'token'
localStorage.setItem('token',token)
}
// 获取数据
let userName = localStorage.getItem('name')
let password = localStorage.getItem('password')
// 如果本地没有,则...
if(!name || !password) {
console.log('输入账号密码')
userName = 'www'
password = 'console'
// 将数据保存到storage
localStorage.setItem('name',userName)
localStorage.setItem('password',password)
}
封装Storage工具
class Cache {
constructor(isLocal = true) {
this.storage = isLocal ? localStorage : sessionStorage
}
setCache(key, value) {
if (!value) {
throw new Error('value error: value 必须有值')
}
if (value) {
// localStorage本身是不能直接存储对象类型
localStorage.setItem(key, JSON.stringify(value))
}
}
getCache(key) {
const result = this.storage.getItem(key)
if (result) {
return JSON.parse(result)
}
}
removeCache(key) {
this.storage.removeItem(key)
}
clearCache(key) {
this.storage.clear()
}
}
const localCache = new Cache()
const sessionCache = new Cache(false)
// 封装过后可以直接存
const info = {
name:'www',
age:19
}
sessionCache.setCache('info',info)
localCache.getCache(info)
正则表达式
更多正则查询:c.runoob.com/front-end/8…
正则表达式是一种字符串匹配利器,可以帮助我们搜索、获取、替代字符串
// 创建正则
const re1 = new RegExp('匹配规则','修饰符')
const re2 = /abc/i // '/规则/修饰符'
常见的正则使用方法:
常见的修饰符:
test:看是否匹配,检测字符串是否符合正则的规则(常用)
// 检测message是否符合re1规则,返回一个布尔值
if(re1.test(message)){
console.log('符合规则')
}else {
console.log('不符合规则')
}
// test案例
const inputEl = document.querySelector('input')
const pEl = document.querySelector('p')
inputEl.oninput = function() {
const value = this.value
// 匹配长度为 5 到 8 的由字符 "a" 组成的字符串
if(/^a{5,8}$/.test(value)) {
pEl.textContent = '输入内容符合规则'
}else {
pEl.textContent = '输入内容不符合规则要求'
}
}
match:拿匹配的结果,使用字符串方法,传入一个正则(常用)
match 不加修饰符g会返回第一个匹配到的结果的详情,加修饰符g会把所有匹配的结果返回
matchAll 传入的正则必须加修饰符g,给到一个迭代器,调用next()
const message = 'abc aa12a Abcd b32bb'
const re1 = /abc/ig
// 获取一个字符串中所有的abc
const res2 = message.match(re1)
// 将一个字符串中的所有abc换成cba
const res3 = message.matchAll(re1,'cba')
res3.next()
for(const item of res3){
console.log(item)
}
规则 – 字符类
字符类:
const num = 'qwdq224 3ewg342'
// + 匹配多个,+ 相当于{1,}
const re = /\d+/ig
console.log(num.match(re)) // ['224', '3', '342']
反向类:
\D 非数字:除 \d 以外的任何字符,例如字母。
\S 非空格符号:除 \s 以外的任何字符,例如字母。
\W 非单字字符:除 \w 以外的任何字符,例如非拉丁字母或空格
规则 – 锚点
符号 ^ 匹配文本开头
符号 $ 匹配文本末尾
// 是否以***开头,***结尾(startsWith/endsWith有大小写限制)
const meg = 'my name is www'
if(meg.startsWith('my')){
console.log('以my开头')
}
if(meg.endsWith('www')){
console.log('以www结尾')
}
if(/^my/i.test(meg)){
console.log('以my开头---')
}
if(/www$/i.test(meg)){
console.log('以www结尾---')
}
词边界测试 \b检查位置的一侧是否匹配 \w,而另一侧则不匹配 \w
const meg = 'my name is www'
// name是一个单独的词,可以有空格
if (/\bname\b/i.test(meg)) {
console.log('有name,有边界')
}
// 词边界的应用
const time = '12:32,234:12,21:11'
const timeRe = /\b\d\d:\d\d\b/ig
console.log(time.match(timeRe)) // ['12:32', '21:11']
规则 – 转义字符串
如果要把特殊字符作为常规字符来使用,需要对其进行转义:在它前面加个反斜杠
常见的需要转义的字符:[] \ ^ $ . | ? * + ( )
斜杠符号 / 并不是一个特殊符号,但是在字面量正则表达式中也需要转义,需要//来转义
// 匹配所有以.js或者jsx结尾的文件名
const fileNames = ['ac.js','index.jsx','index.html','utils.js']
const re = /\.jsx?$/ // ? 表示0个或者1个
// for...of做法
const newFileNames1 = []
for (const fileName of fileNames){
if(re.test(fileName)){
newFileNames1.push(fileName)
}
}
// fillter过滤做法
const newFileNames2 = fileNames.filter(fileName => re.test(fileName))
console.log(newFileNames1,newFileNames2) // ['ac.js', 'index.jsx', 'utils.js']
集合和范围
集合:[eao] 意味着查找在 3 个字符 ‘a’、‘e’ 或者 `‘o’ 中的任意一个
范围:方括号也可以包含字符范围
[a-z]会匹配从 a 到 z 范围内的字母,[0-5] 表示从 0 到 5 的数字[0-9A-F]表示两个范围:它搜索一个字符,满足数字 0 到 9 或字母 A 到 F\d ——和[0-9]相同\w ——和[a-zA-Z0-9_]相同
const number = ['199','183','110','155']
const numberRe = /^1[3456789]\d/
console.log(number.filter(phone => numberRe.test(phone))) // ['199', '183', '155']
const phoneNum = '19944446666'
const phoneNumRe = /^1[3-9]\d{9}$/
console.log( phoneNumRe.test(phoneNum)) // true
量词
数量 :
- 确切的位数:
{5} - 某个范围的位数:
{3,5}
缩写:
+:代表“一个或多个”,相当于 {1,}?:代表“零个或一个”,相当于 {0,1}。换句话说,它使得符号变得可选;*:代表着“零个或多个”,相当于 {0,}。也就是说,这个字符可以多次出现或不出现
// 匹配标签名字
const htmlEl = '<div><span>哈哈哈哈</span><p>我是标题</p></div>'
const htmlRe = /<\/?[a-z][a-z0-9]*>/ig
console.log(htmlEl.match(htmlRe)) // ['</span>', '</p>', '</div>']
贪婪和惰性模式
贪婪模式:默认情况匹配规则是查找到匹配的内容后,会继续向后查找,一直找到最后一个匹配的内容
惰性模式:只要获取到对应的内容后,就不再继续向后匹配;在量词后面再加一个问号 ‘?’ 来启用它;
所以匹配模式变为 *? 或 +?,甚至将 '?' 变为 ??
const message = '我最喜欢《缘之空》和《进阶的巨人》、《美女与野兽》'
// 默认: .+采用贪婪模式
const re = /《.+》/ig
// 惰性模式
const re2 = /《.+?》/ig
console.log(message.match(re)) // ['《缘之空》和《进阶的巨人》、《美女与野兽》']
console.log(message.match(re2)) // ['《缘之空》', '《进阶的巨人》', '《美女与野兽》']
捕获组
一部分可以用括号括起来 (...),这称为捕获组,视为一个整体
允许将匹配的一部分作为结果数组中的单独项
或:在正则表达式中,它用竖线 | 表示;通常会和捕获组一起来使用,在其中表示多个值
// 捕获组
const message = '我最喜欢《缘之空》和《进阶的巨人》、《美女与野兽》'
const re2 = /《(.+?)》/ig
const iterator = message.matchAll(re2)
for (const item of iterator) {
console.log(item[1]) // 缘之空 进阶的巨人 美女与野兽
}
// 将捕获组作为一个整体(连续两个abc)
const info = 'asdabciasjdabcabcjd'
const re3 = /(abc){2,}/ig
console.log(info.match(re3)) // ['abcabc']
案例练习
- 歌词解析
function parseLyric(lyricString) {
// 1. 根据换行符切割
const lyricLineStrings = lyricString.split('\n')
const lyricLines = []
// 匹配时间的正则
const re = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/
// 2. 针对每一行歌词进行解析 [00:24.380]我好想住你隔壁
for (const lyric of lyricLineStrings) {
// ['[00:00.000]', '00', '00', '000', index: 0, input: '[00:00.000] 作词 : 许嵩', groups: undefined]
const timeString = re.exec(lyric)
if (!timeString) continue // 如果有值再做操作,没值过掉这次循环,进行下次循环
// 转毫秒
const time1 = timeString[1] * 60 * 1000
const time2 = timeString[2] * 1000
const time3 = timeString[3].length === 3 ? timeString[3] * 1 : timeString[3] * 10
// 获取时间
const time = time1 + time2 + time3 // 0 1000 2000...
// 3. 获取歌词, trim去空格
const content = lyric.replace(re, '').trim()
// 讲对象放在数组中
lyricLines.push({ time, content })
}
console.log(lyricLines)
/*
{time: 0, content: '作词 : 许嵩'}
{time: 1000, content: '作曲 : 许嵩'}
{time: 2000, content: '编曲 : 许嵩'}
...
*/
return lyricLines
}
// 请求的歌词...
const lyricString = '[00:00.000] 作词 : 许嵩\n
[00:01.000] 作曲 : 许嵩\n
[00:02.000] 编曲 : 许嵩\n
[00:22.240]天空好想下雨\n
[00:24.380]我好想住你隔壁\n'
// 解析歌词
const lyricStringInfo = parseLyric(lyricString)
- 时间格式化
相关库:dayjs、moment
function formatDate(time, formatString) {
// 1.将时间戳转Date
const date = new Date(time)
// 2.获取时间值 定义正则和值之间的关系
const obj = {
'y+': date.getFullYear(),
'M+': date.getMonth() + 1,
'd+': date.getDate(),
'h+': date.getHours(),
'm+': date.getMinutes(),
's+': date.getSeconds()
}
// 替换
for (const key in obj) {
const keysRe = new RegExp(key)
if (keysRe.test(formatString)) {
// padStart:有两位,用0补齐
const value = (obj[key] + '').padStart(2, '0')
formatString = formatString.replace(keysRe, value)
}
}
return formatString
}
const result = formatDate(1332322323, 'yyyy-MM-dd hh:mm:ss') // 1970-01-16 18:05:22
防抖和节流
防抖
当事件触发时,相应的函数并不会立即触发,而是会等待一定的时间 (游戏回城)
应用场景很多:
- 输入框中频繁的输入内容,搜索或者提交信息
- 频繁的点击按钮,触发某个事件
- 监听浏览器滚动事件,完成某些特定操作
- 用户缩放浏览器的resize事件
第三方库来实现防抖操作:lodash、underscore
<input type="text">
<script src="https://cdn.jsdelivr.net/npm/underscore@1.13.1/underscore-umd-min.js"></script>
<script>
const inputEl = document.querySelector('input')
let counter = 1
inputEl.oninput = _.debounce(function() {
console.log(`发送网络请求${counter++}`,this.value)
},1000)
</script>
手写防抖
// 简约版
<input type="text">
<script>
const inputEl = document.querySelector('input')
function wqDebounce(fn, delay) {
// 1. 记录上一次事件触发的timer
let timer = null
// 2. 触发事件执行的函数
const _debounce = function(...args) {
// 多次触发事件,取消上一次事件
if (timer) clearTimeout(timer)
// 延迟执行传入的回调函数
timer = setTimeout(() => {
fn.apply(this,args)
timer = null // 执行函数后,讲timer置为null
}, delay)
}
// 返回一个新的函数
return _debounce
}
let counter = 1
inputEl.oninput = wqDebounce(function(event) {
console.log(`发送网络请求${counter++}`,this.value)
}, 3000)
</script>
// 完善取消和立即执行功能
<input type="text">
<button>取消</button>
<script>
const inputEl = document.querySelector('input')
const buttonEl = document.querySelector('button')
function wqDebounce (fn, delay, immediate = true) {
// 1.记录上一次事件触发的timer
let timer = null
let isInvode = false // 记录有没有立即执行过
// 2.触发事件执行的函数
const _debounce = function (...args) {
// 多次触发事件,取消上一次事件
if (timer) clearTimeout(timer)
// 第一次操作是不需要延迟
if (immediate && !isInvode) {
fn.apply(this, args)
isInvode = true
return
}
// 延迟执行传入的回调函数
timer = setTimeout(() => {
fn.apply(this, args)
timer = null // 执行函数后,讲timer置为null
isInvode = false
}, delay)
}
// 3.给_debounce绑定一个取消函数
_debounce.cancel = function () {
if (timer) clearTimeout(timer)
timer = null
isInvode = false
}
// 返回一个新的函数
return _debounce
}
let counter = 1
const debounceFn = wqDebounce(function (event) {
console.log(`发送网络请求${counter++}`, this.value)
}, 3000)
inputEl.oninput = debounceFn
// 4.实现取消事件
buttonEl.onclick = function () {
debounceFn.cancel()
}
节流
事件被频繁触发,节流函数会按照一定的频率来执行函数;不管在这个中间有多少次触发这个事件,执行函数的频繁总是固定的 (技能平A)
节流的应用场景:
- 监听页面的滚动事件
- 鼠标移动事件
- 用户频繁点击按钮操作
- 游戏中的一些设计
手写节流
<input type="text">
<script>
const inputEl = document.querySelector('input')
function wqthrottle (fn, intervar,immediate = true) {
let startTime = 0
return function (...args) {
const nowTime = new Date().getTime() // 1.当前时间
// 对立即执行进行控制
if(!immediate && startTime === 0){
startTime = nowTime
}
const waitTime = intervar - (nowTime - startTime) // 2.需要等待的时间
if (waitTime <= 0) {
fn.apply(this,args)
startTime = nowTime
}
}
}
let counter = 1
inputEl.oninput = wqthrottle(function (event) {
console.log(`发送网络请求${counter++}`,this.value,event)
}, 2000)
</script>
深拷贝浅拷贝
浅拷贝:内部引入对象时,依旧会相互影响
const info = {
name: 'wqq',
friend: {
age: 18
}
}
// 引用赋值
const obj1 = info
// 浅拷贝
const obj2 = {...info}
obj2.friend.age = 20
console.log(info.friend.age); // 20
const obj3 = Object.assign({},info) // 将info拷贝到{}中生成一个新的对象
obj3.friend.age = 20
console.log(info.friend.age); // 20
// 深拷贝(使用JSON)
const obj4 = JSON.parse(JSON.stringify(info))
obj4.friend.age = 99
console.log(info.friend.age); // 18
深拷贝:两个对象不再有任何关系,不会相互影响
// 手动实现深拷贝
// 判断一个标识符是否是对象类型
function isObject(value) {
const valueType = typeof value
return (value !== null) && (valueType === 'object' || valueType === 'function')
}
function deepCopy(originValue,map = new WeakMap()) {
// 如果值是symbol类型
if (typeof originValue === 'symbol') {
return Symbol(originValue.description)
}
// 原始类型直接返回
if (!isObject(originValue)) {
return originValue
}
// 如果是set类型
if (originValue instanceof Set) {
const newSet = new Set()
for (const setItem of originValue) {
newSet.add(deepCopy(setItem))
}
}
// 如果是函数类型,不需要进行深拷贝
if (typeof originValue === 'function') {
return originValue
}
// 如果是对象类型,才需要创建对象
if (map.get(originValue)) {
return map.get(originValue)
}
const newObj = Array.isArray(originValue) ? [] : {}
map.set(originValue, newObj)
// 便利普通的key
for (const key in originValue) {
newObj[key] = deepCopy(originValue[key],map) // 递归调用
}
// 单独便利symbol,直接for...of不会便利symbol
const symbolKeys = Object.getOwnPropertySymbols(originValue)
for (const symbolKey of symbolKeys) {
newObj[Symbol(symbolKey.description)] = deepCopy(originValue[symbolKey],map)
}
return newObj
}
const set = new Set(['abc', 'cba'])
const s1 = Symbol('s1')
const info = {
name: 'wqq',
friend: {
message: '大美女',
age: 18
},
// 特殊类型
set: set,
running: () => console.log('running'),
symbolKey: Symbol(),
[s1]: 'aaa',
}
info.self = info
const newObj = deepCopy(info)
console.log(newObj);
AJAX发送请求
// 1.创建XMLHttpRequest对象
const xml = new XMLHttpRequest()
// 2.监听状态变化(宏任务)
xml.onreadystatechange = function () {
// 如果状态不是DOME,直接返回
if(xml.readyState !== XMLHttpRequest.DONE) return
// 将字符串转成json
const resJSON = JSON.parse(xml.response)
}
// 3.配置请求option
// 参数一:请求方式 参数二:请求地址 参数三:发送同步请求 实际开发采用异步,不会阻塞js代码继续执行
xml.open('get','http://localhost:8080',false)
// 4.发送请求(浏览器帮助发送响应请求)
xml.send()
- XMLHttpRequest的state
- 其他的事件监听
loadstart:请求开始progress: 一个响应数据包到达,此时整个 response body 都在 response 中abort:调用 xhr.abort() 取消了请求error:发生连接错误,例如,域错误。不会发生诸如 404 这类的 HTTP 错误load:请求成功完成timeout:由于请求超时而取消了该请求(仅发生在设置了 timeout 的情况下)loadend:在 load,error,timeout 或 abort 之后触发。
获取HTTP响应的网络状态,可以通过status和statusText来获取:
- ajax请求封装
function wqajax({
url,
method = "get",
data = {},
timeout = 5000,
headers = {},
success,
failure
} = {}) {
// 1.创建xhr对象
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = function() {
if (xhr.readyState !== XMLHttpRequest.DONE) return
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
success && success(xhr.response)
} else {
failure && failure(xhr.response)
}
}
xhr.ontimeout = function() {
failure && failure("timeout")
}
// 2.设置响应的类型
xhr.responseType = "json"
xhr.timeout = timeout
// 3.发送请求
const params = Object.keys(data).map(key => `${key}=${encodeURIComponent(data[key])}`)
const paramsString = params.join("&")
// 设置header
// for (const headerKey in headers) {
// xhr.setRequestHeader(headerKey, headers[headerKey])
// }
if (method.toLowerCase() === "get") {
xhr.open(method, url + "?" + paramsString)
xhr.send()
} else {
xhr.open(method, url)
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
xhr.send(paramsString)
}
return xhr
}