《前端100问》10

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

key 是给每一个 VNode 的唯一 id,可以依靠 key,在 diff 算法 执行时更快的找到对应的节点。

更准确、更快的拿到 Old VNode 中对应的 VNode 节点。

在数据变化时强制更新组件,以避免「原地复用」带来的副作用,使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

2、[‘1’, ‘2’, ‘3’].map(parseInt) what & why ?

parseInt

parseInt() 函数解析一个字符串参数,并返回一个指定基数的整数 (数学系统的基础)。

const intValue = parseInt(string[, radix]);

string 要被解析的值。如果参数不是一个字符串,则将其转换为字符串(使用 toString 抽象操作)。字符串开头的空白符将会被忽略。

radix 一个介于 2 和 36 之间的整数(数学系统的基础),表示上述字符串的基数。默认为10
返回值 返回一个整数或 NaN

parseInt(100); // 100
parseInt(100, 10); // 100
parseInt(100, 2); // 4 -> converts 100 in base 2 to base 10

注意:
radixundefined,或者 radix0 或者没有指定的情况下,JavaScript 作如下处理:

  • 如果字符串 string 以 0x 或者 0X 开头, 则基数是16 (16进制).
  • 如果字符串 string 以 0 开头, 基数是8(八进制)或者10(十进制),那么具体是哪个基数由实现环境决定。ECMAScript 5 规定使用10,但是并不是所有的浏览器都遵循这个规定。因此,永远都要明确给出 radix 参数的值。
  • 如果字符串 string 以其它任何值开头,则基数是10 (十进制)。

map

map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。

var new_array = arr.map(function callback(currentValue[,index[, array]]) {
 // Return element for new_array
 }[, thisArg])

可以看到 callback 回调函数需要三个参数, 我们通常只使用第一个参数 (其他两个参数是可选的)。

  • currentValue 是 callback 数组中正在处理的当前元素。
  • index 可选, 是 callback 数组中正在处理的当前元素的索引。
  • array 可选, 是 callback map 方法被调用的数组。
  • 另外还有 thisArg 可选, 执行 callback 函数时使用的 this 值。
const arr = [1, 2, 3];
arr.map((num) => num + 1); // [2, 3, 4]

所以对于题目:

['1', '2', '3'].map(parseInt)

对于每个迭代map, parseInt()传递两个参数: 字符串和基数

所以实际执行的的代码是:

['1', '2', '3'].map((item, index) => {
	return parseInt(item, index)
})

所以结果为:

parseInt('1', 0) // 1
parseInt('2', 1) // NaN
parseInt('3', 2) // NaN, 3 不是二进制
['1', '2', '3'].map(parseInt)
// 1, NaN, NaN

3、什么是防抖和节流?有什么区别?如何实现?

防抖 (debounce):防抖,顾名思义,防止抖动,以免把一次事件误认为多次,敲键盘就是一个每天都会接触到的防抖操作。

防抖适用场景:

  1. 登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖。
  2. 调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖。
  3. 文本编辑器实时保存,当无任何更改操作一秒后进行保存。

可以看出来防抖重在清零 clearTimeout(timer)

节流(throttle):节流,顾名思义,控制水的流量。控制事件发生的频率,如控制为1s发生一次,甚至1分钟发生一次。与服务端(server)及网关(gateway)控制的限流(Rate Limit) 类似。

节流适用场景:

  1. scroll 事件,每隔一秒计算一次位置信息等。
  2. 浏览器播放事件,每个一秒计算一次进度信息等。
  3. input 框实时搜索并发送请求展示下拉列表,每隔一秒发送一次请求 (也可做防抖)。

可以看出来节流重在加锁 timer=timeout

代码实现:

/**防抖
 * @param {Function} fn
 * @param {Number} wait
 * @return {Function}
 */
function debounce(fn, wait) {
    let timer;
    // 箭头函数始终指向上一层 this
    return (...args) => {
        // 使用闭包 timer持久化
        clearTimeout(timer);
        timer = setTimeout(() => {
            fn(...args);
        }, wait);
    };
}
/**节流
 * @param {Function} fn
 * @param {Number} wait
 * @return {Function} 
 */
function throttle(fn, wait) {
    let timer;
    return (...args) => {
        if (timer) return;
        timer = setTimeout(() => {
            fn(...args);
            timer = null;
        },wait);
    };
}

4、介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

Set 和 Map 主要的应用场景在于 数据重组数据储存

Set 是一种叫做集合的数据结构,Map 是一种叫做字典的数据结构

集合(Set)

ES6 新增的一种新的数据结构,类似于数组,但成员是唯一且无序的,没有重复的值。

Set 本身是一种构造函数,用来生成 Set 数据结构。

Set 对象允许你储存任何类型的唯一值,无论是原始值或者是对象引用。

WeakSet

WeakSet 对象允许你将弱引用对象储存在一个集合中

WeakSet 与 Set 的区别:

  • WeakSet 只能储存对象引用,不能存放值,而 Set 对象都可以。
  • WeakSet 对象中储存的对象值都是被弱引用的,即垃圾回收机制不考虑 WeakSet 对该对象的应用,如果没有其他的变量或属性引用这个对象值,则这个对象将会被垃圾回收掉(不考虑该对象还存在于 WeakSet 中),所以,WeakSet 对象里有多少个成员元素,取决于垃圾回收机制有没有运行,运行前后成员个数可能不一致,遍历结束之后,有的成员可能取不到了(被垃圾回收了),WeakSet 对象是无法被遍历的(ES6 规定 WeakSet 不可遍历),也没有办法拿到它包含的所有元素。

字典(Map)

集合 与 字典 的区别:

  • 共同点:集合、字典 可以储存不重复的值
  • 不同点:集合 是以 [value, value] 的形式储存元素,字典 是以 [ key, value ] 的形式储存

WeakMap

WeakMap 对象是一组键值对的集合,其中的键是弱引用对象,而值可以是任意

注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

WeakMap 中,每个键对自己所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象,这个对象将会被垃圾回收(相应的 key 则变成无效的),所以,WeakMap 的 key 是不可枚举的。

5、介绍下深度优先遍历和广度优先遍历,如何实现?

深度优先遍历:从根节点出发,沿着左子树方向进行纵向遍历,直到找到叶子节点为止。然后回溯到前一个节点,进行右子树节点的遍历,直到遍历完所有可达节点为止。

广度优先遍历:从根节点出发,在横向遍历二叉树层段节点的基础上纵向遍历二叉树的层次。

以遍历 Dom 树为例子:

<div class="one">
  <div class="one-one">
    <div class="one-one-one"></div>
    <div class="one-one-two"></div>
    <div class="one-one-three"></div>
  </div>
  <div class="one-two">
    <div class="one-two-one"></div>
    <div class="one-two-two"></div>
  </div>
  <div class="one-three"></div>
  <div class="onr-four"></div>
</div>

深度优先遍历:

const depthFirstSearch = (node, nodeList = []) => {
    if (node !== null) {
      nodeList.push(node)
      //ParentNode.children 返回子节点集合(动态)
      let children = node.children
      for (let index = 0; index < children.length; index++) {
        depthFirstSearch(children[index], nodeList)
      }
    }
    return nodeList
  }
//test
const node = document.querySelector('.one')
console.dir(depthFirstSearch(node))
//结果:
0: div.one
1: div.one-one
2: div.one-one-one
3: div.one-one-two
4: div.one-one-three
5: div.one-two
6: div.one-two-one
7: div.one-two-two
8: div.one-three
9: div.onr-four

广度遍历优先:

const breadthFirstSearch = node => {
  let nodes = []
  let stack = []
  if (node !== null) {
    stack.push(node)
    while (stack.length) {
      let item = stack.shift()
      let children = item.children
      nodes.push(item)
      for (let index = 0; index < children.length; index++) {
        stack.push(children[index])
      }
    }
  }
  return nodes
}
//test
const node = document.querySelector('.one')
console.dir(breadthFirstSearch(node))
//结果:
0: div.one
1: div.one-one
2: div.one-two
3: div.one-three
4: div.onr-four
5: div.one-one-one
6: div.one-one-two
7: div.one-one-three
8: div.one-two-one
9: div.one-two-two

6、请分别用深度优先思想和广度优先思想实现一个拷贝函数?

题目用意应该是考察遍历树和重复引用吧

只深拷贝了 Object, Array,其他的非基本类型都是浅拷贝

/**工具函数
 * 如果是对象/数组,返回一个空的对象/数组
 * 都不是的话直接返回原对象
 * 判断返回的对象和原有对象是否相同就可以知道是否需要继续深拷贝
 * 处理其他的数据类型的话就在这里加判断
 * @param {*} o 拷贝的对象
 * 
 */
function getEmpty(o) {
  if (Object.prototype.toString.call(o) === '[object Object]') {
    return {}
  }
  if (Object.prototype.toString.call(o) === '[object Array]') {
    return []
  }
  return target
}
// 深度优先拷贝
function deepCopyDFS(origin) {
  let stack = []
  let map = new Map() // 记录出现过的对象,用于处理环
  let target = getEmpty(origin)
  if (target !== origin) {
    stack.push([origin, target])
    map.set(origin, target)
  }
  while (stack.length) {
    let [ori, tar] = stack.pop()
    for (let key in ori) {
      // 处理环状
      if (map.get(ori[key])) {
        tar[key] = map.get(ori[key])
        continue
      }

      tar[key] = getEmpty(ori[key])
      if (tar[key] !== ori[key]) {
        stack.push([ori[key], tar[key]])
        map.set(ori[key], tar[key])
      }
    }
  }

  return target
}

7、ES5/ES6 的继承除了写法以外还有什么区别?

  1. class 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 letconst 声明变量。
  2. class 声明内部会启用严格模式。
  3. class 的所有方法(包括静态方法和实例方法)都是不可枚举的。
  4. class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。
  5. 必须使用 new 调用 class
  6. class 内部无法重写类名

8、setTimeout、Promise、Async/Await 的区别

setTimeout 是宿主环境(浏览器、Node)所发起的异步任务,相同的还有事件和 ajax

而 Promise 和 Async/Await 分别是 ES6、ES7,JavaScript 语言本身所实现的异步任务。

它们在 JavaScript 引擎中别分为由宿主发起的异步宏任务,和 JavaScript 语言自身发起的异步微任务。

当异步任务的回调函数注册完毕,分别进入宏任务队列和微任务队列,在执行顺序上优先执行微任务队列,执行完微任务队列再去读取宏任务队列,每执行完一个异步宏任务,都先读取一遍微任务队列,有就执行,没有就就继续执行宏任务队列

9、Async/Await 如何通过同步的方式实现异步

Async/Await 就是一个自执行generator 函数。利用 generator 函数的特性把异步的代码写成“同步”的形式。

生成器函数在执行时能暂停,后面又能从暂停处继续执行。

调用一个生成器函数并不会马上执行它里面的语句,而是返回一个这个生成器的 迭代器 ( iterator)对象。当这个迭代器的 next() 方法被首次(后续)调用时,其内的语句会执行到第一个(后续)出现yield的位置为止,yield后紧跟迭代器要返回的值。或者如果用的是 yield*)(多了个星号),则表示将执行权移交给另一个生成器函数(当前生成器暂停执行)。

10、异步笔试题

请写出下面代码的运行结果

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)//1
async1();
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

await 做了什么?

await 等待的是一个表达式,这个表达式的返回值可以是一个promise对象也可以是其他值。

很多人以为 await 会一直等待之后的表达式执行完之后才会继续执行后面的代码,实际上await是一个让出线程的标志。await 后面的表达式会先执行一遍,将 await 后面的代码加入到 microtask 中,然后就会跳出整个async函数来执行后面的代码。

结果:

//script start
//async1 start
//async2
//promise1
//script end
//async1 end
//promise2
//setTimeout

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!