​个人主页:前端青山
系列专栏:JavaScript篇
人终将被年少不可得之物困其一生

依旧青山,本期给大家带来JavaScript篇专栏内容:JavaScript-场景应用

目录

三、场景应用

1. 循环打印红黄绿

(1)用 callback 实现

(2)用 promise 实现

(3)用 async/await 实现

2. 实现每隔一秒打印 1,2,3,4

3. 小孩报数问题

4. 用Promise实现图片的异步加载

5. 实现发布-订阅模式

6. 查找文章中出现频率最高的单词

7. 封装异步的fetch,使用async await方式来使用

8. 实现prototype继承

9. 实现双向数据绑定

10. 实现简单路由

11. 实现斐波那契数列

12. 字符串出现的不重复最长长度

13. 使用 setTimeout 实现 setInterval

15. 判断对象是否存在循环引用

场景应用

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)  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;// truesubInstance.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 = newValspan.innerHTML = newVal}})// 输入监听input.addEventListener('keyup', function(e) { obj.text = e.target.value})复制代码

10. 实现简单路由

// hash路由class Route{ constructor(){// 路由存储对象this.routes = {}// 当前hashthis.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 0if(n==1) return 1return 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 = -1let res = 0let n = s.lengthfor (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;}

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)
查找有序二维数组的目标值:
var findNumberIn2DArray = function(matrix, target) {if (matrix == null || matrix.length == 0) {return false; }let row = 0;let column = matrix[0].length - 1;while (row = 0) {if (matrix[row][column] == target) {return true; } else if (matrix[row][column] > target) {column--; } else {row++; } }return false;};

二维数组斜向打印:

function printMatrix(arr){ let m = arr.length, n = arr[0].lengthlet res = []// 左上角,从0 到 n - 1 列进行打印 for (let k = 0; k < n; k++) {for (let i = 0, j = k; i = 0; i++, j--) { res.push(arr[i][j]); }}​ // 右下角,从1 到 n - 1 行进行打印 for (let k = 1; k < m; k++) {for (let i = k, j = n - 1; i = 0; i++, j--) { res.push(arr[i][j]); }} return res}