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
typeof NaN; // "number"

NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN != NaN
为 true。

null是对象吗?为什么?

1
2
3
4
5
6
7
8
typeof 运算符对基本数据类型的运算:
typeof 'str' // 'string'
typeof NaN // 'number'
typeof 1 // 'number'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof Symbol() // 'symbol'
typeof null // 'object'

结论: null不是对象。

解释: 虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object 。

JavaScript 有几种类型的值?你能画一下他们的内存图吗?

涉及知识点:

  • 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
  • 堆:引用数据类型(对象、数组和函数)
  1. 两种类型的区别是:存储位置不同。

  2. 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。

  3. 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在
    栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实
    体。

回答

1
2
3
4
5
6
7
8
js 可以分为两种类型的值,一种是基本数据类型,一种是复杂数据类型。

基本数据类型....(参考1

复杂数据类型指的是 Object 类型,所有其他的如 ArrayDate 等数据类型都可以理解为 Object 类型的子类。

两种类型间的主要区别是它们的存储位置不同,基本数据类型的值直接保存在栈中,而复杂数据类型的值保存在堆中,通过使用在栈中
保存对应的指针来获取堆中的值。

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
2
3
4
5
6
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, 1000);
}
//55555

这道题挺经典的,输出结果是什么呢?结果是1000毫秒之后,输出5个5(隔一秒输出在1000上乘个i就行)
原因是,for循环在主线程内,setTimeout是异步方法,在任务队列里面,只有主线程执行完后,任务队列才执行,此时i的值已经是5,所以得到结果是5个5

那么怎么解决呢?其实思路很容易,只要每次循环把当前的i值传入setTimeout内即可

方法1:使用let

1
2
3
4
5
6
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, 1000);
}
//01234

使用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
2
3
4
5
6
7
8
for (var i = 0; i < 5; i++) {
function a(i) {
setTimeout(() => {
console.log(i)
}, 1000);
}
a(i)
}

方法3:IIFE(立即执行函数)

1
2
3
4
5
6
7
for (var i = 1; i <= 5; i++) {
~function(i) {
setTimeout(() => {
console.log(i)
}, 1000);
}(i)
}

MDN
IIFE(立即调用函数表达式)
IIFE( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。

1
2
3
(function () {
statements
})();

这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分。第一部分是包围在 圆括号运算符 () 里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。

第二部分再一次使用 () 创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。

方法4:使用闭包

1
2
3
4
5
6
for (var i = 1; i <= 5; i++) {
setTimeout(
(i =>
() => console.log(i)
)(i), 1000);
}

方法5:setTimeout第三个参数传入i(是的,你没看错,setTimeout还有第三个参数)

1
2
3
for (var i = 1; i <= 5; i++) {
setTimeout((i) => console.log(i),1000,i);
}
参考文章

0.1+0.2 !== 0.3?
JavaScript 浮点数陷阱及解法
经典面试题 for循环内setTimeout顺序输出的解法


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!