面试题总结

279 阅读22分钟

说说ES6新增了什么

简单描述

  1. Promise

    1. Promise 是异步编程的一种解决方案,为一个对象,有三种状态,pending、fulfilled、rejected
    2. 他接受一个参数为函数,该函数有两个参数 resolvereject,成功执行 resolve,失败执行 reject
    3. 他解决了,异步执行函数多层嵌套的问题,(回调地狱)
    4. 我们可以把 Promise 比作一个保姆,家里的一连串的事情,你只需要吩咐给他,他就能帮你做,你就可以去做其他事情了。
  2. async/await

    1. 简化 Promise 的语法糖
  3. 快级作用域、const,let

  4. Proxy

    1.Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)

  5. class

  6. 箭头函数

  7. 模块化开发

  8. 模板字符串

  9. 结构赋值

  10. Object 新的 API

    1. Object.is(a,b); 判断a和b的值是否相等
    2. Object.assign(); 对象深拷贝/合并对象( 三个参数代表的是合并对象)
    3. Object.setPrototypeOf(obj1,obj2) ; 设置原型对象中的方法,为目标对象设置原型,将obj2设置成obj1的原型
    4. Object.getPrototypeOf(obj) 获取原型对象中的方法(内部[[Prototype]]属性的值)
    5. Object.keys() Object.values() Object.entries() 依次为:获取键、获取值、获取键值对组成的数组
  11. Array 新的 API

    1. 静态方法
      • Array.from(); 方法对一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例
      • Array.of(); 将传入的参数转换为数组,总是返回参数值组成的数组。如果没有参数,就返回一个空数组
    2. 实例方法
      • Array.prototype.find(); 查找第一个符合条件的元素
      • Array.prototype.findIndex(); 查找第一个符合条件的元素的下标
      • Array.prototype.includes(); 检查数组是否包含某个元素
      • Array.prototype.fill() 用来填充数组
      • Array.prototype.keys(); Array.prototype.values(); Array.prototype.entries() keys,values,entries变量当前是迭代器对象
  12. Set、Map与WeakMap

    1. set : 是一种数据结构,类似于数组,但它的成员都是唯一的
    2. Map:普通的对象只能以字符串为键名,Map可以已任何类型数据来作为键名
    3. WeakMap:键名只能为一个对象或引用类数据
  13. 偏平化数组(拓展)

    1. 数组方法 flat(),对每一项的值进行循环处理,如果该项的值也是数组,则取出来(相当于去掉了这项数组的[]括号)

防抖与节流

定义

  • 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
  • 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时

一个经典的比喻:

想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应

假设电梯有两种运行策略 debounce 和 throttle,超时设定为15秒,不考虑容量限制

电梯第一个人进来后,15秒后准时运送一次,这是节流

电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这是防抖

JavaScript原型,原型链 ? 有什么特点?

原型

当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾

准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非实例对象本身

原型链

原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法

在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法

总结

  • 一切对象都是继承自Object对象,Object 对象直接继承根源对象null
  • 一切的函数对象(包括 Object 对象),都是继承自 Function 对象
  • Object 对象直接继承自 Function 对象
  • Function对象的__proto__会指向自己的原型对象,最终还是继承自Object对象

你是怎么理解ES6新增Set、Map两种数据结构的?

如果要用一句来描述,我们可以说

Set是一种叫做集合的数据结构,Map是一种叫做字典的数据结构

什么是集合?什么又是字典?

  • 集合
    是由一堆无序的、相关联的,且不重复的内存结构【数学中称为元素】组成的组合
  • 字典
    是一些元素的集合。每个元素有一个称作key 的域,不同元素的key 各不相同

区别?

  • 共同点:集合、字典都可以存储不重复的值
  • 不同点:集合是以[值,值]的形式存储元素,字典是以[键,值]的形式存储

一、Set

Setes6新增的数据结构,类似于数组,但是成员的值都是唯一的,没有重复的值,我们一般称为集合

Set本身是一个构造函数,用来生成 Set 数据结构

二、Map

Map类型是键值对的有序列表,而键和值都可以是任意类型

Map本身是一个构造函数,用来生成 Map 数据结构

说说你对事件循环的理解

首先,JavaScript是一门单线程的语言,意味着同一时间内只能做一件事,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环

JavaScript中,所有的任务都可以分为

  • 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
  • 异步任务:异步执行的任务,比如ajax网络请求,setTimeout定时函数等

同步任务与异步任务的运行流程图如下:

从上面我们可以看到,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复就事件循环

一、微任务

一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前

常见的微任务有:

  • Promise.then
  • MutaionObserver
  • Object.observe(已废弃;Proxy 对象替代)
  • process.nextTick(Node.js)

二、宏任务

宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合

常见的宏任务有:

  • script (可以理解为外层同步代码)
  • setTimeout/setInterval
  • UI rendering/UI事件
  • postMessage、MessageChannel
  • setImmediate、I/O(Node.js)

这时候,事件循环,宏任务,微任务的关系如图所示

按照这个流程,它的执行机制是:

  • 执行一个宏任务,如果遇到微任务就将它放到微任务的事件队列中
  • 当前宏任务执行完成后,会查看微任务的事件队列,然后将里面的所有微任务依次执行完

说说 JavaScript 中内存泄漏的几种情况?

在程序的运行需要内存。只要程序提出要求,操作系统或者运行时就必须供给内存

对于持续运行的服务进程,必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃

一、垃圾回收机制

Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存

原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存

通常情况下有两种实现方式:

  • 标记清除
  • 引用计数

标记清除

JavaScript最常用的垃圾收回机制

当变量进入执行环境是,就标记这个变量为“进入环境“。进入环境的变量所占用的内存就不能释放,当变量离开环境时,则将其标记为“离开环境“

垃圾回收程序运行的时候,会标记内存中存储的所有变量。然后,它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉

在此之后再被加上标记的变量就是待删除的了,原因是任何在上下文中的变量都访问不到它们了

随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存

举个例子:

var m = 0,n = 19 // 把 m,n,add() 标记为进入环境。
add(m, n) // 把 a, b, c标记为进入环境。
console.log(n) // a,b,c标记为离开环境,等待垃圾回收。
function add(a, b) {
  a++
  var c = a + b
  return c
}

引用计数

语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此可以将这块内存释放

如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏

const arr = [1, 2, 3, 4];
console.log('hello world');

面代码中,数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引用,因此引用次数为1。尽管后面的代码没有用到arr,它还是会持续占用内存

如果需要这块内存被垃圾回收机制释放,只需要设置如下:

arr = null

通过设置arrnull,就解除了对数组[1,2,3,4]的引用,引用次数变为 0,就被垃圾回收了

小结

有了垃圾回收机制,不代表不用关注内存泄露。那些很占空间的值,一旦不再用到,需要检查是否还存在对它们的引用。如果是的话,就必须手动解除引用

二、常见内存泄露情况

意外的全局变量

function foo(arg) {
    bar = "this is a hidden global variable";
}

另一种意外的全局变量可能由 this 创建:

function foo() {
    this.variable = "potential accidental global";
}
// foo 调用自己,this 指向了全局对象(window)
foo();

上述使用严格模式,可以避免意外的全局变量

定时器也常会造成内存泄露

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // 处理 node 和 someResource
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);

如果id为Node的元素从DOM中移除,该定时器仍会存在,同时,因为回调函数中包含对someResource的引用,定时器外面的someResource也不会被释放

包括我们之前所说的闭包,维持函数内局部变量,使其得不到释放

function bindEvent() {
  var obj = document.createElement('XXX');
  var unused = function () {
    console.log(obj, '闭包内引用obj obj不会被释放');
  };
  obj = null; // 解决方法
}

没有清理对DOM元素的引用同样造成内存泄露

const refA = document.getElementById('refA');
document.body.removeChild(refA); // dom删除了
console.log(refA, 'refA'); // 但是还存在引用能console出整个div 没有被回收
refA = null;
console.log(refA, 'refA'); // 解除引用

包括使用事件监听addEventListener监听的时候,在不监听的情况下使用removeEventListener取消对事件监听

mvvm 与 mvc

mvvm: MVVM:MVVM即 Model-View-ViewModel,(模型-视图-控制器)它是一种双向数据绑定的模式,用viewModel来建立起model数据层和view视图层的连接,数据改变会影响视图,视图改变会影响数据、

MVC: MVC即model-view-controller(模型-视图-控制器)是项目的一种分层架构思想,它把复杂的业务逻辑,抽离为职能单一的小模块,每个模块看似相互独立,其实又各自有相互依赖关系。它的好处是:保证了模块的智能单一性,方便程序的开发、维护、耦合度低。

http 状态码分别代表什么意思

常见状态

1(信息类):表示接收到请求并且继续处理 100——客户必须继续发出请求

101——客户要求服务器根据请求转换HTTP协议版本

2(响应成功):表示动作被成功接收、理解和接受 200——表明该请求被成功地完成,所请求的资源发送回客户端

3(重定向类):为了完成指定的动作,必须接受进一步处理 300——请求的资源可在多处得到

304——自从上次请求后,请求的网页未修改过,服务器返回此响应时,不会返回网页内容,代表上次的文档已经被缓存了,还可以继续使用

4(客户端错误类):请求包含错误语法或不能正确执行

400——客户端请求有语法错误,不能被服务器所理解

404——一个404错误表明可连接服务器,但服务器无法取得所请求的网页,请求资源不存在。(输入了错误的URL)

5(服务端错误类):服务器不能正确执行一个正确的请求

500 一 服务器遇到错误,无法完成请求(也可能是客户端传入了一个后端无法处理的数据

502 一 网关错误

503 — 由于超载或停机维护,服务器目前无法使用,一段时间后可能恢复正常

图片懒加载

一张图片就是一个 <img> 标签,浏览器是否发起请求图片是根据<img>src属性,所以实现懒加载的关键就是,在图片没有进入可视区域时,先不给<img>src赋值,这样浏览器就不会发送请求了,等到图片进入可视区域再给src赋值。

v-for 循环为什么一定要绑定key ?

页面上的标签都对应具体的虚拟 dom 对象(虚拟 dom 就是js对象), 循环中 ,如果没有唯一key , 页面上删除一条标签, 由于并不知道删除的是那一条! 所以要把全部虚拟dom重新渲染, 如果知道key为x标签被删除掉, 只需要把渲染的dom为x的标签去掉即可!

为什么遍历列表时,key最好不要用index?

虚拟DOM中key的作用:

简单的说: key 是虚拟 DOM 对象的标识, 在更新显示时 key 起着极其重要的作用。

详细的说: 当状态中的数据发生变化时,react/vue 会根据【新数据】生成【新的虚拟DOM】, 随后 react/vue 进行【新虚拟DOM】与【旧虚拟DOM】的diff比较,比较规则如下:

  1. 旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key:

    (1).若虚拟 DOM 中内容没变, 直接使用之前的真实 DOM

    (2).若虚拟 DOM 中内容变了, 则生成新的真实 DOM,随后替换掉页面中之前的真实 DOM

  2. 旧虚拟 DOM 中未找到与新虚拟 DOM 相同的 key 根据数据创建新的真实 DOM,随后渲染到到页面

用 index 作为 key 可能会引发的问题:

  1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作: 会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。

  2. 如果结构中还包含输入类的 DOM: 会产生错误DOM更新 ==> 界面有问题。

  3. 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作, 仅用于渲染列表用于展示,使用 index 作为 key 是没有问题的。

开发中如何选择 key?

  1. 最好使用每条数据的唯一标识作为 key, 比如id、手机号、身份证号、学号等唯一值。

  2. 如果确定只是简单的展示数据,用 index 也是可以的。

后台管理系统中的权限管理是怎么实现的?

权限管理分为两种:页面权限按钮权限,实现的原理为,当用户登录验证通过后,服务端会返回对应用户的信息,其中会带有该用户的权限列表(路由表),此时在全局钩子函数 router.beforeEach() 中进行拦截,将用户信息上的 权限列表(路由表)与本地的所有权限的路由表进行匹配筛选对应的路由,之后在使用router.addRoutes 动态的挂载路由,在路由表中的没个路由也会有对应的按钮列表,但按钮的是用的自定义指令,通过按钮列表来判断对应的页面中的某个按钮是否存在,如果有就显示没有则会在自定义指令里删除该按钮的dom节点

Object.defineProperty 和 Proxy 的区别

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。IE8不兼容。

Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。IE不兼容

  1. Proxy使用上比Object.defineProperty方便的多。
  2. Proxy代理整个对象,Object.defineProperty只代理对象上的某个属性。
  3. vue中,Proxy在调用时递归,Object.defineProperty在一开始就全部递归,Proxy性能优于Object.defineProperty
  4. 对象上定义新属性时,Proxy可以监听到,Object.defineProperty监听不到。
  5. 数组新增删除修改时,Proxy可以监听到,Object.defineProperty监听不到。
  6. Proxy不兼容IE,Object.defineProperty不兼容IE8及以下。

为什么避免 v-if 和 v-for 用在一起

这个在vue2中 v-for 的权限是比 v-if 要高的,因此在组件进行渲染时候,v-for 会把所有的组件都渲染一遍,之后在去 v-if 判断哪些组件是需要渲染哪些是不需要的,所以这样一来,对于浏览器的性能就不太友好,因为不需要渲染的组件一开始就应该不渲染,所以在vue2中要避免他们一起使用。但在vue3中就没有这个问题了,vue3 的 v-if 比 v-for 的权限高。

watch、methods、computed的区别

computed 计算属性就是为了简化template 里面的模板字符串的计算复杂度,防止模板太过于冗余。他具有缓存特性,computed 用来监听自己定义的变量,该变量不存在 data 里面声明,直接在 computed 里面定义,然后就可以在页面上进行双向数据绑定展示出结果或者用作其他处理。

watch 主要监听 vue 实例的变化,他监控的变量当然必须在data 里面进行声明才可以,它可以监控一个变量,也可以是一个对象,一般用于路径监控、imput 输入框的特殊处理等,他比较适合场景是一个数据影响多个数据,他不具有缓存。

methods 将被混入到 Vue 实例中。可以直接通过 VM 实例访问这些方法,或者在指令表达式中使用。方法中的 this 自动绑定为 Vue 实例。

三者加载的顺序

  1. computed 是在 HTML DOM 加载后马上执行的,如赋值;(属性将被混入到 Vue 实例)
  2. methods 则必须要有一定的触发条件才能执行,如点击事件,
  3. watch 它用于观察 vue 实例上的数据变化
  4. 默认加载的时候 先 computed 再 watch,不执行 methods;
  5. 触发某一事件后 先 computed 再 methods 再到 watch,computed 属性 vs method 方,computed 计算属性是基于它们的依赖进行缓存的

watch:监测的是属性值, 只要属性值发生变化,其都会触发执行回调函数来执行一系列操作。

computed:监测的是依赖值,依赖值不变的情况下其会直接读取缓存进行复用,变化的情况下才会重新计算。

除此之外,有点很重要的区别是:计算属性不能执行异步任务,计算属性必须同步执行。也就是说计算属性不能向服务器请求或者执行异步任务。如果遇到异步任务,就交给侦听属性。watch也可以检测computed属性。

Vue项目中你是如何解决跨域的呢?

而在vue项目中,我们主要针对CORSProxy这两种方案进行展开

CORS:

CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应

CORS 实现起来非常方便,只需要增加一些 HTTP 头,让服务器能声明允许的访问来源

只要后端实现了 CORS,就实现了跨域

Proxy:

代理(Proxy)也称网络代理,是一种特殊的网络服务,允许一个(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击

通过vue-cli脚手架工具搭建项目,我们可以通过webpack为我们起一个本地服务器作为请求的代理对象

通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果web应用和接口服务器不在一起仍会跨域

amodule.exports = {
    devServer: {
        host: '127.0.0.1',
        port: 8084,
        open: true,// vue项目启动时自动打开浏览器
        proxy: {
            '/api': { // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的
                target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址
                changeOrigin: true, //是否跨域
                pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'用""代替
                    '^/api': "" 
                }
            }
        }
    }
}

vue项目本地开发完成后部署到服务器后报404是什么原因呢?

vue项目在本地时运行正常,但部署到服务器中,刷新页面,出现了404错误

先定位一下,HTTP 404 错误意味着链接指向的资源不存在

为什么history模式下有问题

由于Vue是属于单页应用而SPA是一种网络应用程序或网站的模型,所有用户交互是通过动态重写当前页面,前面我们也看到了,不管我们应用有多少页面,构建物都只会产出一个index.html

当我们在地址栏输入 www.xxx.com 时,这时会打开我们 dist 目录下的 index.html 文件,然后我们在跳转路由进入到 www.xxx.com/login

关键在这里,当我们在 website.com/login 页执行刷新操作,nginx location 是没有相关配置的,所以就会出现 404 的情况

为什么hash模式下没有问题

router hash 模式我们都知道是用符号#表示的,如 website.com/#/loginhash 的值为 #/login

它的特点在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对服务端完全没有影响,因此改变 hash 不会重新加载页面

hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 website.com/#/login 只有 website.com 会被包含在请求中 ,因此对于服务端来说,即使没有配置location,也不会返回404错误

解决方案

产生问题的本质是因为我们的路由是通过JS来执行视图切换的,

当我们进入到子路由时刷新页面,web容器没有相对应的页面此时会出现404

所以我们后端只需要配置将任意页面都重定向到 index.html

为了避免这种情况,前端应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面

vuex ,谈谈你对它的理解?

  1. 首先vuex的出现是为了解决web组件化开发的过程中,各组件之间传值的复杂和混乱的问题
  2. 将我们在多个组件中需要共享的数据放到store中,
  3. 要获取或格式化数据需要使用getters,
  4. 改变store中的数据,使用mutation,但是只能包含同步的操作,在具体组件里面调用的方式this.$store.commit('xxxx')
  5. Action也是改变store中的数据,不过是提交的mutation,并且可以包含异步操作,在组件中的调用方式this.$store.dispatch('xxx'); 在actions里面使用的commit('调用mutation')

Vue中的$nextTick有什么作用?

Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新

如果想要在修改数据后立刻得到更新后的DOM结构,可以使用Vue.nextTick()

第一个参数为:回调函数(可以获取最近的DOM结构)

第二个参数为:执行函数上下文

谈谈你平时都用了哪些方法进行性能优化?

减少http请求次数、打包压缩上线代码、使用懒加载、使用雪碧图、动态渲染组件、CDN加载包。