面试题

[toc]

备注:大部分从面试题从晒兜斯收录

JavaScript

call 和 apply 的区别是什么,哪个性能更好一些?

  • Function.prototype.applyFunction.prototype.call 的作用是一样的,区别在于传入参数的不同;
  • 第一个参数都是指定函数体内 this的指向;
  • 第二个参数开始不同,apply是传入带下标的集合,数组或者类数组,apply把它传给函数作为参数,call从第二个开始传入的参数是不固定的,都会传给函数作为参数;
  • callapply的性能要好,call传入参数的格式正式内部所需要的格式;

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

防抖触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间。

  1. function debounce(fn, delay) {
  2. let timer;
  3. return function() {
  4. clearTimeout(timer);
  5. timer = setTimeout(() => {
  6. fn();
  7. }, delay);
  8. }
  9. }

节流高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行效率。

  1. function throttle(fn, delay) {
  2. let trigger;
  3. return function() {
  4. if (trigger) return;
  5. trigger = true;
  6. fn();
  7. setTimeout(() => {
  8. trigger = false;
  9. }, delay);
  10. }
  11. }

JavaScript 中有哪几种内存泄露的情况?

  • 意外的全局变量;
  • 闭包;
  • 未被清空的定时器;
  • 未被销毁的事件监听;
  • DOM 引用;

如何简述执行上下文和执行栈?

执行期上下文

  • 全局执行上下文:默认的上下文,任何不在函数内部的代码都在全局上下文里面。它会执行两件事情:创建一个全局的的 window 对象,并且设置 this 为这个全局对象。一个程序只有一个全局对象。
  • 函数执行上下文:每当一个函数被调用时,就会为该函数创建一个新的上下文,每个函数都有自己的上下文,不过是在被函数调用的时候创建的。函数上下文可以有任意多个,每当一个新的执行上下文被创建,他会按照定义的顺序执行一系列的步骤。
  • Eval 函数执行上下文:执行在 eval 函数内部的代码有他自己的执行上下文。

执行栈就是一个调用栈,是一个后进先出数据结构的栈,用来存储代码运行时创建的执行上下文。

this绑定

  • 全局执行上下文中,this 指向全局对象。
  • 函数执行上下文中,this 取决于函数是如何被调用的。如果他被一个引用对象调用,那么 this 会设置成那个对象,否则是全局对象。

如何理解的函数式编程?

“函数式变成”是一种“编程范式”,也就是如何编写程序的方法论。

它具有以下特性:闭包和高阶函数、惰性运算、递归、函数是“第一等公民”、只用“表达式”。

什么是尾调用,使用尾调用有什么好处?

尾调用指的是函数的最后一步调用另一个函数。我们代码执行是基于执行栈的,所以当我们在一个函数里调用另一个函数时,我们会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这个时候我们可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

如何实现函数的柯里化?

函数柯里化是把接收多个参数的函数变换为接收一个单一参数(最初函数的第一个参数)的函数,并返回接收剩余参数而且返回结果的新函数的技术。

JS 函数柯里化的优点:

  • 可以延迟计算,即如果调用柯里化函数传入参数是不调用的,会将参数添加到数组中存储,等到没有参数传入的时候进行调用;
  • 参数复用,当在多次调用同一个函数,并且传递的参数绝大多数是相同的,那么该函数可能是一个很好的柯里化候选;

实现:

  1. function curringAdd() {
  2. let args = [].slice.call(arguments, 0);
  3. function add() {
  4. args = [...args, [].slice.call(arguments, 0)];
  5. return add
  6. }
  7. add.toString = function() {
  8. return args.reduce((t, a) => t + +a, 0);
  9. }
  10. return add;
  11. }
  12. console.log(curringAdd(1)(2)(3)) // 6
  13. console.log(curringAdd(1, 2, 3)(4)) // 10
  14. console.log(curringAdd(1)(2)(3)(4)(5)) // 15
  15. console.log(curringAdd(2, 6)(1)) // 9
  16. console.log(curringAdd(1)) // 1

DOM

什么是事件委托?

事件委托也叫事件代理,在 dom 节点中,因为有事件冒泡机制,所以子节点的事件可以被父节点捕获。因此,在适当的场景下将子节点的事件用父节点监听处理,支持的事件 点击事件 鼠标事件监听。

浏览器中的事件触发有三个阶段:

  1. 最从外层开始往里传播,即事件捕获阶段
  2. 事件抵达了目标节点,即目标阶段
  3. 从目标阶段往外层返回,即冒泡阶段

事件代理的优势:

  • 可以减少监听器的数量,减少内存占用
  • 对于动态新增的子节点,可以实现事件监听

移动端点击会存在哪些问题?

300 ms 点击(click 事件)延迟,由于移动端会有双击缩放的这个操作,因此浏览器在 click 之后要等待 300ms,判断这次操作是不是双击。

解决方案:

  • css touch-action touch-action的默为 auto,将其置为 none即可移除目标元素的 300 毫秒延迟 缺点: 新属性,可能存在浏览器兼容问题
  • 利用touchstarttouchend来模拟click事件,缺点有点击穿透
  • fastclick原理是在检测到touchend事件的时候,会通过DOM自定义事件立即出发模拟一个click事件,并把浏览器在300ms之后真正的click事件阻止掉
  • 所有版本的Android Chrome浏览器,如果设置viewport meta的值有user-scalable=no,浏览器也是会马上出发点击事件。

点击穿透问题,因为 click 事件的 300ms 延迟问题,所以有可能会在某些情况触发多次事件。

解决方案: 只用 touch 或 只用 click

ES6

箭头函数与普通函数(function)的区别是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?

箭头函数是普通函数的简写,可以更优雅的定义一个函数,和普通函数相比,有以下几点差异:

  1. 函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象;
  2. 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest参数代替;
  3. 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数;
  4. 不可以使用 new 命令, 因为没有自己的 this,无法调用 callapply;没有 prototype属性,而 new命令在执行时需要将钩子函数的 prototype赋值给新的对象的__proto__

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

  • Set

    • 成员不能重复;
    • 只有键值,没有键名,有点类似数组;
    • 可以遍历,方法有 add、delete、has
  • WeekSet

    • 成员都是对象(引用);
    • 成员都是弱引用,随时可以消失(不计入垃圾回收机制)。可以用来保存 DOM节点,不容易造成内存泄露;
    • 不能遍历,方法有 add、delete、has
  • Map

    • 本质上是键值对的集合,类似集合;
    • 可以遍历,方法很多,可以跟各种数据格式转换;
  • WeekMap

    • 只接收对象为键名(null除外),不接受其他类型的值作为键名;
    • 键名指向的对象,不计入垃圾回收机制;
    • 不能遍历,方法同 get、set、has、delete

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

  • class 声明会提升,但不会初始化赋值。(类似于 let、const 声明变量)
  • class 声明内部会启用严格模式;
  • class 的所有方法(包括静态方法和实例方法)都是不可枚举的;
  • class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有 [[constructor]],不能使用 new 来调用;
  • 必须使用 new 来调用 class
  • class 内部无法重写类名;

ES6 代码转成 ES5 代码的实现思路是什么?

Babel 的实现方式:

  1. 将代码字符串解析成抽象语法树,即所谓的 AST;
  2. 对 AST 进行处理,在这个阶段可以对 ES6 AST 进行相应转换,即转换成 ES5 AST;
  3. 根据处理后的 AST 再生成代码字符串;

异步

JS 异步解决方案的发展历程和有哪些优缺点?

回调函数

  • 优点:解决了同步的问题(整体任务执行时长);
  • 缺点:回调地狱,不能用 try catch 捕获错误,不能 return;

Promise

  • 优点:解决了回调地狱的问题;
  • 缺点:无法取消 Promise,错误需要通过回调函数来捕获;

Generator

  • 特点:可以控制函数的执行。

Async/Await

  • 优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题;
  • 缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低;

setTimeout、Promise、Async/Await 有什么区别?

  • setTimeout的回调函数放到宏任务队列里,等到执行栈清空以后执行;

  • Promise 本身是同步的立即执行函数,当在 executor 中执行 resolve 或者 reject 的时候,此时是异步操作,会先执行 then/catch 等,当主栈完成时,才会去调用 resolve/reject 方法中存放的方法。

  • async函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。

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

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

  1. var fetch = require("node-fetch");
  2. function *gen() { // 这里的 * 可以看成 async
  3. var url = "https://api.github.com/users/github";
  4. var result = yield fetch(url); // 这里的 yield 可以看成 await
  5. console.log(result.bio);
  6. }
  7. var g = gen();
  8. var result = g.next();
  9. result.value.then(data => data.json()).then(data => g.next(data));

Generator 函数是什么?

传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做“协程”(coroutine),意思是多个线程互相协作,完成异步任务。

协程有点像函数,又有点像线程,它的运行流程大致如下:

  • 第一步,协程 A 开始执行;
  • 第二步,协程 A 执行到一半,进入暂停,执行权转移到协程 B
  • 第三步,(一段时间后)协程 B 交还执行权;
  • 第四步,协程 A 恢复执行;

上面流程的协程 A,就是异步任务,因为它分成两段(或多段)执行。

举例来说,读取文件的协程写法如下:

  1. function* asyncJob() {
  2. // ...
  3. var f = yield readFile(fileA);
  4. // ...
  5. }

上面代码的函数 asyncJob 是一个协程,它的奥妙就在其中的 yield 命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield 命令是异步两个阶段的分界线。协程遇到 yield 命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。

Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。

  1. function* gen(x) {
  2. var y = yield x + 2;
  3. return y;
  4. }
  5. var g = gen(1);
  6. g.next() // { value: 3, done: false }
  7. g.next(2) // { value: 2, done: false }

next 是返回值的 value 属性,是 Generator 函数向外输出数据;next 方法还可以接受参数,向 Generator 函数体内输入数据。

上面代码中,第一个 next 方法的 value 属性,返回表达式 x + 2 的值 3。第二个 next 方法带有参数 2,这个参数可以传入 Generator 函数,作为 上个阶段 异步任务的返回结果,被函数体内的变量 y 接收。因此,这一步的 value 属性,返回的就是 2(变量 y 的值)。

NodeJS

如何简述浏览器与 Node 的事件循环?

浏览器环境

  • 宏任务:script 中的代码、setTimeout、setInterval、I/O、UI render;
  • 微任务:promise(async/await)、Object.observe、MutationObserver;

Node环境

  • 宏任务:setTimeout、setInterval、setImmediate、script(整体代码)、I/O 操作等;
  • 微任务:process.nextTick(与普通微任务有区别,在微任务队列执行之前执行)、new Promise().then(回调) 等

区别

  • node 环境下的 setTimeout 定时器会依次一起执行,浏览器是一个一个分开的;
  • 浏览器环境下微任务的执行是每个宏任务执行之后,而 node 中微任务会在各个阶段执行,一个阶段结束立刻执行 microTask;
  1. //浏览器环境下:
  2. while(true){
  3. 宏任务队列.shift()
  4. 微任务队列全部任务()
  5. }
  6. //Node 环境下:
  7. while(true){
  8. loop.forEach((阶段)=>{
  9. 阶段全部任务()
  10. nextTick全部任务()
  11. microTask全部任务()
  12. })
  13. }

如何简述require 的模块加载机制?

  1. 计算模块绝对路径;
  2. 如果缓存中有该模块,则从缓存中取出该模块;
  3. 按优先级依次寻找并编译执行模块,将模块推入缓存(require.cache)中;
  4. 输出模块的 exports 属性;

Node 如何实现热更新?

Node 中有一个 api 是 require.cache,如果这个对象中的引用被清除后,下次再调用就会重新加载,这个机制可以用来热加载更新的模块。

  1. function clearCache(modulePath) {
  2. const path = require.resolve(modulePath);
  3. if (require.cache[path]) {
  4. require.cache[path] = null;
  5. }
  6. }

然后使用 fs.watchFile 监听文件的更改,文件更改后调用 clearCache 传入对应的模块名即可。

为什么Node 更适合处理 I/O 密集型任务而不是 CPU 密集型任务?

Node 更适合处理 I/O 密集型的任务。因为 Node 的 I/O 密集型任务可以异步调用,利用事件循环的处理能力,资源占用极少,并且事件循环能力避开了多线程的调用,在调用方面是单线程,内部处理其实是多线程的。

并且由于 Javascript 是单线程的原因,Node 不适合处理 CPU 密集型的任务,CPU 密集型的任务会导致 CPU 时间片不能释放,使得后续 I/O 无法发起,从而造成阻塞。但是可以利用到多进程的特点完成对一些 CPU 密集型任务的处理,不过由于 Javascript 并不支持多线程,所以在这方面的处理能力会弱于其他多线程语言(例如 Java、Go)。

如何简述Buffer?

Buffer 是 Node 中用于处理二进制数据的类,其中与 IO 相关的操作(网络/文件等)均基于 Buffer。Buffer 类的实例非常类似于整数数组,但其大小是固定不变的,并且其内存在 V8 堆栈外分配原始内存空间。Buffer 类的实例创建之后,其所占用的内存大小就不能再进行调整。

如何简述Stream?

流(stream)是 Node 中处理流式数据的抽象接口,stream 模块用于构建实现了流接口的对象。Node 中提供了多种流对象,例如 HTTP 服务器的请求process.stdout。流可以是可读的、可写的、或者可读可写的,所有的流都是 EventEmitter 的实例。

工程化

webpack 中 loader 和 plugin 的区别是什么?

loader 是一个转换器,将 A 文件进行编译成 B 文件,属于单纯的文件转换过程;

plugin 是一个扩展器,它丰富了 webpack 本身,针对是 loader 结束后,webpack 打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听 webpack 打包过程中的某些节点,执行广泛的任务。

webpack 热更新原理是如何做到在不刷新浏览器的前提下更新页面的?

  1. 当修改了一个或多个文件;
  2. 文件系统接收更改并通知 webpack
  3. webpack重新编译构建一个或多个模块,并通知 HMR(Hot Module Replacement) 服务器进行更新;
  4. HMR Server 使用 Websocket 通知 HMR runtime 需要更新,HMR runtime 通过 HTTP 请求更新 jsonp
  5. HMR runtime 替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新;