1. 数组去重
const arr = [1,1,1,2,3,4,4,4,4,5]
// 第一种方式
const arr1 = Array.from(new Set(arr))
// 第二种方式
const arr2 = [...new Set(arr)]
// 第三种方式
const arr3 = arr.filter((item,index) => arr.indexOf(item) === index)
// 第四种方式
const arr4 = arr.reduce((pre,cur) => {
if (!pre.includes(cur)) {
pre.push(cur)
}
return pre;
},[])
2. 防抖和节流
- 防抖 debounce: 在一段时间内,事件只会最后触发一次。
- 节流 throttle: 事件,按照一段时间的间隔来进行触发。
// 防抖
const debounce = (fn, delay) => {
let timer = null;
return function () {
const ctx = this;
const args = arguments;
if (timer) clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(ctx, args)
}, delay);
}
}
dom.addEventListener('click',debounce(fn,delay));
// 节流
const throttle = (fn,delay) => {
let start = Date.now();
return function() {
const ctx = this;
const args = arguments;
if (Date.now() - start > delay) {
fn.apply(ctx,args);
start = Date.now();
}
}
}
dom.addEventListener('click',throttle(fn,delay));
// fn函数传递参数
const handler = throttle(fn,delay)
dom.addEventListener('click',() => {
handler(args)
});
3. 不使用a标签,实现a标签的功能
window.open("URL")
4. 深拷贝和浅拷贝
- 浅拷贝: 对于复杂数据类型,浅拷贝只是把引用地址赋值给了新的对象,改变这个新对象的深层的值,原对象的值也会一起改变。第一层不会改变。
- 深拷贝: 对于复杂数据类型,拷贝后地址引用都是新的,改变拷贝后新对象的值,不会影响原对象的值。
const obj = {
a: {
name: 'dd'
},
b: 33
}
// 第一种方式
const obj1 = {...obj}; // obj1.a === obj.a
// 第二种方式
const obj2 = Object.assign({},obj); // obj2.a === obj.a
// 第三种方式
function shallowClone(obj) {
const newObj = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return newObj;
}
// 第一种简单方式
const obj3 = JSON.parse(JSON.stringify(obj)); // obj3.a !== obj.a
存在一些问题:
- 无法处理循环引用,遇到会报错。
- 不支持function和undefined,拷贝后会丢失。
- 无法复制某些对象类型, 遇到比如Date、RegExp、Map、Set、Error、Symbol 等 JavaScript 内置对象及其属性时,会被转换为基本的字符串或数组形式,导致前后的数据类型不一样,不符合预期。
// 第二种源码方式
const isObj = (obj) => {
return typeof obj === 'object' && obj !== null;
}
const cloneDeep = (obj, hash = new WeakMap()) => {
if (isObj(obj)) {
// 这个hash的作用应该是处理循环引用的。比如说,如果一个对象有一个属性指向自己,或者两个对象互相引用,这时候普通的深克隆会无限递归,导致栈溢出。使用WeakMap来记录已经克隆过的对象,这样当再次遇到同一个对象时,可以直接返回之前克隆的副本,避免循环引用的问题。
// 另外,WeakMap的键是对象,并且是弱引用的,不会阻止垃圾回收。这样当原对象不再被引用时,WeakMap中的记录也会被自动清除,这有助于内存管理。而如果使用普通的Map,可能会导致内存泄漏,因为即使原对象不再使用,Map中的引用仍然存在,无法被回收。所以这里使用WeakMap是正确的选择。
if (hash.has(obj)) {
return hash.get(obj)
}
const newObj = Array.isArray(obj) ? [] : {};
hash.set(obj, newObj); // 记录已克隆对象
for (let key in obj) { // 这里会克隆原型链上的内容,可以优化一下
if(obj.hasOwnProperty(key)) {
newObj[key] = cloneDeep(obj[key], hash);
}
}
return newObj;
} else {
return obj;
}
}
5. new 操作符
new 操作符用于创建实例,执行以下步骤:
(1)创建一个新的空对象
(2)将构造函数的作用域传给新对象,这时this指向新对象
(3)执行构造函数中的内容,为这个新对象增加属性
(4)返回新对象
const myNew = (fn,...args) => {
// newObj.__proto__ === fn.prototype; 新创建这个对象是fn的实例,需要连接到fn的原型对象上
// Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
const newObj = Object.create(fn.prototype)
// 作用域赋值,将fn的this指向新的对象,添加属性到新的对象上
const res = fn.call(newObj,...args);
// 4. 如果构造函数返回的是一个对象,则返回这个对象;否则返回新创建的对象
return res && typeof res === 'object' ? res : newObj;
}
// 使用示例
function Person(name,age) {
this.name = name;
this.age = age;
}
Person.prototype.greet = () => {
console.log('greet');
}
const person1 = myNew(Person,'wang',13)
console.log(person1.name)
6. instanceof
右边构造函数的原型对象是否在左边对象的原型链上
Object.getPrototypeOf(obj) 获取对象的原型
function myInstanceof(left, right) {
const prototype = right.prototype;
const proto = Object.getPrototypeOf(left);
while (proto) {
if (proto === prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
7. 实现 Object.create
创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
// 定义一个名为 create 的函数,接受一个参数 obj,该参数是一个对象。
function create(obj) {
// 定义一个空的构造函数 Func。
function Func() {}
// 将传入的对象 obj 设置为 Func 的原型。
// 这意味着通过 Func 构造函数创建的新对象将继承 obj 的所有属性和方法。
Func.prototype = obj;
// 这里不需要
// 修正原型链中的构造函数指向。
// 因为当我们设置 `Func.prototype = obj` 之后,Func.prototype.constructor 默认指向的是 Object 构造函数,
// 所以我们需要显式地将其设置回 Func,以保持正确的原型链。
Func.prototype.constructor = Func;
// 使用 new Func() 创建一个新的实例,并返回这个实例。
// 这个新实例将会继承 obj 的所有属性和方法。
return new Func();
}
8. 手写call,bind,apply
call,bind,apply都是用于改变this指向
- call 和 apply 立即执行,call 参数为数值,apply参数为数组
- bind 不立即执行,bind参数为数值
参考:
Function.prototype.myCall = function (context, ...args) {
// this是调用myCall方法的函数
if (typeof this !== 'function') {
throw new TypeError('必须是函数');
}
// context 如果没有指定的话就会指向全局对象, 使用 globalThis 来访问全局对象。
context = context || globalThis;
// 唯一fn。防止冲突
let fn = Symbol();
// 使得context拥有 函数的能力
context[fn] = context;
// 执行获得结果
const res = context[fn](...args);
// 删除新增的属性
delete context[fn];
return res;
}
Function.prototype.myApply = function (context, args) {
if (typeof this !== 'function') {
throw new TypeError('必须是函数')
}
// 参数检测
if (args && !Array.isArray(args)) {
throw new TypeError('参数必须是数组');
}
// 如果未传值则给默认值
context = context || globalThis;
args = args || [];
let fn = Symbol();
context[fn] = this;
const res = context[fn](...args);
delete context[fn];
return res;
}
Function.prototype.myBind = function (context, ...args) {
if (typeof this !== 'function') {
throw new TypeError('必须是函数')
}
context = context || globalThis;
// 保存原始函数的引用,这里的this是调用bind的函数
const _this = this;
// 返回的函数可能会被new 使用,new 之后,fn 内部的this指向新创建的那个对象,
// 返回的这个 fn就是那时候的构造函数
// 通过检查 this instanceof fn,可以判断返回出去的函数是否被作为构造函数调用
return function fn(...args2) {
if (this instanceof fn) {
// 作为构造函数,则应该使用New 方法生成一个对象,这个对象是上面的_this的生产的
return new _this(...args, ...args2);
}
// 否则的话就是正常的apply行为 context 夺舍 上面的那个_this
return _this.apply(context, args.concat(args2));
}
}
9. promise
(1) promise :处理封装异步操作,防止出现回调地狱。 .then()返回一个新的Promise实例,每个promise对应一个异步任务,所以它可以链式调用,异常传透。
手写promise
const PENDING = 'PENDING'
const FULLFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
class MyPromise {
constructor(executor) {
this.state = PENDING;
this.value = '';
this.reason = '';
this.onfullfilledCallbackArr = []
this.onrejectedCallbackArr = []
const resolve = (value) => {
if (this.state === PENDING) {
this.state = FULLFILLED
this.value = value
this.onfullfilledCallbackArr.forEach(cb => cb())
}
}
const reject = (reason) => {
if (this.state === PENDING) {
this.state = REJECTED
this.reason = reason
this.onrejectedCallbackArr.forEach(cb => cb())
}
}
// 这里只会捕获到同步代码的错误
// 异步执行时已经脱离了当前executor的执行作用域(executor已经执行完了,所以捕获不到)
// 错误会被抛到全局,导致未捕获的异常
// 使用unhandledrejection 全局捕获或者在settimeout里手动reject
// window.addEventListener('unhandledrejection', (event) => {
// console.log('全局捕获:', event.reason);
// event.preventDefault();
// })
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
then(onfullfilled, onrejected) {
onfullfilled = typeof onfullfilled === 'function' ? onfullfilled : value => value
onrejected = typeof onrejected === 'function' ? onrejected : reason => { throw reason }
// then可以进行链式调用,所以需要返回promise
return new MyPromise((resolve, reject) => {
// 执行onfullfilled 和 onrejected 的过程中,需要对异常情况进行捕捉
const onfulfilledCallback = () => {
setTimeout(() => {
try {
const res = onfulfilled(this.value)
// 如果 res 是 Promise,需要特殊处理
if (res instanceof MyPromise) {
return res.then(resolve, reject)
} else {
resolve(res)
}
} catch (err) {
reject(err)
}
})
}
const onrejectedCallback = () => {
setTimeout(() => {
try {
const res = onrejected(this.reason)
if (res instanceof MyPromise) {
return res.then(resolve, reject)
} else {
resolve(res)
}
} catch (err) {
reject(err)
}
})
}
// 判断当前状态,两种情况
// 1:promise中是异步函数,那这里状态仍然是pedding,需要将当前处理函数保存在队列里,等状态改变了再执行
// 2:promise中是同步函数,那这里状态已经改变了,为了不让它立即执行,需要使用settimeout模拟异步,保证then中内容在当前事件循环之后再执行
if (this.state === FULLFILLED) {
onfullfilledCallback()
} else if (this.state === REJECTED) {
onrejectedCallback()
} else {
// 保证then中内容在自己被调用的那一轮事件循环之后的新执行栈中执行
// 如下例子,输出顺序应该为 1,3, 2
// const p1 = new MyPromise((resolve, reject) => {
// setTimeout(() => {
// console.log(1)
// resolve(2)
// console.log(3)
// })
// }).then(reg => {
// console.log('reg', reg)
// })
this.onfullfilledCallbackArr.push(onfullfilledCallback)
this.onrejectedCallbackArr.push(onrejectedCallback)
}
})
}
//catch() 方法返回一个Promise,并且处理拒绝的情况。它的行为与调用Promise.prototype.then(undefined, onRejected) 相同。
catch(reject) {
return this.then(null, reject)
}
// finally() 方法返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。
// 这为在Promise是否成功完成后都需要执行的代码提供了一种方式。
// 由于无法知道promise的最终状态,所以finally的回调函数中不接收任何参数,它仅用于无论最终结果如何都要执行的情况。
finally(callback) {
return this.then(callback, callback)
}
// Promise.resolve(value) 将给定的一个值转为Promise对象。
// 静态方法,只能类本身调用,不能通过类实例调用
static resolve(value) {
// primise直接返回
if (value instanceof MyPromise) {
return value
} else if (value instanceof Object && 'then' in value) {
return new MyPromise((resolve, reject) => {
value.then(resolve, reject)
})
}
// 否则返回promise
return new MyPromise(resolve => {
resolve(value)
})
}
// Promise.reject()方法返回一个带有拒绝原因的Promise对象。
static reject(reason) {
return new MyPromise((resolve, reject) => {
reject(reason)
})
}
// promise.all 等待所有成功或一个失败
static all(arr) {
return new MyPromise((resolve, reject) => {
if (!Array.isArray(arr)) {
reject('类型错误')
}
let res = []
let count = 0;
for (let i = 0; i < arr.length; i++) {
MyPromise.resolve(arr[i]).then(curRes => {
res[i] = curRes;
count++;
if (count === arr.length) {
resolve(res)
}
}, (err) => {
reject(err)
})
}
})
}
// promise.race 等待第一个成功或一个失败
static race(arr) {
return new MyPromise((resolve, reject) => {
if (!Array.isArray(arr)) {
reject('类型错误')
}
for (let i = 0; i < arr.length; i++) {
MyPromise.resolve(arr[i]).then(curRes => {
resolve(curRes)
}, (err) => {
reject(err)
})
}
})
}
// promise.allsettled 等待所有完成
static allSettled(arr) {
return new MyPromise((resolve, reject) => {
if (!Array.isArray(arr)) {
reject('类型错误')
}
let res = []
let count = 0;
for (let i = 0; i < arr.length; i++) {
MyPromise.resolve(arr[i]).then(value => {
res[i] = {
staus: 'fulfilled',
value
};
count++;
if (count === arr.length) {
resolve(res)
}
}, (reason) => {
res[i] = {
staus: 'rejected',
reason
};
count++;
if (count === arr.length) {
resolve(res)
}
})
}
})
}
// promise.any 等待第一个成功,或者全部失败
static any(arr) {
return new MyPromise((resolve, reject) => {
if (!Array.isArray(arr)) {
reject('类型错误')
}
let errors = []
let count = 0;
for (let i = 0; i < arr.length; i++) {
MyPromise.resolve(arr[i]).then(value => {
resolve(value) // 第一个成功
}, (reason) => {
errors[i] = reason;
count++;
if (count === arr.length) {
reject(errors)
}
})
}
})
}
}
promise 打印顺序相关的题目
10. promise 封装相关题目
- promise封装ajax
// Promise封装Ajax请求
function ajax(method, url, data) {
var xhr = new XMLHttpRequest();
return new Promise(function (resolve, reject) {
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
if (xhr.status === 200) {
resolve(xhr.responseText);
} else {
reject(xhr.statusText);
}
};
xhr.open(method, url);
xhr.send(data);
});
}
ajax('get',url,data).then((res) => {
const data = res;
}.catch(err => {
console.log(err)
})
- promise 封装图片加载,异步加载image
function loadImage = (url) => {
return new Promise((resolve,reject) => {
const img = new Image();
img.onload = () => {
resolve(img)
}
img.onerror = (err) => {
reject(err)
}
img.src = url;
}
}
loadImage(url).then(img => {
document.body.appendChild(img);
console.log('图片加载成功');
})
.catch(error => {
console.error('图片加载失败', error);
});
- promise实现交替打印红绿灯
红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?
function red() {
console.log("red");
}
function green() {
console.log("green");
}
function yellow() {
console.log("yellow");
}
const task = (timer, light) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (light === 'red') {
red()
}
else if (light === 'green') {
green()
}
else if (light === 'yellow') {
yellow()
}
resolve()
}, timer)
})
}
const step = () => {
task(3000, 'red')
.then(() => task(2000, 'green'))
.then(() => task(1000, 'yellow'))
.then(step)
}
step()
// 使用async await
const arr = [1, 2, 3]
const delays = [1000, 2000, 3000]
function sleep(item, delay) {
return new Promise((res) => {
setTimeout(() => {
console.log(item)
res()
}, delay)
})
}
async function handler(arr, delays) {
for (let i = 0; i < arr.length; i++) {
await sleep(arr[i], delays[i])
}
handler(arr, delays)
}
handler(arr, delays)
- promise 实现每隔一秒输出1,2,3
const arr = [1, 2, 3];
arr.reduce((pre, cur) => {
return pre.then(() => {
return new Promise(res => {
setTimeout(() => {
console.log(cur);
res();
}, 1000)
})
})
}, Promise.resolve())
// 使用 async await实现]
function print(item) {
return new Promise((resove) => {
setTimeout(() => {
console.log(item)
resove()
}, 1000)
})
}
async function handle() {
for (let item of arr) {
await print(item)
}
}
handle()
- 实现mergePromise函数
实现mergePromise函数,把传进去的数组按顺序先后执行,并且把返回的数据先后放到数组data中。
这道题有点类似于Promise.all()
,不过.all()
不需要管执行顺序,只需要并发执行就行了。但是这里需要等上一个执行完毕之后才能执行下一个。
解题思路:
- 定义一个数组
data
用于保存所有异步操作的结果 - 初始化一个
const promise = Promise.resolve()
,然后循环遍历数组,在promise
后面添加执行ajax
任务,同时要将添加的结果重新赋值到promise
上。
// 生成的promise链
Promise.resolve()
.then(ajax1).then(res => { data.push(res); return data; })
.then(ajax2).then(res => { data.push(res); return data; })
.then(ajax3).then(res => { data.push(res); return data; });
// 不能直接定义为promise,那样直接就执行了
const p1 = () => new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1)
resolve(1)
}, 2000)
})
const p2 = () => new Promise((resolve, reject) => {
setTimeout(() => {
console.log(2)
resolve(2)
}, 1000)
})
const p3 = () => new Promise((resolve, reject) => {
setTimeout(() => {
console.log(3)
resolve(3)
}, 500)
})
function mergePromise () {
// 存放每个ajax的结果
const data = [];
let promise = Promise.resolve();
ajaxArray.forEach(ajax => {
// 第一次的then为了用来调用ajax 第二次的then是为了获取ajax的结果
promise = promise.then(ajax).then(res => {
data.push(res);
return data; // 把每次的结果返回
})
})
// 最后得到的promise它的值就是data
return promise;
}
// 使用reduce实现
function mergePromise(arr) {
let res = [];
return arr.reduce((pre, cur) => {
return pre.then(cur).then(c => {
res.push(c)
return res;
})
}, Promise.resolve())
}
mergePromise([p1, p2, p3]).then(data => {
console.log("done");
console.log(data); // data 为 [1, 2, 3]
});
// 要求分别输出
// 1
// 2
// 3
// done
// [1, 2, 3]
限制异步操作的并发个数并尽可能快的完成全部
既然题目的要求是保证每次并发请求的数量为3,那么我们可以先请求urls
中的前面三个(下标为0,1,2
),并且请求的时候使用Promise.race()
来同时请求,三个中有一个先完成了(例如下标为1
的图片),我们就把这个当前数组中已经完成的那一项(第1
项)换成还没有请求的那一项(urls
中下标为3
)。
直到urls
已经遍历完了,然后将最后三个没有完成的请求(也就是状态没有改变的Promise
)用Promise.all()
来加载它们。
function limitLoad(urls, handler, limit) {
let sequence = [].concat(urls); // 复制urls
// 这一步是为了初始化 promises 这个"容器"
let promises = sequence.splice(0, limit).map((url, index) => {
return handler(url).then(() => {
// 返回下标是为了知道数组中是哪一项最先完成
return index;
});
});
// 注意这里要将整个变量过程返回,这样得到的就是一个Promise,可以在外面链式调用
return sequence
.reduce((pCollect, url) => {
return pCollect
.then(() => {
return Promise.race(promises); // 返回已经完成的下标
})
.then(fastestIndex => { // 获取到已经完成的下标
// 将"容器"内已经完成的那一项替换
promises[fastestIndex] = handler(url).then(
() => {
return fastestIndex; // 要继续将这个下标返回,以便下一次变量
}
);
})
.catch(err => {
console.error(err);
});
}, Promise.resolve()) // 初始化传入
.then(() => { // 最后三个用.all来调用
return Promise.all(promises);
});
}
const urls = []
for (let i = 0; i < 10; i++) {
urls.push(i);
}
function loadImg(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(url)
resolve()
}, Math.random() * 1000)
})
}
limitLoad(urls, loadImg, 3)
.then(res => {
console.log("图片全部加载完毕");
console.log(res);
})
.catch(err => {
console.error(err);
});
并发请求控制
class Scheduler {
constructor(limit = 6) {
this.limit = limit;
this.tasks = []; // 任务队列:保存待执行任务
this.pool = new Set(); // 执行池:当前正在执行的任务
this.taskIndex = 0; // 任务唯一索引标识
}
// 添加任务到队列
addTask(task, ...args) {
const index = this.taskIndex++;
this.tasks.push({ task, args, index });
}
// 启动任务调度
async start() {
const results = new Array(this.taskIndex); // 按索引存储结果
this.taskIndex = 0; // 重置索引(确保多次调用start()的正确性)
while (this.tasks.length > 0) {
const { task, args, index } = this.tasks.shift();
// 包装任务,确保错误被捕获并按索引存储结果
const wrapped = Promise.resolve()
.then(() => task(...args))
.then(value => {
results[index] = { status: 'fulfilled', value };
})
.catch(reason => {
results[index] = { status: 'rejected', reason };
})
.finally(() => {
this.pool.delete(wrapped);
});
this.pool.add(wrapped);
// 并发控制:当执行池满时,等待最快完成的任务
// await 的时候,会阻塞循环,等待当前执行池中的一个任务完成
if (this.pool.size >= this.limit) {
await Promise.race(this.pool);
}
}
// 等待所有剩余任务完成
await Promise.allSettled(this.pool);
return results;
}
}
// 使用示例
const scheduler = new Scheduler()
// 模拟异步任务
const request = (id) => new Promise(resolve =>
setTimeout(() => {
console.log(`Task ${id} completed`)
resolve(id)
}, id * 2000)
)
// 添加100个任务
for (let i = 0; i < 20; i++) {
scheduler.addTask(request, i)
}
// 启动调度
scheduler.start().then((res) => console.log('All tasks completed', res))
核心机制对比
特性 | Scheduler类方案 | limitLoad函数方案 |
---|---|---|
任务队列结构 | 先进先出队列(FIFO) | 固定长度数组(动态替换) |
初始化加载 | 动态逐个添加 | 预先加载前N个任务 |
并发维持方式 | Promise.race + 自动清理 | Promise.race + 索引替换 |
任务执行顺序 | 严格按添加顺序执行 | 完成顺序可能影响后续任务分配 |
内存管理 | Set自动清理 | 数组索引替换 |
结果 | 结果按照添加顺序输出 |
终止promise
Promise
一旦实例化之后,状态就只能由 Pending
转变为 Rejected
或者 Fulfilled
, 本身是不可以取消已经实例化之后的 Promise
了。
但是我们可以通过一些其他的手段来实现终止 Promise
的继续执行来模拟 Promise
取消的效果。
标志位取消
在 Promise 内部检查外部变量状态,若需终止则主动调用 reject
。
let isCancel = false
function newPromise() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (isCancel) {
reject('cancel')
} else {
resolve('resolve')
}
}, 2000)
})
}
const mypromise = newPromise();
isCancel = true;
mypromise.then(res => console.log(res)).catch(err => console.log(err))
promise.race
Promise.race
方法接收多个 Promise
,一旦这些 Promise
中任意一个 Promise
存在 resove
或者 reject
其他的 Promise
就不会执行了,基于这个特点,我们可以构造代码实现终止 Promise
的执行
let abort = null;
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1)
resolve('resolve')
}, 2000)
})
const p2 = new Promise((resolve, reject) => {
abort = () => reject('err')
})
Promise.race([p1, p2]).then(res => console.log(res)).catch(err => console.log(err))
abort();
abortcontroller
const controller = new AbortController();
const { signal } = controller;
fetch("https://api.example.com", { signal })
.then(response => response.json())
.catch(err => {
if (err.name === "AbortError") console.log("Request aborted");
});
controller.abort(); // 终止请求
手动触发reject
function cancellablePromise() {
let abort
const p1 = new Promise((resolve, reject) => {
abort = reject;
setTimeout(resolve, 3000)
})
return {
p1,
abort
}
}
const { p1, abort } = cancellablePromise();
p1.then(res => console.log(res))
abort('cancel')
第三方库
11. 函数柯里化
函数柯里化(Currying)可以将一个接受多个参数的函数转换成一系列使用一个参数的函数。这个转换后的函数链中的每一个函数都返回下一个函数,直到最后一个函数返回最终的结果。
const curry = (fn, ...args) =>
// 函数的参数个数可以直接通过函数数的.length属性来访问
args.length >= fn.length // 这个判断很关键!!!
// 传入的参数大于等于原始函数fn的参数个数,则直接执行该函数
? fn(...args)
/**
* 传入的参数小于原始函数fn的参数个数时
* 则继续对当前函数进行柯里化,返回一个接受所有参数(当前参数和剩余参数) 的函数,递归
*/
: (..._args) => curry(fn, ...args, ..._args);
const curry = (fn, ...args) => args.length >= fn.length ? fn(...args) : (...args2) => curry(fn, ...args, ...args2)
function add1(x, y, z) {
return x + y + z;
}
const add = curry(add1);
console.log(add(1, 2, 3));
console.log(add(1)(2)(3));
console.log(add(1, 2)(3));
console.log(add(1)(2, 3));
// 实现add(1)(2)(3)
function add(num) {
sum = num || 0;
if (!num) {
return sum
}
function innerAdd(num2) {
if (num2) {
sum += num2;
return innerAdd;
} else {
return sum;
}
}
return innerAdd;
}
console.log(add(1)(2)(3)())
12. 数组的flat
// 第一种方法:递归
const arr = [1, 2, [3, [4, 5], 6], [6, 7]]
const flat = (arr) => {
let newArr = [];
arr.forEach(item => {
if (Array.isArray(item)) {
newArr = newArr.concat(flat(item));
} else {
newArr.push(item);
}
});
console.log('new', newArr)
return newArr;
}
// 第二种方法: reduce
const flat = (arr) => {
return arr.reduce((pre, cur) => {
if (Array.isArray(cur)) {
pre = pre.concat(flat(cur))
} else {
pre.push(cur);
}
return pre;
}, [])
}
// 第三种方式,使用扩展运算符
const flat = (arr) => {
while(arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
// 第四种方法,使用es10的flat方法,可能会有兼容问题。参数表示要展开多少层
console.log(arr.flat(Infinity));
13. 对象的flat
function flattenObject(obj) {
const result = {};
function traverse(currentObj, path = '') {
for (const key in currentObj) {
if (currentObj.hasOwnProperty(key)) {
const currentPath = path ? `${path}.${key}` : key;
const value = currentObj[key];
// 是对象就继续处理
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
traverse(value, currentPath);
} else {
result[currentPath] = value;
}
}
}
}
traverse(obj);
return result;
}
const obj = {
a: {
b: {
c: '1'
},
b2: {
c: 3
}
},
d: 0
}
14. sleep 函数
sleep函数使js开始休眠一段时间
第一种方法:promise
async function handleSleep(delay) {
console.log('start')
await sleep(delay);
console.log('end')
function sleep(dealy) {
return new Promise((res,rej) => {
setTimeout(res,delay);
})
}
}
// 第二种方法,完全堵塞进程来达到sleep
function handleSleep(delay) {
console.log('start');
sleep(delay);
console.log('end');
function sleep(delay) {
const start = Date.now();
while (Date.now() - start < delay) {
continue;
}
}
}
handleSleep(1000);
15. 类数组转换为数组
- 类数组:具有数字索引和
length
属性的对象
let arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
let arrayLike2 = document.getElementsByTagName('div');
// (1)通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
// (2)通过 call 调用数组的 splice 方法来实现转换,不适用于arrayLike2类型的,dom不能删除属性
Array.prototype.splice.call(arrayLike,0);
// (2)通过 call 调用数组的 concat 方法来实现转换, 不适用于arrayLike类型的,是对象,不能用于apply参数
Array.prototype.concat.apply([], arrayLike);
// (4)通过 Array.from 方法来实现转换
Array.from(arrayLike);
// (5) 使用扩展运算符,不适用于arrayLike类型的,是对象,需要用{...arrayLike}
[...arrayLike]
(6)使用for in 或for of迭代然后逐个加入新数组中
16. 将对象数组转换为树形结构
递归
const items = [
{ id: 1, name: 'Item 1', parentId: null },
{ id: 2, name: 'Item 1.1', parentId: 1 },
{ id: 3, name: 'Item 1.2', parentId: 1 },
{ id: 4, name: 'Item 2', parentId: null },
{ id: 5, name: 'Item 2.1', parentId: 4 },
// ... 更多的项目
];
const arrToTree = (arr, parentId = null) => {
// 判断是否是顶层,是的话直接返回否则过滤出对应的子元素
const filterArr = arr.filter(item => {
return item.parentId === parentId;
})
// 递归返回对应的子
filterArr.forEach(item => {
item.childNode = arrToTree(arr, item.id);
})
return filterArr;
}
console.log(arrToTree(items))
迭代
function arrayToTreeIterative(data) {
const map = {};
const tree = [];
// 创建哈希映射并保留引用
data.forEach(node => {
map[node.id] = { ...node, children: [] };
});
// 构建树结构
data.forEach(node => {
if (node.parentId) {
// 把当前元素插入其父元素的children中,使用map获取,这样里面才有children属性
map[node.parentId].children.push(map[node.id]);
} else {
tree.push(map[node.id]);
}
});
return tree;
}
17. 发布-订阅模式
发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态生改变时,所有依赖于它的对象都将得到状态改变的通知。
- 订阅者(Subscriber)把自己想订阅的事件 注册(Subscribe)到调度中心(Event Channel);
- 当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由 调度中心 统一调度(Fire Event)订阅者注册到调度中心的处理代码。
发布/订阅模式的优点是对象之间解耦,异步编程中,可以更松耦合的代码编写;缺点是创建订阅者本身要消耗一定的时间和内存,虽然可以弱化对象之间的联系,多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护
class Observer {
constructor() {
this.message = {};
}
// 订阅者注册订阅事件到调度中心
// 向消息队列中添加内容
$on(type, callback) {
if (!this.message[type]) {
this.message[type] = [];
}
this.message[type].push(callback);
}
// 订阅者取消订阅事件
// 删除消息队列中的内容
$off(type, callback) {
if (!this.message[type]) {
return;
}
if (!callback) {
this.message[type] = undefined;
}
this.message[type] = this.message.filter(item => item !== callback);
}
// 发布者发布事件到调度中心,调度中心处理代码
// 触发消息队列中的内容
$emit(type, data) {
if (!this.message[type]) {
return;
}
this.message[type].forEach(item => {
item(data);
})
}
// 使用once注册,然后emit执行一次之后就off掉
$once(event, fn) {
const _fn = (...args) => {
fn.apply(this, args)
this.$off(event, _fn);
}
this.$on(event, _fn)
}
}
const person1 = new Observer();
person1.$on('onclick', () => { console.log('ddd') });
person1.$emir('onclick');
person1.$off('onclick', () => { console.log('ddd') });
18. 斐波那契数
斐波那契数列是一个经典的数列,其中每个数都是前两个数的和。例如,从 0 和 1 开始,后续的数依次为 0、1、1、2、3、5、8、13、21 等等
// 递归方法
function fi(n) {
if (n <= 1){
return n;
}
return fi(n - 1) + fi(n - 2);
}
console.log(fi(10))
// 迭代方法
function fi2(n) {
let a = 0, b = 1, temp;
if (n === 0) return a;
if (n === 1) return b;
for (let i = 2; i <= n; i++) {
temp = a + b;
a = b;
b = temp;
}
return b;
}
// 动态规划方法
function fi3(n) {
let dp = [0, 1];
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
19. 使用setTimeout 模拟 setInterval
function myInterval(callback, delay) {
let timer = null;
function execute() {
callback();
timer = setTimeout(execute, delay);
}
timer = setTimeout(execute, delay);
return () => clearTimeout(timer);
}
const id = myInterval(() => console.log('a'), 1000);
// 停止定时器
setTimeout(() => {
id();
}, 6000)
20. 大数相加
js中数字的最大值不能超过2^53,但是某些情况下可能不得不去进行一些操作导致数字大于这个值,请问在这种情况下该怎么将他们相加(不允许使用bigInt类型)
- padStart()方法是一种能够方便地填充字符串的方法,可以在字符串左侧添加指定数量的字符以达到规定的长度。
给的数必须是字符串类型,否则直接报错
let a = '875223123123123123123123123123';
let b = '234234235879';
function addBig(a,b) {
// 首先进行补位,给短的数前面加上0
const maxLength = Math.max(num1.length, num2.length);
a = a.padStart(maxLength,'0') // 扩充字符串达到指定长度
b = b.padStart(maxLength,'0') // 扩充字符串达到指定长度
let res = [];
let cnt = 0;
// 从后往前一个个加,进位
for (let j = maxLength - 1; j >= 0; j--) {
const ans = cnt + Number(a[j]) + Number(b[j]) // a[j]是字符串,不能直接相加
res[j] = ans % 10;
cnt = ans >= 10 ? 1 : 0;
}
// 如果最后还有进位。则加到前面去
if (cnt === 1) {
res.unshift(1);
}
console.log(res.join(""));
}
21. 手写reduce/push/map/filter
Array.prototype.myReduce = function(callback, initialValue) {
// 1. 检查回调函数是否为函数类型
if (typeof callback !== 'function') {
throw new TypeError('Callback must be a function');
}
const array = this; // 原始数组
const length = array.length;
// 2. 处理空数组且无初始值的情况
if (length === 0 && initialValue === undefined) {
throw new TypeError('Reduce of empty array with no initial value');
}
let accumulator;
let startIndex;
// 3. 初始化累加器
if (initialValue !== undefined) {
accumulator = initialValue;
startIndex = 0; // 从第0个元素开始遍历
} else {
// 查找第一个存在的元素作为初始值
let found = false;
for (let i = 0; i < length; i++) {
if (i in array) {
accumulator = array[i];
startIndex = i + 1;
found = true;
break;
}
}
if (!found) {
throw new TypeError('Reduce of empty array with no initial value');
}
}
// 4. 遍历处理每个元素
for (let i = startIndex; i < length; i++) {
// 跳过稀疏数组中的空元素
if (!(i in array)) continue;
const current = array[i];
// 5. 执行回调并更新累加器
accumulator = callback(accumulator, current, i, array);
}
return accumulator;
};
Array.prototype.mypush = function (...args) {
const arr = this;
for (let i = 0; i < args.length; i++) {
arr[arr.length] = args[i];
}
return arr.length;
}
Array.prototype.myfilter = function (fn) {
const arr = this;
const res = []
for (let i = 0; i < arr.length; i++) {
if (fn(arr[i], i, arr)) {
res.push(arr[i])
}
}
return res;
}
Array.prototype.myMap = function (fn) {
const arr = this;
const res = []
for (let i = 0; i < arr.length; i++) {
res.push(fn(arr[i], i, arr))
}
return res;
}
22. 数字千分位分割
function format(num) {
num = String(num)
let dim = num.indexOf(".") > -1 ? num.split(".")[1] : '';
let res = [];
num = dim ? num.split(".")[0].split("") : num.split("")
while (num.length >= 3) {
res.push(num.splice(-3))
}
if (num.length) {
res.push(num);
}
res.reverse()
res = res.map(item => item.join(""))
return dim ? res.join(",") + '.' + dim : res.join(",")
}
console.log(format(12345678.334)); // 包含处理小数
23. isEqual
function isEqual(obj1, obj2) {
//不是对象,直接返回比较结果
if (typeof obj1 !== 'object' || typeof obj2 !== 'object') {
return obj1 === obj2
}
//都是对象,且地址相同,返回true
if (obj2 === obj1) return true
//是对象或数组
let keys1 = Object.keys(obj1)
let keys2 = Object.keys(obj2)
//比较keys的个数,若不同,肯定不相等
if (keys1.length !== keys2.length) return false
for (let k of keys1) {
//递归比较键值对
let res = isEqual(obj1[k], obj2[k])
if (!res) return false
}
return true
}
const obj1 = {
a: 100,
b: {
x: 100,
y: 200,
},
}
const obj2 = {
a: 200,
b: {
x: 100,
y: 200,
},
}
console.log(isEqual(obj1, obj2)) //false