前端手写题(三)

161 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

三、场景应用

1. 循环打印红黄绿

实现红绿灯,,红灯3s,绿灯2s,黄灯1s,不不断闪烁。这道题主要是对比几种异步编程的方法

  1. 嵌套回调函数
  • 红灯亮,3s后绿灯
  • 绿灯亮,2s后黄灯亮
  • 黄灯亮,1s后红灯,不断重复
const main = () => {
  console.log('红灯')
  setTimeout(() => {
    console.log('绿灯');
    setTimeout(() => {
      console.log('黄灯');
      setTimeout(() => {
        main();
      },1000)
    },2000)
  },3000)
}
main();
  1. 使用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();
  1. 使用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

  1. 使用let块级作用域 let与var的区别,使用var的话打印出来4个5
for (let i = 1;i < 5;i++) {
  setTimeout(() => {
    console.log(i);
  },i * 1000)
} // 1 2 3 4
  1. 使用闭包 通过函数调用将参数传递
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就是发布订阅模式;比如用户关注公众号,当有文章发送的时候,给用户发送通知,在这公众号属于发布者,用户属于订阅者;用户将订阅公众号的事件注册到调度中心,公众号作为发布者,当有新文章发布时,公众号发布该事件到调度中心,调度中心会及时发消息告知用户。

  1. 主要实现步骤:
  • 创建一个对象
  • 在该对象上创建一个缓存列表(调度中心)
  • 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, 23337761098715972584......
  1. 普通递归 有重复计算的问题,当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
  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];
}
console.log(fibonacci2(10)) // 55
  1. 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的父级,所以这里就是循环引用了。

image.png

首先,循环引用对象本来没有什么问题,序列化的时候才会发生问题,比如调用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. 字符串出现的不重复最长长度

  1. 滑动窗口法,定义一个窗口在字符串上 向右滑动,向右滑动时判断进入窗口的字符是否有重复的,如果有重复的,窗口也向右滑动,直到没有重复字符,滑动过程中,记录窗口中没有重复字符的最大长度。
  • 双指针维护一个滑动窗口,用来剪切字串
  • 不断移动右指针,遇到重复的,将左指针移动到重复字符的下一位
  • 过程中,记录所有窗口的长度,并返回最大值
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