本文已参与「新人创作礼」活动,一起开启掘金创作之路。
三、场景应用
1. 循环打印红黄绿
实现红绿灯,,红灯3s,绿灯2s,黄灯1s,不不断闪烁。这道题主要是对比几种异步编程的方法
- 嵌套回调函数
- 红灯亮,3s后绿灯
- 绿灯亮,2s后黄灯亮
- 黄灯亮,1s后红灯,不断重复
const main = () => {
console.log('红灯')
setTimeout(() => {
console.log('绿灯');
setTimeout(() => {
console.log('黄灯');
setTimeout(() => {
main();
},1000)
},2000)
},3000)
}
main();
- 使用promise链式调用
- 使用.then来进行红绿灯的切换
- then()返回一个promise对象,resolve()使then()可以持续调用
const traffic = (color,delay) => {
return new Promise((resolve,reject) => {
console.log(`${color}灯`);
setTimeout(() => {
resolve()
},delay)
})
}
const main = () =>{
Promise.resolve()
.then(() => {
return traffic('红', 3000);
})
.then(() => {
return traffic('绿', 2000);
})
.then(() => {
return traffic('黄', 1000);
})
.then(() =>{
main();
})
}
main();
- 使用async,await
- async返回一个promise对象
- 在指定的时间进行resolve()返回
- while (true)无限循环
const traffic = async(color,delay) => {
return new Promise((resolve,reject) => {
console.log(`${color}灯`);
setTimeout(() => {
resolve();
},delay)
})
}
const main = async() => {
while (true) {
await traffic('红', 3000);
await traffic('绿', 2000);
await traffic('黄', 1000);
}
}
main();
2. 每隔一秒打印1,2,3,4
- 使用let块级作用域 let与var的区别,使用var的话打印出来4个5
for (let i = 1;i < 5;i++) {
setTimeout(() => {
console.log(i);
},i * 1000)
} // 1 2 3 4
- 使用闭包 通过函数调用将参数传递
for (var i = 1;i < 5;i++) {
(function(x) {
setTimeout(() => {
console.log(x)
},x * 1000)
})(i)
}
3. 用promise实现图片异步加载
const imgLoad = (src) => {
return new Promise((resolve,reject) => {
const img = new Image();
img.src = src;
img.onload = () => {
resolve(img);
}
img.onerror = (err) => {
console.error(`图片加载失败${src}`);
reject(err)
}
})
}
const url1 = '../img/1.jpg';
const url2 = '../img/2.jpg'; // 图片地址不对
imgLoad(url1).then(img1 => {
console.log(img1);
}).catch((error) => {
console.log('加载失败')
})
// <img src="../img/1.jpg">
4. 实现发布-订阅模式
发布订阅模式:订阅者将想订阅的事件注册到调度中心,发布者发布此事件时,也就是事件触发时,调度中心统一处理订阅者注册到调度中心的事件代码
发布-订阅模式是一对多的依赖关系,当一个对象的状态发生变化时,所依赖的所有对象都会收到状态变化的通知
例如在vue中$on,$emit就是发布订阅模式;比如用户关注公众号,当有文章发送的时候,给用户发送通知,在这公众号属于发布者,用户属于订阅者;用户将订阅公众号的事件注册到调度中心,公众号作为发布者,当有新文章发布时,公众号发布该事件到调度中心,调度中心会及时发消息告知用户。
- 主要实现步骤:
- 创建一个对象
- 在该对象上创建一个缓存列表(调度中心)
- on 方法用来把函数 fn 都加到缓存列表中(订阅者注册事件到调度中心)
- emit 方法取到 arguments 里第一个当做 event,根据 event 值去执行对应缓存列表中的函数(发布者发布事件到调度中心,调度中心处理代码)
- off 方法可以根据 event 值取消订阅(取消订阅)
- once 方法只监听一次,调用完毕后删除缓存函数(订阅一次)
let eventEmitter = {
// 缓存列表,调度中心
list: {},
// 订阅者注册事件到调度中心,将fn添加到缓存列表list中
on(event,fn) {
let self = this;
// 如果对象中没有对应的 event 值,也就是说明没有订阅过,就给 event 创建个缓存列表
// 如有对象中有相应的 event 值,把 fn 添加到对应 event 的缓存列表里
(self.list[event] || (self.list[event] = [])).push(fn);
return self;
},
// 取消订阅
off (event, fn) {
let self = this;
let fns = self.list[event];
// 如果缓存列表中没有相应的fn,返回false
if (!fns) return false;
// 如果没有传fn的话,就将event值对应缓存列表中的fn都清空
if (!fn) {
fns && (fns.length = 0);
}
else {
// 有fn,遍历缓存列表,查看传入的fn与缓存中哪个函数相同,如果相同,从缓存中删除
let cb;
for (let i = 0,cbLen = fns.length;i < cbLen;i++) {
cb = fns[i];
if (cb === fn || cb.fn === fn) {
fns.splice(i, 1);
break
}
}
}
return self;
},
// 监听一次,调用完毕后删除
once(event,fn) {
let self = this;
function on () {
self.off(event, on);
fn.apply(self, arguments);
}
on.fn = fn;
self.on(event, on);
return self;
},
// 发布,取arguments第一个做event,执行缓存列表函数
// 发布者发布事件到调度中心,调度中心处理代码
emit() {
let self = this;
// 第一个参数对应的event值,直接使用shift方法取出
let event = [].shift.call(arguments);
let fns = [...self.list[event]];
// 如果缓存列表中没有fn就返回false
if (!fns || fns.length === 0) {
return false;
}
// 缓存列表中有fn,遍历依次执行fn
fns.forEach(fn => {
fn.apply(self,arguments);
})
return self;
},
}
function user1(content) {
console.log(`用户1订阅了:${content}`);
}
function user2(content) {
console.log(`用户2订阅了:${content}`);
}
// 订阅
eventEmitter.on('article1', user1);
eventEmitter.on('article1', user2);
eventEmitter.on('article2', user2);
// 取消订阅,用户2取消订阅
eventEmitter.off('article2', user2)
// 发布
eventEmitter.emit('article1', 'javascript 发布-订阅模式')
eventEmitter.emit('article2', 'javascript 观察者模式')
/**
* 用户1订阅了:javascript 发布-订阅模式
* 用户2订阅了:javascript 发布-订阅模式
*/
5. 查找文章中出现频率最高的单词
- 单词去空格变小写,去掉标点符号变为数组
- 数组遍历,匹配文章中相同的单词,并取长度
function findMostWord(article) {
// 单词去空格变小写
article = article.trim().toLowerCase();
// 变为数组,去标点符号
let wordList = article.match(/[a-z]+/g);
let maxNum = 0;
let maxWord = "";
article = " " + wordList.join(" ") + " ";
wordList.forEach(item => {
// 匹配文章中所有相同的item,并取长度
let word = new RegExp(" " + item + " ","g");
let num = article.match(word).length;
if (num > maxNum) {
maxNum = num;
maxWord = item;
}
})
return maxWord + " " + maxNum;
}
const article = 'Life is not only about the immediate quirks, but also about poetry and selection '
console.log(findMostWord(article)) // about 2
6. 实现斐波那契数列
斐波那契数列是从第三项开始,每一项都等于前两项之和
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584......
- 普通递归 有重复计算的问题,当n为5的时候,需要计算n=3与n=4的值,运行 fibonacci(50) 会出现浏览器假死现象,递归需要堆栈,数字过大内存不够。
function fibonacci(n) {
if (n === 1 || n === 2) {
return 1;
}
return fibonacci(n - 2) + fibonacci(n -1)
}
console.log(fibonacci(10)) // 55
- 对普通递归进行优化 将计算好的值存入到数组中
function fibonacci2(n) {
const arr = [1,1,2];
const arrLen = arr.length;
if (n <= arrLen) {
return arr[n];
}
for (let i = arrLen;i < n;i++) {
arr.push(arr[i - 1] + arr[i - 2]);
}
return arr[arr.length - 1];
}
console.log(fibonacci2(10)) // 55
- for循环+解构赋值
function fibonacci3(n) {
let n1 = 1,n2 = 1;
for (let i = 2;i < n;i++) {
[n1, n2] = [n2, n1 + n2];
}
return n2
}
console.log(fibonacci3(10)) // 55
8. 使用setTimeout实现setInterval
setTimeout是在这个时间之后将需要执行的代码添加到队列中,而不是时间后执行代码
例如:setTimeout执行过程
let btn = document.getElementById("my-btn");
btn.onclick = function(){
setTimeout(function(){
document.getElementById("message").style.visibility = "visible";
}, 200);
};
有个点击事件onclick,此点击事件执行了300ms,setTimeout是200ms后添加到队列中,但是至少需要在300ms后执行setTimeout中的代码
setInterval执行过程是:
- 200ms时添加setInterval (a)事件,300ms时才执行完点击事件onclick
- 在400ms时又加入了setInterval下一个(b)事件
- 在600ms时又加入了setInterval下一个(c)事件;
- 如果在600ms还未执行完第一次加入的setInterval(a)事件,就不会将600ms的(c)事件加入到队列中;
- 但是在执行完setInterval(a)事件后就会立马执行setInterval(b)事件,中间不会停留
setInterval缺点:
- 某些时间间隔会被跳过(丢帧现象)
- 定时器之间的间隔会比预期小
使用setTimeout实现setInterval可以定时器代码加入到队列中的最小时间间隔为指定间隔
function mySetInterval(fn, delay) {
// 控制器,控制定时器是否继续执行
var timer = {
flag: true
};
// 设置递归函数,模拟定时器执行。
function interval() {
if (timer.flag) {
fn();
setTimeout(interval, delay);
}
}
// 启动定时器
setTimeout(interval, delay);
// 返回控制器
return timer;
}
let fn = () => {
console.log('执行')
}
mySetInterval(fn, 2000, 3)
let fn1 = () => {
console.log('执行1')
}
setInterval(fn1, 2000)
两个定时器比较出来,使用setTimeout为指定时间间隔
9. 实现jsonp
jsonp是前端跨域的一种方式,主要是通过script不受同源策略的影响来进行跨域
前端实现方法:
假如需要从服务器www.a.com/user?id=123 获取的数据如下:
{"id": 123, "name" : 张三, "age": 17}
使用jsonp获取的数据就是:
callbackjsonp({"id": 123, "name" : 张三, "age": 17});
这时候我们只要定义一个foo()函数,并动态地创建一个script标签,使其的src属性为www.a.com/user?id=123…
<script>
function callbackjsonp (data) {
console.log(data);
}
let script = document.createElement('script');
script.src = 'http://www.a.com/user' + '&callback=callbackjsonp'
document.body.appendChild(script)
</script>
// 可以看到返回的结果
<script>
function QQmap (data) {
console.log(data);
}
let script = document.createElement('script');
script.src = 'https://apis.map.qq.com/ws/location/v1/ip?key=CAABZ-AVSAQ-RDR5L-GTBDJ-HLA4O-A5FDB&output=jsonp&_=1599182599164' + '&callback=QQmap'
document.body.appendChild(script)
</script>
实现jsonp:
function addScript(src) {
const script = document.createElement('script');
script.src = src;
script.type = 'text/javascript';
document.body.appendChild(script);
}
addScript('http://www.a.com/user?id=123&callback=callbackjsonp')
function callbackjsonp (data) {
console.log(data);
}
// 模拟接口返回的数据
callbackjsonp({"id":123,"name":'张三',"age":17})
10. 判断对象是否存在循环引用
个人的理解是,如果一个对象的值等于父级(祖父级,曾祖父级....),则说明是循环引用了。
来看下面一个例子:a.info的值是a,而a恰好是a.info的父级,所以这里就是循环引用了。
首先,循环引用对象本来没有什么问题,序列化的时候才会发生问题,比如调用JSON.stringify()对该类对象进行序列化,就会报错: Converting circular structure to JSON.,而序列化需求很常见,比如发起一个ajax请求提交一个对象就需要对对象进行序列化。
手动判断对象是否存在循环引用:
- 取父级集合
- 对象循环,判断父级数组中是否有与对象一样的值,如果有说明循环引用
- 接着再进行递归调用,并且把对应取值链上的父级集合传递下去
function cycleObject(obj,parent) {
// 取父级集合
const parentArr = parent || [obj];
for (let i in obj) {
if (typeof obj === 'object') {
let flag = false;
parentArr.forEach(item => {
if (item === obj[i]) {
flag = true;
}
})
if (flag) return true;
flag = cycleObject(obj[i], [...parentArr,obj[i]]);
if (flag) return true;
}
}
return false;
}
const a = {
name: '循环引用'
}
a.info = a;
console.log(cycleObject(a))
11. 字符串出现的不重复最长长度
- 滑动窗口法,定义一个窗口在字符串上 向右滑动,向右滑动时判断进入窗口的字符是否有重复的,如果有重复的,窗口也向右滑动,直到没有重复字符,滑动过程中,记录窗口中没有重复字符的最大长度。
- 双指针维护一个滑动窗口,用来剪切字串
- 不断移动右指针,遇到重复的,将左指针移动到重复字符的下一位
- 过程中,记录所有窗口的长度,并返回最大值
function lengthOfLongest(str) {
const strLen = str.length;
let map = new Map();
let left = 0;
let res = 0;
for (let i = 0;i < strLen;i++) {
if (map.has(str[i]) && map.get(str[i]) >= left) {
left = map.get(str[i]) + 1;
}
res = Math.max(res, i - left + 1);
map.set(str[i], i);
}
return res;
}
const str = 'abbcdea';
console.log(lengthOfLongest(str)) // 5