三、场景应用
1. 循环打印红黄绿
下面来看一道比较典型的问题,通过这个问题来对比几种异步编程方法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?
三个亮灯函数:
function red() { console.log('red'); } function green() { console.log('green'); } function yellow() { console.log('yellow'); }
这道题复杂的地方在于需要“交替重复”亮灯,而不是“亮完一次”就结束了。
(1)用 callback 实现
const task = (timer, light, callback) => { setTimeout(() => { if (light === 'red') { red() } else if (light === 'green') { green() } else if (light === 'yellow') { yellow() } callback() }, timer) } task(3000, 'red', () => { task(2000, 'green', () => { task(1000, 'yellow', Function.prototype) }) })
这里存在一个 bug:代码只是完成了一次流程,执行后红黄绿灯分别只亮一次。该如何让它交替重复进行呢?
上面提到过递归,可以递归亮灯的一个周期:
const step = () => { task(3000, 'red', () => { task(2000, 'green', () => { task(1000, 'yellow', step) }) }) } step()
注意看黄灯亮的回调里又再次调用了 step 方法 以完成循环亮灯。
(2)用 promise 实现
const task = (timer, light) => 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(2100, 'yellow')) .then(step) } step()
这里将回调移除,在一次亮灯结束后,resolve 当前 promise,并依然使用递归进行。
(3)用 async/await 实现
const taskRunner = async () => { await task(3000, 'red') await task(2000, 'green') await task(2100, 'yellow') taskRunner() } taskRunner()
2. 实现每隔一秒打印 1,2,3,4
// 使用闭包实现 for (var i = 0; i < 5; i++) { (function(i) { setTimeout(function() { console.log(i); }, i * 1000); })(i); } // 使用 let 块级作用域 for (let i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, i * 1000); }
3. 小孩报数问题
有30个小孩儿,编号从1-30,围成一圈依此报数,1、2、3 数到 3 的小孩儿退出这个圈, 然后下一个小孩 重新报数 1、2、3,问最后剩下的那个小孩儿的编号是多少?
function childNum(num, count){ let allplayer = []; for(let i = 0; i < num; i++){ allplayer[i] = i + 1; } let exitCount = 0; // 离开人数 let counter = 0; // 记录报数 let curIndex = 0; // 当前下标 while(exitCount < num - 1){ if(allplayer[curIndex] !== 0) counter++; if(counter == count){ allplayer[curIndex] = 0; counter = 0; exitCount++; } curIndex++; if(curIndex == num){ curIndex = 0 }; } for(i = 0; i < num; i++){ if(allplayer[i] !== 0){ return allplayer[i] } } } childNum(30, 3)
4. 用Promise实现图片的异步加载
let imageAsync=(url)=>{ return new Promise((resolve,reject)=>{ let img = new Image(); img.src = url; img.οnlοad=()=>{ console.log(`图片请求成功,此处进行通用操作`); resolve(image); } img.οnerrοr=(err)=>{ console.log(`失败,此处进行失败的通用操作`); reject(err); } }) } imageAsync("url").then(()=>{ console.log("加载成功"); }).catch((error)=>{ console.log("加载失败"); })
5. 实现发布-订阅模式
class EventCenter{ // 1. 定义事件容器,用来装事件数组 let handlers = {} // 2. 添加事件方法,参数:事件名 事件方法 addEventListener(type, handler) { // 创建新数组容器 if (!this.handlers[type]) { this.handlers[type] = [] } // 存入事件 this.handlers[type].push(handler) } // 3. 触发事件,参数:事件名 事件参数 dispatchEvent(type, params) { // 若没有注册该事件则抛出错误 if (!this.handlers[type]) { return new Error('该事件未注册') } // 触发事件 this.handlers[type].forEach(handler => { handler(...params) }) } // 4. 事件移除,参数:事件名 要删除事件,若无第二个参数则删除该事件的订阅和发布 removeEventListener(type, handler) { if (!this.handlers[type]) { return new Error('事件无效') } if (!handler) { // 移除事件 delete this.handlers[type] } else { const index = this.handlers[type].findIndex(el => el === handler) if (index === -1) { return new Error('无该绑定事件') } // 移除事件 this.handlers[type].splice(index, 1) if (this.handlers[type].length === 0) { delete this.handlers[type] } } } }
6. 查找文章中出现频率最高的单词
function findMostWord(article) { // 合法性判断 if (!article) return; // 参数处理 article = article.trim().toLowerCase(); let wordList = article.match(/[a-z]+/g), visited = [], maxNum = 0, maxWord = ""; article = " " + wordList.join(" ") + " "; // 遍历判断单词出现次数 wordList.forEach(function(item) { if (visited.indexOf(item) < 0) { // 加入 visited visited.push(item); let word = new RegExp(" " + item + " ", "g"), num = article.match(word).length; if (num > maxNum) { maxNum = num; maxWord = item; } } }); return maxWord + " " + maxNum; }
7. 封装异步的fetch,使用async await方式来使用
(async () => { class HttpRequestUtil { async get(url) { const res = await fetch(url); const data = await res.json(); return data; } async post(url, data) { const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await res.json(); return result; } async put(url, data) { const res = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(data) }); const result = await res.json(); return result; } async delete(url, data) { const res = await fetch(url, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(data) }); const result = await res.json(); return result; } } const httpRequestUtil = new HttpRequestUtil(); const res = await httpRequestUtil.get('http://golderbrother.cn/'); console.log(res); })();
8. 实现prototype继承
所谓的原型链继承就是让新实例的原型等于父类的实例:
//父方法 function SupperFunction(flag1){ this.flag1 = flag1; } //子方法 function SubFunction(flag2){ this.flag2 = flag2; } //父实例 var superInstance = new SupperFunction(true); //子继承父 SubFunction.prototype = superInstance; //子实例 var subInstance = new SubFunction(false); //子调用自己和父的属性 subInstance.flag1; // true subInstance.flag2; // false
9. 实现双向数据绑定
let obj = {} let input = document.getElementById('input') let span = document.getElementById('span') // 数据劫持 Object.defineProperty(obj, 'text', { configurable: true, enumerable: true, get() { console.log('获取数据了') }, set(newVal) { console.log('数据更新了') input.value = newVal span.innerHTML = newVal } }) // 输入监听 input.addEventListener('keyup', function(e) { obj.text = e.target.value })
10. 实现简单路由
// hash路由 class Route{ constructor(){ // 路由存储对象 this.routes = {} // 当前hash this.currentHash = '' // 绑定this,避免监听时this指向改变 this.freshRoute = this.freshRoute.bind(this) // 监听 window.addEventListener('load', this.freshRoute, false) window.addEventListener('hashchange', this.freshRoute, false) } // 存储 storeRoute (path, cb) { this.routes[path] = cb || function () {} } // 更新 freshRoute () { this.currentHash = location.hash.slice(1) || '/' this.routes[this.currentHash]() } }
11. 实现斐波那契数列
// 递归 function fn (n){ if(n==0) return 0 if(n==1) return 1 return fn(n-2)+fn(n-1) } // 优化 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]; } // 非递归 function fn(n) { let pre1 = 1; let pre2 = 1; let current = 2; if (n <= 2) { return current; } for (let i = 2; i < n; i++) { pre1 = pre2; pre2 = current; current = pre1 + pre2; } return current; }
12. 字符串出现的不重复最长长度
用一个滑动窗口装没有重复的字符,枚举字符记录最大值即可。用 map 维护字符的索引,遇到相同的字符,把左边界移动过去即可。挪动的过程中记录最大长度:
var lengthOfLongestSubstring = function (s) { let map = new Map(); let i = -1 let res = 0 let n = s.length for (let j = 0; j < n; j++) { if (map.has(s[j])) { i = Math.max(i, map.get(s[j])) } res = Math.max(res, j - i) map.set(s[j], j) } return res };
13. 使用 setTimeout 实现 setInterval
setInterval 的作用是每隔一段指定时间执行一个函数,但是这个执行不是真的到了时间立即执行,它真正的作用是每隔一段时间将事件加入事件队列中去,只有当当前的执行栈为空的时候,才能去从事件队列中取出事件执行。所以可能会出现这样的情况,就是当前执行栈执行的时间很长,导致事件队列里边积累多个定时器加入的事件,当执行栈结束的时候,这些事件会依次执行,因此就不能到间隔一段时间执行的效果。
针对 setInterval 的这个缺点,我们可以使用 setTimeout 递归调用来模拟 setInterval,这样我们就确保了只有一个事件结束了,我们才会触发下一个定时器事件,这样解决了 setInterval 的问题。
实现思路是使用递归函数,不断地去执行 setTimeout 从而达到 setInterval 的效果
function mySetInterval(fn, timeout) { // 控制器,控制定时器是否继续执行 var timer = { flag: true }; // 设置递归函数,模拟定时器执行。 function interval() { if (timer.flag) { fn(); setTimeout(interval, timeout); } } // 启动定时器 setTimeout(interval, timeout); // 返回控制器 return timer; }
14. 实现 jsonp
// 动态的加载js文件 function addScript(src) { const script = document.createElement('script'); script.src = src; script.type = "text/javascript"; document.body.appendChild(script); } addScript("http://xxx.xxx.com/xxx.js?callback=handleRes"); // 设置一个全局的callback函数来接收回调结果 function handleRes(res) { console.log(res); } // 接口返回的数据格式 handleRes({a: 1, b: 2});
15. 判断对象是否存在循环引用
循环引用对象本来没有什么问题,但是序列化的时候就会发生问题,比如调用JSON.stringify()
对该类对象进行序列化,就会报错: Converting circular structure to JSON.
下面方法可以用来判断一个对象中是否已存在循环引用:
const isCycleObject = (obj,parent) => { const parentArr = parent || [obj]; for(let i in obj) { if(typeof obj[i] === 'object') { let flag = false; parentArr.forEach((pObj) => { if(pObj === obj[i]){ flag = true; } }) if(flag) return true; flag = isCycleObject(obj[i],[...parentArr,obj[i]]); if(flag) return true; } } return false; } const a = 1; const b = {a}; const c = {b}; const o = {d:{a:3},c} o.c.b.aa = a; console.log(isCycleObject(o))
[j])) { i = Math.max(i, map.get(s[j])) } res = Math.max(res, j - i) map.set(s[j], j) } return res };
### 13. 使用 setTimeout 实现 setInterval setInterval 的作用是每隔一段指定时间执行一个函数,但是这个执行不是真的到了时间立即执行,它真正的作用是每隔一段时间将事件加入事件队列中去,只有当当前的执行栈为空的时候,才能去从事件队列中取出事件执行。所以可能会出现这样的情况,就是当前执行栈执行的时间很长,导致事件队列里边积累多个定时器加入的事件,当执行栈结束的时候,这些事件会依次执行,因此就不能到间隔一段时间执行的效果。 针对 setInterval 的这个缺点,我们可以使用 setTimeout 递归调用来模拟 setInterval,这样我们就确保了只有一个事件结束了,我们才会触发下一个定时器事件,这样解决了 setInterval 的问题。 实现思路是使用递归函数,不断地去执行 setTimeout 从而达到 setInterval 的效果 ```js function mySetInterval(fn, timeout) { // 控制器,控制定时器是否继续执行 var timer = { flag: true }; // 设置递归函数,模拟定时器执行。 function interval() { if (timer.flag) { fn(); setTimeout(interval, timeout); } } // 启动定时器 setTimeout(interval, timeout); // 返回控制器 return timer; }
14. 实现 jsonp
// 动态的加载js文件 function addScript(src) { const script = document.createElement('script'); script.src = src; script.type = "text/javascript"; document.body.appendChild(script); } addScript("http://xxx.xxx.com/xxx.js?callback=handleRes"); // 设置一个全局的callback函数来接收回调结果 function handleRes(res) { console.log(res); } // 接口返回的数据格式 handleRes({a: 1, b: 2});
15. 判断对象是否存在循环引用
循环引用对象本来没有什么问题,但是序列化的时候就会发生问题,比如调用JSON.stringify()
对该类对象进行序列化,就会报错: Converting circular structure to JSON.
下面方法可以用来判断一个对象中是否已存在循环引用:
const isCycleObject = (obj,parent) => { const parentArr = parent || [obj]; for(let i in obj) { if(typeof obj[i] === 'object') { let flag = false; parentArr.forEach((pObj) => { if(pObj === obj[i]){ flag = true; } }) if(flag) return true; flag = isCycleObject(obj[i],[...parentArr,obj[i]]); if(flag) return true; } } return false; } const a = 1; const b = {a}; const c = {b}; const o = {d:{a:3},c} o.c.b.aa = a; console.log(isCycleObject(o))