1. 循环打印红黄绿

下面来看一道比较典型的问题,通过这个问题来对比几种异步编程方法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯?

三个亮灯函数:

  1. function red() {
  2. console.log('red');
  3. }
  4. function green() {
  5. console.log('green');
  6. }
  7. function yellow() {
  8. console.log('yellow');
  9. }

这道题复杂的地方在于需要“交替重复”亮灯,而不是“亮完一次”就结束了。

(1)用 callback 实现

  1. const task = (timer, light, callback) => {
  2. setTimeout(() => {
  3. if (light === 'red') {
  4. red()
  5. }
  6. else if (light === 'green') {
  7. green()
  8. }
  9. else if (light === 'yellow') {
  10. yellow()
  11. }
  12. callback()
  13. }, timer)
  14. }
  15. task(3000, 'red', () => {
  16. task(2000, 'green', () => {
  17. task(1000, 'yellow', Function.prototype)
  18. })
  19. })

这里存在一个 bug:代码只是完成了一次流程,执行后红黄绿灯分别只亮一次。该如何让它交替重复进行呢?

上面提到过递归,可以递归亮灯的一个周期:

  1. const step = () => {
  2. task(3000, 'red', () => {
  3. task(2000, 'green', () => {
  4. task(1000, 'yellow', step)
  5. })
  6. })
  7. }
  8. step()

注意看黄灯亮的回调里又再次调用了 step 方法 以完成循环亮灯。

(2)用 promise 实现

  1. const task = (timer, light) =>
  2. new Promise((resolve, reject) => {
  3. setTimeout(() => {
  4. if (light === 'red') {
  5. red()
  6. }
  7. else if (light === 'green') {
  8. green()
  9. }
  10. else if (light === 'yellow') {
  11. yellow()
  12. }
  13. resolve()
  14. }, timer)
  15. })
  16. const step = () => {
  17. task(3000, 'red')
  18. .then(() => task(2000, 'green'))
  19. .then(() => task(2100, 'yellow'))
  20. .then(step)
  21. }
  22. step()

这里将回调移除,在一次亮灯结束后,resolve 当前 promise,并依然使用递归进行。

(3)用 async/await 实现

  1. const taskRunner = async () => {
  2. await task(3000, 'red')
  3. await task(2000, 'green')
  4. await task(2100, 'yellow')
  5. taskRunner()
  6. }
  7. taskRunner()

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

  1. // 使用闭包实现
  2. for (var i = 0; i < 5; i++) {
  3. (function(i) {
  4. setTimeout(function() {
  5. console.log(i);
  6. }, i * 1000);
  7. })(i);
  8. }
  9. // 使用 let 块级作用域
  10. for (let i = 0; i < 5; i++) {
  11. setTimeout(function() {
  12. console.log(i);
  13. }, i * 1000);
  14. }

3. 小孩报数问题

有30个小孩儿,编号从1-30,围成一圈依此报数,1、2、3 数到 3 的小孩儿退出这个圈, 然后下一个小孩 重新报数 1、2、3,问最后剩下的那个小孩儿的编号是多少?

  1. function childNum(num, count){
  2. let allplayer = [];
  3. for(let i = 0; i < num; i++){
  4. allplayer[i] = i + 1;
  5. }
  6. let exitCount = 0; // 离开人数
  7. let counter = 0; // 记录报数
  8. let curIndex = 0; // 当前下标
  9. while(exitCount < num - 1){
  10. if(allplayer[curIndex] !== 0) counter++;
  11. if(counter == count){
  12. allplayer[curIndex] = 0;
  13. counter = 0;
  14. exitCount++;
  15. }
  16. curIndex++;
  17. if(curIndex == num){
  18. curIndex = 0
  19. };
  20. }
  21. for(i = 0; i < num; i++){
  22. if(allplayer[i] !== 0){
  23. return allplayer[i]
  24. }
  25. }
  26. }
  27. childNum(30, 3)

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

  1. let imageAsync=(url)=>{
  2. return new Promise((resolve,reject)=>{
  3. let img = new Image();
  4. img.src = url;
  5. img.οnlοad=()=>{
  6. console.log(`图片请求成功,此处进行通用操作`);
  7. resolve(image);
  8. }
  9. img.οnerrοr=(err)=>{
  10. console.log(`失败,此处进行失败的通用操作`);
  11. reject(err);
  12. }
  13. })
  14. }
  15. imageAsync("url").then(()=>{
  16. console.log("加载成功");
  17. }).catch((error)=>{
  18. console.log("加载失败");
  19. })

5. 实现发布-订阅模式

  1. class EventCenter{
  2. // 1. 定义事件容器,用来装事件数组
  3. let handlers = {}
  4. // 2. 添加事件方法,参数:事件名 事件方法
  5. addEventListener(type, handler) {
  6. // 创建新数组容器
  7. if (!this.handlers[type]) {
  8. this.handlers[type] = []
  9. }
  10. // 存入事件
  11. this.handlers[type].push(handler)
  12. }
  13. // 3. 触发事件,参数:事件名 事件参数
  14. dispatchEvent(type, params) {
  15. // 若没有注册该事件则抛出错误
  16. if (!this.handlers[type]) {
  17. return new Error('该事件未注册')
  18. }
  19. // 触发事件
  20. this.handlers[type].forEach(handler => {
  21. handler(...params)
  22. })
  23. }
  24. // 4. 事件移除,参数:事件名 要删除事件,若无第二个参数则删除该事件的订阅和发布
  25. removeEventListener(type, handler) {
  26. if (!this.handlers[type]) {
  27. return new Error('事件无效')
  28. }
  29. if (!handler) {
  30. // 移除事件
  31. delete this.handlers[type]
  32. } else {
  33. const index = this.handlers[type].findIndex(el => el === handler)
  34. if (index === -1) {
  35. return new Error('无该绑定事件')
  36. }
  37. // 移除事件
  38. this.handlers[type].splice(index, 1)
  39. if (this.handlers[type].length === 0) {
  40. delete this.handlers[type]
  41. }
  42. }
  43. }
  44. }

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

  1. function findMostWord(article) {
  2. // 合法性判断
  3. if (!article) return;
  4. // 参数处理
  5. article = article.trim().toLowerCase();
  6. let wordList = article.match(/[a-z]+/g),
  7. visited = [],
  8. maxNum = 0,
  9. maxWord = "";
  10. article = " " + wordList.join(" ") + " ";
  11. // 遍历判断单词出现次数
  12. wordList.forEach(function(item) {
  13. if (visited.indexOf(item) < 0) {
  14. // 加入 visited
  15. visited.push(item);
  16. let word = new RegExp(" " + item + " ", "g"),
  17. num = article.match(word).length;
  18. if (num > maxNum) {
  19. maxNum = num;
  20. maxWord = item;
  21. }
  22. }
  23. });
  24. return maxWord + " " + maxNum;
  25. }

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

  1. (async () => {
  2. class HttpRequestUtil {
  3. async get(url) {
  4. const res = await fetch(url);
  5. const data = await res.json();
  6. return data;
  7. }
  8. async post(url, data) {
  9. const res = await fetch(url, {
  10. method: 'POST',
  11. headers: {
  12. 'Content-Type': 'application/json'
  13. },
  14. body: JSON.stringify(data)
  15. });
  16. const result = await res.json();
  17. return result;
  18. }
  19. async put(url, data) {
  20. const res = await fetch(url, {
  21. method: 'PUT',
  22. headers: {
  23. 'Content-Type': 'application/json'
  24. },
  25. data: JSON.stringify(data)
  26. });
  27. const result = await res.json();
  28. return result;
  29. }
  30. async delete(url, data) {
  31. const res = await fetch(url, {
  32. method: 'DELETE',
  33. headers: {
  34. 'Content-Type': 'application/json'
  35. },
  36. data: JSON.stringify(data)
  37. });
  38. const result = await res.json();
  39. return result;
  40. }
  41. }
  42. const httpRequestUtil = new HttpRequestUtil();
  43. const res = await httpRequestUtil.get('http://golderbrother.cn/');
  44. console.log(res);
  45. })();

8. 实现prototype继承

所谓的原型链继承就是让新实例的原型等于父类的实例:

  1. //父方法
  2. function SupperFunction(flag1){
  3. this.flag1 = flag1;
  4. }
  5. //子方法
  6. function SubFunction(flag2){
  7. this.flag2 = flag2;
  8. }
  9. //父实例
  10. var superInstance = new SupperFunction(true);
  11. //子继承父
  12. SubFunction.prototype = superInstance;
  13. //子实例
  14. var subInstance = new SubFunction(false);
  15. //子调用自己和父的属性
  16. subInstance.flag1; // true
  17. subInstance.flag2; // false

9. 实现双向数据绑定

  1. let obj = {}
  2. let input = document.getElementById('input')
  3. let span = document.getElementById('span')
  4. // 数据劫持
  5. Object.defineProperty(obj, 'text', {
  6. configurable: true,
  7. enumerable: true,
  8. get() {
  9. console.log('获取数据了')
  10. },
  11. set(newVal) {
  12. console.log('数据更新了')
  13. input.value = newVal
  14. span.innerHTML = newVal
  15. }
  16. })
  17. // 输入监听
  18. input.addEventListener('keyup', function(e) {
  19. obj.text = e.target.value
  20. })

10. 实现简单路由

  1. // hash路由
  2. class Route{
  3. constructor(){
  4. // 路由存储对象
  5. this.routes = {}
  6. // 当前hash
  7. this.currentHash = ''
  8. // 绑定this,避免监听时this指向改变
  9. this.freshRoute = this.freshRoute.bind(this)
  10. // 监听
  11. window.addEventListener('load', this.freshRoute, false)
  12. window.addEventListener('hashchange', this.freshRoute, false)
  13. }
  14. // 存储
  15. storeRoute (path, cb) {
  16. this.routes[path] = cb || function () {}
  17. }
  18. // 更新
  19. freshRoute () {
  20. this.currentHash = location.hash.slice(1) || '/'
  21. this.routes[this.currentHash]()
  22. }
  23. }

11. 实现斐波那契数列

  1. // 递归
  2. function fn (n){
  3. if(n==0) return 0
  4. if(n==1) return 1
  5. return fn(n-2)+fn(n-1)
  6. }
  7. // 优化
  8. function fibonacci2(n) {
  9. const arr = [1, 1, 2];
  10. const arrLen = arr.length;
  11. if (n <= arrLen) {
  12. return arr[n];
  13. }
  14. for (let i = arrLen; i < n; i++) {
  15. arr.push(arr[i - 1] + arr[ i - 2]);
  16. }
  17. return arr[arr.length - 1];
  18. }
  19. // 非递归
  20. function fn(n) {
  21. let pre1 = 1;
  22. let pre2 = 1;
  23. let current = 2;
  24. if (n <= 2) {
  25. return current;
  26. }
  27. for (let i = 2; i < n; i++) {
  28. pre1 = pre2;
  29. pre2 = current;
  30. current = pre1 + pre2;
  31. }
  32. return current;
  33. }

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

用一个滑动窗口装没有重复的字符,枚举字符记录最大值即可。用 map 维护字符的索引,遇到相同的字符,把左边界移动过去即可。挪动的过程中记录最大长度:

  1. var lengthOfLongestSubstring = function (s) {
  2. let map = new Map();
  3. let i = -1
  4. let res = 0
  5. let n = s.length
  6. for (let j = 0; j < n; j++) {
  7. if (map.has(s[j])) {
  8. i = Math.max(i, map.get(s[j]))
  9. }
  10. res = Math.max(res, j - i)
  11. map.set(s[j], j)
  12. }
  13. return res
  14. };

13. 使用 setTimeout 实现 setInterval

setInterval 的作用是每隔一段指定时间执行一个函数,但是这个执行不是真的到了时间立即执行,它真正的作用是每隔一段时间将事件加入事件队列中去,只有当当前的执行栈为空的时候,才能去从事件队列中取出事件执行。所以可能会出现这样的情况,就是当前执行栈执行的时间很长,导致事件队列里边积累多个定时器加入的事件,当执行栈结束的时候,这些事件会依次执行,因此就不能到间隔一段时间执行的效果。

针对 setInterval 的这个缺点,我们可以使用 setTimeout 递归调用来模拟 setInterval,这样我们就确保了只有一个事件结束了,我们才会触发下一个定时器事件,这样解决了 setInterval 的问题。

实现思路是使用递归函数,不断地去执行 setTimeout 从而达到 setInterval 的效果

  1. function mySetInterval(fn, timeout) {
  2. // 控制器,控制定时器是否继续执行
  3. var timer = {
  4. flag: true
  5. };
  6. // 设置递归函数,模拟定时器执行。
  7. function interval() {
  8. if (timer.flag) {
  9. fn();
  10. setTimeout(interval, timeout);
  11. }
  12. }
  13. // 启动定时器
  14. setTimeout(interval, timeout);
  15. // 返回控制器
  16. return timer;
  17. }

14. 实现 jsonp

  1. // 动态的加载js文件
  2. function addScript(src) {
  3. const script = document.createElement('script');
  4. script.src = src;
  5. script.type = "text/javascript";
  6. document.body.appendChild(script);
  7. }
  8. addScript("http://xxx.xxx.com/xxx.js?callback=handleRes");
  9. // 设置一个全局的callback函数来接收回调结果
  10. function handleRes(res) {
  11. console.log(res);
  12. }
  13. // 接口返回的数据格式
  14. handleRes({a: 1, b: 2});

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

循环引用对象本来没有什么问题,但是序列化的时候就会发生问题,比如调用JSON.stringify()对该类对象进行序列化,就会报错: Converting circular structure to JSON.

下面方法可以用来判断一个对象中是否已存在循环引用:

  1. const isCycleObject = (obj,parent) => {
  2. const parentArr = parent || [obj];
  3. for(let i in obj) {
  4. if(typeof obj[i] === 'object') {
  5. let flag = false;
  6. parentArr.forEach((pObj) => {
  7. if(pObj === obj[i]){
  8. flag = true;
  9. }
  10. })
  11. if(flag) return true;
  12. flag = isCycleObject(obj[i],[...parentArr,obj[i]]);
  13. if(flag) return true;
  14. }
  15. }
  16. return false;
  17. }
  18. const a = 1;
  19. const b = {a};
  20. const c = {b};
  21. const o = {d:{a:3},c}
  22. o.c.b.aa = a;
  23. console.log(isCycleObject(o))