面试题
[toc]
备注:大部分从面试题从晒兜斯收录
JavaScript
call 和 apply 的区别是什么,哪个性能更好一些?
Function.prototype.apply和Function.prototype.call的作用是一样的,区别在于传入参数的不同;- 第一个参数都是指定函数体内
this的指向; - 第二个参数开始不同,
apply是传入带下标的集合,数组或者类数组,apply把它传给函数作为参数,call从第二个开始传入的参数是不固定的,都会传给函数作为参数; call比apply的性能要好,call传入参数的格式正式内部所需要的格式;
什么是防抖和节流?有什么区别?如何实现?
防抖触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间。
function debounce(fn, delay) {let timer;return function() {clearTimeout(timer);timer = setTimeout(() => {fn();}, delay);}}
节流高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行效率。
function throttle(fn, delay) {let trigger;return function() {if (trigger) return;trigger = true;fn();setTimeout(() => {trigger = false;}, delay);}}
JavaScript 中有哪几种内存泄露的情况?
- 意外的全局变量;
- 闭包;
- 未被清空的定时器;
- 未被销毁的事件监听;
- DOM 引用;
如何简述执行上下文和执行栈?
执行期上下文
- 全局执行上下文:默认的上下文,任何不在函数内部的代码都在全局上下文里面。它会执行两件事情:创建一个全局的的 window 对象,并且设置 this 为这个全局对象。一个程序只有一个全局对象。
- 函数执行上下文:每当一个函数被调用时,就会为该函数创建一个新的上下文,每个函数都有自己的上下文,不过是在被函数调用的时候创建的。函数上下文可以有任意多个,每当一个新的执行上下文被创建,他会按照定义的顺序执行一系列的步骤。
- Eval 函数执行上下文:执行在
eval函数内部的代码有他自己的执行上下文。
执行栈就是一个调用栈,是一个后进先出数据结构的栈,用来存储代码运行时创建的执行上下文。
this绑定
- 全局执行上下文中,
this指向全局对象。 - 函数执行上下文中,this 取决于函数是如何被调用的。如果他被一个引用对象调用,那么
this会设置成那个对象,否则是全局对象。
如何理解的函数式编程?
“函数式变成”是一种“编程范式”,也就是如何编写程序的方法论。
它具有以下特性:闭包和高阶函数、惰性运算、递归、函数是“第一等公民”、只用“表达式”。
什么是尾调用,使用尾调用有什么好处?
尾调用指的是函数的最后一步调用另一个函数。我们代码执行是基于执行栈的,所以当我们在一个函数里调用另一个函数时,我们会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这个时候我们可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
如何实现函数的柯里化?
函数柯里化是把接收多个参数的函数变换为接收一个单一参数(最初函数的第一个参数)的函数,并返回接收剩余参数而且返回结果的新函数的技术。
JS 函数柯里化的优点:
- 可以延迟计算,即如果调用柯里化函数传入参数是不调用的,会将参数添加到数组中存储,等到没有参数传入的时候进行调用;
- 参数复用,当在多次调用同一个函数,并且传递的参数绝大多数是相同的,那么该函数可能是一个很好的柯里化候选;
实现:
function curringAdd() {let args = [].slice.call(arguments, 0);function add() {args = [...args, [].slice.call(arguments, 0)];return add}add.toString = function() {return args.reduce((t, a) => t + +a, 0);}return add;}console.log(curringAdd(1)(2)(3)) // 6console.log(curringAdd(1, 2, 3)(4)) // 10console.log(curringAdd(1)(2)(3)(4)(5)) // 15console.log(curringAdd(2, 6)(1)) // 9console.log(curringAdd(1)) // 1
DOM
什么是事件委托?
事件委托也叫事件代理,在 dom 节点中,因为有事件冒泡机制,所以子节点的事件可以被父节点捕获。因此,在适当的场景下将子节点的事件用父节点监听处理,支持的事件 点击事件 鼠标事件监听。
浏览器中的事件触发有三个阶段:
- 最从外层开始往里传播,即事件捕获阶段
- 事件抵达了目标节点,即目标阶段
- 从目标阶段往外层返回,即冒泡阶段
事件代理的优势:
- 可以减少监听器的数量,减少内存占用
- 对于动态新增的子节点,可以实现事件监听
移动端点击会存在哪些问题?
300 ms 点击(click 事件)延迟,由于移动端会有双击缩放的这个操作,因此浏览器在 click 之后要等待 300ms,判断这次操作是不是双击。
解决方案:
css touch-action touch-action的默为auto,将其置为none即可移除目标元素的 300 毫秒延迟 缺点: 新属性,可能存在浏览器兼容问题- 利用
touchstart和touchend来模拟click事件,缺点有点击穿透 fastclick原理是在检测到touchend事件的时候,会通过DOM自定义事件立即出发模拟一个click事件,并把浏览器在300ms之后真正的click事件阻止掉- 所有版本的Android Chrome浏览器,如果设置
viewport meta的值有user-scalable=no,浏览器也是会马上出发点击事件。
点击穿透问题,因为 click 事件的 300ms 延迟问题,所以有可能会在某些情况触发多次事件。
解决方案: 只用 touch 或 只用 click
ES6
箭头函数与普通函数(function)的区别是什么?构造函数(function)可以使用 new 生成实例,那么箭头函数可以吗?为什么?
箭头函数是普通函数的简写,可以更优雅的定义一个函数,和普通函数相比,有以下几点差异:
- 函数体内的
this对象,就是定义时所在的对象,而不是使用时所在的对象; - 不可以使用
arguments对象,该对象在函数体内不存在。如果要用,可以用rest参数代替; - 不可以使用
yield命令,因此箭头函数不能用作Generator函数; - 不可以使用
new命令, 因为没有自己的this,无法调用call、apply;没有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 的实现方式:
- 将代码字符串解析成抽象语法树,即所谓的 AST;
- 对 AST 进行处理,在这个阶段可以对 ES6 AST 进行相应转换,即转换成 ES5 AST;
- 根据处理后的 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 函数的特性把异步的代码写成“同步”的形式。
var fetch = require("node-fetch");function *gen() { // 这里的 * 可以看成 asyncvar url = "https://api.github.com/users/github";var result = yield fetch(url); // 这里的 yield 可以看成 awaitconsole.log(result.bio);}var g = gen();var result = g.next();result.value.then(data => data.json()).then(data => g.next(data));
Generator 函数是什么?
传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做“协程”(coroutine),意思是多个线程互相协作,完成异步任务。
协程有点像函数,又有点像线程,它的运行流程大致如下:
- 第一步,协程
A开始执行; - 第二步,协程
A执行到一半,进入暂停,执行权转移到协程B; - 第三步,(一段时间后)协程
B交还执行权; - 第四步,协程
A恢复执行;
上面流程的协程 A,就是异步任务,因为它分成两段(或多段)执行。
举例来说,读取文件的协程写法如下:
function* asyncJob() {// ...var f = yield readFile(fileA);// ...}
上面代码的函数 asyncJob 是一个协程,它的奥妙就在其中的 yield 命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield 命令是异步两个阶段的分界线。协程遇到 yield 命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。
Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
function* gen(x) {var y = yield x + 2;return y;}var g = gen(1);g.next() // { value: 3, done: false }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;
//浏览器环境下:while(true){宏任务队列.shift()微任务队列全部任务()}//Node 环境下:while(true){loop.forEach((阶段)=>{阶段全部任务()nextTick全部任务()microTask全部任务()})}
如何简述require 的模块加载机制?
- 计算模块绝对路径;
- 如果缓存中有该模块,则从缓存中取出该模块;
- 按优先级依次寻找并编译执行模块,将模块推入缓存(require.cache)中;
- 输出模块的
exports属性;
Node 如何实现热更新?
Node 中有一个 api 是 require.cache,如果这个对象中的引用被清除后,下次再调用就会重新加载,这个机制可以用来热加载更新的模块。
function clearCache(modulePath) {const path = require.resolve(modulePath);if (require.cache[path]) {require.cache[path] = null;}}
然后使用 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 热更新原理是如何做到在不刷新浏览器的前提下更新页面的?
- 当修改了一个或多个文件;
- 文件系统接收更改并通知
webpack; webpack重新编译构建一个或多个模块,并通知HMR(Hot Module Replacement)服务器进行更新;HMR Server使用Websocket通知HMR runtime需要更新,HMR runtime通过 HTTP 请求更新jsonp;HMR runtime替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新;
