【面试宝典】高频前端面试题之JavaScript原理篇

807 阅读13分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

引言

本文对JS中一些重要的原理以问题的形式进行整理汇总。本文章节结构以从易到难进行组织,建议读者按章节顺序进行阅读。希望读者读完本文,有一定的启发思考,也能对自己的JS掌握程度有一定的认识,对缺漏之处进行弥补,对JS有更好的掌握。

笔者也会站在面试者的角度对下述问题进行回答,并加以适当的分析。

1. JavaScript有哪些数据类型,它们的区别是什么?

JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。

  • 基本数据类型:Undefined、Null、Boolean、Number、String、Symbol、BigInt
  • 引用数据类型:Object

其中 Symbol 和 BigInt 是ES6 中新增的数据类型:

  • Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
  • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

两种类型的区别在于存储位置的不同:

  • 基本数据类型是直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
  • 引用数据类型是存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

2. 数据类型检测的方式有哪些

数据类型判断主要可以使用以下三种方式进行判断,但是都具有一定的局限性,因此在项目实际开发过程中会重写类型判断方法。

typeof:Undefined、Boolean、Number、String、Symbol、Function 等基本数据类型,但是对于其他的都会认为是 object,例如 Null、Date 等,typeof返回的是一个变量的基本类型。

instanceof:可以准确地判断复杂引用数据类型,instanceof返回的是一个布尔值。

Object.prototype.toString.call():调用该方法,统一返回格式“[object Xxx]”的字符串。

function typeOf(obj) {
   let res = Object.prototype.toString.call(obj).split(' ')[1]
   res = res.substring(0, res.length - 1).toLowerCase()
   return res
}

typeOf([])        // 'array'
typeOf({})        // 'object'
typeOf(new Date)  // 'date'

3. null和undefined有什么区别

首先 null 和 undefined 都是基本数据类型,undefined 代表的含义是未定义,null 代表的含义是空对象

最初设计JS的时候只有Null,但是使用 typeof 进行判断时,Null 类型化会返回 “object”,因此JS的作者认为一个判断空的数据类型,会被判断为引用类型存在问题,因此创造了undefined

4.为什么 0.1 + 0.2 != 0.3 ?

0.1 + 0.2 != 0.3 是因为在进制转换和进阶运算的过程中出现精度损失。

因为计算机底层是采用二进制数进行对阶运算的,在进制之间进行转换的过程中精度会损失因此0.1 + 0.2 != 0.3 ?

一般在项目中会采用toFaild()进行数据四舍五入,或者做乘10转化为整数放置精度损失之后在除10的处理。

5. 谈谈let、const、var的区别

(1)块级作用域: 块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:

  • 内层变量可能覆盖外层变量
  • 用来计数的循环变量泄露为全局变量

(2)变量提升: var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。

(3)给全局添加属性: 浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。

(4)重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。

(5)暂时性死区: 在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。

(6)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。

(7)指针指向: let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。

6. 谈谈箭头函数与普通函数的区别

(1)箭头函数比普通函数在代码书写层面更加简洁

(2)箭头函数不能被new

(3)箭头函数没有自己的this

(4)箭头函数没有自己的arguments

(5)箭头函数没有prototype

(6)箭头函数不能用作Generator函数,不能使用yeild关键字

(7)箭头函数继承来的this指向永远不会改变

(8)不能通过call()、apply()、bind()等方法改变箭头函数中this的指向

7. 谈谈你对深浅拷贝的理解

浅拷贝即只复制对象的引用,所以副本最终也是指向父对象在堆内存中的对象,无论是副本还是父对象修改了这个对象,副本或者父对象都会因此发生同样的改变

而深拷贝则是直接复制父对象在堆内存中的对象,最终在堆内存中生成一个独立的,与父对象无关的新对象。深拷贝的对象虽然与父对象无关,但是却与父对象一致。当深拷贝完成之后,如果对父对象进行了改变,不会影响到深拷贝的副本。

深拷贝一般使用JSON.parse()嵌套JSON.stringify()得形式,浅拷贝一般使用Object.assign()

8、 谈谈你对 new 实现的理解

  1. 首先创一个新的空对象。
  2. 根据原型链,设置空对象的 __proto__ 为构造函数的 prototype
  3. 构造函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)。
  4. 判断函数的返回值类型,如果是引用类型,就返回这个引用类型的对象。

9. 谈谈你对作用域和作用域链的理解

作用域: 作用域是定义变量的区域,它有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找。

作用域链: 在函数执行过程中,每遇到一个变量,都会检索从哪里获取和存储数据,该过程从作用域链头部,也就是从活动对象开始搜索,查找同名的标识符,如果找到了就使用这个标识符对应的变量,如果没有则继续搜索作用域链中的下一个对象,如果搜索完所有对象都未找到,则认为该标识符未定义,函数执行过程中,每个标识符都要经历这样的搜索过程。

因为作用域链是栈的结构,全局变量在栈底,每次访问全局变量都会遍历一次栈,这样肯定会影响效率,所以应该减少定义全局变量,从而进行优化。

变量查找时原型链是优先于作用域链的。js引擎先在函数AO对象查找,再到原型链查找,接着是作用域链。

10. 谈谈你对原型和原型链的理解

原型: 在 js 中我们是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对 象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法

原型链: 其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。在js中,用 proto 属性来表示一个对象的原型链。当查找一个对象的属性时,js 会向上遍历原型链,直到找到给定名称的属性为止

__proto__这个属性只有在firefox(火狐)或者chrome(谷歌)浏览器中才是公开允许访问的。

简单来说,每个对象都会在其内部初始化一个属性,就是 proto,当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去__proto__里找这个属性,这个 proto__又会有自己的__proto,于是就这样一直找下去,这便是原型链的概念。

变量查找时原型链是优先于作用域链的。js引擎先在函数AO对象查找,再到原型链查找,接着是作用域链。

11. 谈谈你对Promise的理解

Promise是异步编程的一种解决方案,它是一个构造函数,可以通过new Promise得到它的实例。在Promise上有两个常用函数和一个常用方法。分别是resolve (成功之后的回调函数)和reject (失败之后的回调函数)还有.then() 方法。

在工作中比较常用的是Promise.all()和Promise.race方法

Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。

Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。

12. 如何将异步操作同步化

1.Promise得then链,缺点:在then链中哪个环节出错了不太好调查,代码书写不美观(不推荐使用)

2.async /await :async和await底层实现的原理是基于Generator函数实现的,generator 函数返回一个遍历器对象,遍历器对象 每次调用next 方法,generator 函数 yield 后面的表达式即为 返回对象 value属性的值。

13. 谈谈你对闭包的理解

我个人觉得比较经典的就是VUE底层响应式原理封装的闭包,它是将Object.defineProperty外层包裹Object.defineRecvite形成闭包环境,主要是因为内部函数可能需要用到一些局部变量,但是又需要避免变量污染全局,因此引入了闭包。

一般在工作当中不常使用闭包,因为闭包可能会引起内存泄漏和对处理速度产生一些负面影响影响,因为闭包查找变量会经过作用域链

所以从代码的整体性能角度出发的话,我一般除非不得不用,不然很少使用闭包

14. 谈谈你对垃圾回收机制的理解

在 JavaScript 内存管理中有一个概念叫做 可达性,就是那些以某种方式可访问或者说可用的值,它们被保证存储在内存中,反之不可访问则需回收

JS垃圾回收机制最常见的便是标记清除算法引用计数算法,但是V8引擎之后各大浏览器主要是采用标记清除算法进行垃圾回收的

引用计数法,这其实是早先的一种垃圾回收算法,它把 对象是否不再需要 简化定义为 对象有没有其他对象引用到它,如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收,目前很少使用这种算法了,因为它的问题很多

标记清除算法,主要分为 标记清除 两个阶段,标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁

但是标记清除算法有一个很大的缺点,就是在清除之后,剩余的对象内存位置是不变的,也会导致空闲内存空间是不连续的,出现了 内存碎片,并且由于剩余空闲内存不是一整块,它是由不同大小内存组成的内存列表,这就牵扯出了内存分配的问题。而 标记整理(Mark-Compact)算法 就可以有效地解决,它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存

15. 谈谈你对Event Loop的理解

JavaScript是一种单线程语言,所有任务都在一个线程上完成。一旦遇到大量任务或者遇到一个耗时的任务,比如加载一个高清图片,网页就会出现"假死",因为JavaScript停不下来,也就无法响应用户的行为。为了防止主线程的阻塞,JavaScript 有了 同步 和 异步 的概念。所以 JavaScript 便使用一套机制去处理同步和异步操作,那就是事件循环 (Event Loop)。

  • 所有同步任务都在主线程上依次执行,形成一个执行栈(调用栈),异步任务则放入一个任务队列

  • 当执行栈中任务执行完,再去检查微任务队列里的微任务是否为空,有就执行,如果执行微任务过程中又遇到微任务,就添加到微任务队列末尾继续执行,把微任务全部执行完

  • 微任务执行完后,再到任务队列检查宏任务是否为空,有就取出最先进入队列的宏任务压入执行栈中执行其同步代码

  • 然后回到第2步执行该宏任务中的微任务,如此反复,直到宏任务也执行完,如此循环

推荐

如果有想要继续学习浏览器篇的读者,可以观看笔者的另一篇文章【面试宝典】高频前端面试题之浏览器篇 - 掘金 (juejin.cn)

如果有想要继续学习CSS原理篇的读者,可以观看笔者的另一篇文章【面试宝典】高频前端面试题之CSS篇 - 掘金 (juejin.cn)

如果有想要继续学习Vue原理篇的读者,可以观看笔者的另一篇文章【面试宝典】高频前端面试题之Vue原理篇 - 掘金 (juejin.cn)

如果有想要继续学习手写JS常用方法的读者,可以观看笔者的另一篇文章【面试宝典】高频前端面试题之手写常用JS方法 - 掘金 (juejin.cn)

结语

本文为笔者个人学习笔记,如有谬误,还请告知,万分感谢!如果本文对你有所帮助,还请点个关注点个赞~,您的支持是笔者不断更新的动力。