近期整理了一下高频的前端面试题,分享给大家一起来学习。如有问题,欢迎指正!
前端面试题系列文章:
【3】「2021」高频前端面试题汇总之JavaScript篇(上)
【4】「2021」高频前端面试题汇总之JavaScript篇(下)
一、数据类型
1. JavaScript有哪些数据类型,它们的区别?
JavaScript共有八种数据类型,分别是 Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。
其中 Symbol 和 BigInt 是ES6 中新增的数据类型:
- Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
- BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
这些数据可以分为原始数据类型和引用数据类型:
- 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
- 堆:引用数据类型(对象、数组和函数)
两种类型的区别在于存储位置的不同:
- 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
- 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:
- 在数据结构中,栈中数据的存取方式为先进后出。
- 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。
在操作系统中,内存被分为栈区和堆区:
- 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
- 堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。
2. 数据类型检测的方式有哪些
(1)typeof
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof []); // object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object
其中数组、对象、null都会被判断为object,其他判断都正确。
(2)instanceof
instanceof
可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
可以看到,instanceof
只能正确判断引用数据类型,而不能判断基本数据类型。instanceof
运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype
属性。
(3) constructor
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
constructor
有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor
对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor
就不能用来判断数据类型了:
function Fn(){};
Fn.prototype = new Array();
var f = new Fn();
console.log(f.constructor=Fn); // false
console.log(f.constructor=Array); // true
(4)Object.prototype.toString.call()
Object.prototype.toString.call()
使用 Object 对象的原型方法 toString 来判断数据类型:
var a = Object.prototype.toString;
console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));
同样是检测对象obj调用toString方法,obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么?
这是因为toString是Object的原型方法,而Array、function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object原型上的toString方法。
3. 判断数组的方式有哪些
- 通过Object.prototype.toString.call()做判断
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
- 通过原型链做判断
obj.__proto__ ===Array.prototype;
- 通过ES6的Array.isArray()做判断
Array.isArrray(obj);
- 通过instanceof做判断
objinstanceof Array
- 通过Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)
4. null和undefined区别
首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。
undefined 在 JavaScript 中不是一个保留字,这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。
当对这两种类型使用 typeof 进行判断时,Null 类型化会返回 “object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。
5. typeof null 的结果是什么,为什么?
typeof null 的结果是Object。
在 JavaScript 第一个版本中,所有值都存储在 32 位的单元中,每个单元包含一个小的 类型标签(1-3 bits) 以及当前要存储值的真实数据。类型标签存储在每个单元的低位中,共有五种数据类型:
000: object - 当前存储的数据指向一个对象。
1: int - 当前存储的数据是一个 31 位的有符号整数。
010: double - 当前存储的数据指向一个双精度的浮点数。
100: string - 当前存储的数据指向一个字符串。
110: boolean - 当前存储的数据是布尔值。
如果最低位是 1,则类型标签标志位的长度只有一位;如果最低位是 0,则类型标签标志位的长度占三位,为存储其他四种数据类型提供了额外两个 bit 的长度。
有两种特殊数据类型:
- undefined的值是 (-2)30(一个超出整数范围的数字);
- null 的值是机器码 NULL 指针(null 指针的值全是 0)
那也就是说null的类型标签也是000,和Object的类型标签一样,所以会被判定为Object。
6. intanceof 操作符的实现原理及实现
instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
function myInstanceof(left, right) {
// 获取对象的原型
let proto = Object.getPrototypeOf(left)
// 获取构造函数的 prototype 对象
let prototype = right.prototype;
// 判断构造函数的 prototype 对象是否在对象的原型链上
while (true) {
if (!proto) return false;
if (proto === prototype) return true;
// 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
proto = Object.getPrototypeOf(proto);
}
}
7. 为什么0.1+0.2 ! == 0.3,如何让其相等
在开发过程中遇到类似这样的问题:
let n1 = 0.1, n2 = 0.2
console.log(n1 + n2) // 0.30000000000000004
这里得到的不是想要的结果,要想等于0.3,就要把它进行转化:
(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入
toFixed(num)
方法可把 Number 四舍五入为指定小数位数的数字。那为什么会出现这样的结果呢?
计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。0.1的二进制是0.0001100110011001100...
(1100循环),0.2的二进制是:0.00110011001100...
(1100循环),这两个数的二进制都是无限循环的数。那JavaScript是如何处理无限循环的二进制小数呢?
一般我们认为数字包括整数和小数,但是在 JavaScript 中只有一种数字类型:Number,它的实现遵循IEEE 754标准,使用64位固定长度来表示,也就是标准的double双精度浮点数。在二进制科学表示法中,双精度浮点数的小数部分最多只能保留52位,再加上前面的1,其实就是保留53位有效数字,剩余的需要舍去,遵从“0舍1入”的原则。
根据这个原则,0.1和0.2的二进制数相加,再转化为十进制数就是:0.30000000000000004
。
下面看一下双精度数是如何保存的:
- 第一部分(蓝色):用来存储符号位(sign),用来区分正负数,0表示正数,占用1位
- 第二部分(绿色):用来存储指数(exponent),占用11位
- 第三部分(红色):用来存储小数(fraction),占用52位
对于0.1,它的二进制为:
0.00011001100110011001100110011001100110011001100110011001 10011...
转为科学计数法(科学计数法的结果就是浮点数):
1.1001100110011001100110011001100110011001100110011001*2^-4
可以看出0.1的符号位为0,指数位为-4,小数位为:
1001100110011001100110011001100110011001100110011001
那么问题又来了,指数位是负数,该如何保存呢?
IEEE标准规定了一个偏移量,对于指数部分,每次都加这个偏移量进行保存,这样即使指数是负数,那么加上这个偏移量也就是正数了。由于JavaScript的数字是双精度数,这里就以双精度数为例,它的指数部分为11位,能表示的范围就是0~2047,IEEE固定双精度数的偏移量为1023。
- 当指数位不全是0也不全是1时(规格化的数值),IEEE规定,阶码计算公式为 e-Bias。 此时e最小值是1,则1-1023= -1022,e最大值是2046,则2046-1023=1023,可以看到,这种情况下取值范围是
-1022~1013
。 - 当指数位全部是0的时候(非规格化的数值),IEEE规定,阶码的计算公式为1-Bias,即1-1023= -1022。
- 当指数位全部是1的时候(特殊值),IEEE规定这个浮点数可用来表示3个特殊值,分别是正无穷,负无穷,NaN。 具体的,小数位不为0的时候表示NaN;小数位为0时,当符号位s=0时表示正无穷,s=1时候表示负无穷。
对于上面的0.1的指数位为-4,-4+1023 = 1019 转化为二进制就是:1111111011
.
所以,0.1表示为:
0 1111111011 1001100110011001100110011001100110011001100110011001
说了这么多,是时候该最开始的问题了,如何实现0.1+0.2=0.3呢?
对于这个问题,一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON
属性,而它的值就是2-52,只要判断0.1+0.2-0.3
是否小于Number.EPSILON
,如果小于,就可以判断为0.1+0.2 ===0.3
function numberepsilon(arg1,arg2){
return Math.abs(arg1 - arg2) < Number.EPSILON;
}
console.log(numberepsilon(0.1 + 0.2, 0.3)); // true
8. 如何获取安全的 undefined 值?
因为 undefined 是一个标识符,所以可以被当作变量来使用和赋值,但是这样会影响 undefined 的正常判断。表达式 void ___ 没有返回值,因此返回结果是 undefined。void 并不改变表达式的结果,只是让表达式不返回值。因此可以用 void 0 来获得 undefined。
9. typeof NaN 的结果是什么?
NaN 指“不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。
typeof NaN; // "number"
NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN !== NaN 为 true。
10. isNaN 和 Number.isNaN 函数的区别?
- 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
- 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。
11. == 操作符的强制类型转换规则?
对于 ==
来说,如果对比双方的类型不一样,就会进行类型转换。假如对比 x
和 y
是否相同,就会进行如下判断流程:
- 首先会判断两者类型是否**相同,**相同的话就比较两者的大小;
- 类型不相同的话,就会进行类型转换;
- 会先判断是否在对比
null
和undefined
,是的话就会返回true
- 判断两者类型是否为
string
和number
,是的话就会将字符串转换为number
1 == '1'
↓
1 == 1
- 判断其中一方是否为
boolean
,是的话就会把boolean
转为number
再进行判断
'1' == true
↓
'1' == 1
↓
1 == 1
- 判断其中一方是否为
object
且另一方为string
、number
或者symbol
,是的话就会把object
转为原始类型再进行判断
'1' == { name: 'js' } ↓'1' == '[object Object]'
其流程图如下: