常见的面试问题总结之二

271 阅读14分钟

之前写过的文章和最近面试遇到的问题做一个串联,便于复习。

一、 作用域

1. 作用域是什么?

作用域是根据名称查找变量的一套规则,用于确定何处以及如何查找变量。作用域经常是嵌套的,当一个块或是函数嵌套在另一个块或函数中时,就发生了作用域嵌套。 在当前作用域中无法找到某个变量时,引擎就会向外层嵌套作用域查找,直到找到该变量,或者是抵达最外层作用域(全局作用域)为止,无论找没有找到都会停止。

2. 词法作用域

词法作用域,即定义词法阶段的作用域,它是由你在写代码时把变量和作用域块写在哪里决定的,因此在词法分析处理代码时会保持作用域不变。

3. 作用域和闭包

函数可以记住并访问所在的词法作用域,即便函数在当前词法作用域外执行,都会产生闭包。另外,在《JavaScript高级程序设计》中定义的闭包是有权访问另一个函数作用域中的变量的函数。

详见 javascript之作用域

二、原型及原型链

1. 构造函数

函数简单的说就是重复执行的代码块。构造函数,它也是函数,所有的函数都可以使用new关键字来创建对象,这种用 new 操作符来调用的函数就叫做构造函数。

function testConstructor() {
    console.log('ha~ ha~ haha~~!');
}

let res = new testConstructor();
console.log(typeof res);    // object
console.log(res.constructor === testConstructor); // true res的构造函数是testConstructor
console.log(res instanceof testConstructor); // true res是testConstructor构造函数的实例

详见 构造函数的特征

2. 什么是原型?

我们在创建一个函数时,都会有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

3. 什么是原型链?

每个构造函数都有原型对象,每个构造函数实例都包含一个指向原型对象的内部指针(prototype),如果我们让一个构造函数A的原型对象等于另一构造函数B的实例b, 结果这个构造函数的原型对象将包含一个指向另一个构造函数B的原型对象的指针,然后第三个构造函数C的原型对象等于构造函数A的实例a,这样构造函数C的原型对象 也将包含一个指向A的原型对象的指针。以此类推,就够成了原型对象等于另一个对象的实例,实则是以这个实例为中介,指向这个实例的原型对象,而这个实例的原型对象 又实质上指向另一个原型对象,以此形成一个链条,这就是原型链。

三、JavaScript中堆栈的理解

堆(heap)是堆内存的简称。栈(stack)是栈内存的简称。堆栈讲的就是内存的使用和分配,没有寄存器的事,也没有硬盘的事。堆是动态分配内存, 内存大小不一,也不会自动释放。栈是自动分配相对固定大小的内存空间,并由系统自动释放。

JavaScript的基本类型就6种:Undefined、Null、Boolean、Number、String和Symbol,它们都是直接按值存储在栈中的,每种类型的数据占用的内存空间的大小是确定的, 并由系统自动分配和自动释放。这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间。

JavaScript中其他类型的数据被称为引用类型的数据 : 如对象(Object)、数组(Array)、函数(Function)、Set(es6) Map(es6)…,它们是通过拷贝和new出来的, 这样的数据存储于堆中,而指向引用类型的数据的地址指针是存储于栈中的,当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针, 然后,在通过地址指针找到堆中的所需要的数据。

栈,线性结构,后进先出,便于管理。堆,一个混沌,杂乱无章,方便存储和开辟内存空间。

内容来源于 JS中的数据类型,包含ES6,set和map等等 堆和栈有什么区别?哪个更快?哪个能继承?

堆和栈都是运行时内存中分配的一个数据区,因此也被称为堆区和栈区,但二者存储的数据类型和处理速度不同。堆(heap)用于复杂数据类型(引用类型)分配空间,例如数组对象、object对象;它是运行时动态分配内存的,因此存取速度较慢。栈(stack)中主要存放一些基本类型的变量和对象的引用,其优势是存取速度比堆要快,并且栈内的数据可以共享,但缺点是存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。

内容来源于 js堆和栈         栈和堆的区别是什么? 为什么说栈的速度快,堆的速度慢?

四、事件循环、宏任务和微任务

详见 事件循环、宏任务和微任务

五、防抖(debounce)与节流(throttle)

防抖是限制某一行为在达到特定时间间隔后执行一次。如果特定时间间隔还没有达到又触发了该行为,就会重新计算间隔时间。实则就是等连续频繁的操作结束后, 再等待特定时间执行一次函数。

节流是限制某一行为在特定的时间内至多执行一次。如果在特定时间内重复触发,将不会像防抖一样重新计算间隔时间,也不会重复执行函数,而是在特定时间结束后执行一次。 实则是在触发函数行为的特定时间内只允许执行一次函数。

详见 防抖(debounce)与节流(throttle)的区别

六、浏览器及JavaScript引擎工作原理

浏览器的主要组成部分:用户界面、呈现引擎、浏览器引擎、网络、用户界面后端、JavaScript 解释器、数据存储。

详见 浏览器及JavaScript引擎工作原理

七、ES6模块

1. 什么是模块?

模块是自动运行在严格模式下无法退出的JavaScript代码。在模块顶部创建的变量不会自动加到全局共享作用域,这些变量只存在模块的顶级作用域中, 并且模块顶部的this值是undefined。模块可以导出一些绑定值供其他模块使用,也可以从其他模块导入绑定值。

2. 模块导出

  • 导出模块用export关键字。
  • 可以在申明变量、类或是函数的同时就导出。
  • 也可以先申明变量、类或是函数,然后再导出申明的变量、类或是函数。
  • 或者在导出的时候重命名,重命名时,用 export { aaa as bbb } 这种形式。

3. 模块导入

  • 导入模块用import关键字。
  • 导入整个模块,import * as aaa from 'xxx.js';
  • 导入模块中的单个绑定,import { aaa } from 'xxx.js';
  • 导入模块中的某个绑定并重命名, import { aaa as bbb } from 'xxx.js';

4. 模块默认值

  • 默认值只有一个,不能导出和导入多个。
  • 导出默认值可以在申明默认的变量、类或是函数的同时就导出,也可以先申明后导出,像 export default ……
  • 也可用export { aaa as default }; 这种形式。
  • 导入默认值时不用加 {},导入默认值和非默认值,默认值必须在前面,用逗号和后面的非默认值隔开。像 import aaa, { bbb } from 'xxx.js';
  • 也可以在导入的模块中给默认值重命名,像 import { default as aaa, bbb } from 'xxx.js';

5. 重新导出一个绑定

  • 从另一个模块中重新导出一个绑定,像 export { aaa } from 'xxx.js';
  • 重新导出并重命名,像 export { aaa as bbb } from 'xxx.js';
  • 重新导出另一个模块的全部值,像 export * from 'xxx.js';

详细例子见 ES6模块封装代码

八、继承的实现方式

该文章基本来自于 《JavaScript高级程序设计》,另外ES6中的class有extend关键字继承实现方式。 JS原型链与继承别再被问倒了

九、箭头函数和普通函数的区别?

箭头函数和普通函数的区别主要集中在以下方面:

  1. 没有thissuperargumentsnew.target绑定。箭头函数中的thissuperarguments以及new.target这些值由外围最近一层非箭头函数决定的。
  2. 不能通过new关键字调用。 箭头函数没有[[Construct]]方法,所以不能被用作构造函数,如果通过new关键字调用箭头函数,程序会抛出错误。
  3. 没有原型。因为不可用通过new关键字调用箭头函数,因而没有构建原型的需求,所以箭头函数不存在prototype这个属性。
  4. 不可改变this的绑定。箭头函数的this值不可改变,在函数的生命周期内始终保持一致。
  5. 不支持arguments对象。箭头函数没有arguments绑定,所以必须通过命名参数或是不定参数这两种形式访问函数的参数。
  6. 不支持重复的命名参数。无论在严格模式还是非严格模式下,箭头函数都不支持重复的参数;而普通函数只是在严格模式下才不支持重复命名参数。

十、CSS怎么实现动画

这里直接贴上一篇文章css3实现动画效果常用方法

十一、怎么通过对象的value值取到对应的key值

let obj = {
    a: 1,
    b: 'sss',
    c: {},
    d: { a: 98, b: 'str' }
}

function findKey(value, data, compare = (a, b) => a === b) {
    return Object.keys(data).find(k => compare(data[k], value))
}

var val = data.b
findKey(val, obj) // b

十二、说说Websocket和http的区别

WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息,而HTTP是单向的; WebSocket是需要浏览器和服务器握手进行建立连接的,而http是浏览器发起向服务器的连接。

详细见websocket和http的区别

十三、flex: 1;是什么的简写

详见flex:1;详解       写给自己看的display: flex布局教程

十四、怎么理解的MVVM,它和MVC有什么区别?

详见什么是MVVM?

十五、es6数组有哪些方法

详见 最新数组方法(包括es6)

十六、require和import的区别

详见 require和import的区别

十七、Vue中双向数据绑定的原理

vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

具体步骤:

第一步:需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到数据的变化;

第二步:compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图;

第三步:Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是:

在自身实例化时往属性订阅器(dep)里面添加自己,自身必须有一个 update()方法,待属性变动 dep.notice()通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调,则功成身退。

第四步:MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。

十八、你都做过哪些 Vue 的性能优化

  • 对象层级不要过深,否则性能就会差

  • 不需要响应式的数据不要放到 data 中(可以用 Object.freeze() 冻结数据)

  • v-if 和 v-show 区分使用场景

  • computed 和 watch 区分使用场景

  • v-for 遍历必须加 key,key 最好是 id 值,且避免同时使用 v-if

  • 大数据列表和表格性能优化-虚拟列表/虚拟表格

  • 防止内部泄漏,组件销毁后把全局变量和事件销毁

  • 图片懒加载

  • 路由懒加载

  • 第三方插件的按需引入

  • 适当采用 keep-alive 缓存组件

  • 防抖、节流运用

  • 服务端渲染 SSR or 预渲染

转至 最全的 Vue 面试题+详解答案

十九、在Vue项目中遇到过些什么问题

参考 Vue 项目里戳中你痛点的问题及解决办法

二十、从输入URL到看到页面发生了什么?

首先我们需要通过 DNS(域名解析系统)将 URL 解析为对应的 IP 地址,然后与这个 IP 地址确定的那台服务器建立起 TCP 网络连接,随后我们向服务端抛出我们的 HTTP 请求,服务端处理完我们的请求之后,把目标数据放在 HTTP 响应里返回给客户端,拿到响应数据的浏览器就可以开始走一个渲染的流程。渲染完毕,页面便呈现给了用户,并时刻等待响应用户的操作

对于这个问题的更详细的解答可以从浏览器的原理方向回答,比如:

  1. UI线程负责处理用户的输入,首先会判断用户输入的是关键词还是一个域名地址,如果是关键词就交给搜索引擎处理,如果是URL地址就通知网络进程负责DNS解析和TCP连接以及最终的请求和响应交互
  2. 浏览器接收到响应后,网络进行还会进行安全处理,检测请求域名是否和某个已知的病毒网站相同,相同会给出提示
  3. 如果网站安全,请求数据已经返回到了浏览器端,此时浏览器进程会申请一个渲染进程负责页面的渲染
  4. 在渲染进程和数据都准备好了的前提下,浏览器进程会通知渲染进程进行提交导航操作,一旦浏览器进程收到渲染进程的回复说导航已经提交了,那导航过程就结束了,此时导航栏被更新,当前tab也的会话历史也被更新,你就可以通过前进后退按钮进入访问的页面
  5. 导航提交完成后,渲染进程开始着手进行资源加载和渲染页面,这里可以在回答一下渲染过程,就是HTML解析成DOM树,CSS解析成CSSOM树....(等等)
  6. 一旦完成渲染,会通过IPC告知浏览器进程,然后UI线程就会停止导航栏上旋转的圈圈

转至 2022前端面试大全

二十一、在项目中做过哪些权限管理类的需求?怎么实现权限控制?怎么防止直接在浏览器中输入地址的问题?

参考 前端权限控制

二十二、cookie, sessionStorage, localStorage有什么区别?

参考 浅谈session,cookie,sessionStorage,localStorage的区别及应用场景

二十三、怎么理解重绘和回流的

参考 你真的了解回流和重绘吗

二十四、数组中reduce的使用

比如用使用reduce将多维数组打平。

let arr = [[0, 1], [2, 3], [4,[5,6,7]]];
const newArr = function(arr){
   return arr.reduce((pre,cur) => pre.concat(Array.isArray(cur) ? newArr(cur) : cur), []);
}
console.log(newArr(arr)); //[0, 1, 2, 3, 4, 5, 6, 7]

参考 JS进阶篇--JS数组reduce()方法详解及高级技巧

二十五、谈谈你对Vue框架的理解

参考 面试官:聊聊对Vue.js框架的理解

二十六、Vue怎么重写数组方法的

参考 Vue如何监听数组的变化?

二十七、遇到过哪些内存泄露的情况

参考 「硬核JS」你的程序中可能存在内存泄漏

二十八、做过大屏类型的没有?怎么做到适应各个屏的?

参考 前端大屏幕项目(数据可视化)的一点思考

二十九、从拿到需求到上线的一系列流程?

参考 一线大厂:从需求提出到上线流程总结

三十、JS的事件机制

参考 JS事件机制