经典面试题讲解(持续更新中)

572 阅读11分钟

1. 写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?

知识点:key和就地复合

  1. 更准确 因为带key就不是就地复用了,在sameNode函数 a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。
  2. 更快 利用key的唯一性生成map对象来获取对应节点,比遍历方式更快。(这个观点,就是我最初的那个观点。从这个角度看,map会比遍历更快。

2. ['1', '2', '3'].map(parseInt) what & why ?

知识点:map和parseInt

结果:[1,NaN,NaN]
  • parseInt()

  • 作用:将一个字符串 string 转换为 radix 进制的整数,radix 为介于2-36之间的数

  • 个人记忆:把二进制到36进制的数转换为十进制

  • 参数: string:要被解析的值。如果参数不是一个字符串,则将其转换为字符串(使用 ToString 抽象操作)。字符串开头的空白符将会被忽略。, radix(可选):从 2 到 36,代表该进位系统的数字。例如说指定 10 就等于指定十进位(如果不写默认为十进制;为0时,且string参数不以“0x”和“0”开头时,按照10为基数处理

  • 例子

parseInt('111',2) // 7
111是一个2进制的数字转换成10进制为7
['1', '2', '3'].map(parseInt) ===>
['1', '2', '3'].map((item, index) => parseInt(item,index)) ===>
parseInt('1', 0) => 1 parseInt 的第二个参数是0,按照10为基础处理结果是1
parseInt('2', 1) => NaN 第二个参数为1,首先不可以为1,就算为1,sting为‘2’也不对,所以NaN
parseInt('3', 2) => NaN 同上2进制最大为2

再来几题


['10','10','10','10','10'].map(parseInt)
结果:[10,NaN,2,3,4]

3、函数的防抖与节流(juejin.im/post/684490…)

  • 防抖

    1. 仅仅执行最后一次点击,如果持续点击10s,仅仅执行最后一次(节流会执行10次)
    2. 应用场景:一般可以使用在用户输入停止一段时间过后再去获取数据,而不是每次输入都去获取
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>防抖</title>
</head>
<body>
  <button id="debounce">点我防抖!</button>

  <script>
    window.onload = function() {
      // 1、获取这个按钮,并绑定事件
      var myDebounce = document.getElementById("debounce");
      myDebounce.addEventListener("click", debounce(sayDebounce));
    }

    // 2、防抖功能函数,接受传参
    function debounce(fn, delay = 200) {
      // 4、创建一个标记用来存放定时器的返回值
      let timeout = null;
      return function() {
        // 5、每次当用户点击/输入的时候,把前一个定时器清除
        clearTimeout(timeout);
        // 6、然后创建一个新的 setTimeout,
        // 这样就能保证点击按钮后的 interval 间隔内
        // 如果用户还点击了的话,就不会执行 fn 函数
        timeout = setTimeout(() => {
          fn.call(this, arguments);
        }, delay);
      };
    }

    // 3、需要进行防抖的事件处理
    function sayDebounce() {
      // ... 有些需要防抖的工作,在这里执行
      console.log("防抖成功!");
    }

  </script>
</body>
</html>

防抖:任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行。

结合上面的代码,我们可以了解到,在触发点击事件后,如果用户再次点击了,我们会清空之前的定时器,重新生成一个定时器。意思就是:这件事儿需要等待,如果你反复催促,我就重新计时!

  • 节流
    1. 时间间隔内仅仅执行第一次,持续点击10s会执行10次(防抖只执行1次)
    2. 比如懒加载时要监听计算滚动条的位置,但不必每次滑动都触发,可以降低计算的频率,而不必去浪费资源;另外还有做商品预览图的放大镜效果时,不必每次鼠标移动都计算位置。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>节流</title>
</head>
<body>

  <button id="throttle">点我节流!</button>

  <script>
    window.onload = function() {
      // 1、获取按钮,绑定点击事件
      var myThrottle = document.getElementById("throttle");
      myThrottle.addEventListener("click", throttle(sayThrottle));
    }

    // 2、节流函数体
    function throttle(fn, delay = 200) {
      // 4、通过闭包保存一个标记
      let canRun = true;
      return function() {
        // 5、在函数开头判断标志是否为 true,不为 true 则中断函数
        if(!canRun) {
          return;
        }
        // 6、将 canRun 设置为 false,防止执行之前再被执行
        canRun = false;
        // 7、定时器
        setTimeout( () => {
          fn.call(this, arguments);
          // 8、执行完事件(比如调用完接口)之后,重新将这个标志设置为 true
          canRun = true;
        }, delay);
      };
    }

    // 3、需要节流的事件
    function sayThrottle() {
      console.log("节流成功!");
    }

  </script>
</body>
</html>

4. settimeout在async 之后执行

直接拿题练手

先做等会看答案
async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
console.log('script start');
setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');
答案:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

再来一题

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    //async2做出如下更改:
    new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
    });
}
console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0)
async1();

new Promise(function(resolve) {
    console.log('promise3');
    resolve();
}).then(function() {
    console.log('promise4');
});

console.log('script end');
答案:
script start
async1 start
promise1
promise3
script end
promise2
async1 end
promise4
setTimeout

5. 下面数组打印结果是什么

for(var i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i)
  })
}
1010
如何才能打印1-9
1for(let i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i)
  })
}
2for(var i = 0; i < 10; i++) {
  (function(i) {
    setTimeout(() => {
      console.log(i)
    }, 0)
  })(i)
}
你还有多少方法?

6. 将数组扁平化并去除其中重复数据,最终得到一个升序且不重复的数组

var arr = [ [1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14] ] ] ], 10]
[...new Set(arr.flat(Infinity))].sort((a,b) => a-b)

知识点:sort和Set、Map flat直接去mdn查就好,讲的很专业

  1. sort
最开始我得出的答案是[...new Set(arr.flat(Infinity))].sort()发现答案不对
数字排序:sort((a, b) => a - b)升序    sort((a, b) => b - a)降序
字符串排序:如果sort()不写默认按照字符串升序排列所以11,会排在2前面
sort((a, b) => {
    if (a > b) return 1
    if (a < b) return -1
    return 0
}) // 升序
sort((a, b) => {
    if (a > b) return -1
    if (a < b) return 1
    return 0
}) // 降序
  1. Set、Map
  2. 持续更新中……

7. 闭包

定义:两个函数,嵌套关系,内部函数还访问了外部函数的变量,形成了一个闭包
作用:私有化数据(全局访问不到,全局不可以修改),保护数据安全,持久化维持数据
闭包与斐波那契的应用
闭包的弊端:
内存泄漏
内存管理
技术,被指像的次数,指向这块内存的数量为0的时候释放
标记清除算法,从window开始找(好,函数里面开辟的空间是找不到的),没有的话就清除,函数里面的,函数调用结束就可以清除,
ret = null;清除闭包(闭包用完不用就释放掉)

8. computed和watch的区别

computed:计算属性

计算属性是由data中的已知值,得到的一个新值。
这个新值只会根据已知值的变化而变化,其他不相关的数据的变化不会影响该新值。
计算属性不在data中,计算属性新值的相关已知值在data中。
别人变化影响我自己。
watch:监听数据的变化

监听data中数据的变化
监听的数据就是data中的已知值
我的变化影响别人

1.watch擅长处理的场景:一个数据影响多个数据

2.computed擅长处理的场景:一个数据受多个数据影响

9. reduce(感谢 www.jianshu.com/p/e375ba1cf…

计算数组中每个元素出现的次数

let names = ['Alice', 'Bob', 'Tiff', 'Bruce', 'Alice'];

let nameNum = names.reduce((pre,cur)=>{
  if(cur in pre){
    pre[cur]++
  }else{
    pre[cur] = 1 
  }
  return pre
},{})
console.log(nameNum); //{Alice: 2, Bob: 1, Tiff: 1, Bruce: 1}

数组去重

let arr = [1,2,3,4,4,1]
let newArr = arr.reduce((pre,cur)=>{
    if(!pre.includes(cur)){
      return pre.concat(cur)
    }else{
      return pre
    }
},[])
console.log(newArr);// [1, 2, 3, 4]

将二维数组转化为一维

let arr = [[0, 1], [2, 3], [4, 5]]
let newArr = arr.reduce((pre,cur)=>{
    return pre.concat(cur)
},[])
console.log(newArr); // [0, 1, 2, 3, 4, 5]

将多维数组转化为一维

let arr = [[0, 1], [2, 3], [4,[5,6,7]]]
const newArr = function(arr){
   return arr.reduce((pre,cur)=>pre.concat(Array.isArray(cur)?newArr(cur):cur),[])
}
console.log(newArr(arr)); //[0, 1, 2, 3, 4, 5, 6, 7]

对象里的属性求和

var result = [
    {
        subject: 'math',
        score: 10
    },
    {
        subject: 'chinese',
        score: 20
    },
    {
        subject: 'english',
        score: 30
    }
];

var sum = result.reduce(function(prev, cur) {
    return cur.score + prev;
}, 0);
console.log(sum) //60

10、e.target与e.currentTarget的区别

  • target指向被单击的对象
  • currentTarget指向当前事件活动的对象 先看个例子
<body>
    <div id="a">
        <div id="b">
            <div id="c">
                <div id="d"></div>
            </div>
        </div>
    </div>
    
    <script>
        document.getElementById('a').addEventListener('click', function ( e ) {
            console.log('target:' + e.target.id + '&currentTarget:' + e.currentTarget.id);
        });
        document.getElementById('b').addEventListener('click', function ( e ) {
            console.log('target:' + e.target.id + '&currentTarget:' + e.currentTarget.id);
        });
        document.getElementById('c').addEventListener('click', function ( e ) {
            console.log('target:' + e.target.id + '&currentTarget:' + e.currentTarget.id);
        });
        document.getElementById('d').addEventListener('click', function ( e ) {
            console.log('target:' + e.target.id + '&currentTarget:' + e.currentTarget.id);
        });
    </script>
</body>

不必记什么时候e.currentTarget和e.target相等,什么时候不等,理解两者的究竟指向的是谁即可。

  • e.target 指向触发事件监听的对象。
  • e.currentTarget 指向添加监听事件的对象 给上面代码a注册事件,currentTarget始终指向了a,点击d的时候,target指向了d,current仍然指向a

参考链接1 参考链接2

11. let arr = ['aecb', 'dcbc', 'fcbg']返回最长的重复字符串cb

let arr = ['aecb', 'dcbc', 'fcbg']
function findStr(arr) {
  let str = ''
  for(let i = 0; i < arr[0].length; i++) {
    str += arr[0][i]
    let isTrue = arr.every(v => v.indexOf(str) > 0)
    str = isTrue ? str : str.slice(0, str.length - 2)
  }
  return str
}
findStr(arr)

12 数字字符串补零

addZero: function (num) {
    // return num + parseInt(num) < 10 ? '0' : ''
    if (parseInt(num) < 10) {
      num = '0' + num;
    }
    return num;
  },

13. 用最精炼的代码实现数组非零非负最小值index

例如:[10,21,0,-7,35,7,9,23,18] 输出5, 7最小

(() => {
    function getIndex(arr) {
      let index;
      let n = Infinity;
      for (let i = 0; i < arr.length; i++) {
        const item = arr[i];
        if (item > 0 && item < n) {
          n = item;
          index = i;
        }
      }

      return index;
    }
    let arr = [-1, 21, 0, -7, 35, 7, 9, 23, 18];
    console.log(getIndex(arr));
})();

14. 求目标值是由数组元素的哪两个值之和的值、下标(算法)

const arr = [3, 4, 6, 39, 6, 42, 76]
const tag = 81
// 求值:
// 思路:
// 1、遍历数组arr
// 2、给arr的每一项都做一个标注为 true
// 3、如果 target - 数组的这一项 以前存在过也就是为true
// 那就得出这两个值
普通方法:
function getTotal(arr, tag) {
  return arr.filter(item => arr.includes(tag - item))
}

优化方法:
function getTotal(array, target) {
  var a = {}
  for (var i = 0; i < array.length; i++) {
    var temp = target - array[i]
    if (a[temp] != undefined) return [temp, array[i]]
    a[array[i]] = true
  }
}

// 求下标
// 求目标值是由数组元素的哪两个值之和的下标(算法)
普通方法:
function getIndex(arr, tag) {
  return arr.reduce((acc, cur, index) => {
    if (arr.includes(tag - cur)) {
      acc.push(index)
    }
    return acc
  }, [])
}
优化方法:
// 只要把原来优化方法里的的true改为i即可
function getTotal(array, target) {
  var a = {}
  for (var i = 0; i < array.length; i++) {
    var temp = target - array[i]
    if (a[temp] != undefined) return [a[temp], i]
    a[array[i]] = i
  }
}

15. 手写一个Promise.all

let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('我是p1')
  }, 2000)
})

let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('我是p2')
  }, 1000)
})
Promise.myAll = function (promiseArr) {
  return new Promise((resolve, reject) => {
    const ans = []
    let index = 0
    for (let i = 0; i < promiseArr.length; i++) {
      promiseArr[i].then((res) => {
        ans[i] = res
        index++
        if (index === promiseArr.length) {
          resolve(ans)
        }
      }).catch((err) => reject(err))
    }
  })
}
Promise.myAll([p1, p2]).then((res) => {
  console.log(res)
})

16. 手写一个Promise.race

Promise.myRace = function (promiseArr) {
  return new Promise((resolve, reject) => {
    promiseArr.forEach((p) => {
      // 如果不是Promise实例需要转化为Promise实例
      Promise.resolve(p).then(
        (val) => resolve(val),
        (err) => reject(err)
      )
    })
  })
}
Promise.myRace([p1, p2]).then((res) => {
  console.log(res)
})

17. 实现一个深拷贝

我们最常想到的是JSON.parse(JSON.stringify(obj))但是我们要知道这种方法的弊端

  1. 如果obj里有时间对象,拷贝后的结果,时间将只是字符串的形式。而不是时间对象。
  2. 如果obj里的属性值为undefined或函数,拷贝后的结果将会丢失属性
  3. 如果obj里的属性值有RegExp、Error对象,则序列化的结果将只得到空对象
  4. 如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null
  5. JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor
  6. 如果对象中存在循环引用的情况也无法正确实现深拷贝

那么我们手写一个深拷贝(考虑简单类型,对象,数组)

function clone(target) { 
    if (typeof target !== 'object' || target === null) return target // 考虑简单类型和null
    let cloneTarget = Array.isArray(target) ? [] : {} // 考虑数组还是对象
    for (const key in target) { 
        cloneTarget[key] = clone(target[key]) // 考虑嵌套
    } 
    return cloneTarget
}

这个已经基本可以满足我们最基本的要求了,如果感觉还不够,完整版提供给大家

const cloneDeep1 = (target, hash = new WeakMap()) => {
  // 对于传入参数处理
  if (typeof target !== 'object' || target === null) {
    return target
  }
  // 哈希表中存在直接返回
  if (hash.has(target)) return hash.get(target)

  const cloneTarget = Array.isArray(target) ? [] : {}
  hash.set(target, cloneTarget)

  // 针对Symbol属性
  const symKeys = Object.getOwnPropertySymbols(target)
  if (symKeys.length) {
    symKeys.forEach((symKey) => {
      if (typeof target[symKey] === 'object' && target[symKey] !== null) {
        cloneTarget[symKey] = cloneDeep1(target[symKey])
      } else {
        cloneTarget[symKey] = target[symKey]
      }
    })
  }

  for (const i in target) {
    if (Object.prototype.hasOwnProperty.call(target, i)) {
      cloneTarget[i] =
        typeof target[i] === 'object' && target[i] !== null
          ? cloneDeep1(target[i], hash)
          : target[i]
    }
  }
  return cloneTarget
}

18.数据扁平化

将数据扁平化,例如下面的这个对象

let nestedObject = {
  name: '文正',
  class: {
    name: '一班',
      num: 99,
      person: {
        name: '张三',
        gender: '男'
      }
  },
  desc: '水城路'
}

转换成

{
  "name": "文正",
  "class.name": "一班",
  "class.num": 99,
  "class.person.name": "张三",
  "class.person.gender": "男",
  "desc": "水城路"
}
function flattenobject(obj, prefix = '', result = {}) {
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      let newKey = prefix ? `${prefix}.${key}` : key
      if (typeof obj[key] === 'object' && obj[key] !== null && !Array.isArray(obj[key])) {
        flattenobject(obj[key], newKey, result)
      } else {
        result[newKey] = obj[key]
      }
    }
  }
  return result
}

19 pc端滚动加载

<template>
  <el-scrollbar>
    <div class="list">
      <div v-for="item in list" :key="item.id">
        <!-- 列表内容 -->
      </div>
      <!-- 底部观察元素 -->
      <div class="text-center gray-color" ref="bottomRef">{{emergencyList.length >= total ? '加载完成' : '加载更多……'}}</div>
    </div>
  </el-scrollbar>
</template>

<script setup>
import { ref, onMounted } from 'vue'

const bottomRef = ref(null)

onMounted(() => {
    getEmergency()

    const observer = new IntersectionObserver((entries) => {
    // 当底部元素可见时触发加载
        if(entries[0].isIntersecting) {
            console.log('触底了')
            if (emergencyList.value.length >= total.value) return
            console.log('加载数据')
            current.value++
            getEmergency() // 请求数据
        }
    })
    if (bottomRef.value) {
        observer.observe(bottomRef.value)
    }
})
const getEmergency = async (clear = false) => {
    loading.value = true
    const { total: sum, records } = await getEmergencyPage({ current: current.value, size: size.value })
    if (clear) {
        current.value = 1
        emergencyList.value = records
    } else {
        emergencyList.value = [...emergencyList.value, ...records]
    }
    total.value = sum
    loading.value = false
}
</script>
  1. 找出字符串中出现次数最多的字母和出现次数
const str = 'abcdddabcdefsaadfdsfa'
let wordObj = {}
function findMaxChar(str) {
    let wordObj = {}
    for(let i = 0; i < str.length; i++) {
        wordObj[str[i]] = (wordObj[str[i]] || 0) + 1
    }
    let maxChar = ''
    let maxCount = 0
    for (let char in wordObj) {
        if (wordObj[char] > maxCount) {
            maxCount = wordObj[char]
            maxChar = char
        }
    }
    return { maxChar, maxCount }
}

const result = findMaxChar('abcdddabcdefsaadfdsfa')
console.log(`出现次数最多的单词是 '${result.maxChar}',出现次数是 ${result.maxCount}`)