前端面试题之JavaScript篇

143 阅读21分钟

1. javaScript的数据类型以及判断方法

基本类型(原始值) :Number、String、Boolean、Null、undefined、Symbol(ES6)、BigInt(ES2020); 引用类型:Object(Array、Function)

判断方法:

  • typeof : 使用 typeof 会返回一个代表数据类型的字符串,返回的结果包含:number、string、boolean、undefined、symbol、object、function,其中 null 和 Array 会返回 object。
  • instanceof : 主要用于对引用类型的判断,通过判断左边的对象的原型链上是否有右边这个构造函数的 prototype 属性,返回一个布尔值。该方法不适用于基本类型。
  • constructor : constructor 是原型对象上的一个属性,指向的是构造函数。原理是根据在实例对象寻找属性和方法的时候,如果没有找到则会在原型链上进行查找。缺点:a、undefined 和 null 是没有 constructor 的,所以不能对其进行判断,否则会报错,导致代码进行不下去。b、由于constructor 的可变更,判断的结果会有一定的不正确性。
  • Object.prototype.toString.call() : toString 是 Object 原型对象上的一个方法,该方法默认返回其调用者的具体类型,call 方法只要是改变 this 的指向,将 this 指向括号中输入的值。返回的类型结构为 [object,xxx],xxx 是具体的数据类型,其中包括:String,Number,Boolean,Undefined,Null,Function,Date,Array,RegEx片,Error,HTMLDocument......基本上所有的对象的类型都可以通过这个方法获取到。

2. 原型和原型链

任何一个对象在被创建的时候,都会有一个内置属性 proto,这个内置属性 __proto__指向了它的构造函数的原型对象,即构造函数的 prototype 属性所指的对象,但只有函数对象才会有 prototype 。

原型链的原理是利用了原型让一个引用类型继承了另一个引用类型的属性和方法来实现的,当我们获取一个对象的属性或方法的时候,会先在该对象上进行查找哦,如果没找到的话,就会去该对象的原型对象上查找是否存在该属性或方法,即对象的 __proto__所指向的构造函数的原型对象,通过这样一层一层往上找,直到最顶层Object.prototype,这个过程就是一个原型链。这其中可以理解为 b 对象是 a 对象的原型对象,而 b 对象又有自己的原型对象,如此层层递进构成了实例与原型的链条。

3. javaScript中的继承

  • 原型继承: Child.prototype = new Parent() ,通过将子类的原型对象指向父类的实例。优点:继承了父类的模版,也继承了父类的原型的对象。缺点:无法实现多继承;在创建子类实例的时候,无法向父类构造函数传参;原型对象的所有属性都会被实例所共享。
  • 构造函数继承: 在子类构造函数内部使用 call 或 apply 方法来调用父类构造函数,复制父类的实例属性和方法给子类。优点:解决了原型链继承中子类实例共享父类引用对象的问题;实现了多继承;创建子类实例的时候,可以向父类构造函数传参。缺点:构造函数只能继承父类实例的属性和方法,不能继承父类原型的属性和方法。

  • 组合继承:就是将原型继承和构造函数继承组合在一起,通过调用父类构造函数,继承父类实例的属性和方法并保留了传参的优点(构造函数继承),然后通过父类实例作为子类原型(原型继承),实现函数复用。优点:可以继承实例的属性和方法,也可以继承原型的属性和方法;可传参;不存在引用属性共享问题;函数可复用。缺点:调用了两次父类构造函数。

  • 寄生式继承: 可以理解为是对原型继承的拓展,一个二次封装的过程,这样新创建的对象不仅仅有父类的属性和方法,还新增了别的属性和方法。

  • 寄生组合继承: 寄生组合式继承是寄生式继承和构造函数继承的组合。

  • class继承: class 继承主要是依靠 extends 和 super 来实现的。

4. 闭包

闭包可以理解为定义在一个函数内部的函数,在外部函数之后的地方被调用,就形成了闭包,这个内部函数还可以访问外部函数的参数和变量。

特点: 函数嵌套函数;函数可以访问另一个函数的作用域中的变量和参数;参数和变量不会被回收机制回收。

优点: 变量会长期驻扎在内存中;避免了全局变量的污染;私有成员的存在,即可以设置私有变量。

缺点:会造成内存泄露。

5. var 和 let 、const 的区别

Var 声明的变量存在变量提升,可以提前被调用,但只会返回一个 undefined。而 let 和 const 声明的变量就不存在变量提升,所以在声明之前是不可以被使用的,语法上称为暂时性死区。const 一但声明变量就必须赋值,且不能用 null 占位,同时声明的变量不能再进行修改,如果声明的是引用数据类型的话,只能修改它的值,而不能修改指针。

6. 作用域和作用域链

作用域可以理解为规定了如何查找变量,也就是确定了当前执行代码对变量的范围权限。ES5 只有全局作用域和函数作用域,ES6 增加了块级作用域(let 和 const)。

javaScript 采用了词法作用域,也就是静态作用域。所以函数的作用域在定义的时候就已经确定了。

作用域和执行上下文的区别:执行上下文在执行之前确定的,随时可以改变;而作用域在定义的时候就已经确定了,并且不会改变。执行上下文最明显的就是 this 的指向是执行的时候确定的,而作用域访问的变量是编写代码的结构确定的。

作用域链:当我们在查找变量的时候,会先从当前上下文的对象中查找,如果没找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象,这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

7. Event Loop 事件循环

javaScript 是一门单线程的语言,它的异步操作都是放在事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。

在主执行栈中,js 代码是自上而下进行执行的,立即执行的都是同步任务,当遇到异步操作的时候,不会马上对其进行执行,而是将其放入到任务队列中,直到主执行栈中的任务全部完成之后,才会到任务队列中查找,将符合条件的任务推入主执行栈中进行执行,这样反复的循环直到全部任务执行完成。

任务队列又分为 宏任务(Macro Task) 和 微任务(Micro Task),主执行栈在任务队列中查找任务的时候,会先从微任务查找,将微任务队列中的任务执行完之后才会去执行宏任务,执行宏任务的过程中,如果遇到微任务,会依次将其放入微任务队列中,每次执行完一个宏任务之后都会先去查看微任务队列中是否有任务,有的话就先把微任务队列中的任务执行完,如何反复循环。

宏任务(Macro Task)主要有:setTimeout、setInterval、script(整体代码)、I/O、UI 交互事件、setImmediate(Node.js 环境)。

微任务(Micro Task)主要有:Promise、MutaionObserver、process.nextTick(Node.js 环境)

8. 防抖和节流(练手写)

防抖(debounce) :在事件被触发 n 秒后执行回调,如果在这 n 秒内又被触发,则会重新计时。

适用场景:search搜索联想,用户在不断输入值时,用防抖来节约请求资源;window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次

节流(throttle) :规定在一个单位时间内,只能触发一次回调函数,如果在这个单位时间内被多次触发,也只会执行一次。

适用场景: 鼠标不断点击触发(按钮点击提交之类),mousedown(单位时间内只触发一次);监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断。

9. Promise、async/await

promise 是 ES6 新增的一个对象,目的是用于管理 js 中的异步编程。promise 本身是同步编程的,只是可以用于管理异步操作的。promise 有三种状态:pending(等待状态)、fulfiled(成功状态)、rejected(失败状态),状态一旦改变,就不再变化。promise 实例被创建之后就会立即执行,执行传入的函数,函数有两个参数:resolve方法(异步操作执行成功调用) 和 rejected 方法(异步操作执行失败地调用)。promise 可以进行链式调用,因为它每次使用完 then()方法和 catch()方法之后会返回一个 promise 对象,可以继续调用 then()方法。

async/await 函数是异步代码的心方式,它是基于 promise 实现的,用起来可以使得异步代码更像同步代码,在使用的过程中,两者只能同时成对出现,他会默认返回一个 promise。await 下面的代码是异步的,后面的代码是同步的。

10. 重绘和重排

重绘: 当一个元素的外观发生改变,但没有改变布局的时候,浏览器就会根据新的元素绘制,使元素呈现出新的外观。比如改变元素的背景颜色、字体的颜色、边框的颜色等。 引起重排的例子:添加或删除可见的DOM元素;元素尺寸的改变(边距、填充、边框、宽度和高度);浏览器窗口尺寸的改变(resize事件发生时);计算 offsetWidth 属性 和 offsetHeight 属性;设置style 属性的值。

重排: 当 DOM 的变化影响了元素的几何信息(元素的位置和尺寸的大小),浏览器就需要重新计算元素的几何属性,然后将其放置到正确的位置,这个过程叫做重排,也叫做回流。

重排优化建议

  • 将多次样式修改,集中为一次,或者直接更改类名而不是修改样式。
  • 分离读写操作。
  • 将 DOM 离线,使用 display:none,先将元素隐藏,这个时候元素不存在渲染树上,操作完之后再将其显示出来,这个过程只触发两次重排。
  • 使用 position 属性为 absolute 和 fixed 脱离文档流,使得就算该元素内的元素发生重排,也只是一个局部的重排,不会导致周围的元素的几何属性的变化。
  • 将需要经常获哪些会导致重排的属性值缓存到变量中。
  • 优化动画,将动画效果应用到 position 属性为 absolute 和 fixed 的元素上,减少对周围元素的影响;启动 GPU 加速,GPU 是专门为处理图形而设计的,在速度和耗能上更有效率。

11. Cookie、localStorage、sessionStorage的区别

cookie 是存储在浏览器中的纯文本,主要用于存储用户的登录信息,它的大小只有4KB左右。

sessionStorage 临时的会话存储,只要当前的会话窗口没有关闭,存储的信息就不会丢失,即使刷新页面或者在编辑页面的代码,存储的会话信息也不会丢失。

localStorage 只要不主动清除,就会一直将数据存储在客户端的存储方式,即使关闭了浏览器,下次打开的时候,仍然可以看到之前未主动清除的数据。

cookie 和 storage 的区别:

  • cookie 可以兼容所有浏览器(本地cookie谷歌不支持),storage 不支持IE6~8;
  • 两者的存储大小都有限制,cookie 的存储大小为4KB左右,而 storage 的存储大小为5MB左右。
  • cookie 有过期时间,而 localStorage 是可以永久存储(不主动清除的话)。
  • 基于安全考虑 cookie 可以被浏览器禁止,而 localStorage 无法禁用。

12. this/call/apply/bind

this 的指向,是在函数被调用的时候确定的,也就是执行上下文创建时确定的。在函数执行过程中,this 一但被确定,就不可以变更。ES6中的箭头函数,会根据当前的词法作用域来绝对 this ,箭头函数会继承外层函数调用的 this 绑定,若是没有外层函数,则是绑定到全局对象。

call(thisObj,a,b,c)、apply(thisObj,[a,b,c])、bind(thisObj,a,b,c)

call、apply 和 bind 第一个参数都是传入一个新的对象,这个对象就是接下来 this 指向的对象,也就是将一个函数的执行上下文从初始的上下文改变为由 thisObj 指向的对象。之后的参数就是传入函数的参数,不同之处在于 apply 将需要传入的参数放入一个数组中当作第二个参数,而 call 和 bind 是依次传入;在执行方面,call 和 apply 是直接执行对应的函数,而 bind 则是返回一个改变了 this 指向的函数,需要被调用的时候才会执行。

13. new操作符做了哪些操作

  • 首先创建一个新的对象
  • 设置原型,将对象的原型(proto)指向构造函数的 prototype 对象
  • 让函数的 this 指向这个对象,执行构造函数的代码,也就为这个对象添加属性和方法
  • 判断函数的返回值类型,如果是值类型,返回创建的对象,如果是引用类型,就返回这个引用类型的对象。

14. 箭头函数和普通函数的区别

  • 箭头函数比普通函数更加简洁、清晰,箭头函数是用 => 定义函数,普通函数需要用 function 定义函数。箭头函数如果没有参数的话,可以直接写一个括号即可,如果只有一个参数可以省略括号,如果多个参数,直接用逗号隔开,如果函数体不需要返回值,且只有一句话,可以给这个语句前面加一个void关键字。最常用的就是调用一个函数: let fn = () =>void doesNotReturn()。
  • 箭头函数没有自己的 this ,它会捕获(继承)其所在上下文的 this 值,作为自己的 this 值,定义的时候就已经确定了,之后就不会再改变。
  • call()、apply()、bind()等方法不会改变箭头函数的 this 指向。
  • 箭头函数不能作为构造函数,所以不能使用 new 关键字。
  • 箭头函数没有自己的 arguments ,在箭头函数中访问到的 arguments 实际上获得的是外层局部(函数)执行环境中的值。
  • 箭头函数没有 prototype。
  • 箭头函数不能作为 Generator 函数,不能使用 yield 关键字。

15. 事件委托(事件代理)

事件委托就是将本应该注册在子元素上的处理事件注册到父元素上,然后利用事件冒泡的机制触发相应的注册事件。

事件流:事件捕获 --> 目标阶段 --> 事件冒泡

  • 捕获阶段:在事件冒泡的模型中,捕获阶段不会响应任何事件;
  • 目标阶段:目标阶段就是指事件响应到触发事件的最底层元素上;
  • 冒泡阶段:冒泡阶段就是事件的触发响应会从最底层目标一层层地向外到最外层(根节点),可以使用 stopPropagation() 阻止事件冒泡。

事件委托的优势:

  • 减少 DOM 的操作,提高性能。
  • 动态绑定事件,随时可以增加子元素,新增的子元素会自动有相应的处理事件。

适合用事件委托的事件:click,mousedown,mouseup,keydown,keyup,keypress。

Tips:  mouseover和mouseout虽然也有事件冒泡,但是处理它们的时候需要特别的注意,因为需要经常计算它们的位置,处理起来不太容易。

16. 深拷贝和浅拷贝

深拷贝的方法:

  • JSON.parse(JSON.stringify());可以实现数组和对象的深拷贝,但不能处理函数和正则。函数会变为 null ,正则会变为空对象。
  • 函数库 lodash 的 _.cloneDeep 方法。
  • jQuery.extend() 方法。
  • 手写递归方法 

浅拷贝方法:

  • Object.assign();
  • 函数库 lodash 的 _.clone 方法;
  • 展开运算符...
  • Array.prototype.concat()
  • Array.prototype.slice()

17. Set、Map、WeakSet、WeakMap的区别

Set 和 WeakSet 都是存储不重复的值的集合,也就是值是唯一的,区别在于 Set 可以存储任何类型的数据;而 WeakSet 只能存储对象,不能是其他类型的值,同时 WeakSet 是不可遍历的。

Map 和 WeakMap 保存的是健值对,区别在于:Map中,任何值都可以作为它的健和值;WeakMap 只接受对象作为键名(null除外),不接受其他类型的值作为键名,同时键名所指的对象不计入垃圾回收机制。

18. map 和 forEach 的区别

map 方法不会改变原来的数组,会返回一个新的数组,新数组中的值是原数组调用函数处理后的值。

forEach 其实是一个迭代器,是挂载在可迭代对象原型上的方法,该方法会对每一个元素执行提供的函数,不会放回新的数组,对于空数组是不会执行回调函数的。不能使用 return 、continue 和 break 中断循环,只能使用 try/catch。

19. 有哪些数组去重的方法

  • 利用 Set 值唯一的特性进行去重,然后用 Array.from 将 Set 数据结构转为数组。Array.from(new Set(arr))。或者结合剩余运算符...:[ ...new Set(arr) ]。
  • 利用 Map 数据结构存值的特点。

  • 建立一个空对象,利用对象属性名不能重复的特性。
  • 使用循环结合 indexOf 判断某个值是否存在数组中的特性。

  • 使用 filter 返回符合条件的集合的方法。(filter + indexOf)

  • 使用 includes 判断数组是否包含某一项,会返回 true/false。

20. 把类数组转换为数组

  • 通过 call 调用数组的 slice 方法来实现转化:Array.prototype.slice.call(arrayLike)
  • 通过 call 调用数组的 splice 方法来实现转化:Array.prorotype.splice.call(arrayLike,0)
  • 通过 apply 调用数组的 concat 方法来实现转化:Array.prototype.concat.apply([],arrayLike)
  • 通过 Array.from 方法来实现:Array.from(arrayLike)

21. for...in 和 for...of 的区别

for...in 遍历对象获取的是键名; for...of 遍历对象获取的是对象的键值。

for...in 会遍历对象的整个原型链,性能比较差;for...of 只会遍历对象,不会遍历原型链。

for...in 遍历数组会返回数组中所有可枚举的属性(包括原型链上的可枚举属性); for...of 遍历数组只会放回数组的下标对应的属性。

for...in 主要用于遍历对象,不适用于遍历数组。for...of 可以用于遍历数组、类数组对象、字符串、Set、Map以及Generator对象。

22. JSON.stringify 有哪些缺点

  • 如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式,而不是对象的形式 
  • 如果obj里有RegExp(正则表达式的缩写)、Error对象,则序列化的结果将只得到空对象; 
  • 如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;
  • 如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null 
  • JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor; 
  • 如果对象中存在循环引用的情况也无法正确实现深拷贝;

23. SPA单页面的优缺点

优点:

  • 体验好,不刷新,减少请求,数据 ajax 异步获取,页面流程。
  • 前后端分离。
  • 减少服务端压力。
  • 共用一套后端程序代码,适配多端

缺点:

  • 首屏加载过慢。
  • SEO 不利于搜索引擎抓取。

24. DOM渲染过程中,遇到 script 标签会阻塞DOM渲染吗

会,script 标签的加载、解析和运行都会阻塞 DOM 的解析和渲染,因为 js 可以操作 DOM,浏览器为了防止渲染过程出现不可预测的结果,让 GUI 渲染线程和 js 引擎线程互斥,即解析器在遇到 script 标签的时候会立即解析并执行(或请求)脚本,文档的解析将停止,直到脚本执行完毕后才会继续。

解决方法:

  • 将 script 标签放在 html 文件底部,避免解析 DOM 时被阻塞。
  • 延迟脚本,在 script 标签上设置 defer 属性,告知浏览器立即下载脚本,但延迟执行,当浏览器解析完 html 文档时,在执行脚本。
  • 异步脚本,对于没有任何依赖的 JS 文件可以设置 async 属性,表示 JS 文件的下载和解析不会阻塞渲染。

css 不会阻塞 DOM 的解析,但 css 会阻塞 render tree 的生成,从而阻塞 DOM 的渲染。css 会阻塞 js 的执行。

25. 前端性能优化

  • 减少 http 请求,可以设置 connection 为 keep-alive 保持 TCP 通道的持久性连接,也可以使用 HTTP2 的多路复用。
  • 善于缓存,可以设置强缓存和协商缓存。
  • 静态资源可以使用 CDN(内容分发网络),将静态资源分发到CDN的边缘网络节点,这样就可以就近获取所需内容,大幅减少光纤传输距离。
  • 压缩文件,减少文件的下载速度,提高用户的体验性。
  • 减少重绘重排。
  • 将 css 放在文件头部,将 js 文件放在文件底部。
  • 使用事件委托,节省内存。
  • 通过 webpack 按需加载代码,提取第三方库代码,减少 ES6 转 ES5 的冗余代码。
  • 图片的优化:图片的压缩、图片的延迟加载、响应式图片、使用 webp 格式的图片(具有更优的图像数据压缩算法,能带来更小体积的图片,具备有损和无损的严肃哦模式、Alpha透明以及动画的特性,在jpeg和png上的转化效果都相当的优秀、稳定和统一)。

26. 哪些情况会导致内存泄漏

  • 意外的全局变量:由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收 
  • 被遗忘的计时器或回调函数:设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。 
  • 脱离 DOM 的引用:获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。 
  • 闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中。

27. options 请求

options 是预检请求,可以用于检测服务器允许的 http 请求。当发起跨域请求的时候,由于安全原因,触发一定的条件时浏览器会在正式请求前亲自发起 options 请求,即 CORS 预检请求,服务器若接受跨域请求,浏览器才可以继续发起正式的请求。

28(持续更新+寻找工作机会ing)