前端面试卷四

192 阅读16分钟

改造下面的代码,使之输出 0 - 9,写出你能想到的所有解法

for (var i = 0; i< 10; i++){
    setTimeout(() => {
	console.log(i);
    }, 1000)
}
结果:10,10,10,10,10,10,10,10,10,10

利用setTimetout函数的第三个参数作为回掉函数的第一个参数传入

for (var i = 0; i < 10; i++) {
  setTimeout(i => {
    console.log(i);
  }, 1000, i)
}
//等价于
for (var i = 0; i < 10; i++) {
  setTimeout(console.log, 1000, i)
}
for (var i = 0; i < 10; i++) {
  setTimeout(console.log(i), 1000)
}
for (var i = 0; i < 10; i++) {
  setTimeout((() => {
    console.log(i);
  })(), 1000)
}
for (var i = 0; i < 10; i++) {
  setTimeout((i => {
    console.log(i);
  })(i), 1000)
}
for (var i = 0; i < 10; i++) {
  setTimeout((i => {
    console.log(i);
  }).call(Object.create(null), i), 1000)
}
for (var i = 0; i < 10; i++) {
  setTimeout((i => {
    console.log(i);
  }).apply(Object.create(null), [i]), 1000)
}
for (var i = 0; i < 10; i++) {
  setTimeout((i => {
    console.log(i);
  }).apply(Object.create(null), { length: 1, '0': i }), 1000)
}

利用bind函数部分执行的特点

for (var i = 0; i < 10; i++) {
  setTimeout(console.log.bind(Object.create(null), i), 1000)
}

利用 let 变量的特性 — 在每一次 for 循环的过程中,let 声明的变量会在当前的块级作用域里面(for 循环的 body 体,也即两个花括号之间的内容区域)创建一个文法环境(Lexical Environment),该环境里面包括了当前 for 循环过程中的 i

for (let i = 0; i < 10; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000)
}
//等价于
for (let i = 0; i < 10; i++) {
  let _i = i;// const _i = i;
  setTimeout(() => {
    console.log(_i);
  }, 1000)
}

利用函数自执行的方式,把当前 for 循环过程中的 i 传递进去,构建出块级作用域。IIFE 其实并不属于闭包的范畴。

for (var i = 0; i < 10; i++) {
  (i => {
    setTimeout(() => {
      console.log(i);
    }, 1000)
  })(i)
}
//利用try catch构建块级作用域
for (var i = 0; i < 10; i++) {
  try {
    throw new Error(i);
  } catch ({
    message: i
  }) {
    setTimeout(() => {
      console.log(i);
    }, 1000)
  }
}

利用 eval 或者 new Function 执行字符串,然后执行过程同上

for (var i = 0; i < 10; i++) {
  setTimeout(eval('console.log(i)'), 1000)
}
for (var i = 0; i < 10; i++) {
  setTimeout(new Function('i', 'console.log(i)')(i), 1000)
}
for (var i = 0; i < 10; i++) {
  setTimeout(new Function('console.log(i)')(), 1000)
}

Virtual DOM 真的比操作原生 DOM 快吗?谈谈你的想法

1\. 原生 DOM 操作 vs 通过框架封装操作。
这是一个性能 vs 可维护性的取舍。框架的意义在于为你掩盖底层的 DOM 操
作,让你用更声明式的方式来描述你的目的,从而让你的代码更容易维护。没 有任何框架可以比纯手动的优化 DOM 操作更快,因为框架的 DOM 操作层需 要应对任何上层 API 可能产生的操作,它的实现必须是普适的。针对任何一
个 benchmark,我都可以写出比任何框架更快的手动优化,但是那有什么意义
呢?在构建一个实际应用的时候,你难道为每一个地方都去做手动优化吗?出
于可维护性的考虑,这显然不可能。框架给你的保证是,你在不需要手动优化
的情况下,我依然可以给你提供过得去的性能。
2\. 对 React 的 Virtual DOM 的误解。
React 从来没有说过 “React 比原生操作 DOM 快”。React 的基本思维模式是
每次有变动就整个重新渲染整个应用。如果没有 Virtual DOM,简单来想就是
直接重置 innerHTML。很多人都没有意识到,在一个大型列表所有数据都变了
的情况下,重置 innerHTML 其实是一个还算合理的操作... 真正的问题是在
“全部重新渲染” 的思维模式下,即使只有一行数据变了,它也需要重置整个
innerHTML,这时候显然就有大量的浪费。
我们可以比较一下 innerHTML vs Virtual DOM 的重绘性能消耗:
innerHTML: render html string O(template size) + 重新创建所有 DOM 元
素 O(DOM size)
Virtual DOM: render Virtual DOM + diff O(template size) + 必要的 DOM
更新 O(DOM change)
Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是!它依然是纯 js
层面的计算,比起后面的 DOM 操作来说,依然便宜了太多。可以看到,
innerHTML 的总计算量不管是 js 计算还是 DOM 操作都是和整个界面的大小
相关,但 Virtual DOM 的计算量里面,只有 js 计算和界面大小相关,DOM 操
作是和数据的变动量相关的。前面说了,和 DOM 操作比起来,js 计算是极其
便宜的。这才是为什么要有 Virtual DOM:它保证了 1)不管你的数据变化多
少,每次重绘的性能都可以接受;2) 你依然可以用类似 innerHTML 的思路去
写你的应用。
3\. MVVM vs. Virtual DOM
相比起 React,其他 MVVM 系框架比如 Angular, Knockout 以及 Vue、Avalon
采用的都是数据绑定:通过 Directive/Binding 对象,观察数据变化并保留对实
际 DOM 元素的引用,当有数据变化时进行对应的操作。MVVM 的变化检查是
数据层面的,而 React 的检查是 DOM 结构层面的。
MVVM 的性能也根据变动检测的实现原理有所不同:Angular 的脏检查使得任
何变动都有固定的 O(watcher count) 的代价;Knockout/Vue/Avalon 都采用了依
赖收集,在 js 和 DOM 层面都是 O(change):
脏检查:scope digest O(watcher count) + 必要 DOM 更新 O(DOM change)
依赖收集:重新收集依赖 O(data change) + 必要 DOM 更新 O(DOM
change)可以看到,Angular 最不效率的地方在于任何小变动都有的和 watcher
数量相关的性能代价。但是!当所有数据都变了的时候,Angular 其实并不吃
亏。依赖收集在初始化和数据变化的时候都需要重新收集依赖,这个代价在小
量更新的时候几乎可以忽略,但在数据量庞大的时候也会产生一定的消耗。
MVVM 渲染列表的时候,由于每一行都有自己的数据作用域,所以通常都是每
一行有一个对应的 ViewModel 实例,或者是一个稍微轻量一些的利用原型继
承的 "scope" 对象,但也有一定的代价。所以,MVVM 列表渲染的初始化几乎
一定比 React 慢,因为创建 ViewModel / scope 实例比起 Virtual DOM 来说要
昂贵很多。
这里所有 MVVM 实现的一个共同问题就是在列表渲染的数据源变动时,尤其
是当数据是全新的对象时,如何有效地复用已经创建的 ViewModel 实例和
DOM 元素。
假如没有任何复用方面的优化,由于数据是 “全新” 的,MVVM 实际上需要销
毁之前的所有实例,重新创建所有实例,最后再进行一次渲染!这就是为什么
题目里链接的 angular/knockout 实现都相对比较慢。相比之下,React 的变动
检查由于是 DOM 结构层面的,即使是全新的数据,只要最后渲染结果没变,
那么就不需要做无用功。
Angular 和 Vue 都提供了列表重绘的优化机制,也就是 “提示” 框架如何有效
地复用实例和 DOM 元素。比如数据库里的同一个对象,在两次前端 API 调用
里面会成为不同的对象,但是它们依然有一样的 uid。这时候你就可以提示
track by uid 来让 Angular 知道,这两个对象其实是同一份数据。那么原来这份
数据对应的实例和 DOM 元素都可以复用,只需要更新变动了的部分。或者,
你也可以直接 track by $index 来进行 “原地复用”:直接根据在数组里的位置
进行复用。在题目给出的例子里,如果 angular 实现加上 track by $index 的话,
后续重绘是不会比 React 慢多少的。甚至在 dbmonster 测试中,Angular 和
Vue 用了 track by $index 以后都比 React 快: dbmon (注意 Angular 默认版本
无优化,优化过的在下面)
顺道说一句,React 渲染列表的时候也需要提供 key 这个特殊 prop,本质上
和 track-by 是一回事。
4\. 性能比较也要看场合
在比较性能的时候,要分清楚初始渲染、小量数据更新、大量数据更新这些不
同的场合。Virtual DOM、脏检查 MVVM、数据收集 MVVM 在不同场合各有不
同的表现和不同的优化需求。Virtual DOM 为了提升小量数据更新时的性能,
也需要针对性的优化,比如 shouldComponentUpdate 或是 immutable data。
初始渲染:Virtual DOM > 脏检查 >= 依赖收集
小量数据更新:依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化) >
Virtual DOM 无优化
大量数据更新:脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无
法/无需优化)>> MVVM 无优化
不要天真地以为 Virtual DOM 就是快,diff 不是免费的,batching MVVM 也
能做,而且最终 patch 的时候还不是要用原生 API。在我看来 Virtual DOM 真
正的价值从来都不是性能,而是它 1) 为函数式的 UI 编程方式打开了大门;2)
可以渲染到 DOM 以外的 backend,比如 ReactNative。
5\. 总结
以上这些比较,更多的是对于框架开发研究者提供一些参考。主流的框架 + 合
理的优化,足以应对绝大部分应用的性能需求。如果是对性能有极致需求的特
殊情况,其实应该牺牲一些可维护性采取手动优化:比如 Atom 编辑器在文件
渲染的实现上放弃了 React 而采用了自己实现的 tile-based rendering;又比如
在移动端需要 DOM-pooling 的虚拟滚动,不需要考虑顺序变化,可以绕过框架
的内置实现自己搞一个

下面的代码打印什么内容,为什么?

var b = 10; 
(function b(){   
    b = 20;   
    console.log(b); 
    console.log(window.b)
})(); 
答:ƒ b(){  b = 20;   console.log(b); } 
10
解析:自执行函数会形成一个内部作用域,b=20时会检查是否存在局部变量b,发现function b存在,就赋值,
但是由于自执行函数是常量不可赋值,所以严格模式下会报错Uncaught TypeError: Assignment to 
constant variable,上例不是严格模式,所以是赋值不成功。然后打印b就是f b(){...};如果是window.b=20,
那么第二句打印就是20,如果var b= 20,那么第一句打印就是20

简单改造下面的代码,使之分别打印 10 和 20

var b = 10; 
(function b(){   
    b = 20;   
    console.log(b); 
})(); 
console.log(window.b) //10
var b = 20 //打印出20

var b = 10; 
(function (){ //去掉b局部变量,b就是全局变量,window.b  
    b = 20;   
    console.log(b); // 20
})(); 

var b = 10; 
(function (){   
    console.log(b); // 10 
})(); 

浏览器缓存读取规则

链接:https://www.jianshu.com/p/54cc04190252 

从缓存位置上来说分为四种,并且各自有优先级,当依次查找缓存且都没有命中的时候,才会去请求网络。

Service Worker:
Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用 Service Worker的话,传输协议必须为 HTTPS。因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 协议来保障安全。Service Worker 的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。
Service Worker 实现缓存功能一般分为三个步骤:首先需要先注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件,那么在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在缓存的话就可以直接读取缓存文件,否则就去请求数据。当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。

Memory Cache:
Memory Cache 也就是内存中的缓存,主要包含的是当前中页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。 一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
那么既然内存缓存这么高效,我们是不是能让数据都存放在内存中呢?
这是不可能的。计算机中的内存一定比硬盘容量小得多,操作系统需要精打细算内存的使用,所以能让我们使用的内存必然不多。当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存
内存缓存中有一块重要的缓存资源是preloader相关指令下载的资源。总所周知preloader的相关指令已经是页面优化的常见手段之一,它可以一边解析js/css文件,一边网络请求下一个资源。
需要注意的事情是,内存缓存在缓存资源时并不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type,CORS等其他特征做校验。

Disk Cache:
Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Herder 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。绝大部分的缓存都来自 Disk Cache
浏览器会把哪些文件丢进内存中?哪些丢进硬盘中?对于大文件来说,大概率是不存储在内存中的,反之优先当前系统内存使用率高的话,文件优先存储进硬盘

Push Cache:
Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在Chrome浏览器中只有5分钟左右,同时它也并非严格执行HTTP头中的缓存指令。
Push Cache 在国内能够查到的资料很少,也是因为 HTTP/2 在国内不够普及。

  • 所有的资源都能被推送,并且能够被缓存,但是 Edge 和 Safari 浏览器支持相对比较差
  • 可以推送 no-cache 和 no-store 的资源
  • 一旦连接被关闭,Push Cache 就被释放
  • 多个页面可以使用同一个HTTP/2的连接,也就可以使用同一个Push Cache。这主要还是依赖浏览器的实现而定,出于对性能的考虑,有的浏览器会对相同域名但不同的tab标签使用同一个HTTP连接。
  • Push Cache 中的缓存只能被使用一次
  • 浏览器可以拒绝接受已经存在的资源推送
  • 你可以给其他域名推送资源

如果以上四种缓存都没有命中的话,那么只能发起请求来获取资源了。那么为了性能上的考虑,大部分的接口都应该选择好缓存策略,通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略都是通过设置 HTTP Header 来实现的。

使用迭代的方式实现 flatten 函数

var arr=[1,2,3,[4,5],[6,[7,[8]]]] 使用递归的方式处理。要求保存结果到ret并返回一个递归函数
function wrap(){
   var ret = []
   return function flat(arr){   
      arr.forEach(item=>{
         if(Array.isArray(item)){
              flat(item)
         }
         else{
            ret.push(item)
         }
      })
      return ret
    }
}
console.log(wrap()(arr)); 

//ES6写法
const flatten=arr=>arr.reduce((acc,cur)=>{
  Array.isArray(cur)?[...acc,...flatten(cur)]:[...acc,cur])
},[])

为什么 Vuex的mutation和Redux的reducer中不能进行异步操作?

Mutation必须是同步函数,vuex和redux都是一种状态管理机制。然后他们会有自己的state(状态)和修改state的方法,修改state的方法涉及到同步和异步,vuex的处理方式是同步在mutation里面,异步在actions里面,然后redux的同步就是reducer,异步更多的是用户自己去通过中间件的方式去实现。
要在reducer中加入异步的操作,如果你只是单纯想执行异步操作,不会等待异步的返回,那么在reducer中执行的意义是什么。如果想把异步操作的结果反应在state中,首先整个应用的状态将变的不可预测,违背Redux的设计原则,其次,此时的currentState将会是promise之类而不是我们想要的应用状态,根本是行不通的。

下面代码中 a 在什么情况下会打印 1?

var a = ?; if(a == 1 && a == 2 && a == 3){   console.log(1); } 
//判断两个值是否相等的时候,会自动调用他们的toString()方法
var a = [1,2,3];
a.toSting = a.shift

var a = {
  i:1,
  toString:()=>{
    return a.i++
  }
}

介绍下 BFC 及其应用。

BFC 就是块级格式上下文,是页面盒模型布局中的一种 CSS 渲染模式,相当于
一个独立的容器,里面的元素和外部的元素相互不影响。
创建 BFC 的方式有:html 根元素,float 浮动,绝对定位,overflow 不为 visiable
display 为表格布局或者弹性布局
BFC 主要的作用是:清除浮动,防止同一 BFC容器中的相邻元素间的外边距重叠问题

在 Vue 中,子组件为何不可以修改父组件传递的Prop

因为vue设计是单向数据流,数据的流动方向只能是自上往下的方向,单向数据流易于监测
数据的流动,出现了错误可以更加迅速的定位到错误发生的位置。

如果修改了,Vue 是如何监控到属性的修改并给出警告的?
在 initProps 的时候,在 defineReactive 时通过判断是否在开发环境,如果是开
发环境,会在触发 set 的时候判断是否此 key 是否处于 updatingChildren 中被修
改,如果不是,说明此修改来自子组件,触发 warning 提示。window.isUpdatingChildComponent = false; 相当于一个Flag;只有当在父组件中 修改传递给子组件的Prop值的时候,才会被赋值为True。
需要特别注意的是,当你从子组件修改的 prop 属于基础类型时会触发提示。这
种情况下,你是无法修改父组件的数据源的, 因为基础类型赋值时是值拷贝。
你直接将另一个非基础类型(Object, array)赋值到此 key 时也会触发提示(但实
际上不会影响父组件的数据源), 当你修改 object 的属性时不会触发提示,并
且会修改父组件数据源的数据。