[JS]面试八股文
文章目录
- JS从运行到结束
-
- 什么是闭包
- 什么是作用域链
- 闭包的数据是如何保存的?
- let和const是如何实现块级作用域的?
- 什么是变量提升
- 垃圾回收是如何进行的?
- this指向什么
- this的绑定方式
- 类型
-
- JS中的基本类型有哪些
- null与undefined的区别
- 如何判断类型
- 为什么0.1+0.2不等于0.3?如何解决?
- 类型转换
- x === x在哪些情况下不为 true?
- 计算结果
- 怎样使对象不可变
- 原型与继承
-
- 什么是原型
- 实现new
- 如何实现类
- ES5怎么实现继承
- ES6继承的两条原型链
- ES5的继承与ES6的继承有什么区别
- 异步
-
- 什么是事件循环?什么是消息队列?
- 如何实现setTimeout?
- 宏任务与微任务
- 浏览器事件循环与node事件循环有什么区别
- Promise原理
- Promise 中为什么要引入微任务?
- Promise 中是如何实现回调函数返回值穿透的?
- Promise 出错后,是怎么通过“冒泡”传递给最后那个捕获
- async/await执行顺序
- 什么是Generator
- Node
-
- node的事件循环
- setTimeout 和 setImmediate 的优先级
- process.nextTick和setImmediate有什么区别?
- async/await 和 Promise 处理错误有什么不同.
- Node的require加载机制
- module.export和export有什么区别?
- a.js 和 b.js 互相引用, 会出现循环引用的问题吗 ?
- Node能否充分利用多核处理器?
- cluster模块有什么作用?
- 不同进程如何监听同一端口?
- EventEmitter是什么?
- 流是什么?
- ReadFile和createReadStream函数有什么区别?
- 什么是yarn和npm?为什么要用yarn代替npm呢?
- 反应堆设计模式是什么?
- 设计模式
-
- 熟悉哪些设计模式?
- TS
-
- type和interface有什么区别?
JS从运行到结束
什么是闭包
答:闭包(Closure)是指“一个函数及其词法环境(即该函数被定义时的作用域)的组合”。简单来说,闭包让一个函数可以访问其外部作用域中的变量,即使在外部函数执行完毕后,这些变量依然可以被内部函数访问和操作
在调用一个函数A时返回了一个函数B,并在B中访问了A中的变量,就会形成一个闭包,在A的执行上下文中会保存一个closure(A)的对象,并将B中访问的变量保存在堆中。闭包其实只是一个绑定了执行环境的函数。
即使函数A的执行上下文退出了,closure(A)仍然存在在函数B的作用域链中,当B在外部被调用时,会沿着“当前执行上下文–> closure(A) –> 全局执行上下文”的顺序来查找变量,从而访问的A中的变量。
闭包的两个非常重要的应用场景,他们分别是模块化与柯里化。
模块化是指创建一个自执行函数,把接口以闭包的形式暴露出来,react的hooks也是采用此原理。
柯里化是指一个函数A接收另一个函数为参数,并返回一个新的函数,这个新的函数可以处理A的剩余参数。是函数式编程的一种重要应用。
反柯里化是将柯里化函数还原成普通函数。
什么是作用域链
答:当一个函数执行时,会为其创建一个执行上下文。执行上下文中存在变量环境和词法环境,变量环境中保存会变量提升的变量,词法环境中保存形成块级作用域的变量。而在变量环境中包含一个外部引用outer,用来指向外部的执行上下文(根据词法作用域决定)。如果一个变量没有在自身的环境中找到,就会在outer指向的执行上下文中寻找,这个查找的路径被称为作用域链。
闭包的数据是如何保存的?
答:原始数据类型都保存在栈中,引用数据类型都保存在堆中。对于闭包而言,在外部函数创建了执行上下文并执行时,遇到内部函数,会先对内部函数进行快速词法扫描,检测到引用了外部函数中的变量,判断出这是一个闭包,于是在堆空间会创建一个closure(外部函数名)的对象,用来保存内部函数有引用的外部变量,从而保存了数据。
let和const是如何实现块级作用域的?
答:函数内部通过 var 声明的变量,在编译阶段全都被存放到变量环境里面了。通过 let 声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。
在词法环境内部,维护了一个小型栈结构。进入一个作用域块后,就会把该作用域块内部的let和const变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,从而实现块级作用域。
在查找变量时,需要在词法环境和变量环境中查找,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。
什么是变量提升
答:引擎会在解释 JavaScript 代码之前首先对其进行编译。 编译阶段中的一部分工作就是找到所有的声明, 并用合适的作用域将它们关联起来。
包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动”到了最上面。 这个过程就叫作提升。在编译时先声明,在执行时赋值。
函数会首先被提升,然后才是变量。即同名变量在声明阶段以函数为准。后面的函数声明还是可以覆盖前面的函数声明。
用let和const声明的变量不会被提升。
垃圾回收是如何进行的?
答:对于栈中的数据,JS有一个记录当前执行状态的指针(称为 ESP),指向当前正在执行的上下文。当一个上下文执行完毕时,ESP下移,之前的执行上下文虽然保存在栈内存中,但是已经是无效内存了,当再次调用另外一个函数时,这块内容会被直接覆盖掉,用来存放另外一个函数的执行上下文。
对于堆中的数据,JS会使用垃圾回收器进行回收。
在V8引擎中,堆分为新生代和老生代两个区域,新生代通常只有1~8M的容量,用于存放生存时间短的变量,使用副垃圾回收器进行回收;老生代中主要存放生存时间较久的变量,使用主垃圾回收器进行回收。
新生代中用 Scavenge 算法来处理。是把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域,新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。
在垃圾回收过程中,首先要对对象区域中的垃圾做标记;标记完成之后,就进入垃圾清理阶段,副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来,复制后空闲区域就没有垃圾了。完成复制后,对象区域与空闲区域进行角色翻转,这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。
JavaScript 引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。
主垃圾回收器是采用标记 - 整理(Mark-Compact)的算法进行垃圾回收的。
标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,如果一个地址没有被引用了,说明是垃圾数据,否则是活动对象。
接下来进行整理,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
this指向什么
this 的指向,是在调用函数时根据执行上下文所动态确定的。
this的绑定方式
传统this绑定方式:
默认绑定:无法应用其他规则时候的默认规则,例如函数独立调用,this指向全局对象
隐式绑定:函数被引用成一个对象的属性,this指向该对象
显示绑定:使用call/apply/bind将函数绑定至一个对象,this指向该对象
new绑定:this指向新对象,如果构造函数中显式返回一个对象,那么 this 就指向这个返回的对象;如果返回的不是一个对象,那么 this 仍然指向实例。
new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
箭头函数中的this取决于外层作用域,并且一经绑定,this的指向就无法修改了。
类型
JS中的基本类型有哪些
答:JavaScript 有八种内置类型:
空值(null)、未定义(undefined)、布尔值( boolean)、数字(number)、字符串(string)、对象(object)、符号(symbol, ES6 中新增)、BigInt(大整数,ES2020新增)。
除对象之外,其他统称为“基本类型”。
基本类型除了null和undefined之外,都可以被包装成对象,被包装后可以使用valueOf进行开箱,获得基础类型值。
null与undefined的区别
答:undefined类型表示此处应该有值,但还未被定义,它是一个变量,而不是一个关键字,这是JS的设计错误之一。为了避免无意中的篡改,可以使用void 0获取undefined。
null表示空值,是关键字。
如何判断类型
typeof
判断null时会返回object,判断函数时会返回function,其他都返回其正常类型的字符串。instanceof
a instanceof b 表示判断 a是否是b的实例,即a的原型链上是否有b的构造函数。
对于基本类型无法判断,但是若基本类型被封装进原生函数,则可以判断。Object.prototype.toString.call()
返回[object type]
,其中 type 是对象的类型。实际上返回的是内部属性[[Class]]
。
对象的[[Class]]属性和创建该对象的内建原生构造函数相对应,但null和undefined的原生构造函数不存在,其仍有[[Class]]属性;其他基本类型取其包装后的构造函数。
在 JavaScript 中,没有任何方法可以更改私有的 Class 属性,因此 Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。constructor
使用constructor也可以查看类型,但它会返回构造函数本身,且无法判断没有构造函数的undefined和null。
const checkType = item => {return Object.prototype.toString.call(item).slice(8, -1);
}
为什么0.1+0.2不等于0.3?如何解决?
答:JS中使用双精度浮点数,其精度问题导致0.1+0.2与0.3相差一个微小的值。这里错误的不是结论,而是比较的方法。可以使用JS提供的最小精度值解决:
Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON
类型转换
基本类型 Number、String、Boolean、Symbol 在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象。