2019十道精选前端面试题整理汇总

39次阅读

共计 5647 个字符,预计需要花费 15 分钟才能阅读完成。

又到了每年的面试季,一些换工作的朋友最近也正在加紧复习中,在这里呢作者整理了十道前端面试中的精选问题和答案,希望对想要换工作的朋友有所帮助,同时如果在阅读的过程中发现文章的问题,也请在评论区告知我。

原文链接

React 和 Vue 的区别?

  1. 数据流:React 是单项数据流(props 从父组件到子组件单向,数据到视图单向),Vue 则是双向绑定(props 在父子组件可以双向绑定,数据和视图双向绑定)
  2. 数据监听 :React 是在 setState 后比较数据引用的方式监听,Vue 通过 Es5 的数据劫持方法defineProperty 监听数据变化。
  3. 模板渲染 :React 是在 js 中通过原生的语法如 if,map 等方法动态渲染模板,Vue 是通过 指令 来动态渲染模板。

为什么 Vue 数据频繁变化但 dom 只会更新一次?

考虑以下场景:

export default {data () {
        return {number: 0};
    },
    mounted () {
    var i = 0;
        for(var i = 0; i < 1000; i++) {this.number++;}
    }
}

当我们在一个同步任务中执行 1000 次 number++ 操作,按照 set -> 订阅器(dep) -> 订阅者(watcher) -> update 的过程,dom 应该会频繁更新,按理说这会很消耗性能,但实际上这个过程中 dom 只会更新一次。

这是因为 vue 的 dom 更新是一个异步操作,在数据更新后会首先被 set 钩子监听到,但是不会马上执行 dom 更新,而是在 下一轮循环 中执行更新。
具体实现是 vue 中实现了一个 queue 队列用于存放本次 事件循环 中的所有 watcher 更新,并且同一个 watcher 的更新只会被推入队列一次,并在本轮事件循环的 微任务 执行结束后执行此更新 (UI Render 阶段),这就是 dom 只会更新一次的原因。

js 中的异步任务会进入 任务队列 在下一轮的事件循环中执行,更多事件循环的内容请参考 Javascript 运行机制之 Event Loop。

如何理解 React Fiber?

在 React 16 前,组件的更新是递归组件树的方式实现的,这个递归更新的过程是同步的,如果组件树过于庞大,实际更新过程会造成一些性能上的问题。

在 React 16 中发布了 React Fiber,React Fiber 是一种能将递归更新组件树的任务 分片 (time-slicing) 执行的算法。它将组件树的更新拆分为多个子任务,在子任务的执行过程中,允许暂存当前进度然后执行其他 优先级较高的任务,如在更新过程中用户产生了交互,那么会优先去处理用户交互,然后在回归更新任务继续执行。更多相关 React Fiber

你要如何设计一个报警系统?

从以下角度出发设计

1、项目错误信息采集

  • 组件生命周期运行错误
  • 事件中的错误

2、代码埋点

  • 在 window.onerror、componentDidCatch、try catch 等收集错误
  • 在具体事件中埋点收集

3、上报时机

  • 实时上传埋点数据,适用于对错误收集的实时性有一定要求项目
  • 定时上传埋点数据。

考虑到服务器压力因素,合理使用以上两种上报方案,(答到服务器压力点)

4、上报数据

  • 错误详细信息
  • 用户的环境数据采集(方便测试还原)

5、错误信息存储

  • 以时间命名的.log 文件收集错误的详细信息
  • 在数据库中存储错误的统计信息用于错误的可视化展示

服务器上的日志存储方案有兴趣可以自己了解,要是再讲一讲 GFSHDFS等分布式文件系统应该有加分。

6、错误的统计和报警

  • 以图表的方式分类并按时间展示错误信息。
  • 设定一个峰值为报警值并邮件通知管理员

关于峰值的设定需要考虑到应用使用的高峰段和低谷段,并且此峰值需要在各个场景下不断调优。

require 和 import 的区别?

  • require 是运行时调用,import 是编译时调用(静态加载)
  • require 是 CommonJs 规范,import 是 Es6 的标准
  • require 的一个模块就是一个文件,一个文件只能一个输出。import 的一个文件可以有多个输出,并且在导入时可以选择部分导入。

import 在编译时加载的特点使得其效率更高,也让静态分析和优化成为了可能。

webpack 的 tree shaking 优化的基础就是 import 的静态导入。

require 加载过程?

在 node 中使用 requireexports时我们发现不管是在模块中还是在全局对象上,都不存在这两个方法,那么这两个方法是从何而来呢?

其实 require 方法本身是定义在 Module 中的,node 在编译阶段将 js 文件包装在函数将其包装成模块:

(function (exports, require, module, __filename, __dirname) {file_content...})

使用 require 加载模块的过程

  1. 根据 require 的参数计算绝对路径path
  2. 根据 path 查找是否有缓存var cache = Module._cache[path],如果有缓存直接return
  3. 判断是否是内置模块如http,如果是直接return
  4. 生成模块实例,并缓存
      var module = new Module(path, parent);
      Module._cache[path] = module;
  1. 加载模块module.load(path);

你知道哪些前端安全问题,如何避免?

1、XSS 攻击

  • 反射型 XSS

攻击步骤

  1. 攻击者构造出带有恶意代码的 URL,诱导用户点击
  2. 服务端取出恶意代码并拼接在 html 中返回给浏览器
  3. 浏览器执行恶意代码,攻击者获取用户信息或冒充用户行为进行攻击

eg:

// 恶意 URL:http://xxx.com?key=<script>document.cookie</script>

// 服务端拼接
<div>@{{params.key}}</div>

// 最终浏览器执行了此恶意代码获取到用户 cookie
<div>
    <script>document.cookie</script>
</div>
  • 存储型 XSS

攻击步骤

  1. 攻击者在商品评论页提交了恶意代码到目标数据库
  2. 用于访问该商品,服务端将商品评论从数据库取出并拼接在 HTML 中返回给浏览器
  3. 浏览器执行恶意代码,攻击者获取用户信息或冒充用户行为进行攻击

存储型 XSS 会将恶意代码保存在数据库中,会影响到所有使用的用户,相比于反射型 XSS 造成后果更严重。

  • DOM 型 XSS

DOM 型 XSS 针对的主要是前端 JS 代码的漏洞

攻击步骤

  1. 攻击者提供带有恶意代码的 URL,或者已经存在于数据库的恶意代码
  2. 浏览器从 URL 中获取恶意代码,或者从后端接口中获取到恶意代码
  3. 前端 Javascript 直接执行了这些恶意代码,造成 DOM 型 XSS 攻击

eg:

// 恶意 URL: http://xxx.com?key=document.cookie

// 前端取出 key 字段并执行
evel(location.key)

防范存储和反射型 XSS

  1. 前端渲染 HTML,数据从接口中获取
  2. 在服务端拼接 HTML 时转义

防范 DOM 型 XSS

  1. 小心 innerHTML,outerHTML、eval 等方法
  2. 小心 setTimeout 等能将字符串直接执行的方法

2、CSRF 攻击

CSRF 攻击实际上是利用了浏览器在向 A 域名发起请求前,会 cookie 列表中查询是否存在 A 域名的 cookie。若是存在,则会将 cookie 添加至请求头中的机制。

这个机制不在乎请求具体是从哪个域名发出,它只是关心目标路由。

攻击步骤

  1. 用户访问正规网站 WebA,浏览器保存下WebA 为此用户设置的cookie
  2. 攻击者诱导用户点击不安全的网站 WebB,此时从恶意网站WebBWebA发送的请求中已经带上了用户的cookie

防范 CSRF 攻击

  1. 如果是 Ajax 跨域请求,在 Access-Control-Allow-Origin 中设置安全的域名,如 WebA 的域名。
  2. 如果是 form 表单请求,后端需要验证 http 的 Referer 字段,确保来源是安全的。
  3. 推荐使用 token 验证

3、自动化脚本攻击

羊毛党 通常使用脚本攻击我们的线上活动,获得非法利润,他们通常使用刷 API 接口,自动 刷单 等方式获取利润。

通常来说,我们需要人机识别来防范脚本攻击,在 web 前端服务端 之间,添加一层风控系统,用于鉴别终端是否是机器。

但前端依然可以为 羊毛党 增加一些收入难度,想要 薅羊毛 先得过前端这一关 ( 纸老虎)。

  1. token 校验,前端通过加密算法生成 token,由风控系统校验token,攻击者必须破解 js 生成token 的算法才能进行下一步。
  2. 代码压缩和混淆,这里根据实际情设置混淆级别,太高级别的混淆可能会影响 JS 本身的执行效率。高级混淆后的代码能防止攻击者 断点调试
  3. 收集用户行为,记录用户进入页面中行为,加密后交给风控系统(风控系统通过大数据分析地理位置、ip 地址、行为数据等进行人机识别分析)

tips:在前端的加密过程中,我们可以使用一些 DOM、BOM,API,因为攻击者通过 API 攻击无法直接模拟出真实浏览器的环境,就算模拟也需要费一番功夫,加大攻击者破解算法难度。

HTTPS 如何实现安全加密传输?

  1. 客户端发起请求,链接到服务器 443 端口
  2. 服务端的证书(自己制作或向三方机构申请),自己制作的证书需要客户端验证通过(用户点一下)。证书中包含了两个密钥,一个公钥一个私钥。
  3. 服务端将公钥返回到客户端,公钥中包含了证书颁发机构,证书过期时间等信息。
  4. 客户端收到公钥后,通过 SSl/TSL 层首先对公钥信息进行验证,如颁发机构过期时间等,如果发现异常,则会弹出一个警告框,提示证书存在问题。否则就生成一个随机值,然后使用公钥对此随机值进行加密,此加密信息只能通过服务端的私钥才能解密获取生成的随机值。
  5. 服务端获取到加密信息后使用私钥解密获得随机值,以后服务端和客户端的通讯都会使用此随机值进行加密,而这个时候,只有服务端和客户端才知道这个随机值(私钥),服务端将要返回给客户端的数据通过随机值加密后返回。
  6. 客户端用之前生成的随机值解密服务段传过来的信息,于是获取了解密后的内容,整个过程第三方即使监听到了数据,也束手无策。

HTTP/ 2 如果实现首部压缩?

HTTP/ 2 通过维护静态字典和动态字典的方式来压缩首部

  • 静态字典中包含了常见的头部名称或者头部名称和值的组合,如 method:GET
  • 动态字典中包含了每个请求特有的键值对,如自定义的头信息,针对每个 TCP connection,都需要维护一份动态字典。
  1. 对于静态字典中匹配的头部名称或头部名称和值的组合,可以使用一个字符表示,如建立连接时:

    method:GET 可以使用 1 表示(完全匹配)
    cookeie: xxx 可以使用 2:xxx 表示(头部匹配)

  2. 同时将 cookeie: xxx 加入动态字典中,这样后续的整个 cookie 键值对都可以使用一个字符表示:

    cookeie: xxx 可以使用 3 表示(加入到动态字典)

  3. 对于静态字典和动态字典中都不存在的内容,使用了哈夫曼(霍夫曼)编码来压缩体积。

更多相关内容请查看详情 HTTP/ 2 新特性

如何优化递归?

在 Js 代码执行时,会产生一个调用栈,执行某个函数时会将其压入栈,当它 return 后就会出栈。

而从某个函数调用另外一个函数时,就会为被调用的函数建立一个新的栈帧,并且进入这个栈帧,这个栈帧称为当前栈,而调用函数的栈帧称为调用栈。

function A(){return 1;}
function B(){A();
}
function C(){B();
}

C();

Js 执行栈中除了当前执行函数的栈帧,还保存着调用其函数的栈帧,在 A 释放前,执行栈中保存了 A、B、C 的栈帧,过长的调用栈帧在 Js 中会导致一个 栈溢出 的错误。

栈溢出 的错误常常出现在递归中。

当递归层次较深影响到代码运行效率,甚至出错后我们应该如何优化呢?

function fibonacci (n) {if ( n <= 1) {return 1};

    return fibonacci(n - 1) + fibonacci(n - 2);
}
fibonacci(100) // 卡死

1、尾递归优化

仔细观察上述调用过程 C -> B -> A,行程此调用栈的主要原因是在 A 执行完成后会将执行权返回 B,此时 B 才能释放,B 释放完成后继续讲执行权返回 C,最后 C 释放。

尾调用

尾调用(Tail Call)是函数式编程的一个重要概念,是指某个函数的最后一步是调用另一个函数。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,所以在 C 调用 B 后就会释放。

尾递归

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

将 fibonacci 函数使用尾递归优化

// 尾递归的优化往往是通过修改函数参数完成的
function fibonacci2 (n , ac1 = 1 , ac2 = 1) {if( n <= 1) {return ac2};

  return fibonacci2 (n - 1, ac2, ac1 + ac2);
}

面试官:尾调用是 ES6 的新功能吧,而且只有严格模式才能生效,因为在非严格模式下,可以通过 function.caller 追踪到调用栈,还有其他方法吗?

2、循环代替递归

使用蹦床函数将递归转为循环执行

function trampoline(fn) {while (fn && fn instanceof Function) {fn = fn();
  }
  return fn;
}

function fibonacci3 (n , ac1 = 1 , ac2 = 1) {if( n <= 1) {return ac2};
  return fibonacci3.bind(null, n - 1, ac2, ac1 + ac2);
}

trampoline(fibonacci3(100)) //573147844013817200000

面试官:嗯,这样确实可以避免栈溢出的错误问题,那你能尝试下不使用递归思想实现求斐波那契数列的和呢?

3、使用动态规划思想实现

function dp(n) {if(n <= 1){return 1}
    var a = 1;
    var b = 2;
    var temp = 0;

    for(let i = 2; i < n; i++){
    temp = a + b;
    a = b;
    b = temp;
    }

    return temp
}

最终我们对递归的优化就是放弃了使用递归????

正文完
 0