JS基础面试题(一)- 0.1+0.2为什么不等于0.3&&typeof NaN ||null&&setTimeout输出--同步与异步
介绍 js 的基本数据类型
js 一共有六种基本数据类型,分别是 Undefined、Null、Boolean、Number、String
,还有在 ES6 中新增的 Symbol 和 ES10 中新增的 BigInt 类型。
Symbol 代表创建后独一无二且不可变的数据类型,它的出现我认为主要是为了解决可能出现的全局变量冲突的问题。
BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number (2^53 -1) 能够表示的安全整数范围。
typeof NaN 的结果是什么?
NaN 意指“不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出
数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。
1 |
|
NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN != NaN
为 true。
null是对象吗?为什么?
1 |
|
结论: null不是对象。
解释: 虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object 。
JavaScript 有几种类型的值?你能画一下他们的内存图吗?
涉及知识点:
- 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
- 堆:引用数据类型(对象、数组和函数)
两种类型的区别是:存储位置不同。
原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。
引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在
栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实
体。
回答
1 |
|
0.1+0.2为什么不等于0.3?
当计算机计算 0.1+0.2 的时候,实际上计算的是这两个数字在计算机里所存储的二进制,0.1 和 0.2 在转换为二进制表示的时候会出现位数无限循环的情况。js 中是以 64 位双精度格式来存储数字的,只有 53 位的有效数字,超过这个长度的位数会被截取掉这样就造成了精度丢失的问题。这是第一个会造成精度丢失的地方。在对两个以 64 位双精度格式的数据进行计算的时候,首先会进行对阶的处理,对阶指的是将阶码对齐,也就是将小数点的位置对齐后,再进行计算,一般是小阶向大阶对齐,因此小阶的数在对齐的过程中,有效数字会向右移动,移动后超过有效位数的位会被截取掉,这是第二个可能会出现精度丢失的地方。当两个数据阶码对齐后,进行相加运算后,得到的结果可能会超过 53 位有效数字,因此超过的位数也会被截取掉,这是可能发生精度丢失的第三个地方。
对于这样的情况,我们可以将其转换为整数后再进行运算,运算后再转换为对应的小数,以这种方式来解决这个问题。
(toPrecision vs toFixed –toPrecision 是处理精度,精度是从左至右第一个不为0的数开始数起。
–toFixed 是小数点后指定位数取整,从小数点开始数起。)
我们还可以将两个数相加的结果和右边相减,如果相减的结果小于一个极小数,那么我们就可以认定结果是相等的,这个极小数可以
使用 es6 的 Number.EPSILON
setTimeout输出值的时候,如何实现i按序输出?
1 |
|
这道题挺经典的,输出结果是什么呢?结果是1000毫秒之后,输出5个5(隔一秒输出在1000上乘个i就行)
原因是,for循环在主线程内,setTimeout是异步方法,在任务队列里面,只有主线程执行完后,任务队列才执行,此时i的值已经是5,所以得到结果是5个5
那么怎么解决呢?其实思路很容易,只要每次循环把当前的i值传入setTimeout内即可
方法1:使用let
1 |
|
使用let 相当于每次循环的时候都新建了1个i并为其赋值
这是因为第一个代码块中setTimeout 的 console.log(i); 的i是 var 定义的,所以是函数级的作用域,不属于 for 循环体,属于 全局变量。等到 for 循环结束,i 已经等于 5 了,这个时候再执行 setTimeout 的五个回调函数(参考上面对事件机制的阐述),里面的 console.log(i); 的 i 去向上找作用域,只能找到 全局作用下 的 i,即 5。所以输出都是 5。
而let是代码块的作用域,即是局部变量,所以每一次 for 循环,console.log(i); 都引用到 for 代码块作用域下的i,因为这样被引用,所以 for 循环结束后,这些作用域在 setTimeout 未执行前都不会被释放。
方法2:定义函数并传值
1 |
|
方法3:IIFE(立即执行函数)
1 |
|
MDN
IIFE(立即调用函数表达式)
IIFE( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。
1 |
|
这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分。第一部分是包围在 圆括号运算符 () 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。
第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。
方法4:使用闭包
1 |
|
方法5:setTimeout第三个参数传入i(是的,你没看错,setTimeout还有第三个参数)
1 |
|
参考文章
0.1+0.2 !== 0.3?
JavaScript 浮点数陷阱及解法
经典面试题 for循环内setTimeout顺序输出的解法
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!