基础篇
@对象操作
手写深拷贝
function deepCopy(data){
switch (true) {
/* 基本数据类型 */
case typeof(data)!=="object" && typeof(data)!=="function":
return data;
/* 函数类型 */
case typeof(data)==="function":
return data;
/* 数组类型 */
case Array.isArray(data):
const arr = []
// 递归深拷贝所有元素 丢入arr
for(let i=0;i<data.length;i++){
arr[i] = deepCopy(data[i])
}
return arr;
/* 对象类型 */
case typeof(data)==="object":
const obj = {}
// 递归深拷贝对象中的所有key-value 丢入obj
for(let key in data){
obj[key] = deepCopy(data[key])
}
return obj;
default:
return data;
}
}
提取查询参数
手写getSearchParams从url中提取所有查询参数,以对象形式返回
/**
* 从url中提取查询参数 /add?a=2&b=3 => {a:2,b:3}
* @param {string} url 用户请求的路径
* @returns 提取好的查询参数形成的对象
*/
function getSearchParams(url) {
// 创建结果对象
const obj = {};
// 摘出a=2&b=3
// "/add?a=123&b=456&c=789#abc".match(/\w+=\w+/g)
// const reg = /?(.*)/;
// const str = reg.exec(url)[1];
// 使用&做分隔符 肢解字符串为[a=2,b=3]
// const arr = str.split("&");
const reg = /\w+=\w+/g
const arr = url.match(reg)//[子串1,子串2] [a=2,b=3]
console.log("getSearchParams:arr=",arr);
// 遍历上述数组 将每个元素以=肢解为 [a,2] 将这一组key-value收集到结果对象中
arr.forEach((item) => {
let [key,value] = item.split("=");
obj[key] = value//obj.a = 2 obj.b=3
});
// 返回结果对象
return obj;//{a:2,b:3}
}
虚拟dom转真实dom
const vnode = {
tag: 'DIV',
attrs: {
id: 'app'
},
children: [{
tag: 'SPAN',
children: [{
tag: 'A',
children: []
}]
},
{
tag: 'SPAN',
children: [{
tag: 'A',
children: []
},
{
tag: 'A',
children: []
}
]
}
]
}
function render(vnode, container) {
return container.appendChild(_render(vnode));
}
function _render(vnode) {
if (typeof vnode === 'number') {
vnode = String(vnode);
}
//处理文本节点
if (typeof vnode === 'string') {
const textNode = document.createTextNode(vnode)
return textNode;
}
//处理组件
if (typeof vnode.tag === 'function') {
const component = createComponent(vnode.tag, vnode.attrs);
setComponentProps(component, vnode.attrs);
return component.base;
}
//普通的dom
const dom = document.createElement(vnode.tag);
if (vnode.attrs) {
Object.keys(vnode.attrs).forEach(key => {
const value = vnode.attrs[key];
setAttribute(dom, key, value); // 设置属性
});
}
vnode.children.forEach(child => render(child, dom)); // 递归渲染子节点
return dom; // 返回虚拟dom为真正的DOM
}
//实现dom挂载到页面某个元素
const ReactDOM = {
render: (vnode, container) => {
container.innerHTML = '';
return render(vnode, container);
}
}
@数组操作
元素速查
给几个数组, 可以通过数值找到对应的数组名称
// 比如这个函数输入一个1,那么要求函数返回A
const A = [1, 2, 3];
const B = [4, 5, 6];
const C = [7, 8, 9];
const test = (num) => {
const newArr = [A, B, C];
let i = 0;
while (i < newArr.length) {
if (newArr[i].includes(num)) return newArr[i];
i++;
}
return [];
}
console.log(test(5));
有序数组原地去重
// 快慢指针
const res = [0, 0, 1, 1, 2, 2, 2, 2, 4, 4, 5, 5, 6]
const removeDuplicates = (nums) => {
let slow = 0, fast = 1;
if (nums.length === 0) return
while (fast < nums.length) {
if (nums[fast] !== nums[slow]) {
slow++;
nums[slow] = nums[fast];
}
fast++;
}
return nums.splice(0, slow + 1);
}
全排列
不定长二维数组的全排列
// 输入 [['A', 'B', ...], [1, 2], ['a', 'b'], ...]
// 输出 ['A1a', 'A1b', ....]
let res = arr.reduce((prev, cur) => {
if (!Array.isArray(prev) || !Array.isArray(cur)) {
return
}
if (prev.length === 0) {
return cur
}
if (cur.length === 0) {
return prev
}
const emptyVal = []
prev.forEach(val => {
cur.forEach(item => {
emptyVal.push(`${val}${item}`)
})
})
return emptyVal
}, [])
console.log(res);
@字符串处理
字符串去重
去除字符串中出现次数最少的字符,不改变原字符串的顺序
// “ababac” —— “ababa”
// “aaabbbcceeff” —— “aaabbb”
const changeStr = (str) => {
let obj = {};
const _str = str.split("");
_str.forEach(item => {
if (obj[item]) {
obj[item]++;
} else {
obj[item] = 1;
}
})
var _obj = Object.values(obj).sort();
var min = _obj[0];
for (let key in obj) {
if (obj[key] <= min) {
var reg = new RegExp(key, "g")
str = str.replace(reg, "")
}
}
return str;
}
console.log(changeStr("aaabbbcceeff"));
最少操作完成变形
两个字符串对比, 得出结论都做了什么操作, 比如插入或者删除
// pre = 'abcde123'
// now = '1abc123'
// a前面插入了1,c后面删除了de
function compareStrings(pre, now) {
let diff = JsDiff.diffChars(pre, now);
let result = [];
diff.forEach(function (part) {
let type = part.added ? '插入' : part.removed ? '删除' : '保持不变';
if (type !== '保持不变') {
result.push('在pre的第' + part.index + '个位置' + type + '了' + part.value);
}
});
return result;
}
let pre = 'abcde123';
let now = '1abc123';
let result = compareStrings(pre, now);
result.forEach(function (part) {
console.log(part);
});
//在pre的第0个位置插入了1
//在pre的第3个位置删除了d
//在pre的第4个位置删除了e
function compareStrings(pre, now) {
let result = [];
let i = 0;
let j = 0;
let pre_len = pre.length;
let now_len = now.length;
while (i < pre_len && j < now_len) {
if (pre[i] === now[j]) {
i++;
j++;
} else {
let pre_end = i;
let now_end = j;
while (pre_end < pre_len && pre[pre_end] !== now[j]) {
pre_end++;
}
while (now_end < now_len && now[now_end] !== pre[i]) {
now_end++;
}
if (pre_end - i < now_end - j) {
result.push('在pre的第' + i + '个位置删除了' + pre.substring(i, pre_end));
i = pre_end;
} else {
result.push('在pre的第' + i + '个位置插入了' + now.substring(j, now_end));
j = now_end;
}
}
}
while (i < pre_len) {
result.push('在pre的第' + i + '个位置删除了' + pre[i]);
i++;
}
while (j < now_len) {
result.push('在pre的第' + i + '个位置插入了' + now[j]);
j++;
}
return result;
}
let pre = 'abcde123';
let now = '1abc123';
let result = compareStrings(pre, now);
result.forEach(function (part) {
console.log(part);
});
//在pre的第0个位置插入了1
//在pre的第3个位置删除了de
数值转汉语
写出一个函数trans,将数字转换成汉语的输出,输入为不超过10000亿的数字
//将数字(整数)转为汉字,从零到一亿亿,需要小数的可自行截取小数点后面的数字直接替换对应arr1的读法就行了
const convertToChinaNum = (num) => {
var arr1 = new Array('零', '一', '二', '三', '四', '五', '六', '七', '八', '九');
var arr2 = new Array('', '十', '百', '千', '万', '十', '百', '千', '亿', '十', '百', '千', '万', '十', '百', '千', '亿');//可继续追加更高位转换值
if (!num || isNaN(num)) {
return "零";
}
var english = num.toString().split("")
var result = "";
for (var i = 0; i < english.length; i++) {
var des_i = english.length - 1 - i;//倒序排列设值
result = arr2[i] + result;
var arr1_index = english[des_i];
result = arr1[arr1_index] + result;
}
//将【零千、零百】换成【零】 【十零】换成【十】
result = result.replace(/零(千|百|十)/g, '零').replace(/十零/g, '十');
//合并中间多个零为一个零
result = result.replace(/零+/g, '零');
//将【零亿】换成【亿】【零万】换成【万】
result = result.replace(/零亿/g, '亿').replace(/零万/g, '万');
//将【亿万】换成【亿】
result = result.replace(/亿万/g, '亿');
//移除末尾的零
result = result.replace(/零+$/, '')
//将【零一十】换成【零十】
//result = result.replace(/零一十/g, '零十');//貌似正规读法是零一十
//将【一十】换成【十】
result = result.replace(/^一十/g, '十');
return result;
}
@OOP
扩展现有类
扩展数组实现arr.countItems,以对象形式返回每个元素分别出现多少次
Array.prototype.countItems = function(){
const result = {}
for(let i of this){
if(result[i]){
result[i]++
}else{
result[i] = 1
}
}
return result
}
原型链
手写Animal-Person-Student三级继承关系(含静态与覆写)原型链实现
function Animal(type, food) {
this.type = type;
this.food = food;
}
Animal.prototype.eat = function () {
console.log(`一只${this.type}正在享用${this.food}`);
};
/* */
function Person(name) {
// this.type = type;
// this.food = food;
/* ES5的属性继承 */
Animal.call(this, "人类", "五谷杂粮");
this.name = name;
}
/* 【以一个父类实例做子类原型】 */
Person.prototype = new Animal();
Person.prototype.think = function () {
console.log(`${this.name}正在思考`);
};
/* 对父类的eat实现不满意 就重写覆盖override它 */
Person.prototype.eat = function(){
Animal.prototype.eat.apply(this)
console.log(`${this.name}正在享用${this.food}`);
}
Person.introduction = "两足无毛大脑袋动物"
Person.getPopulation = function(){
//查UN的数据库
return 8 * Math.pow(10,9)
}
/* */
function Student(name,major){
Person.apply(this,[name])
this.major = major
}
Student.prototype = new Person()
Student.prototype.study = function(){
console.log(`${this.name}正在研习${this.major}`);
}
~(function () {
// const dog = new Animal("狗狗", "大骨棒");
// dog.eat();
// const p = new Person("张三疯");
// p.think();
const xm = new Student("小明","前端开发")
console.log(xm);
// xm.study()
xm.eat()
})();
class
手写Animal-Person-Student三级继承关系(含静态与覆写)class实现
class Animal {
constructor(type, food) {
this.type = type;
this.food = food;
}
eat() {
console.log(`一只${this.type}正在摄入${this.food}`);
}
}
/* extends的一刹那 已经将Animal的实例绑定给Person做为原型了 */
class Person extends Animal {
/* 与具体实例无关的属性和方法——静态成员 */
static legs = 4;
static getInfo() {
// console.log("this in static method", this);
return `人类是一种四足无毛动物`;
}
constructor(name, age) {
// Must call super constructor in derived class before accessing 'this' or returning from derived constructor
// Animal.call(this,"人类", "大米白面")
super("人类", "大米白面");
this.name = name;
this.age = age;
}
/* 扩展父类方法(通过重写/覆盖/override) */
eat() {
// 调用父类方法
super.eat(); //Animal.prototype.eat.call(this)
console.log(`一只${this.type}${this.name}正在享用${this.food}`);
}
/* 人类会思考 */
// Person.prototype.think = function(){}
think() {
console.log(`${this.name}正在思考`);
}
}
class Student extends Person {
// 如果想扩展属性 就要重新写构造器
constructor(name, age, major) {
// 只要重新写构造器 就一定要先调用父类构造器
// Person.call(this,name,age)
super(name, age);
this.major = major;
}
study() {
console.log(`${this.name}正在学习${this.major}`);
}
/* 重写父类think方法 */
think() {
super.think();
console.log(`学生【${this.name}】正在潜心钻研${this.major}`);
}
toString() {
return `[姓名:${this.name},专业:${this.major}]`;
}
}
手写MyMap
class MyMap {
constructor(dataObj) {
// 用于存储底层数据的对象
this.dataObj = dataObj || {};
this.updateSize()
}
updateSize() {
// Object.keys(obj)将对象的【自有属性/非继承属性】以数组形式罗列
this.size = this.keys().length;
}
set(key, value) {
// 往【底层数据对象】中丢入一个key-value
this.dataObj[key] = value;
this.updateSize();
}
get(key) {
return this.dataObj[key];
}
/* 获取全部keys */
keys() {
return Object.keys(this.dataObj);
}
/* 获取全部值 */
values() {
// 将keys数组映射成value数组
return this.keys().map((key) => this.get(key));
}
/* 获取全部键值 */
entries() {
// 将keys数组映射成{key,value}对象数组
return this.keys().map((key) => ({ key: key, value: this.get(key) }));
}
/* 查询key是否存在 */
has(key) {
// 判断对象中是否有【自有属性(继承来的不算)】
return this.dataObj.hasOwnProperty(key);
}
/* 删除key-value */
delete(key) {
delete this.dataObj[key];
this.updateSize();
}
/* 批处理函数forEach */
forEach(handler) {
for (let key in this.dataObj) {
handler(this.get(key), key);
}
}
/* filter */
filter(handler) {
const noahMap = {};
for (let key in this.dataObj) {
let ok = handler(this.get(key), key);
ok && (noahMap[key] = this.get(key));
}
return new MyMap(noahMap);
}
}
<script>
const mmap = new MyMap();
/* 增加Key-value */
mmap.set("a", "foo");
mmap.set("b", "bar");
mmap.set("c", "baz");
mmap.set("d", "boo");
/* 根据Key查询value */
console.log(mmap.get("a"));
console.log(mmap.get("b"));
// 查询尺寸
console.log(mmap.size);
// 获取全部keys
console.log(mmap.keys());
// 获取全部values
console.log(mmap.values());
// 获取全部entries
console.log(mmap.entries());
/* 遍历mmap中的全部entry */
for (let entry of mmap.entries()) {
console.log(entry);
}
// 查询key是否存在
console.log("has a?", mmap.has("a"));
console.log("has toString?", mmap.has("toString"));
console.log("has c?", mmap.has("c"));
// 删除Key-value
mmap.delete("b");
console.log(mmap);
// 修改
mmap.set("a", "张真人");
console.log(mmap.get("a"));
/* for-Each */
mmap.forEach((value, key) => {
console.log(`{key:${key},value:${value}}`);
});
mmap.set("tesla", { boss: "mask", age: 48 });
mmap.set("microsoft", { name: "bill", age: 80 });
mmap.set("meta", { name: "zuckberg", age: 38 });
mmap.set("alibaba", { name: "jack", age: 55 });
/* 过滤掉年龄大于50的家伙 */
const newMap = mmap.filter((value, key) => value.age < 50);
console.log(newMap);
</script>
手写MySet
class MySet {
constructor(dataArr){
this.dataArr = dataArr || []
this.removeRepeat()
this.updateSize()
}
/* 过滤掉重复元素 */
removeRepeat(){
this.dataArr = this.dataArr.filter(
(item,index) => this.dataArr.indexOf(item)===index
)
}
/* 更新size */
updateSize(){
this.size = this.dataArr.length
}
/* 追加一个元素 */
add(value){
// 当前dataArr中如果没有value 就追加之
if(this.dataArr.indexOf(value)===-1){
this.dataArr.push(value)
}
this.updateSize()
}
/* 删除元素 */
delete(value){
const index = this.dataArr.indexOf(value)
if(index!==-1){
this.dataArr.splice(index,1)
this.updateSize()
return true
}else{
return false
}
}
/* 清空set */
clear(){
this.dataArr = []
this.updateSize()
}
/* 得到所有元素形成的数组 */
values(){
return this.dataArr
}
/* 查询元素是否存在 */
has(value){
return this.dataArr.indexOf(value)!==-1
}
/* forEach */
forEach(handler){
for(let i=0;i<this.dataArr.length;i++){
handler.apply(null,[this.dataArr[i],i,this])
}
}
}
<script>
const mset = new MySet([3,1,4,1,5,9,2,6,5,3,5,8,9,7,9,3])
mset.add("foo")
mset.add("bar")
mset.add("bar")
mset.delete("bar")
mset.delete(5)
// mset.clear()
console.log(mset);
console.log(mset.values());
console.log(mset.has(3));
console.log(mset.has(13));
mset.forEach(
(value,index,set)=>{
console.log(index,value,set);
}
)
</script>
@异步编程
Promise链式编程
手写Promise链求5的阶乘
function multiply(a, b, callback) {
setTimeout(() => {
const ret = a * b;
callback(ret);
}, 2000);
}
// multiply(2,3,ret=>console.log("ret=",ret))
/* 但凡耗时任务的结果,要么回调,要么Promise */
function mulPromise(a, b) {
return new Promise((resolve, reject) => {
multiply(2, 3, (ret) => resolve(ret));
});
}
mulPromise(2, 3)
// .then((ret) => ret * 4)
.then((ret) => mulPromise(ret, 4))
.then((ret) => mulPromise(ret, 4))
.then((ret) => console.log("最终结果", ret));
async / await
手写async-await求5的阶乘
function multiply(a, b, callback) {
setTimeout(() => {
const ret = a * b;
callback(ret);
}, 2000);
}
// multiply(2,3,ret=>console.log("ret=",ret))
/* 但凡耗时任务的结果,要么回调,要么Promise */
function mulPromise(a, b) {
return new Promise((resolve, reject) => {
multiply(2, 3, (ret) => resolve(ret));
});
}
/* await */
~(async function awaitDemo() {
try {
let ret = await mulPromise(2, 3);
ret = await mulPromise(ret, 4);
ret = await mulPromise(ret, 5);
console.log("最终结果", ret);
} catch (err) {
console.log("err=", err);
}
})();
三大静态调度
手写Promise三大静态调度API demo各一个
/* 连坐期约:全部Promise成员都履约才视为整体履约 只要有一个Promise成员毁约 整体就视为毁约 */
function demoPromiseAll(){
Promise.all(
[
// 成员Promise1
Promise.resolve("foo"),
// 成员Promise2
// Promise.resolve("bar"),
Promise.reject("我妈喊我回家吃饭"),
// 成员Promise3
new Promise(
(resolve,reject)=>setTimeout(() => {
resolve("baz")
}, 3000)
)
]
)
// 全部Promise成员都履约才视为整体履约
.then(
values => console.log("整体成功:values=",values)
)
// 只要有一个Promise成员毁约 整体就视为毁约
.catch(
err => console.error("整体失败:err=",err)
)
}
demoPromiseAll()
/* 详查期约: 所有的Promise成员都独立汇报结果:人人都得尘埃落定(谁也不许浑水摸鱼)=>all settled */
function demoPromiseAllSettled(){
Promise.allSettled(
[
Promise.resolve("foo"),
Promise.reject("宝宝喊我回家吃饭"),
new Promise((resolve,reject)=>{
setTimeout(() => {
Math.random()>0.5?resolve("bar"):reject("春霞喊我做核酸")
}, 3000);
})
]
)
/*
0: {status: 'fulfilled', value: 'foo'}
1: {status: 'rejected', reason: '宝宝喊我回家吃饭'}
2: {status: 'rejected', reason: '春霞喊我做核酸'}
*/
.then(results=>console.log("所有成员都尘埃落定了:results=",results))
}
demoPromiseAllSettled()
/* 竞速期约:以最快出结果的成员Promise的结果为整体结果 */
function demoPromiseRace(){
Promise.race(
[
// Promise成员1
new Promise((resolve)=>setTimeout(() => {
resolve("foo")
}, 3000)),
// Promise成员2
new Promise((resolve,reject)=>setTimeout(() => {
// resolve("bar")
reject("宝宝和春霞都喊我去做核酸,opacity:0")
}, 500)),
]
)
.then(value=>console.log("整体成功了:value=",value))
.catch(err=>console.log("整体失败了:err=",err))
}
demoPromiseRace()
手封ajaxPromise
/* 使用合并对象的方式做配置的合并 */
function ajax(conf) {
/* 使用合并对象的方式做默认配置 */
// 默认配置
const defaultConf = {
method: "GET",
onSuccess: (data) => console.log("data=", data),
onFail: (err) => console.error("err=", err),
// 将用户配置中的所有key-value打散 合并到defaultConf中
...conf,
};
let { method, url, data, dataType, onSuccess, onFail } = defaultConf;
// console.log(method, url, data, dataType, onSuccess, onFail);
/* 如果用户未传递URL 办它/弄死这只傻鸟 */
if (!url) {
// 弄死它的目的是为了引起其注意 以提前修正错误
throw new Error("亲爱的傻鸟,URL必须传递哦~");
}
let body = null;
const xhr = new XMLHttpRequest();
xhr.onload = () => {
onSuccess(xhr.responseText);
};
xhr.onerror = (err) => onFail(err);
/* 为GET请求拼接查询参数字符串 */
if (method === "GET" && data) {
url += `?${getSearchParams(data)}`;
}
xhr.open(method, url);
/* 设置Content-Type请求头(必须在xhr打开连接以后才能配置) */
switch (true) {
case dataType === "form":
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
body = getSearchParams(data);
break;
case dataType === "json":
xhr.setRequestHeader("Content-Type", "application/json");
body = JSON.stringify(data);
break;
default:
break;
}
xhr.send(body);
}
/* 得到一个执行AJAX任务的Promise对象 */
function ajaxPromise(conf) {
/* 返回一个履约/毁约未可知的Promise对象 */
return new Promise(
// 执行网络请求
(resolve, reject) => {
ajax({
// method,url,data,dataType...
...conf,
/* 重构成功与失败回调 */
// 成功时resolve数据
onSuccess: (data) => resolve(data),
// 失败时reject错误信息
onFail: (err) => reject(err),
});
}
);
}
限制并发量
实现一个批量请求函数, 能够限制并发量
const sendRequests = (reqs, max, callback = () => { }) => {
let waitList = [];
let currentNum = 0;
let NumReqDone = 0;
const results = new Array(reqs.length).fill(false);
const init = () => {
reqs.forEach((element, index) => {
request(index, element);
});
}
const request = async (index, reqUrl) => {
if (currentNum >= max) {
await new Promise(resolve => waitList.push(resolve))
}
reqHandler(index, reqUrl);
}
const reqHandler = async (index, reqUrl) => {
currentNum++;
try {
const result = await fetch(reqUrl);
results[index] = result;
} catch (err) {
results[index] = err;
} finally {
currentNum--;
NumReqDone++;
if (waitList.length) {
waitList[0]();
waitList.shift();
}
if (NumReqDone === max) {
callback(results);
}
}
}
init()
}
const allRequest = [
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=1",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=2",
"https://dog-facts-api.herokuapp.com/api/v1/resources/dogs?index=3"
];
sendRequests(allRequest, 2, (res) => console.log(res))
睡眠阻塞
实现sleep(milliseeconds)函数
- 循环阻塞法
function sleep(delay) {
var start = (new Date()).getTime();
while ((new Date()).getTime() - start < delay) {
continue;
}
}
function test() {
console.log('111');
sleep(2000);
console.log('222');
}
- 定时器回调法
function sleep(ms, callback) {
setTimeout(callback, ms)
}
sleep(2000, () => {
console.log("sleep")
})
- Promise定时履约
const sleep = time => {
return new Promise(resolve => setTimeout(resolve, time))
}
sleep(1000).then(() => { console.log(1) })
- async/await
function sleep(time) {
return new Promise(resolve =>
setTimeout(resolve, time))
}
async function output() {
let out = await sleep(1000);
console.log(1);
return out;
}
output();
- 使用生成器
function sleepGenerator(time) {
yield new Promise(function (resolve, reject) {
setTimeout(resolve, time);
})
}
sleepGenerator(1000).next().value.then(() => { console.log(1) })
自动重试
实现一个函数, fetchWithRetry 会自动重试3次,任意一次成功直接返回
const fetchWithRetry = async (
url,
options,
retryCount = 0,
) => {
const { MAXRET = 3, ...remainingOptions } = options;
try {
return await fetch(url, remainingOptions);
} catch (error) {
// 如果重试次数没有超过,那么重新调用
if (retryCount < maxRetries) {
return fetchWithRetry(url, options, retryCount + 1);
}
// 超过最大重试次数
throw error;
}
}
// 补充超时和取消
// 创建一个 reject 的 promise
// `timeout` 毫秒
const throwOnTimeout = (timeout) =>
new Promise((_, reject) =>
setTimeout(() =>
reject(new Error("Timeout")),
timeout
),
);
const fetchWithTimeout = (
url,
options = {},
) => {
const { timeout, ...remainingOptions } = options;
// 如果超时选项被指定,那么 fetch 调用和超时通过 Promise.race 竞争
if (timeout) {
return Promise.race([
fetch(url, remainingOptions),
throwOnTimeout(timeout),
]);
}
return fetch(url, remainingOptions);
}
// 取消
const fetchWithCancel = (url, options = {}) => {
const controller = new AbortController();
const call = fetch(
url,
{ ...options, signal: controller.signal },
);
const cancel = () => controller.abort();
return [call, cancel];
};
算法篇
@树的操作
树的深度优先遍历
树的深度优先遍历有三种方式:前序遍历、中序遍历和后序遍历。其中,前序遍历的遍历顺序是先遍历根节点,然后遍历左子树,最后遍历右子树;中序遍历的遍历顺序是先遍历左子树,然后遍历根节点,最后遍历右子树;后序遍历的遍历顺序是先遍历左子树,然后遍历右子树,最后遍历根节点。以下是JS实现树的深度优先遍历的代码,附有详细的注释说明:
// 定义树节点类
class TreeNode {
constructor(val) {
this.val = val;
this.left = null;
this.right = null;
}
}
// 构建二叉树
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
root.right.left = new TreeNode(6);
root.right.right = new TreeNode(7);
// 前序遍历
function preorderTraversal(root, result = []) {
if (root) {
result.push(root.val); // 访问根节点
preorderTraversal(root.left, result); // 遍历左子树
preorderTraversal(root.right, result); // 遍历右子树
}
return result;
}
// 中序遍历
function inorderTraversal(root, result = []) {
if (root) {
inorderTraversal(root.left, result); // 遍历左子树
result.push(root.val); // 访问根节点
inorderTraversal(root.right, result); // 遍历右子树
}
return result;
}
// 后序遍历
function postorderTraversal(root, result = []) {
if (root) {
postorderTraversal(root.left, result); // 遍历左子树
postorderTraversal(root.right, result); // 遍历右子树
result.push(root.val); // 访问根节点
}
return result;
}
// 测试
console.log(preorderTraversal(root)); // [1, 2, 4, 5, 3, 6, 7]
console.log(inorderTraversal(root)); // [4, 2, 5, 1, 6, 3, 7]
console.log(postorderTraversal(root)); // [4, 5, 2, 6, 7, 3, 1]
树的广度优先遍历
树的广度优先遍历是按照层次顺序遍历树的节点,从根节点开始,逐层遍历直到最后一层。在遍历每一层时,按照从左到右的顺序访问节点。以下是JS实现树的广度优先遍历的代码,附有详细的注释说明:
// 定义树节点类
class TreeNode {
constructor(val) {
this.val = val;
this.left = null;
this.right = null;
}
}
// 构建二叉树
const root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
root.right.left = new TreeNode(6);
root.right.right = new TreeNode(7);
// 广度优先遍历
function bfsTraversal(root) {
const queue = [root]; // 定义队列,初始值为根节点
const result = []; // 定义遍历结果数组
while (queue.length) { // 当队列不为空时
const node = queue.shift(); // 取出队首节点
result.push(node.val); // 访问队首节点
if (node.left) { // 如果队首节点有左子节点,则将左子节点加入队列
queue.push(node.left);
}
if (node.right) { // 如果队首节点有右子节点,则将右子节点加入队列
queue.push(node.right);
}
}
return result;
}
// 测试
console.log(bfsTraversal(root)); // [1, 2, 3, 4, 5, 6, 7]
数组转树结构
const arr = [{
id: 2,
name: '部门B',
parentId: 0
},
{
id: 3,
name: '部门C',
parentId: 1
},
{
id: 1,
name: '部门A',
parentId: 2
},
{
id: 4,
name: '部门D',
parentId: 1
},
{
id: 5,
name: '部门E',
parentId: 2
},
{
id: 6,
name: '部门F',
parentId: 3
},
{
id: 7,
name: '部门G',
parentId: 2
},
{
id: 8,
name: '部门H',
parentId: 4
}
];
const transTree = (list, pId) => {
const loop = (pId) => {
let res = [];
let i = 0;
while (i < list.length) {
let item = list[i];
i++;
if (item.pid !== pId) continue
item.children = loop(item.id);
res.push(item);
}
return res;
}
return loop(pId);
}
const transTree = (list, pId) => {
const loop = (pId) => {
return list.reduce((pre, cur) => {
if(cur.pid === pId) {
cur.children = loop(cur.id);
pre.push(cur);
};
return pre;
},[])
}
return loop(pId);
}
树的层序遍历
二叉树层序遍历, 每层的节点放到一个数组里
// 给定一个二叉树,返回该二叉树层序遍历的结果,(从左到右,一层一层地遍历)例如:
// 给定的二叉树是{ 3, 9, 20,#,#, 15, 7 },
// 该二叉树层序遍历的结果是[[3], [9, 20], [15, 7]]
var levelOrder = function (root) {
let res = [];
if (root === null) return res;
let list = [];
list.push(root);
while (list.length) {
let curLen = list.length;//上一轮剩下的节点,全属于下一层
let newLevel = [];
for (let i = 0; i < curLen; i++) {//同层所有节点
let node = list.shift();//出列
newLevel.push(node.val);//push进newLevel数组
//左右子节点push进队列
if (node.left) list.push(node.left);
if (node.right) list.push(node.right);
}
res.push(newLevel);//push到res数组
};
return res;
};
console.log(levelOrder(res));
光照树问题
二叉树光照,输出能被光照到的节点, dfs能否解决
// 输入: [1, 2, 3, null, 5, null, 4]
// 输出: [1, 3, 4]
/**
* @param {TreeNode} root
* @return {number[]}
*/
function exposedElement(root) {
// 实现之
};
// 将list转化为树结构
class TreeNode {
constructor(val, left, right) {
this.val = (val === undefined ? 0 : val);
this.left = (left === undefined ? 0 : left);
this.right = (right === undefined ? 0 : right);
}
}
function buildTree(arr) {
if (!arr || arr.length === 0) {
return null;
}
let root = new TreeNode(arr.shift());
let nodeQueue = [root];
while (arr.length > 0) {
let node = nodeQueue.shift();
if (!arr.length) {
break;
}
let left = new TreeNode(arr.shift());
node.left = left;
nodeQueue.push(left);
if (!arr.length) {
break;
}
// 左侧叶子拼完,右边一样的操作
let right = new TreeNode(arr.shift());
node.right = right;
nodeQueue.push(right);
}
// 最后返回根结点,通过根结点就能得到整个二叉树的结构
return root;
}
const rightSideView = (root) => {
const res = [];
const dfs = (node, level) => {
if (!node) return;
res[level] = node.val;
dfs(node.left, level + 1);
dfs(node.right, level + 1);
}
dfs(root, 0);
return res;
}
统计层节点之和
多叉树, 获取每一层的节点之和
const res = {
value: 2,
children: [
{ value: 6, children: [{ value: 1 }] },
{ value: 3, children: [{ value: 2 }, { value: 3 }, { value: 4 }] },
{ value: 5, children: [{ value: 7 }, { value: 8 }] }
]
};
const layerSum = function (root) {
let result = [], index = 0;
const level = (root, index) => {
if (!root) return;
if (!result[index]) result[index] = 0;
result[index] += root.value;
if (root.children) root.children.forEach(child => level(child, index + 1))
};
level(root, index);
return result;
};
console.log(levelOrder(res));
@链表操作
删除链表的一个节点
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @param {number} val
* @return {ListNode}
*/
var deleteNode = function (head, val) {
// 定义虚拟节点
const res = new ListNode(-1);
// 虚拟节点连接到head
res.next = head;
// 定义p指针,最开始指向虚拟节点
let p = res;
// 从虚拟节点开始遍历链表
while (p?.next) {
// 如果下一个值等于val,则删除下一个值
if (p.next.val === val)
p.next = p.next.next;
p = p.next;
}
return res.next;
};
链表中环的入口节点
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var detectCycle = function (head) {
var hashSet = new Set()
while (head) {
if (hashSet.has(head)) {
return head
}
hashSet.add(head)
head = head.next
}
return null
};
// 快慢指针
var detectCycle = function (head) {
if (!head) return null;
let fast = head, slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast === slow) {
fast = head;
while (fast !== slow) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
return null;
};
@经典算法
冒泡排序
/**
* 冒泡排序number数组(升序)
* @param { number[] } arr
*/
function bubbleSort(arr) {
console.log("bubbleSort", arr);
var temp;
for (var n = 1; n < arr.length; n++) {
// 从01比干到89比
for (var i = 0; i < arr.length - n; i++) {
if (arr[i] > arr[i + 1]) {
temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
}
}
}
console.log(arr);
}
选择排序
/**
* 选择排序算法对number数组升序排序
* @param {Array} arr
*/
function selectSort(arr) {
console.log("selectSort", arr);
/* 选择排序 */
// 用于互换位置的暂存箱
var temp;
/* 依次锁定/选定【0~倒数第二把】交椅 */
for (var i = 0; i < arr.length - 1; i++) {
/* 将[i,末尾]区间内最小的元素找出来 跟i互换位置 */
// 先假定i号位最小
var minValue = arr[i];
var minIndex = i;
// 遍历[i+1,末尾]的所有元素
for (var j = i + 1; j < arr.length; j++) {
if (arr[j] < minValue) {
minValue = arr[j];
minIndex = j;
}
}
// 最小元素跟i互换位置
temp = arr[i];
arr[i] = minValue;
arr[minIndex] = temp;
}
console.log(arr);
}
插入排序
插入排序是一种简单直观的排序算法,其基本思想是将一个记录插入到已经排好序的有序表中,从而得到一个新的有序表。以下是JS实现插入排序的代码,带有详细的注释说明:
function insertionSort(arr) {
// 遍历数组,从第二个元素开始插入排序
for (let i = 1; i < arr.length; i++) {
// 将当前元素保存到临时变量temp中
let temp = arr[i];
let j = i - 1;
// 在已经排好序的元素中查找插入位置
while (j >= 0 && arr[j] > temp) {
// 如果当前元素比插入元素大,就将当前元素后移一位
arr[j + 1] = arr[j];
j--;
}
// 将插入元素放入正确的位置
arr[j + 1] = temp;
}
// 返回排好序的数组
return arr;
}
// 测试
const arr = [5, 3, 8, 4, 2];
console.log(insertionSort(arr)); // [2, 3, 4, 5, 8]
- 在上面的代码中,我们使用for循环遍历数组,从第二个元素开始进行插入排序。
- 对于每个元素,我们使用while循环在已经排好序的元素中查找插入位置,如果当前元素比插入元素大,就将当前元素后移一位,直到找到正确的位置。
- 最后,将插入元素放入正确的位置,完成一次插入排序。
- 重复这个过程,直到所有元素都排好序。
快速排序
快速排序是一种高效的排序算法,其基本思想是选择一个基准元素,将数组分成两个部分,小于基准元素的放在左边,大于基准元素的放在右边,然后对左右两个部分分别进行递归排序。以下是JS实现快速排序的代码,附有详细的注释说明:
function quickSort(arr) {
// 如果数组长度小于等于1,就直接返回
if (arr.length <= 1) {
return arr;
}
// 选择一个基准元素
const pivot = arr[0];
// 定义左右两个数组
const left = [];
const right = [];
// 将数组分成两个部分
for (let i = 1; i < arr.length; i++) {
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
// 分别对左右两个部分进行递归排序
return quickSort(left).concat([pivot], quickSort(right));
}
// 测试
const arr = [5, 3, 8, 4, 2];
console.log(quickSort(arr)); // [2, 3, 4, 5, 8]
二分查找
二分查找是一种高效的查找算法,它的基本思想是将有序数组分成两半,然后取中间元素进行比较,如果相等就返回,如果小于中间元素就在左半边继续查找,如果大于中间元素就在右半边继续查找。以下是JS实现二分查找的代码,附有详细的注释说明:
function binarySearch(arr, target) {
let left = 0; // 左边界
let right = arr.length - 1; // 右边界
while (left <= right) {
const mid = Math.floor((left + right) / 2); // 中间位置
if (arr[mid] === target) {
return mid; // 找到目标元素,返回下标
} else if (arr[mid] < target) {
left = mid + 1; // 目标元素在右半边,更新左边界
} else {
right = mid - 1; // 目标元素在左半边,更新右边界
}
}
return -1; // 没有找到目标元素,返回-1
}
// 测试
const arr = [1, 2, 3, 4, 5, 6];
console.log(binarySearch(arr, 3)); // 2
console.log(binarySearch(arr, 7)); // -1
- 在上面的代码中,我们使用while循环来进行二分查找。
- 对于一个有序数组,我们首先获取左右边界,然后计算中间位置,比较中间元素和目标元素的大小关系,如果相等就返回下标,如果小于中间元素就在右半边继续查找,如果大于中间元素就在左半边继续查找。
- 重复这个过程,直到找到目标元素或者左右边界重合。
- 如果没有找到目标元素,就返回-1。
实现LRU算法
- 缓存淘汰算法:内存容量是有限的,当你要缓存的数据超出容量,就得有部分数据删除,这时候哪些数据删除,哪些数据保留,就是LRU算法和LFU算法,FU强调的是访问次数,而LRU强调的是访问时间。
- 选择内存中最近最久未使用的页面予以淘汰,如果我们想要实现缓存机制 – 满足最近最少使用淘汰原则,我们就可以使用LRU算法缓存机制。如:vue 中 keep-alive 中就用到了此算法。
- LRU:即Least Recently Used(最近最少使用算法)。把长期不使用的数据被认定为无用数据,在缓存容量满了后,会优先删除这些被认定的数据。
- keep-alive内部即使用LRU算法
var LRUCache = function (capacity) {
this.cache = new Map();
this.capacity = capacity;
};
LRUCache.prototype.get = function (key) {
if (this.cache.has(key)) {
let temp = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, temp);
return temp;
}
return -1;
}
LRUCache.prototype.put = function (key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
} else if (this.cache.size >= this.capacity) {
this.cache.delete(this.cache.keys().next().value);
};
this.cache.set(key, value);
}
设计篇
@设计模式
组合+观察者
使用观察者模式实现以下需求:放学铃响10分钟后,所有电灯熄灭+大门反锁+服务器备份数据
略,请参考以下案例自行实现 小米智能家居案例
@闭包 & 函数式编程
柯里化
手写多参柯里化高阶函数curry
function curry(fn){
return function cfn(...args){
// 如果参数给够 则直接调用fn求结果并返回
// fn.length指的是fn定义的参数个数
if(args.length === fn.length){
return fn.apply(null,args)
}
/*
否则继续返回函数 以继续接收后续参数
利用外层函数缓存内层函数的所有参数
*/
return function(...bs){
// 外层闭包先将bs统计起来
args = args.concat(...bs)
/* 继续返回函数或结果 */
return cfn(...args)
}
}
}
const add = (a,b,c,d) => a+b+c+d
const cadd = curry(add)
console.log(cadd(1,2,3,4))
console.log(cadd(1)(2,3)(4))
同步函数的Promise化
手写同步函数的Promise化高阶函数promisify
/* 同步函数的Promise化 */
function promisify(fn){
return function pfn(...args){
/* 有回调函数 */
if(typeof args[args.length-1] === "function"){
fn.apply(null,args)
return
}
/* 无回调函数 */
return new Promise(
(resolve,reject)=>{
try {
const result = fn.apply(null,args)
resolve(result)
} catch (err) {
reject(err)
}
}
)
}
}
/* 代码规范:如果有回调函数,确保只有一个,且放在队伍末尾 */
const add = (a,b,callback)=>{
const result = a + b
if(!callback){
return result
}
callback(result)
}
const padd = promisify(add)
padd(2,3)
.then(ret => console.log("then",ret))
padd(2,3,ret => console.log("callback",ret))
管道与组合
手写管道高阶函数pipe
const pipe = (...fns) => (initialValue) => fns.reduce(
(pv,cv)=>cv(pv),
initialValue
)
const len = (n) => (n + "").length; //求数值的长度
const pow = (n) => n * n; //求n的平方
const log = (n) => console.log(n); //打印n
/* 生成流水线函数:先长度=>对长度做平方=>打印平方值 */
const streamline = pipe(pow, len, log);
streamline(10); //3
手写组合高阶函数compose
// 不reverse的话也可以直接使用fns.reduceRight()
const compose =
(...fns) =>
(initialValue) =>
fns.reverse().reduce((pv, cv) => cv(pv), initialValue);
const len = (n) => (n + "").length; //求数值的长度
const pow = (n) => n * n; //求n的平方
const cubicRoot = (n) => Math.cbrt(n);// 求立方根
console.log(compose(len, pow, cubicRoot)(1000));//3
手写防抖节流
手写防抖函数debounce
function debounce(fn, delay) {
let timer = null;
return function (...args) {
if (timer) {
clearTimeout(timer);
timer = null
}
timer = setTimeout(() => {
fn.apply(null, args);
timer = null;
}, delay);
};
}
手写节流函数throttle
function throttle(fn, delay) {
let lock = false;
return function (...args) {
if (!lock) {
fn.apply(null, args);
lock = true;
/* 接下来的1秒内禁止点击回调触发 */
setTimeout(() => {
lock = false;
}, delay);
}
};
}
场景篇
大文件上传
// 拆分的方法
function slice(file, piece = 1024 * 1024 * 5) {
let totalSize = file.size; // 文件总大小
let start = 0; // 每次上传的开始字节
let end = start + piece; // 每次上传的结尾字节
let chunks = []
while (start < totalSize) {
// 根据长度截取每次需要上传的数据
// File对象继承自Blob对象,因此包含slice方法
let blob = file.slice(start, end);
chunks.push(blob)
start = end;
end = start + piece;
}
return chunks
}
// 举个栗子
// 获取context,同一个文件会返回相同的值
function createContext(file,chunks) {
return file.name + file.length + "-" + chunks.length
}
/* 完成切片 */
let file = document.querySelector("[name=file]").files[0];
const LENGTH = 1024 * 1024 * 0.1;
let chunks = slice(file, LENGTH);
// 获取对于同一个文件,获取其的context
let context = createContext(file);
let tasks = [];
chunks.forEach((chunk, index) => {
let fd = new FormData();
fd.append("file", chunk);
// 传递context
fd.append("context", context);
// 传递切片索引值
fd.append("chunk", index + 1);
// 上传单片
tasks.push(post("/mkblk.php", fd));
});
/* 通知服务端组装 */
function notifyUploadCompleted(context) {
/* 所有切片都上传成功后通知服务端组装 */
let fd = new FormData();
fd.append("context", context);
// fd.append("chunks", chunks.length);
post("/mkfile.php", fd).then(res => {
console.log(res);
});
}
// 所有切片上传完毕后,调用mkfile接口
function upload(tasks) {
const rejectedTasks = []
Promise.allSettled(tasks).then(rets => {
rets.forEach((ret, i) => {
if (ret.status === "rejected") {
rejectedTasks.push(tasks[i])
}
})
if (!rejectedTasks.length) {
upload(rejectedTasks)
} else {
notifyUploadCompleted(context)
}
});
}
虚拟列表
虚拟列表是一种优化长列表渲染性能的技术,它的基本思想是只渲染可见区域内的部分列表项,而不是渲染整个列表。在可见区域之外的列表项可以通过滚动来进行懒加载,从而减少渲染开销。以下是JS实现虚拟列表的代码,附有详细的注释说明:
// 定义虚拟列表组件
class VirtualList {
constructor(container, total, itemHeight) {
this.container = container; // 列表容器
this.total = total; // 列表项总数
this.itemHeight = itemHeight; // 列表项高度
this.renderCount = Math.ceil(container.clientHeight / itemHeight) + 1; // 渲染列表项数量
this.renderItems = []; // 已渲染的列表项
this.lastScrollTop = 0; // 上次滚动位置
this.render(); // 渲染列表
this.bindEvent(); // 绑定滚动事件
}
// 渲染列表
render() {
const { container, total, itemHeight, renderCount } = this;
const fragment = document.createDocumentFragment(); // 创建文档片段
for (let i = 0; i < renderCount; i++) {
const item = document.createElement('div'); // 创建列表项
item.className = 'item';
item.style.height = itemHeight + 'px';
item.innerText = i < total ? i : ''; // 设置列表项内容
fragment.appendChild(item);
this.renderItems.push(item); // 将列表项添加到已渲染列表中
}
container.appendChild(fragment); // 将文档片段添加到容器中
}
// 绑定滚动事件
bindEvent() {
const { container } = this;
container.addEventListener('scroll', this.handleScroll.bind(this));
}
// 处理滚动事件
handleScroll() {
const { container, total, itemHeight, renderCount, renderItems, lastScrollTop } = this;
const scrollTop = container.scrollTop; // 获取当前滚动位置
const direction = scrollTop > lastScrollTop ? 'down' : 'up'; // 判断滚动方向
const offset = Math.floor(scrollTop / itemHeight); // 计算偏移量
const start = direction === 'down' ? offset : Math.max(offset - renderCount + 1, 0); // 计算起始位置
const end = start + renderCount - 1; // 计算结束位置
if (start === 0) {
container.scrollTop = 0; // 到达顶部,重置滚动位置
} else if (end === total - 1) {
container.scrollTop = container.scrollHeight - container.clientHeight; // 到达底部,重置滚动位置
}
for (let i = 0; i < renderCount; i++) {
const item = renderItems[i];
const index = start + i;
if (index >= 0 && index < total) {
item.innerText = index; // 更新列表项内容
} else {
item.innerText = ''; // 隐藏不可见的列表项
}
}
this.lastScrollTop = scrollTop; // 更新上次滚动位置
}
}
// 测试
const container = document.getElementById('container');
new VirtualList(container, 1000, 30);
- 在上面的代码中,我们使用ES6的class语法定义了一个虚拟列表组件。
- 在构造函数中,我们首先计算渲染列表项数量,然后创建文档片段和列表项,并将列表项添加到已渲染列表中。
- 在绑定滚动事件时,我们使用bind方法将this绑定到当前实例上,以便在处理滚动事件时可以访问到实例的属性和方法。
- 在处理滚动事件时,我们首先获取当前滚动位置和滚动方向,然后计算偏移量、起始位置和结束位置。
- 根据起始位置和结束位置,我们更新已渲染的列表项的内容,将不可见的列表项隐藏起来。
- 最后,我们更新上次滚动位置。
图片懒加载
图片懒加载是一种优化网页性能的技术,它可以减少页面的加载时间和带宽消耗。图片懒加载的原理是,当页面滚动到某个位置时,才加载该位置的图片。以下是JS实现图片懒加载的代码,附有详细的注释说明:
// 获取所有需要懒加载的图片元素
const lazyImages = document.querySelectorAll('.lazy');
// 定义一个变量,用于存储可视区域的高度
let viewHeight = window.innerHeight || document.documentElement.clientHeight;
// 定义一个函数,用于判断图片是否在可视区域内
function isInView(el) {
const rect = el.getBoundingClientRect(); // 获取元素相对于视口的位置信息
return rect.top >= 0 && rect.bottom <= viewHeight; // 判断元素是否在可视区域内
}
// 定义一个函数,用于加载图片
function loadImages() {
lazyImages.forEach((img) => {
if (isInView(img)) { // 如果图片在可视区域内
img.src = img.dataset.src; // 加载图片
img.classList.remove('lazy'); // 移除lazy类
}
});
}
// 初始化页面时加载可视区域内的图片
loadImages();
// 监听页面滚动事件,滚动时加载可视区域内的图片
window.addEventListener('scroll', () => {
loadImages();
});
- 在上面的代码中,我们首先获取所有需要懒加载的图片元素,并定义一个变量viewHeight,用于存储可视区域的高度。
- 然后,我们定义一个函数isInView,用于判断图片是否在可视区域内。该函数首先使用getBoundingClientRect方法获取元素相对于视口的位置信息,然后判断元素是否在可视区域内。
- 接下来,我们定义一个函数loadImages,用于加载图片。该函数遍历所有需要懒加载的图片元素,如果图片在可视区域内,则加载该图片,并移除lazy类。
- 最后,我们在初始化页面时加载可视区域内的图片,并在页面滚动时监听滚动事件,加载可视区域内的图片。
- 在HTML中,我们需要为需要懒加载的图片元素添加lazy类,并将图片的真实地址存储在data-src属性中,例如:
<img class="lazy" data-src="image.jpg" alt="image">
这样,当页面滚动到某个位置时,才会加载该位置的图片,从而减少页面的加载时间和带宽消耗。