JS常见基础面试题二
Promise 状态,多次链式调用以后的状态 —— 读代码
作用:解决回调地狱问题,另一种异步代码的写法
回调地狱(Callback Hell)是指在异步编程中,多个嵌套的回调函数形成的代码结构复杂、难以维护和理解的情况。
1.链式使用
当promise 的状态从pending --> resolved时会执行then函数
当promise 的状态从pending --> rejected时会执行catch函数
then、catch函数里的参数代表promise里面的值
如果没有返回promise对象的话,可以无限的点then,这个then继承上一个then的状态和值,可以一直链式下去。直到遇到return 一个promise对象才会根据这个对象的状态和值,不再继承之前的
var p1 = new Promise((resolve, reject)=>{
// 异步代码
// resolve({a:1})
setTimeout(() => {
reject('abc')
},3000)
})
// 当p1的状态从pending --> resolved时会执行
p1.then((obj)=>{
// obj就是p1的promiseValue
console.log('then...', obj)
}).catch(err => { // 当p1的状态从pending --> rejected时会执行
// err就是p1的promiseValue
console.log('catch...',err)
})
// 构造函数中的 resolve 或 reject 只有第一次执行有效
var p1 = new Promise((resolve, reject) => {
resolve('success1')
reject('error')
resolve('success2') // 不执行 Promise 状态一旦改变则不能再变
})
p1.then((res) => {
console.log('then: ', res) // success1
}).catch((err) => {
console.log('catch: ', err) // error
})
console.log(p1); // then: success1
2.Promise常见面试题整理
1. promise有几种状态,什么时候会进入catch?
三个状态:pending(初始)、reslove(成功)、reject(失败)
两个过程:pending-> resloved、pending-> rejected当pending为rejectd时,会进入catch
什么时候会进入 catch 当 pending 为 rejectd 时,会进入 catch
2.Promise 构造函数是同步执行还是异步执行,那么 then 方 法呢?
Promise 构造函数是同步执行的,then 方法是异步执行
3.Promise 只有成功和失败 2 个状态,怎么让一个函数无论成 功还是失败都能被调用?
Promise.all()。用于将多个 Promise 实例,包装成一个新的 Promise 实例
4.Promise 中 reject 和 catch 处理上有什么区别
reject 是用来抛出异常,catch 是用来处理异常
reject 是 Promise 的方法,而 catch 是 Promise 实例的方法
reject 后的东西,一定会进入 then 中的第二个回调,如果 then 中没有写第二个回调,则进入 catch
网络异常(比如断网),会直接进入 catch 而不会进入 then的第二个回调
Promise async await 的执行顺序 —— 读代码
1.Promise 的优先级高于 SetTimeout 宏任务,所以 SetTimeout 回调会在最后执行。
2.Promise 一旦被定义,就会立即执行。
3.Promise 的 resolve 与 reject 是异步执行的回调。所以,resolve 会被放到回调队列中,在主函数执行完 和 宏任务 setTimeout 执行前调用
4.async 声明的函数,返回的结果是一个Promise 对象
5.await 会立即执行同行代码,阻塞下一行代码,
(await 会暂停async后面的代码,先执行async外面的同步代码)
// 案例1
console.log('start')
const promise1 = Promise.resolve().then(() => {
console.log('promise1')
const timer2 = setTimeout(() => {
console.log('timer2')
}, 0)
})
const timer1 = setTimeout(() => {
console.log('timer1')
const promise2 = Promise.resolve().then(() => {
console.log('promise2')
})
}, 0)
console.log('end')
// 执行顺序
1.先执行所有同步代码 输出:start end
2.执行所有微任务。Promise立马执行 输出:promise1
3.执行第一个宏任务。输出: timer1
4.执行所有新添加的微任务。输出:promise2
5.执行下一个宏任务。输出:timer2
// 案例2
async function async1 () {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2 () {
console.log('async2')
}
console.log('script start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
async1()
new Promise(function (resolve) {
console.log('proimse1')
resolve()
}).then(function() {
console.log('promise2')
})
console.log('script end')
// 执行顺序
1.主线程同步代码先执行 ~~输出:script start~~
2.setTimeout宏任务,放到下一个宏任务队列中
3.执行 async1()实质是创建了一个Promise对象,所以立即执行 ~~输出:async1 start~~
4.执行await async2() 首先await 会立即执行同行代码,阻塞下一行代码,
也会暂停async后面的代码,先执行async外面的同步代码
执行 async2()即返回 async2.then(() => {console.log('async1 end')})
.then()里面的内容会放到当前宏任务的微任务列表中,下次执行
~~输出:async2~~
5.执行 new Promise() 立即执行 ~~输出:promise1~~
6. resolve() 异步微任务 所以会被放到当前宏任务的微任务队列中,下次执行
7. 主线程同步代码执行 ~~输出:script end~~
8. 此时宏任务中的同步代码已经执行完成,开始依次处理微任务队列中的代码~~输出:async1 end ; promise2~~
9.在最后执行下一个宏任务队列,即setTimeout ~~输出:setTimeout~~
ES6 新的数据结构Set Map
介绍下 Set和Map的区别
应用场景:Set用于数据重组,Map用于数据储存
Set:
(1)成员不能重复
(2)只有键值没有键名,类似数组
(3)可以遍历
Map:
(1)本质上是健值对的集合,类似集合
(2)可以遍历,可以跟各种数据格式转换
Set 数据结构:相当于是一种叫集合的数据结构,集合的元素都是唯一的,是构造函数。
可以轻松地实现数组去重
1.声明方式
let s = new Set();
// 唯一的
let s = new Set([1, 2, 3, 2]);
console.log('唯一性',s) // Set(3) {1, 2, 3}
// 常规方法
s.add('kaka').add('Dorothy') // 添加新的值 支持链式操作
s.delete(2) // 删除2
s.clear() // 清空
console.log(s.has('kaka')) //true,判断是否包含某个值
console.log(s.size) // 输出Set的长度
2.遍历方法
// 遍历的4种方式
// 1.forEach()
s.forEach(item => console.log(item));
// 2.keys() 返回键名的遍历器
console.log('keys---',s.keys());
// 3.values() 返回键值的遍历器
console.log('values---',s.values());
// 4.entries() 返回键值对的遍历器
console.log('entries---',s.entries());
3.实现数组去重和Set的交集,并集,差集
// 数组去重
let set = new Set([1,2,3,3])
console.log(set) // {1,2,3}
let [...arr] = set //将set转换为数组
comsole.log(arr) // [1,2,3]
let set11Arr = new Set([1, 2, 3])
let set12Arr = new Set([2, 3, 4])
//交集
let jiao = new Set([...set11Arr].filter(x => set12Arr.has(x)));
console.log("jiao", jiao) //Set(2) {2, 3}
//并集
console.log(new Set([...set11Arr, ...set12Arr])) //Set(4) {1, 2, 3, 4}
//差集
let cha = new Set([...set11Arr].filter(x => !set12Arr.has(x)));
console.log("cha", cha) // Set(1) {1}
Map 数据结构:类似于对象,Object 结构提供了“字符串—值”的对应,但普通对象的 key 必须是字符串或是数字,而Map 结构提供了“值—值”的对应,Map 的 key 可以是任何数据类型,是一种更完善的 Hash 结构实现。Map 的用法和普通对象基本一致,如果你需要“键值对”的数据结构,Map 比 Object 更合适。可以更加全面的描述对象的属性
- 创建Map实例
const map = new Map();
const map = new Map([
[1, 'a'],
[2, 'b'],
[3, 'c']
]); //Map(3) Map(3) {1 => 'a', 2 => 'b', 3 => 'c'}
- Map实例属性和操作方法
map.size; // size属性
// 操作方法
map.set('a',1); // 设置某个值(键,值)
map.get('a'); // 读取key对应的键值
map.has('a'); // 某个键是否存在当前的Map对象之中
map.delete('a'); // 删除某个键。删除失败返回false
map.clear(); // 清楚所有成员
- 遍历的4种方式
// 1.forEach()
map.forEach(item => console.log(item));
// 2.keys() 返回键名的遍历器
console.log('keys---',map.keys());
// 3.values() 返回键值的遍历器
console.log('values---',map.values());
// 4.entries() 返回键值对的遍历器
console.log('entries---',map.entries());
- Map与数组的互相转换
// 1.Map 转为数组
[...map]; // 扩展运算符
Array.from(map); // 将一个类数组对象或者可遍历对象转成一个真正的数组
// 2.数组转为Map
let map = new Map(数组);
手写 debounce throttle EventBus
节流函数(throttle):需要频繁触发的函数,在设定规定时间内,函数只触发一次,后面的触发要等间隔时间。应用场景:鼠标点击、滚轮事件
//fn:要被节流的函数 delay:间隔时间
function throttle(fn, delay) {
let lastTime = 0; //记录上一次的函数触发时间
return function() {
let nowTime = Date.now(); //记录本次的函数触发时间
if (nowTime - lastTime > delay) {
lastTime = nowTime;
return fn.apply(this); //修正this指向
}
}
}
document.onscroll = throttle(function() {
console.log('throttle函数执行了' + Date.now());
}, 600)
防抖函数(debounce):在规定时间内,需要频繁触发的函数,每次触发会重新计时,只让函数最后一次的触发生效。
应用场景:用户在input框不断输入值时、不断的调整浏览器窗口大小时
function debounce(fn, delay) {
var timer = null;
return function() {
clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(this);
}, delay);
}
}
document.onscroll = debounce(function() {
console.log('debounce函数执行了' + Date.now());
}, 1200)
EvenBus:事件总线,通常作为多个模块间的通信机制,相当于一个事件管理中心,一个模块发送消息,其它模块接受消息,就达到了通信的作用。本质上是采用了发布-订阅的设计模式
class EventBus {
constructor() {
// 初始化事件列表
this.eventObject = {};
// 回调函数列表的id
this.callbackId = 0;
}
// 发布事件
publish(eventName, ...args) {
// 取出当前事件所有的回调函数
const callbackObject = this.eventObject[eventName];
if (!callbackObject) return console.warn(eventName + " not found!");
// 执行每一个回调函数
for (let id in callbackObject) {
// 执行时传入参数
callbackObject[id](...args);
}
}
// 订阅事件
subscribe(eventName, callback) {
// 初始化这个事件
if (!this.eventObject[eventName]) {
// 使用对象存储,注销回调函数的时候提高删除的效率
this.eventObject[eventName] = {};
}
const id = this.callbackId++;
// 存储订阅者的回调函数
// callbackId使用后需要自增,供下一个回调函数使用
this.eventObject[eventName][id] = callback;
// 每一次订阅事件,都生成唯一一个取消订阅的函数
const unSubscribe = () => {
// 清除这个订阅者的回调函数
delete this.eventObject[eventName][id];
// 如果这个事件没有订阅者了,也把整个事件对象清除
if (Object.keys(this.eventObject[eventName]).length === 0) {
delete this.eventObject[eventName];
}
};
return { unSubscribe };
}
}
// 测试
const eventBus = new EventBus();
// 订阅事件eventX
eventBus.subscribe("eventX", (obj, num) => {
console.log("模块A", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
console.log("模块B", obj, num);
});
const subscriberC = eventBus.subscribe("eventX", (obj, num) => {
console.log("模块C", obj, num);
});
// 发布事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);
// 模块C取消订阅
subscriberC.unSubscribe();
// 再次发布事件eventX,模块C不会再收到消息了
eventBus.publish("eventX", { msg: "EventX published again!" }, 2);