1. 闭包
作用域链:
- 执行上下文
- 创建阶段
- 作用域链
- 变量对象( 参数 ,变量 , 函数声明)
- this
- 执行阶段
- 变量赋值
- 函数引用
++i
- 先 加 1 在返回值,
i++
- 先返回值 再 加 1
练习题:
for(var i=0;i<5;i++){
setTimeout(()=>{
console.log(i++,'for循环中的i')
})
}
console.log(i,"for循环外面的i")
分析:
异步队列,等全局任务执行结束后再执行异步队列,等for循环结束后,i的值为4 所以输出结果如下.
输出结果:
5 "for循环外面的i"
5 "for循环中的i"
6 "for循环中的i"
7 "for循环中的i"
8 "for循环中的i"
9 "for循环中的i"
改后代码:
for(var i=0;i<5;i++){
(function(x){
setTimeout(()=>{
console.log(x++,'for循环中的x')
})
})(i)
}
console.log(i,"for循环外面的i")
分析:
全局事件执行结束后,直接执行 立即执行函数, 立即执行函数传入的参数 是从 0 开始.
输出结果:
5 "for循环外面的i"
0 "for循环中的x"
1 "for循环中的x"
2 "for循环中的x"
3 "for循环中的x"
4 "for循环中的x"
2. async await 关键字
- async 函数永远返回一个 promise 函数 (函数内部要有 return 返回值)
- async await 是promise 的语法糖
- promise.then 可以接收 执行成功和 拒绝两种状态的返回值.
- 使用多个 await 同时请求同一个 地址 , 需要等第一个 url 处理完 才回去处理其他的 await 请求,这时我们就可以使用 promise.all 方法 并行处理,来解决.
function bb(){
return '别bb,专心学习'
}
console.log(bb()) // '别bb,专心学习'
//async 永远返回一个 promise 函数.
async function bbAsync(){
return '别bb,专心学习'
}
console.log(bbAsync())// [object promise]
//使用 promise .then 方法 查看函数返回值
bbAsync().then(val=>console.log(val)) // '别 bb ,专心学习'
async function bbAsync(){
console.log('1')
let two = await Promise.resolve('2') // 使用 await 可直接拿到 promise 的返回值.
console.log(two)
console.log('3')
return Promise.resolve('别bb,专心学习')
}
bbAsync().then(val=>console.log(val))
// 输出顺序 1 2 3 '别bb,专心学习'
优化fetch 请求
const url = "https://gorest.co.in/public/v1/users";
fetch(`${url}?page=1`)
.then((response) => response.json())
.then((json) => json.data)
.then((data) => console.log(data, "1"));
fetch(`${url}?page=2`)
.then((response) => response.json())
.then((json) => json.data)
.then((data) => console.log(data, "2"));
fetch(`${url}?page=3`)
.then((response) => response.json())
.then((json) => json.data)
.then((data) => console.log(data, "3"));
// 打印顺序 (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}] '3'
//.html:18 (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}] '1'
//.html:23 (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}] '2'
使用 async await 优化:
// 改种优化 可能会造成 线程阻塞, 因为 三个 fetch 同时处理 url 请求,可以使用 promise.all来解决线程阻塞的问题
let bb = async () => {
let response1 = await fetch(`${url}?page=1`);
let json1 = await response1.json();
let data1 = await json1.data;
let response2 = await fetch(`${url}?page=2`);
let json2 = await response2.json();
let data2 = await json2.data;
let response3 = await fetch(`${url}?page=3`);
let json3 = await response3.json();
let data3 = await json3.data;
console.log(data1, "1");
console.log(data2, "2");
console.log(data3, "3");
};
bb();
//打印顺序: (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}] '1'
//.html:27 (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}] '2'
//.html:28 (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}] '3'
使用promise.all 优化:
// promise.all 可以使 三个 请求并行处理.可以解决线程阻塞的问题
let bb = async () => {
let responses = await Promise.all([
fetch(`${url}?page=1`),
fetch(`${url}?page=2`),
fetch(`${url}?page=3`),
]);
let jsons = responses.map((response) => response.json());
// 使用 json() 方法 还是会返回 promise. 所以还需要 promise.all 来处理一下
let values = await Promise.all(jsons);
values.map((data, index) => console.log(data.data, index));
}
bb();
//打印顺序:(10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}] 0
//.html:23 (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}] 1
//.html:23 (10) [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}] 2
async await 补充知识
返回 Promise 对象
async 函数返回一个 promise 对象
async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。
async function f() {
return "hello world";
}
f().then((v) => console.log(v));
// "hello world"
async 函数内部抛出错误,会导致返回的 promise 对象变为 reject 状态。抛出的错误对象会被 catch 方法回调函数接收到。
async function f() {
throw new Error("出错了");
}
f().then(
(v) => console.log("resolve", v),
(e) => console.log("reject", e)
);
//reject Error: 出错了
注意
任何一个 await 语句后面的
Promise 对象变为reject状态
,那么整个async
函数都会中断执行
。
async function f() {
await Promise.reject("出错了");
await Promise.resolve("hello world"); // 不会执行
}
解决办法
可以将第一个 await 放在 try…catch 结构里面,这样不管这个异步操作是否成功,第二个 await 都会执行。
async function f() {
try {
await Promise.reject("出错了");
} catch (e) {}
return await Promise.resolve("hello world");
}
f().then((v) => console.log(v));
// hello world
另一种方法是 await 后面的 Promise 对象再跟一个 catch 方法,处理前面可能出现的错误。
async function f() {
await Promise.reject("出错了").catch((e) => console.log(e));
return await Promise.resolve("hello world");
}
f().then((v) => console.log(v));
// 出错了
// hello world
async 函数可以保留运行堆栈。
const a = () => {
b().then(() => c());
};
上面代码中,函数 a 内部运行了一个异步任务 b()。当 b()运行的时候,函数 a()不会中断,而是继续执行。等到 b()运行结束,可能 a()早就运行结束了,b()所在的上下文环境已经消失了。如果 b()或 c()报错,错误堆栈将不包括 a()。
const a = async () => {
await b();
c();
};
上面代码中,b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦 b()或 c()报错,错误堆栈将包括 a()。
3. 解决跨域的方法
- jsonp
- 利用 javascript 标签来实现 服务器之间的跨域,向服务器请求 JSON 数据,服务器收到请求后,将数据放在一个指定名字的回调函数的参数位置传回来。
- cors
- 后端 通过配置头部 Access-Control-Allow-Origin 来解决跨域的问题.
- 服务器配置代理
- 通过 搭建一个中间层 来实现 不同源地址之间 资源的互换,从而解决跨域的问题.
4. cookie , sessionStorage , localStorage 区别
注意:
- 浏览器都兼容 cookie , web 存储只有 h5 支持.
- localStorage 虽然存储容量大,但是有可能影响浏览器的渲染速度.
- cookie 存储在浏览器和服务器, web 存储保存在浏览器.
5. fetch API 使用技巧
get 实战
通过点击按钮, 发送 fetch 请求,获取图片路径链接.
<!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>fetch</title>
</head>
<body>
<button>点击获取单身狗照片</button>
<img />
<script>
// get 实战
const button = document.querySelector("button");
const img = document.querySelector("img");
button.addEventListener("click", () => {
fetch("https://dog.ceo/api/breeds/image/random")
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("快去看蛋老师的视频");
})
.then((data) => (img.src = data.message));
});
</script>
</body>
</html>
post 实战
通过点击 按钮 , 发送 fetch 请求, 向服务器推送一条消息,
注意:
- fetch 的第二个参数中可以配置
请求方法
,请求体参数
,请求头
等method
属性 不能添加s
body
属性携带请求数据, 不是data
属性- 请求体参数 要使用 JSON.stringify() 转化为服务器可以处理识别的格式
- 要在 请求头中 配置 内容类型 ,
Content-Type
.
<button>点击推送消息</button>
<input type="text" />
// post 实战
const button = document.querySelector("button");
const input = document.querySelector("input");
button.addEventListener("click", () => {
fetch("https://jsonplaceholder.typicode.com/users", {
method: "POST",
body: JSON.stringify({ name: input.value }),
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response.json())
.then((data) => console.log(data));
});
6. promise 基础知识
举例说明 promise 的基础属性
- 女人承诺(promise) 男人,如果怀孕了就让他当
孩子他爹
,如果没怀孕就让男人当老公
.- 成功怀孕就是
resolve
,失败的话就是reject
,然后把成功和失败的结果作为参数返回.(成功怀孕的结果孩子他爹
,失败怀孕的结果老公
)- 兑现承诺的时候 , 成功用
then
表示,失败用catch
表示 , 但是 无论成功还是失败,最终finally
他们都会结婚.
const isPregnant = false;
// 判断女人是否怀孕,怀孕就是孩子太爹,没怀孕就是老公
const promise = new Promise((resolve, reject) => {
if (isPregnant) {
resolve("孩子他爹");
} else {
reject("老公");
}
});
promise
.then((result) => console.log(`男人成为了${result}!!`))
.catch((result) => console.log(`男人成为了${result}!!`))
.finally(() => console.log("男人和女人结婚了!!"));
promise 实战
注意:
- img.onload = ()=>{} 不可以写成回调函数的形式 img.onload( () =>{} )
- document.body.innerHTML = error
const url =
"https://www.tp88.net/uploads/allimg/151206/1-151206161408.gif";
const imgFunction = (url) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.src = url;
//加载图片的时候,抛出图片
img.onload = () => {
resolve(img);
};
//加载图片有误的时候,抛出信息
img.onerror = () => {
new Error("图片有误");
};
});
};
imgFunction(url)
.then((result) => {
document.body.appendChild(result);
})
.catch((error) => {
document.body.innerHTML = error;
});
涉及到的相关知识点
img.onload
属性 和img.onerror
属性
onerror事件
(img标签加载失败的方法)onload事件
(img标签加载完成的方法)- 参考文章@一月清辉
1.onerror属性:
< img src=“123” :οnerrοr=“defaultImg()” />
function defaultImg(){ //地址错误时,直接删除img标签
this.remove()
}
或者
function defaultImg(event){ //地址错误时,设置默认图片
let img = event.srcElement;
img.src = ‘默认图片地址’;
img.onerror = null; //防止闪图
}
2.onload属性:
< img src=“123” :οnload=“successImg()” />
function successImg(){
//code ......
}
7. 手写一个 promise
promise 相关知识点:
-
promise.then 方法有两个参数,都为回调函数
- 第一个 回调函数会返回 fulfilled(成功) 状态下的返回值
- 第二个 回调函数会返回 rejected(失败) 状态下的返回值
-
promise 回调函数中的两个参数如果不是 函数,将会被忽略
-
constructor 中的this 指向,和 class 类中的this 指向不同,需要使用 bind 重新进行this绑定
难点 :
- this 指向问题.
- 注意在 constructor 实例中 对resolve 和 reject 两个参数重新绑定this
- then 方法的实现
- 回调函数中如果传入的不是 function 类型的将会被忽略。
- 执行异常 try catch
- then 方法的第二个参数,会将执行失败的消息抛出
- 异步问题
- then 方法中的代码是最后执行的。
- 回调保存
- resolve 和 reject 是要在 事件循环末尾执行的, 所以要在 resolve 和 reject 方法中加入 setTimeout方法
- 链式调用
- 在 then 方法中 在写一个 promise 方法即可
手写 代码
class Commitment {
static pending = "待定";
static fulfilled = "成功";
static rejected = "失败";
constructor(func) {
// 声明两个数组 是用来保存 状态为 pending 状态时的 resolve 和 reject 函数的
// 用数组 是因为,数组 采用的是 先入先出的机制.
this.resolveCallbacks = [];
this.rejectCallbacks = [];
this.status = Commitment.pending;
this.result = null;
//这里使用 try catch 是为了模仿,then 方法的第二个回调函数,将错误抛出去
try {
func(this.resolve.bind(this), this.reject.bind(this));
} catch (error) {
this.reject(error);
}
}
resolve(result) {
setTimeout(() => {
if (this.status === Commitment.pending) {
this.status = Commitment.fulfilled;
this.result = result;
// 遍历数组,看看 then 方法 是否有保留的待执行函数,然后逐个执行,并将结果抛出去
this.resolveCallbacks.forEach((callback) => {
callback(result);
});
}
});
}
reject(result) {
setTimeout(() => {
if (this.status === Commitment.pending) {
this.status = Commitment.rejected;
this.result = result;
// 遍历数组,看看 then 方法 是否有保留的待执行函数,然后逐个执行,并将结果抛出去
this.rejectCallbacks.forEach((callback) => {
callback(result);
});
}
});
}
then(onFulfilled, onRejected) {
// 返回一个新实例是为了让 promise 实现 链式调用
return new Commitment((resolve, reject) => {
// 这一步是为了防止 传入promise 函数的参数不是函数类型.
onFulfilled =
typeof onFulfilled === "function" ? onFulfilled : () => {};
onRejected =
typeof onFulfilled === "function" ? onFulfilled : () => {};
//如果状态为 pending 将 resolve 和 reject 函数 存入数组
if (this.status === Commitment.pending) {
this.resolveCallbacks.push(onFulfilled);
this.rejectCallbacks.push(onRejected);
}
if (this.status === Commitment.fulfilled) {
// then 方法是一个异步微任务,所以 需要添加 setTimeout ,同步执行完了以后,最后执行then 里面的方法
setTimeout(() => {
resolve(onFulfilled(this.result));
});
}
if (this.status === Commitment.rejected) {
setTimeout(() => {
reject(onRejected(this.result));
});
}
});
}
}
//实战 模拟promise 部分
console.log("第一步");
let commitment = new Commitment((resolve, reject) => {
console.log("第二步");
setTimeout(() => {
// 因为 '这次一定' 没有输出 , 初步暂定 then 方法除了问题, then 方法中是根据 status 进行判断输出 result 的.所以 在三个位置('这次一定'前后,和then 方法中.) 查看是那个 status 出了问题.
// console.log(commitment.status);
resolve("这次一定");
reject("下次也不一定");
// console.log(commitment.status);
console.log("第四步");
});
// throw new Error("白嫖成功");
});
commitment.then(
(result) => console.log(result),
(result) => console.log(result.message)
);
console.log("第三步");
手写一个 promise.all()
//详细参考 前端胖头鱼;(我也是看的胖头鱼的文章)
Promise.myAll = (promises) => {
return new Promise((rs, rj) => {
// 计数器
let count = 0
// 存放结果
let result = []
const len = promises.length
if (len === 0) {
return rs([])
}
promises.forEach((p, i) => {
// 注意有的数组项有可能不是Promise,需要手动转化一下
Promise.resolve(p).then((res) => {
count += 1
// 收集每个Promise的返回值
result[ i ] = res
// 当所有的Promise都成功了,那么将返回的Promise结果设置为result
if (count === len) {
rs(result)
}
// 监听数组项中的Promise catch只要有一个失败,那么我们自己返回的Promise也会失败
}).catch(rj)
})
})
}
// 测试一下
const p1 = Promise.resolve(1)
const p2 = new Promise((resolve) => {
setTimeout(() => resolve(2), 1000)
})
const p3 = new Promise((resolve) => {
setTimeout(() => resolve(3), 3000)
})
const p4 = Promise.reject('err4')
const p5 = Promise.reject('err5')
// 1. 所有的Promise都成功了
const p11 = Promise.myAll([ p1, p2, p3 ])
.then(console.log) // [ 1, 2, 3 ]
.catch(console.log)
// 2. 有一个Promise失败了
const p12 = Promise.myAll([ p1, p2, p4 ])
.then(console.log)
.catch(console.log) // err4
// 3. 有两个Promise失败了,可以看到最终输出的是err4,第一个失败的返回值
const p13 = Promise.myAll([ p1, p4, p5 ])
.then(console.log)
.catch(console.log) // err4
// 与原生的Promise.all返回是一致的
8. class 类核心知识
-
用class,将 ‘属性’ 和 ‘方法’ 封装起来的操作,叫做类。
-
constructor
,class 中的方法.- 每次 new 一个类的时候都会触发这个 constructor方法,需要提前定义好需要传入的参数
- 用于接收 实例传入的参数
-
extends
,继承 用于 子类继承父类中的属性和方法。- 在子类中写 constructor 方法,其实就是在继承父类中的 constructor方法。
-
super()
- 如果子类写了 constructor 方法,就必须要写 super 方法,用于初始化从父类中继承过来属性的 this 指向。
- 在子类 constructor 方法中使用 this 之前,一定要先写 super() 方法。(也就是 super() 要写在constructor 方法中的第一行)
-
static
- 用于在 class 类中声明 静态方法,由类来调用 声明的静态方法,而不是 new的实例。
- 在类中,静态方法可以互相调用。
- 建议将 static 声明的静态方法 放在 class 类的最上面。
-
get 和 set
- set ,可以给set 赋值,在class 类中定义的时候也像是一个方法,但是 set 定义的是一个属性,是一个 访问器属性。
- get 和 set 是一对,设置和访问同一个属性.
-
class 中的 then()方法。
一个 class 类 当中如果有 then() 方法,js 会自动将其封装为一个 promise 函数.
类属性 和 对象属性
类属性
通过定义的类直接打点添加一个属性
,也可以在 定义的class中使用 static 关键字来定义 类属性.
对象属性
在定义的class类中,不添加关键字定义的属性是对象属性
类属性 和 对象属性的读取方式
类属性
,可以通过定义的class类 直接打点调用(读取)
对象属性
,首先要 new class() ,实例一个定义的class类,然后打点调用(读取)
class test {
// 对象属性
a = 100
// 类属性
static b = 88
}
// 类属性
test.a = 66
//类属性读取
console.log(test.a, test.b) // 66 88
//对象属性读取
console.log(new test().a) // 100
class 实战
废话生成器:
1. 需求,写一个废话生成器。
2. 一个 input 两个 按钮,一个p 标签。
3. 点击按钮根据 输入框的内容 生成废话
html部分:
<input type="text" />
<button class="button_1st">一级废话</button>
<button class="button_2nd">二级废话</button>
<p></p>
js部分:
const input = document.querySelector("input"),
button_1st = document.querySelector(".button_1st"),
button_2nd = document.querySelector(".button_2nd"),
p = document.querySelector("p");
class Bullshit {
static welcometips() {
return "温馨提示,您这是在说废话";
}
static welcome() {
p.innerHTML = this.welcometips();
}
// 用于接收实例传入的参数
constructor(text, color) {
this.text = text;
this.color = color;
}
show() {
p.innerHTML = this.text + input.value;
p.style.color = this.color;
}
set extra(value) {
this.value = value;
p.innerHTML += this.value;
}
get extra() {
return `'${this.text}' 和 '${this.value}' 是废话`;
}
}
class Son_of_Bullshit extends Bullshit {
constructor(text, color, fontsize) {
// 用于继承 父类中的属性。
super(text, color);
this.fontsize = fontsize;
}
show() {
p.innerHTML = this.text + p.innerHTML;
p.style.color = this.color;
p.style.fontSize = this.fontsize;
}
}
// button_1st 调用父类的属性和方法。
button_1st.addEventListener("click", () => {
const bullshit = new Bullshit("我知道", "#00A1d6");
bullshit.show();
// 设置extra 属性的值,调用 set 属性
bullshit.extra = "儿";
// 访问 extra 属性 ,调用 get 属性
console.log(bullshit.extra);
});
// button_2nd 调用子类自己的属性和方法
button_2nd.addEventListener("click", () => {
const son_of_bullshit = new Son_of_Bullshit("我知道", "tomato", "26px");
son_of_bullshit.show();
});
// p 用于调用父类的静态方法
p.addEventListener("click", () => {
Bullshit.welcome();
});
9. ajax 相关知识
XMLHttpRequest
是一个构造函数可以通过 new 一个 XMLHttpRequest 实例,来与服务器进行通讯.
XMLHttpRequest.send( 请求体 )
用于发送 请求的方法.
如果是 post 请求,括号中可以存放 请求体参数(body)XMLHttpRequest.open( )
第一个参数:
- 发送请求的方式 (get post)
第二个参数:
- 发送请求的地址(地址)
第三个参数:
- 发送请求的是否异步处理
- true 异步请求处理
- false 不进行异步处理
XMLHttpRequest.readyState
0 : 表示未调用 open 方法
1 : 表示调用 open ,但是没有调用 send 方法
2 : 表示 发送请求,但是没有收到响应
3 : 表示收到了部分响应
4 : 表示响应已经全部接受完XMLHttpRequest.onreadystatechange = () =>{}
用于监听
XMLHttpRequest.readyState
的状态,然后根据状态进行下一步的操作.
图示:
实战
<img src="" alt="" />
<script>
const image = document.querySelector("img");
//创建实例
const xhr = new XMLHttpRequest();
//写入请求方式,和请求地址
xhr.open(
"get",
"https://www.tp88.net/uploads/allimg/151206/1-151206161408.gif",
true
);
// 监听是否拿到 请求的响应结果。
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
//将响应结果 赋值个 image
image.src = xhr.responseURL;
console.log(xhr, "response");
}
}
};
xhr.send();
</script>
基于 ajax 封装一个 promise
<button>点击获取野猴子照片</button>
<img src="" alt="" />
js部分:
const button = document.querySelector("button");
const image = document.querySelector("img");
function xhrRequest() {
let promise = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(
"get",
"https://www.tp88.net/uploads/allimg/151206/1-151206161408.gif",
true
);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
//xhr.status >= 200 && xhr.status < 300 表示请求成功,
// xhr.status === 304 表示从 缓存中读取数据
if (
(xhr.status >= 200 && xhr.status < 300) ||
xhr.status === 304
) {
resolve(xhr.responseURL);
} else {
reject(new Error(xhr.statusText));
}
}
};
// 注意:一定要调用 xhr 的send 方法,否则是不会发送请求的。
xhr.send();
});
return promise;
}
button.addEventListener("click", () => {
xhrRequest()
.then((url) => {
image.src = url;
})
.catch((error) => {
console.log(error);
});
});
10. 深拷贝 和 浅拷贝
举例说明深拷贝和浅拷贝的区别:
// 原始值,赋值以后并不会影响到 原属性.
let video = "100";
let videoCopy = video;
videoCopy = "10000";
console.log("video" + video);
console.log("videoCopy" + videoCopy);
// 引用值,赋值以后 原属性也会随着改变.
let videoObj = { like: "10" };
let videoObjCopy = videoObj;
videoObjCopy.like = "10000";
console.log("videoObj", videoObj);
console.log("videoObjCopy", videoObjCopy);
使用递归,实现一个深拷贝
function deepCopy(obj){
//判断是否是简单数据类型,
if(typeof obj == "object"){
//复杂数据类型
var result = obj.constructor == Array ? [] : {};
for(let i in obj){
result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
}
}else {
//简单数据类型 直接 == 赋值
var result = obj;
}
return result;
}
11. 图片懒加载
经典方法
获取每一张图片到顶部的距离,与可视窗口进行比较
缺点:
- 滚动事件会一直触发,影响浏览器性能
html部分:
<p>
你要写上班你就不能只写上班。 要写奇葩的同事,要写窒息的领导,
要写突如其来的急活和它着急的程度。 要写办公室里叮了咣啷的电话响,
写打电话的人和你抬杠, 文件在哪乱放。 写毫无必要的加班,
写卷到飞起的现状, 甚至是周日下午三点就开始的焦虑。
</p>
<img data-src="./img/2.jpg" alt="" />
<img data-src="./img/1.jpg" alt="" />
<img data-src="./img/3.jpg" alt="" />
js部分:
const images = document.querySelectorAll("img");
// 经典做法,获取每一张图片到顶部的距离,与可视窗口进行比较
// 缺点:滚动事件会一直触发,影响浏览器性能
//
window.addEventListener("scroll", () => {
images.forEach((image) => {
const imageTop = image.getBoundingClientRect().top;
if (imageTop < window.innerHeight) {
const src = image.getAttribute("data-src");
image.setAttribute("src", src);
}
console.log("触发了滚动事件");
});
});
优雅的做法
IntersectionObserver
, 是浏览器提供的构造函数(交叉观察)。
- 该实例需要传入两个参数, 第一个参数是 回调函数。需要有触发条件
- 第一个回调函数一般被触发两次, 目标元素能看见 触发一次,目标元素看不见在
- 优点:
- 在完成懒加载以后,就不会重复触发滚动事件
const observer = new IntersectionObserver();
observe()
方法中传入需要观察的 dom 节点
- observer.observe(DOM节点);
unobserve()
方法中传入 需要解除观察的 dom 节点
- observer.unobserve(DOM节点);
const images = document.querySelectorAll("img");
const callback = (result) => {
result.forEach((item) => {
// isIntersecting 该属性 用来表示目标元素是否滚动到
//可是区域 false 表示没有,true 表示目标元素在可视区域
// target 表示 目标元素
if (item.isIntersecting) {
//拿到目标元素,并获取 目标元素的 data-src 属性
const image = item.target;
const data_src = image.getAttribute("data-src");
image.setAttribute("src", data_src);
// 在完成懒加载后,取消目标元素的观察
observer.unobserve(image);
console.log("触发");
}
});
};
const observer = new IntersectionObserver(callback);
// 遍历每一个 图片节点,并观察
images.forEach((image) => {
observer.observe(image);
});
12. 宏任务与微任务
javascript 单线程,只有一个
调用栈
,调用栈遵循先入后出
的 处理逻辑执行调用栈的时候:
- 先执行同步任务
- 后执行异步任务
微任务优先于宏任务
要点:
- setTimeout 要等到 script 结束时,才执行。
宏任务队列 (先入先出)
- 新程序或子程序被直接调用 (最常见的就是 script 元素里的代码就是代码在执行。
- 事件的回调函数
- setTimeout() 和 setInterval()
微任务队列 (先入先出)
- promise. then() .catch() .finally()
- MutationObserver
- Object.observe
script 元素内的代码执行完了之后,会优先执行 微任务,最后是宏任务。
事件循环
13. 可选连操作符
语法结构:
- obj?.a?.b
- 参考文章@云的另一端
- 报错结果;
定义对象:
obj: {},
操作对象:
console.log(obj.a) // undefined
console.log(obj.a.b) // 控制台会报错
使用可选连操作符操作对象:
console.log(obj?.a) // undefined
console.log(obj?.a?.b) // undefined (控制台不会报错 )
14. 节流
和防抖
防抖相关难点
- 高阶函数:为了让传入的函数在程序初始化的时候不执行
- this指向问题:
- 当直接调用 payMoney 函数时,this 指向 button这个dom元素
- 当把 payMoney 函数传入 debounce 函数执行时,this指向 window,
因为回调函数在运行时已经在 window 下了
,所以我们需要重新在 setTimeout 之前 保留 this ,然后再 payMoney 函数执行时,使用.call 重新绑定 this
。- 参数问题:要考虑函数没有设置参数时,被传入参数的情况
防抖分为:
非立即防抖:
一段时间后,再执行该事件,若在n秒内被重复触发,则重新计时;立即防抖:
触发事件后函数会立即执行,然后n秒内不触发才会执行函数的效果
注意:我们平时写的都是非立即防抖
节流实战
function coloring() {
let r = Math.floor(Math.random() * 255);
let g = Math.floor(Math.random() * 255);
let b = Math.floor(Math.random() * 255);
document.body.style.backgroundColor = `rgb(${r},${g},${b})`;
}
function throttle(func, delay) {
let timer;
return function () {
let that = this;
let args = arguments;
if (timer) {
this.$message.warning("请勿频繁操作 -- throttle");
return false;
}
timer = setTimeout(() => {
func.apply(that, args);
timer = null; // 节流中 清除timer 可以不用 clearTimeout ,因为清除动作是在 定时任务之后进行的。
}, delay);
};
}
window.addEventListener("resize", throttle(coloring, 2000));
防抖实战
<button>防抖操作按钮</button>
const button = document.querySelector("button");
function payMoney() {
console.log("防抖");
console.log(this);
}
// 封装防抖函数
function debounce(func, delay) {
// 防抖函数中需要 用到高阶函数,为了防止 程序已启动就调用传入的函数
// 高阶函数:就是函数中再返回一个函数
let timer;
return function () {
let that = this;
let args = arguments;
if (timer) {
clearTimeout(timer);
this.$message.warning("请勿频繁操作 -- debounce");
}
timer = setTimeout(() => {
func.call(that, args);
timer = null;
}, delay);
};
}
button.addEventListener("click", debounce(payMoney, 3000));
// button.addEventListener("click", payMoney);
Vue 中使用节流和防抖函数
正确使用
<el-button type="primary" @click="saveHandler">保存</el-button>
<script>
import { deepCopy, debounce } from '@/utils/wheelUtils.js'
methods:{
saveHandler: debounce(function () {
this.tableData[this.tableIndex] = this.inputFormList.form
let bodyData = {
...this.formList.form,
busnissKey: this.businessKey,
taskId: this.taskId,
handleUserCode: this.currentUserInfo.userCode,
deliverablesList: this.tableData
}
API.deliverablesSubmitEdit(bodyData).then((res) => {
this.inputDialogVisible = false
this.deliverablesSubmitSearchFn()
this.$message({ type: 'success', message: '操作成功' })
})
}, 1000),
}
</script>
15. Symbol () 函数
symbol:
- 对象中如果有两个相同的属性,那么下面的属性会覆盖上面的属性,这时候就需要用到 symbol 方法。
- 可以使用
Object.getOwnPropertySymbols(obj)
方法来查看 对象中存在的 symbol 类型的数据- Symbol 是
原始值
,不能使用 new- Symbol 函数接收一个可选参数,方便后期的阅读和调试。
Symbol.for() 创建一个共享的 Symbol
, 如果已存在,直接返回 已有的SymbolSymbol 函数创建的原始值都是唯一的
Symbol.keyfor()
返回已登记Symbol有关的键
const obj = {
prince: ["prince1", "prince2", "prince3"],
princess: ["princess1", "princess2", "princess3"],
};
// const prince = Symbol("bastard");
// obj[prince] = "bastard";
// const bastard1 = Symbol("如花");
// obj[bastard1] = "如花的儿子";
// const bastard2 = Symbol("如花");
// obj[bastard2] = "如花的儿子";
// const bastard3 = Symbol("似水");
// obj[bastard3] = "似水的儿子";
console.log(Object.getOwnPropertySymbols(obj));
//[Symbol(bastard)]
console.log(obj);
// 没用 symbol.for 属性 ,打印结果有三个 symbol 数据
const bastard1 = Symbol.for("如花");
obj[bastard1] = "如花的儿子";
const bastard2 = Symbol.for("如花");
obj[bastard2] = "如花的儿子";
const bastard3 = Symbol.for("似水");
obj[bastard3] = "似水的儿子";
console.log(obj); // 此时打印obj 只有两个symbol 数据
console.log(Symbol.keyFor(bastard1)); // 如花
console.log(Symbol.keyFor(bastard2)); // 如花
console.log(Symbol.keyFor(bastard3)); // 似水
使用场景
当一个班级中有重名的两个学生时,使用对象录入成绩的时候,下面的学生会覆盖上面学生的成绩
注意:在对象中如果要在 key的地方使用变量,需要使用 [] 中括号包裹,否则会被识别为一个字符串
// 此时打印下面的obj ,下面的李四 会覆盖上面的李四
let obj = {
李四: { css: 35, js: 66 },
李四: { css: 43, js: 88 },
}
// 使用 symbol 解决上面的问题
let user1 = {
name: '李四',
key: Symbol(),
}
let user2 = {
name: '李四',
key: Symbol(),
}
let obj2 = {
[user1.key]: { css: 35, js: 66 },
[user2.key]: { css: 43, js: 88 },
}
// 查看某一个学生成绩的时候
console.log(obj2[user2.key])
16. 传值和传址
传值(栈内存)
- 针对于
基本数据类型
,在数据进行赋值的时候,会重新开辟一块内存空间
.- 赋值 前后的两个
变量之间互不影响
.
传址(堆内存)
- 针对于
复杂数据类型
,在变量进行赋值的时候,会复制一个内存指针给新的变量
.- 赋值 前后的两个变量,共用一块内存空间,修改
内部属性会互相影响
.- 可以使用
深拷贝
解决该问题.
17. null 和 undefined
定义基本数据类型
(空字符串)的时候,可以使用undefined
- 当
声明一个变量但是没有赋值
的时候,会返回undefined
- 当
使用一个没有声明的变量
也会返回undefined
定义引用数据类型
(空对象)的时候,可以使用null
18. while 语句
while(条件 condition) { 循环体 }
while(条件语句){
当while中的条件一直成立时,循环体内的代码就一直执行.
}
19. do while
do{ 循环体 } while (条件 condition)
不管条件是否成立
都会先执行一次 do 循环体中的代码.- 然后根据 while 的条件进行判断.
do{
先执行一次do循环体内的代码,然后判断 while 中的条件是否成立,如果成立则继续执行 do循环体内的代码.
}while(条件condition)
20. break continue 和 循环体 label 标签
- break 可以配合 label 标签,指定终止哪一层循环
- continue 同样可以配合 label 标签,指定下一次循环从那一层开始.
outsideCoco: for (let i = 1; i <= 10; i++) {
insideCoco: for (let j = 1; j <= 10; j++) {
if (i + j > 10) continue outsideCoco
}
}
// break 使用标签同样适用
outsideCoco: for (let i = 1; i <= 10; i++) {
insideCoco: for (let j = 1; j <= 10; j++) {
if (i + j > 10) break outsideCoco
}
}
21. 标签模板(在模板字符串之前使用 标签
)
- tag
${name}
- tab(strings,…arguments){}
- 第一个参数 : 是模板字符串中的
所有字符串集合成的一个数组
.- 第二个参数: 是模板字符串中
所有的变量
let name = 'coco'
let age = 18
let string = tag`大家好,我叫${name},今年${age}岁了.`
console.log(string)
// 第一个参数 : 是模板字符串中的所有 字符串集合成的一个数组.
// 第二个参数: 是模板字符串中所有的 变量
function tag(strings, ...arg) {
console.log(strings, arg)
}
22. reduce 使用 (标题替换)
- pre 表示上一次循环返回(
return
)的值.- reduce 的第二个参数表示给 pre 赋一个初始值.
let word = ['php', 'css']
let string = '我喜欢在后端人学习php和css'
const replaceString = word.reduce((pre, word) => {
return pre.replace(word, `<a href='${word}' >${word}</a>`)
}, string)
console.log(replaceString)
document.write(replaceString)
23. 数据显示类型转换
的两种方式
- 使用
两个感叹号(!!)
第一个 !
: 会将数据先隐式转换为 boolean 类型并取反,第二个 !
: 会将取反后的数据再次 取反,就会得到数据本身的 boolean 类型.
- 使用
Boolean()
- 将需要进行类型转换的数据传入 Boolean() 函数中.
let number = 0
console.log(!!number) //false
console.log(Boolean(number)) //false
let string = ''
console.log(!!string) //false
console.log(Boolean(string)) //false
// 空数组 和 空对象转换为 boolean类型都会 true
let arr = []
console.log(!!arr) //true
console.log(Boolean(arr)) //true
let obj = {}
console.log(!!obj) //true
console.log(Boolean(obj)) //true
24. 判断 Nan
的两种方法
- 使用 Number.isNan( number ) // 返回 true 和 false
- 使用 Object.is(value1 , value2(Nan)) // 返回 true 和 false
- Object.is() 方法用于比较 传入的 value1 和 value2 是否相等.
25. Math 数学计算
- Math.max(val1,val2,val3,val4) :可以比较多参数的
最大值
,不支持传入一个数组
,会返回 Nan- Math.min(val1,val2,val3,val4) :可以比较多参数的
最小值
,不支持传入一个数组
,会返回 Nan
console.log(Math.max(1, 2, 3, 4, 5)) // 5
// 当传入一个数组的时候 会返回 Nan.
const arr = [11, 2, 3, 12, 3, 124]
Math.max(arr) // Nan
解决不能传入数组:
// 方法一: 使用 apply 改变指向.
const arr = [11, 2, 3, 12, 3, 124]
Math.max.apply(null,arr) // 124
// 方法二: 使用 展开运算符 展开 数组.
const arr = [11, 2, 3, 12, 3, 124]
Math.max(...arr)
26. map类型
可以使任何数据类型作为键
- 新增: map.set(
key,value
)- 查询: map.get(
key
)- 删除: map.delete(
key
) ,返回值是 boolean 值(删除成功是 true, 删除失败是 false).- map.clear() :清空 map 中的所有值.
map 方法
clear
从映射中移除所有元素。
delete
从映射中移除指定的元素。
forEach
对映射中的每个元素执行指定操作。
get
返回映射中的指定元素。
has
如果映射包含指定元素,则返回 true。
set
添加一个新建元素到映射。
toString
返回映射的字符串表示形式。
valueOf
返回指定对象的原始值。
实操案例
let mapArr = [
["name", "coco"],
["age", 19],
["sex", "male"],
];
let map = new Map(mapArr); //
console.log(map); // Map { name: 'coco', age: 19, sex: 'male' }
console.log(map.get("name")); // coco
console.log(map.get("age")); // 19
// has方法
console.log(map.has("name")); // true
map.set("address", "郑州");
map.set(["size", 10], 1);
console.log(map);
/*
Map {
'name': 'coco',
'age': 19,
'sex': 'male',
'address': '郑州',
[ 'size', 10 ]: 1
}
*/
let mapArr2 = [
[1, ["状态节点1", "提起流程"]],
[2, ["状态节点2", "职员审批"]],
[3, ["状态节点3", "科长审批"]],
];
let map2 = new Map(mapArr2);
console.log(map2); // Map { 1: [ '状态节点1', '提起流程' ], 2: [ '状态节点2', '职员审批' ], 3: [ '状态节点3', '科长审批' ] }
console.log(map2.get(1)); // [ '状态节点1', '提起流程' ]
遍历 map 数据类型的方法
- mapArr.keys()
- mapArr.values()
- mapArr.entries()
创建方式一:
let map = new Map()
map.set([], '数组')
map.set({}, '数组')
console.log(map)
创建方式二:
let map2 = new Map([
['name', 'coco'],
['age', '18'],
])
console.log(map2)
// 遍历 map 数据类型
获取键:
let map = new Map([
['后盾人', 'hdcms'],
['name', 'coco'],
['age', '18'],
])
for (const item of map.keys()) {
console.log(item)
}
获取值:
let map = new Map([
['后盾人', 'hdcms'],
['name', 'coco'],
['age', '18'],
])
for (const item of map.values()) {
console.log(item)
}
27. 函数声明
function fn(){}
: 这种声明方式,在程序执行的时候,会将改函数压入 window 中,也就是 window.fn() 也可以调用该函数.
存在 函数提升的问题
- 缺点:当声明的函数
与 window中原有的属性名冲突
的时候,window会优先调用我们声明的函数
,这样 window 中原有的属性方法就会无法调用.let fn = function(){}
: 这种声明函数的方式,window 则没办法调用该函数
不存在 函数提升的问题
建议: 可以将函数写进 class 类中,模块化使用,方便统一管理
28. switch
- switch( 变量 ) 判断不通的值 进行不通的操作.
- switch( true ) case 后面可以写表达式,
- 注意要写 default ,养成良好的习惯.
// 传入变量
let error = '1'
let msg = ''
switch (error) {
case '1':
msg = '一级告警'
return msg
case '2':
msg = '二级告警'
return msg
case '3':
msg = '三级告警'
return msg
default:
return msg
}
// 传入表达式
switch (true) {
case '1':
msg = '一级告警'
return msg
case '2':
msg = '二级告警'
return msg
case '3':
msg = '三级告警'
return msg
default:
return msg
}
29. apply 与 call
共同点:
- 都会立即执行,
- 都可以改变 this 指向.
异同点:
传递的参数不同
:
- apply 第二个参数传入一个数组
- call 可以传入多个参数
- apply(obj,[arg1,arg2,arg3])
- call(obj , arg1,arg2,arg3)
let user1 = {
name: '李四',
}
let user2 = {
name: '王五',
}
function User(age, lesson) {
console.log(this.name)
console.log(this.name + age + lesson)
}
User.call(user1, '18', 'js') // 李四 李四18js
User.apply(user2, ['88', '非常cowB']) // 王五 王五88非常cowB
bind()
- 不会立即执行
- 可以传两次参数
- 如果想要立即执行,则需要再 bind()() 添加一个括号
- bind 会
首先接收第一次传递的参数
,如果第一次没有传递
参数,则会接收第二次传递的参数
.
function user(a, b) {
console.log(a, b) // 2,3
return this.f + a + b
}
user.bind({ name: 'coco' }, 2, 3)(5, 6)
30. 延长函数生命周期
核心原理:
只要函数一直被使用,就不会被系统删除,从而达到延伸函数的生命周期
// 这种写法,每次调用 sum() 函数都会重新开辟一块新的内存空间, 所以 打印结果就一直都是 2.
function sum() {
let n = 1
function show() {
console.log(++n)
}
show()
}
// 多次调用 sum 函数.返回值都是 2
sum() // 2
sum() // 2
sum() // 2
延长函数声明周期的做法:
// 下面这个写法,将 show()函数返回出去,并一直被 a 引用,
// 则 sum 的内存地址就不会被删除,且多次调用sum()使用的都是一块内存地址,
// n也会一直保留.所以 n就会被一直++
function sum() {
let n = 1
return function show() {
console.log(++n)
}
}
// 调用 sum 函数.
let a = sum()
a() // 2
a() // 3
a() // 4
多层函数嵌套也是一样的:
function sum() {
let n = 1
return function show() {
let m =1;
return function show2(){
console.log(++n,'n')
console.log(++m,'m');
}
}
}
let a = sum()() // show2 赋值给a,并一直引用
a()
a()
31. js函数、变量提升
。
相关概念:函数提升,函数声明,函数表达式;
- 函数声明必须带有标示符(Identifier)(就是大家常说的函数名称),而函数表达式则可以省略这个标示符;
- 如果function foo(){}是作为赋值表达式的一部分的话,那它就是一个函数表达式,如果function foo(){}被包含在一个函数体内,或者位于程序的最顶部的话,那它就是一个函数声明。
函数声明:
function 函数名称 (参数:可选){ 函数体 }
函数表达式:
function 函数名称(可选)(参数:可选){ 函数体 }
举个例子:
function foo(){} // 声明,因为它是程序的一部分
var bar = function foo(){}; // 表达式,因为它是赋值表达式的一部分
new function bar(){}; // 表达式,因为它是new表达式
(function(){
function bar(){} // 声明,因为它是函数体的一部分
})();
总结
- 函数及变量的声明都将被提升到函数的最顶部。
- 函数声明,函数的声明以及定义都提升,函数表达式和变量表达式只是将函数或者变量的声明提升到函数顶部,而函数表达式和变量的初始化将不被提升.
32. 使用工厂模式
使用场景
当有大量代码使用同一个逻辑的时候,我们可以使用工厂模式来避免大量的复制粘贴
// 多段代码使用同一逻辑
let lisi = {
name: '李四',
show() {
return `${this.name} - cocotest`
},
}
let wangwu = {
name: '王五',
show() {
return `${this.name} - cocotest`
},
}
// console.log(wangwu.show())
使用工厂模式
// 使用工厂模式
function user(name, age) {
// console.log(obj);
// console.log(age);
return {
name,
age,
show() {
console.log(`${this.name} - cocotest - 统一处理`)
},
info() {
console.log(`${this.name}的年龄是${this.age}`)
},
}
}
let xye = user('小鱼儿', 18)
xye.show()
xye.info()
let hwq = user('花无缺', 22)
hwq.show()
hwq.info()
// 极简版
function user(name, age) {
return {
show() {
console.log(`我叫${name},今年${age}岁了`)
},
}
}
let lisi = user('李四', 18)
lisi.show()
33. 使用构造函数
创建对象
- 初始化一个构造函数,函数名首字母需要
大写
- 使用构造函数创建对象,需要使用
new 关键字
注意:
使用 构造函数创建的对象调用 show()方法,this指向当前对象
如果将构造函数中 show() 方法,当作普通函数去调用,this指向 全局(window)
// 使用构造函数创建对象
function User(name) {
this.name = name
this.show = function (text) {
console.log(this, text)
console.log(`我叫${this.name}`)
}
}
let lisi = new User('coco')
// 使用 构造函数创建的对象调用 show()方法,this指向当前对象
lisi.show('对象调用')
// 注意:如果将构造函数中 show() 方法,当作普通函数去调用,this指向 全局(window)
let func = lisi.show
func('普通函数调用')
41. Proxy 对象代理
let coco = {
name: 'jack',
age: 20,
}
let proxy = new Proxy(coco, {
// obj 表示当前对象,property表示当前访问的属性
get(obj, property) {
console.log(obj, property)
return obj[property]
},
set(obj, property, value) {
obj[property] = value
return obj[property]
},
})
proxy.age = 46
console.log(proxy.age)
通过 proxy 对象代理来实现 vue 双向绑定
<input type="text" v-model ="content">
<h4 v-bind="content">同时更新</h4>
<hr>
<input type="text" v-model="title"></input>
<input type="text" v-model="title"></input>
<h4 v-bind="title">同时更新</h4>
<script>
// let input = document.querySelectorAll('[v-model]')
// console.log(input[0]);
function View(){
let proxy = new Proxy({},{
get(obj,property){
},
set(obj,property,value){
const el = document.querySelectorAll(`[v-model='${property}']`)
const bind = document.querySelectorAll(`[v-bind='${property}']`)
// console.log(bind);
el.forEach(item=>{
item.value = value
})
bind.forEach((item=>{
item.innerHTML = value
}))
}
})
this.init = function (){
const el = document.querySelectorAll('[v-model]')
el.forEach(item=>{
item.addEventListener('keyup',function(){
proxy[this.getAttribute('v-model')] = this.value
})
})
}
}
new View().init()
</script>
42. JSON.stringify(obj/arr,null,数字)
将数据转换为 json 类型
- JSON.stringify(
需要转换的对象或数组
,需要保留的属性
,格式(数字)
)- 第二个参数传
null
,表示保留所有属性.- 第三个参数传的数字越大,格式距离就越长
43. JSON.parse(json,(key,value)=>{})
将 json 类型数据转换为 js 使用的数据类型.
- 第一个参数是需要转换的 json 数据.
- 第二个参数是一个回调函数,用来对 转换后的
key
和value
进行处理
let user = {
name: 'coco',
age: 18,
address: '南京市玄武区',
}
// let json = JSON.stringify(user,null,2)
let json = JSON.stringify(user, ['name'], 2)
console.log(json) // { 'name' : 'coco'}
let obj = JSON.parse(json, (key, value) => {
if (key == 'name') {
value = value + '练习'
}
return value
})
console.log(obj)
44. 原型关系
- 对象的原型等于 其构造函数的 prototype
let arr = []
console.log(arr.__proto__ == Array.prototype) // true
let string = ''
console.log(string.__proto__ == String.prototype) // true
let bool = true
console.log(bool.__proto__ == Boolean.prototype) // true
let obj = {}
console.log(obj.__proto__ == Object.prototype) // true
let reg = /a/g
console.log(reg.__proto__ == RegExp.prototype) // true
45. 原型继承
- js 中是原型的继承,而不是改变构造函数的原型
改变构造函数的原型不叫 原型继承
- 如果在构造函数中生命方法,那么每次 新建实例对象的时候都会带有 构造函数中声明的方法.会影响部分性能
解决:
- 我们可以在 构造函数的原型中去声明公共的方法 User.prototype.show = function(){}
- 这样 每次创建实例对象之后,实例中不会带有该方法,但是我们仍然可以从原型 prototype 中继承该方法使用.
function User() {
// 在构造函数中声明自己的方法,每次新建实例后的对象都会带有该方法.
this.test = function () {
console.log('user test')
}
}
User.prototype.show = function () {
console.log('user show')
}
let user = new User()
console.log(user)
改变构造函数的原型:
function Admin() {}
// 把 User的原型赋值给 Admin的原型,这样是改变了 Admin的原型,这种不是原型继承
Admin.prototype = User.prototype
原型的继承:
Admin.prototype.__proto__ = User.prototype
46. 模块化开发
开发一个模块需要考虑的条件
- 该模块的名称
- 该模块依赖哪些模块
- 该模块需要哪些动作
// 模块的添加
let module = (function () {
const modulesList = {}
function define(name, modules, action) {
modules.map((m, k) => {
modules[k] = modulesList[m]
})
modulesList[name] = action.apply(null, modules)
}
return {
define,
}
})()
// 模块的定义
module.define('hd', [], function () {
return {
first(arr) {
return arr[0]
},
max(arr, key) {
return arr.sort((a, b) => b[key] - a[key])[0]
},
}
})
module.define('coco', [], function () {
return {
min(arr, key) {
return arr.sort((a, b) => a[key] - b[key])[0]
},
}
})
// 模块的依赖
module.define('lesson', ['hd', 'coco'], function (hd, coco) {
console.log(coco, 'coco')
let data = [
{ name: 'js', price: 199 },
{ name: 'css', price: 79 },
{ name: 'mysql', price: 66 },
]
let maxPrice = hd.max(data, 'price')
let minPrice = coco.min(data, 'price')
})
47. script 标签支持模块化语法的操作
<script type="module">import {a} from './hd.js'</script>
模块化语法(混合导入导出
)
// 导出 (默认导出 和 具名导出)
let title = 'coco'
function app() {
console.log('app method')
}
export { title as default, app }
// 导入
import title, { app } from './test.js'
48. 冒泡排序
冒泡排序的原理:
每一趟只能确定将一个数归位。即第一趟只能确定将末位上的数归位,第二趟只能将倒数第 2 位上的数归位,依次类推下去。如果有 n 个数进行排序,只需将 n-1 个数归位,也就是要进行 n-1 趟操作。
而 “每一趟 ” 都需要从第一位开始进行相邻的两个数的比较,将较大的数放后面,比较完毕之后向后挪一位继续比较下面两个相邻的两个数大小关系,重复此步骤,直到最后一个还没归位的数。
let arr = [999, 123, 42343, 13, 34, 2423]
function sort(arr) {
let temp
for (let i = 0; i < arr.length-1; i++) { //控制比较轮次,一共 n-1 趟
for (let j = 0; j < arr.length-1-i; j++) { //控制两个挨着的元素进行比较
if (arr[j] > arr[j+1]) {
temp = arr[j]
arr[j] = arr[j+1]
arr[j+1] = temp
}
}
}
return arr
}
console.log(sort(arr))
49. 函数柯里化
简单来说,函数柯里化就是
将接收多个参数的函数,转化为接收单一参数函数 的一种方法.
add(1)(2)(3) //6
add(1, 2, 3)(4) //10
add(1)(2)(3)(4)(5) //15
function add() {
// let args = Array.prototype.slice.call(arguments)
// let args = Array.of(...arguments)
let args = [...arguments]
let inner = function () {
args.push(...arguments)
return inner
}
inner.toString = function () {
return args.reduce((pre, num) => pre + num)
}
return inner
}
console.log(add(1, 2, 3, 4)(5).toString())
50. 事件的三个阶段
事件的三个阶段:事件捕获->事件目标->事件冒泡
- 捕获阶段:先由文档的根节点 document 往事件触发对象,往外向内捕获事件对象
- 目标阶段:到达目标事件位置(事发地),触发事件
- 冒泡阶段:再从目标事件位置往文档的根节点方向回溯,从内向外冒泡事件对象
当我们点击目标元素之后,不会马上触发目标元素的事件,而是会
先执行事件捕获从根元素逐步到目标元素
,接着在事件目标阶段
,顺序触发目标元素事件,到了冒泡阶段
,从目标元素向外到根元素,执行冒泡
默认情况addEventListener的第三个参数为false,为冒泡
wrap1.addEventListener('click',function(e){ console.log("1")})
捕获事件: 把 addEventListener 的第三个参数设置为 true 即为捕获事件。
wrap1.addEventListener('click',function(e){ console.log("1")},true)
// 同时包含 事件捕获和事件冒泡
//css代码
<style>
.wrap1{
width: 300px;
height: 300px;
background-color: blue;
}
.wrap2{
width: 200px;
height: 200px;
background-color: red;
}
.wrap3{
width: 100px;
height: 100px;
background-color: green;
}
.wrap4{
width: 50px;
height: 50px;
background-color: orange;
}
</style>
//html代码
<div class="wrap1">
<div class="wrap2">
<div class="wrap3">
<div class="wrap4">
</div>
</div>
</div>
</div>
//js代码
var wrap1 = document.getElementsByClassName('wrap1')[0]
var wrap2 = document.getElementsByClassName('wrap2')[0]
var wrap3 = document.getElementsByClassName('wrap3')[0]
var wrap4 = document.getElementsByClassName('wrap4')[0]
wrap1.addEventListener('click',function(e){console.log("1")})
wrap2.addEventListener("click",function(e){console.log("6")},true)
wrap3.addEventListener("click",function(e){console.log("3")})
wrap3.addEventListener("click",function(e){console.log("7")},true)
wrap4.addEventListener("click",function(e){console.log("4")})
wrap4.addEventListener("click",function(e){console.log("8")},true)
// 输出结果 6,7,4,8,3,1
目标事件被点击之后,会经历这个阶段:事件捕获->目标事件阶段->事件冒泡阶段。首先是事件捕获阶段,从wrap1开始往wrap4捕获,wrap1没有捕获事件,继续往内,wrap2有捕获事件,输出6,继续往内,wrap3有事件捕获,输出7,继续往内到wrap4,此时到了目标事件阶段,顺序执行目标事件代码,输出4和8 ,接着进行事件冒泡阶段,由wrap往外冒泡,到wrap3有事件冒泡,输出3,继续往外,到wrap2,发现没有事件冒泡,不输出,继续往外,到wrap1,有事件冒泡,输出1.最终结果就是 6,7,4,8,3,1
51. 指数运算符 ()
用于指数运算
let a = 3;
a **= 2; // 等同于 a= a**2
console.log(a); // 9
let b = 4;
console.log(b ** 2); // 16
52.链判断运算符
读取对象内部的某个属性,往往需要判断一下,属性的上层对象是否存在。
// 错误的写法
const firstName = message.body.user.firstName || "default";
// 正确的写法
const firstName =
(message &&
message.body &&
message.body.user &&
message.body.user.firstName) ||
"default";
// 等同于
const firstName = message?.body?.user?.firstName || "default";
// 举例
let response = {
info: {
part: {
firstName: "coco",
},
},
};
console.log(response?.info?.part?.firstName); // coco
console.log(response.info.part.firstName); // coco
// 判断 某一属性的上层对象是否存在
console.log(response?.info?.change?.firstName); // undefined ,系统更不会报错。
console.log(response.info.change.firstName); // 系统会报错,Cannot read properties of undefined (reading 'firstName')
53. Null 判断运算符
简单来说就是, 使用 || 或运算符时,当属性值为 空字符串的情况下,就会触发 默认值。
使用 ?? Null 判断运算符时,当属性值为 控制符串的情况下,会保留空字符串,不会触发默认值。
let response = {
settings: {
headerText: "test",
animationDuration: "",
},
};
const headerText = response.settings.headerText || "Hello, world!";
const animationDuration = response.settings.animationDuration || 300;
console.log(headerText, animationDuration); //"test" 300
let response1 = {
settings1: {
headerText1: "test",
animationDuration1: "",
},
};
const headerText1 = response1.settings1.headerText1 ?? "Hello, world!";
const animationDuration1 = response1.settings1.animationDuration1 ?? 300;
console.log(headerText1, animationDuration1); // "test" ""
JavaScript 双问号(空值合并运算符)
不同于 JavaScript 逻辑或(||),空值合并运算符不会在左侧操作数为假值时返回右侧操作数。
说的直白一点儿就是
||
运算符在左侧为假值时会输出右侧的默认值
??
空值运算符只有在左侧为null
undefined
时会输出右侧的默认值
const testString1 = undefined ?? "空值运算符 - testString1";
const testString2 = null ?? "空值运算符 - testString2";
const testString3 = "" ?? "空值运算符 - testString3";
const testString4 = 0 ?? "空值运算符 - testString4";
const testString5 = false ?? "空值运算符 - testString5";
console.log(testString1); //空值运算符 - testString1
console.log(testString2); //空值运算符 - testString2
console.log(testString3); // ''
console.log(testString4); // 0
console.log(testString5); // false
54. Generator 函数的语法
Generator
函数是 ES6 提供的一种异步编程解决方案
,语法行为与传统函数完全不同
Generator
函数是一个普通函数,但是有两个特征。一是,function
关键字与函数名之间有一个星号
;二是,函数体内部使用yield
表达式,定义不同的内部状态(yield
在英语里的意思就是“产出”)
必须调用遍历器对象的 next 方法,使得指针移向下一个状态。也就是说,每次调用 next 方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式(或 return 语句)为止。换言之,Generator 函数是分段执行的,yield 表达式是暂停执行的标记,而 next 方法可以恢复执行
调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的
next方法
,就会返回一个有着value和done两个属性的对象
。value
属性表示当前的内部状态的值,是 yield 表达式后面那个表达式的值;done
属性是一个布尔值,表示是否遍历结束
。
function* helloGenerator() {
yield "hello";
yield "world";
return "worldOver";
}
const hellowGenerator = helloGenerator();
console.log(hellowGenerator.next()); //{ value: 'hello', done: false }
console.log(hellowGenerator.next()); //{ value: 'world', done: false }
console.log(hellowGenerator.next()); //{ value: 'worldOver', done: true }
console.log(hellowGenerator.next()); //{ value: undefined, done: true }
yield 表达式如果用在另一个表达式之中,必须放在圆括号里面。
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
yield* 表达式
在 Generator 函数内部 去调用另一个 Generator 函数,有两种方法。
- 方法一:我们可以在 Generator 函数中使用 forOf 进行遍历另一个 Generator 函数。
- 方法二:我们可以在 Generator 函数中使用 yield* 表达式来调用另一个 Generator 函数。
方法一:
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
// 手动遍历 foo()
for (let i of foo()) {
console.log(i);
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
方法二:
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
55. js 实现两个异步请求,要求前一个请求完成,拿前一个请求的结果,请求下一个数据.
方式一:使用 async/await 语法来让两个异步请求有先后循序执行.
async getData() {
// 发送第一个请求,获取 id
const res1 = await this.$http.get('/api/get-id');
const id = res1.data.id;
// 使用 id 发送第二个请求
const res2 = await this.$http.get(`/api/get-data?id=${id}`);
console.log(res2.data);
}
首先使用 await 关键字来等待第一个请求的响应。然后,我们获取响应数据中的 id,并使用这个 id 发送第二个请求。还可以在执行异步代码的函数中使用 try/catch 语句来捕获/处理错误。
如果两个请求都写在不同的函数里面,在这个函数里面来请求其他函数的结果,再进行别的操作,就往往会出现,第一个函数的结果还没有收到,下一条请求就完成的现象。
代码如下:
async getData1() {
// 发送第一个请求,获取 id
const res1 = await this.$http.get('/api/get-id');
this.id = res1.data.id;
}
async getData2() {
await this.getData1()
console.log(this.id) //此时的id就有可能为undefined;
// 使用 id 发送第二个请求
const res2 = await this.$http.get(`/api/get-data?id=${this.id}`);
console.log(res2.data);
}
方式二:使用 Promise.then()来让两个异步请求有先后顺序执行
getData1() {
return new Promise((resolve, reject) => {
// 发送第一个请求,获取 id
this.$http.get('/api/get-id').then(({ data }) => {
this.id = data.id;
resolve();
}).catch(err => {
console.log(err.msg);
reject(err.msg);
});
})
}
getData2() {
this.getData1().then(() => {
console.log(this.id) //此时的id就会正常出来;
// 使用 id 发送第二个请求
this.$http.get(`/api/get-data?id=${this.id}`).then(({ data }) => {
console.log(data);
}).catch(err2 => {
console.log(err2.msg);
reject(err2.msg);
});
}).catch(err => {
console.log(err.msg);
reject(err.msg);
});
}
56.javascript 取整方法
常用方法有:
- parseInt()
- Number.toFixed()
从性能角度看,位运算取整和Math函数性能最佳,内置方法parseInt次之,toFixed性能最劣。
let pointNumber = 1.555555;
// 1. parseInt() js内置函数,注意接受参数是string,所以调用该方法时存在类型转换
console.log(parseInt(pointNumber)); // => 1
/*
2. Number.toFixed()
注意:
- toFixed返回的字符串,若想获得整数还需要做类型转换
- toFixed() 参数是小数点后保留多少位 该数值在必要时进行四舍五入,
另外在必要时会用 0 来填充小数部分,以便小数部分有指定的位数。
*/
console.log(pointNumber.toFixed()); // '2'
// 3. Math.ceil() 向上取整
console.log(Math.ceil(pointNumber)); // 2
// 4. Math.floor() 向下取整
console.log(Math.floor(pointNumber)); // 1
// 5. Math.round() 四舍五入取整
console.log(Math.round(pointNumber)); // 2
// 6. Math.trunc() 舍弃小数取整
console.log(Math.trunc(pointNumber)); // 1
// 7. 双按位非取整 利用位运算取整,仅支持32位有符号整型数,小数位会舍弃,下同
console.log(~~pointNumber); // 1
// 8. 按位或取整
console.log(pointNumber | 0); // 1
// 9. 按位异或取整
console.log(pointNumber ^ 0); // 1
// 10. 左移0位取整
console.log(pointNumber << 0); // 1
57. JS 字符串 ‘true’ ‘false’ 转为 布尔 true false
方法一: 使用 JSON.parse()
方法二: 使用 eval()
let trueStr = "true";
let falseStr = "false";
console.log(trueStr); // 'true' String
console.log(falseStr); // 'false' String
console.log(JSON.parse(trueStr)); // true Boolean
console.log(JSON.parse(falseStr)); // false Boolean
console.log(eval(trueStr)); // true Boolean
console.log(eval(falseStr)); // false Boolean
58. for-in,for-of,forEach,for 的区别
- js 中所有数组都是对象,当我们手动给数组添加属性时,区别如下 (
只有 for-in 才能遍历到 test 属性
)
- for-in (可以遍历到 test 属性)
- for-of (遍历不到 test 属性)
- forEach (遍历不到 test 属性)
- for (遍历不到 test 属性)
- 当数组中含有空值时,区别如下 (
for-in,forEach 可以跳过空值
)
- for-in (跳过空值)
- for-of (没有跳过空值)
- forEach (跳过空值)
- for (没有跳过空值)
// js 中所有数组都是对象,可以给数组添加属性
// 如果手动给 数组对象添加属性,以下几种方法只有 for-in 才能遍历出来 (test 属性)
let arr = [1, 2, 3];
arr.test = "testing";
console.log(arr); // [ 1, 2, 3, test: 'testing' ]
// for-in (可以遍历到 test 属性)
for (let i in arr) {
console.log(arr[i]); // 1,2,3,'testing'
}
// for-of (遍历不到 test 属性)
for (let item of arr) {
console.log(item); // 1,2,3
}
// forEach (遍历不到 test 属性)
arr.forEach((item) => {
console.log(item); // 1,2,3
});
// for (遍历不到 test 属性)
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]); // 1,2,3
}
// 数组空值 --------------------------------------------------------------------
let arr2 = ["name", , "age"];
// for-in (跳过空值)
for (let key in arr2) {
console.log(arr2[key]); // 'name','age'
}
// for-of (没有跳过空值)
for (let item of arr2) {
console.log(item); // 'name','undefined','age'
}
// forEach (跳过空值)
arr2.forEach((item) => {
console.log(item); // 'name','age'
});
for (let index = 0; index < arr2.length; index++) {
console.log(arr2[index]); // 'name','undefined','age'
}
59. js 解释文档相关知识
- 快捷键 cmmb (
需安装插件
)- 输入
/**
然后回车可以快速生成
优点:
- 当鼠标放在
拥有解释文档
的函数上时,编辑器会有写好的注释内容
- 当规定好入参类型时,在使用入参时编辑器会有相关数据类型的属性提示.
传入多个参数(解释文档示例)
/**
* 函数防抖
* @author Coco <i_ke@qq.com> 作者注释
* @license Es6-2.01 版本注释
* @param {function} fn 需要执行的函数
* @param {number} [delay] 延期执行时间,默认1s
* @return {Function} 防抖函数
*/
function debounce(fn, delay = 1000) {}
传入一个对象(解释文档示例)
{“GET”|“POST”} 这种写法,当传入 method 属性时,编辑器会有这两种选择提示
/**
* @param {object} options 配置对象
* @param {string} options.url 请求地址
* @param {"GET"|"POST"} options.method 请求方法
* @param {object} options.body 请求方式
* @param {object} options.header 请求头
*
*/
function request(options) {}
// 函数调用
request({ method: "GET" });
60. JS 参数归一化
在封装公用函数时,要考虑到参数的兼容性,
比如:timeFormate(time, format)
- time 输入 : ‘date’ 或 ‘yyyy-MM-dd hh:mm:ss’ 返回的结果都应该是
年月日 时分秒
的格式
实操案例
function _formatter(formatter) {
// 判断格式是否是 function
if (typeof formatter === "function") {
return formatter;
}
if (typeof formatter !== "string") {
throw new Error("format is not a string");
}
if (formatter === "date") {
formatter = "yyyy-MM-dd";
} else if (formatter === "time") {
formatter = "yyyy-MM-dd hh:mm:ss";
}
return (dateInfo) => {
const { yyyy, MM, dd, hh, mm, ss } = dateInfo;
return formatter
.replace(/yyyy/g, yyyy)
.replace(/MM/g, MM)
.replace(/dd/g, dd)
.replace(/hh/g, hh)
.replace(/mm/g, mm)
.replace(/ss/g, ss);
};
}
/**
*
* @param {*} date 需要格式化的时间
* @param {*} formatter 需要的格式
* @param {*} isPad 是否自动补零
*/
function format(date, formatter, isPad = false) {
// 参数归一化 将需要处理的格式统一处理
formatter = _formatter(formatter);
// 补零
function _pad(value, length) {
if (isPad) {
return (value + "").padStart(length, "0");
} else {
return value.toString();
}
}
// 如果是时间戳格式 就转换成时间对象,避免调用时间对象原生 API时报错
if (typeof date === "number") {
date = new Date(date);
}
if (typeof date === "string") {
throw new Error("请输入正确时间戳格式。");
}
const dateInfo = {
yyyy: date.getFullYear(), // 年
MM: _pad(date.getMonth() + 1, 2), // 月
dd: _pad(date.getDate(), 2), // 日
hh: _pad(date.getHours(), 2), // 时
mm: _pad(date.getMinutes(), 2), // 分
ss: _pad(date.getSeconds(), 2), // 秒
};
return formatter(dateInfo);
}
console.log(format(new Date(), "date", true)); // '2024-06-04'
console.log(format(new Date(), "date")); // 2024-06-04
console.log(format(new Date(), "yyyy-MM-dd hh:mm:ss")); // 2024-06-04 15:37:39
console.log(format(1717482641343, "yyyy-MM-dd hh:mm:ss")); // '2024-6-4 14:30:41'
// console.log(format("1717482641343", "yyyy-MM-dd hh:mm:ss")); // '请输入正确时间戳格式'
console.log(typeof +new Date()); // 'number'