文章目录
前言
这里总结的所有面试题都是从牛客网找的各种大厂真实场景的面试题,并且做了汇总,各位看官看后做好总结,绝对可以应对90%JS相关的面试题。
1、javascript有几种数据类型?分别是什么?
JS数据类型分为两类:一类是基本数据类型
,包含7
种类型,分别是Number
、String
、Boolean
、BigInt
、Symbol
、Null
、Undefined
。另一类是引用数据类型
,通常用Object
代表,普通对象,数组,正则,日期,Math数学函数都属于Object。
本质区别:基本数据类型和引用数据类型它们在内存中的存储方式不同
。
基本数据类型是直接存储在栈
中的简单数据段,占据空间小,属于被频繁使用的数据。
引用数据类型是存储在堆内存
中,占据空间大。引用数据类型在栈中存储了指针
,该指针指向堆中该实体的起始地址
,当解释器寻找引用值时,会检索其在栈中的地址,取得地址后从堆中获得实体。
Symbol
是ES6新出的一种数据类型,这种数据类型的特点就是没有重复的数据
,可以作为object的key
。
BigInt
也是ES6新出的一种数据类型,这种数据类型的特点就是数据涵盖的范围大
,能够解决超出普通数据类型范围报错的问题
。
2、let,const和var有什么区别?
1.变量提升:var声明的变量存在变量提升,即变量可以在声明之前调用,值为undefined。let和const不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错
2.重复声明:var允许重复声明变量。let和const在同一作用域不允许重复声明变量
3.暂时性死区:var不存在暂时性死区。let和const存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量
4.变量修改:const 声明的变量不能被重新赋值,而 let 和 var 可以。但是,使用 const 声明的对象或数组可以修改它们的属性或元素。
5.块级作用域:let 和 const 声明的变量具有块级作用域,可以在不同的块中重新定义变量。而使用 var 声明的变量在不同的块中被重新定义,会影响整个函数的作用域。
3、说说你了解的暂时性死区
如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
上面代码中,在let命令声明变量tmp之前,都属于变量tmp的“死区”。
4,箭头函数
- 箭头函数声明的全都是匿名函数,不能用于构造函数,并且不具有arguments对象,
- 箭头函数中的this指向与普通函数不同
- 在普通函数中,this总是指向调用它的对象,如果用作构造函数,他指向创建的对象实例。
- 箭头函数没有自己的this,他的this是继承而来的,默认指向在定义时他所在的对象,而不是执行时的对象。
5. 常用的数组方法
- push:向数组末尾添加一个或多个元素,并返回新数组的长度
- pop:从数组末尾删除一个元素,并返回该元素的值
- shift:从数组开头删除一个元素,并返回该元素的值
- unshift:向数组开头添加一个或多个元素,并返回新数组的长度
- splice:从数组中删除或替换一个或多个元素,可以传入三个参数,分别是
开始位置、要删除的元素数量、插入的元素
(一直与slice记混,重点记一下) - slice:返回数组的一个片段或子数组,可以传入两个参数,分别是
开始索引位置、截取到的索引位置
- concat:将两个或多个数组合并成一个新数组
- join:将数组元素连接成一个字符串
- indexOf():返回要查找的元素在数组中的位置,如果没找到则返回 -1
- includes():返回要查找的元素在数组中的位置,找到返回true,否则false
- find():返回第一个匹配的元素
- reverse:将数组元素反转
- sort:对数组元素进行排序
- map:对数组中的每个元素执行指定的函数,并返回一个新数组
- filter:对数组中的每个元素执行指定的测试函数,并返回一个新数组,其中包含所有通过测试的元素
- reduce:对数组中的每个元素执行指定的累加器函数,并返回一个累加器值。(
这个函数在实际开发中是一个特别好用的方法,如果能用好可以大大提高开发效率,具体的使用方式不在这里展开,如果想进一步了解这个方法,可以到我的另一篇文章:
reduce()方法详解) - forEach:对数组中的每个元素执行指定的函数,无返回值
需要留意的是哪些方法会对原数组产生影响,哪些方法不会,这也是一个比较重要的特性
会改变原数组的方法:push(),pop(),shift(),unshift(),splice(),sort(),reverse()
6. js数据类型判断的方式
JavaScript有4种方法判断变量的类型,分别是typeof
、instanceof
、Object.prototype.toString.call()
(对象原型链判断方法)、 constructor
(用于引用数据类型)
1.typeof
:常用于判断基本数据类型
,对于引用数据类型除了function返回function,其余全部返回object
。
2.instanceof
:主要用于区分引用数据类型
,检测方法是检测的类型在当前实例的原型链上
,用其检测出来的结果都是true,不太适合用于简单数据类型的检测,检测过程繁琐且对于简单数据类型中的undefined, null, symbol检测不出来。
原理:验证当前类的原型prototype
是否会出现在实例
的原型链__proto__
上,只要在它的原型链上,则结果都为true。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,找到返回true,未找到返回false。
3.constructor
:用于检测引用数据类型
,检测方法是获取实例的构造函数判断和某个类是否相同
,如果相同就说明该数据是符合那个数据类型的,这种方法不会把原型链上的其他类也加入进来,避免了原型链的干扰。
4.Object.prototype.toString.call()
:适用于所有类型的判断检测
,检测方法是Object.prototype.toString.call(数据) 返回的是该数据类型的字符串。
原理:Object.prototype.toString 表示一个返回对象类型的字符串
,call()方法可以改变this的指向,那么把Object.prototype.toString()方法指向不同的数据类型上面,返回不同的结果
这四种判断数据类型的方法中,各种数据类型都能检测且检测精准的就是Object.prototype.toString.call()这种方法。
7. 垃圾回收机制
js引擎会定期找出那些不在使用的变量,然后释放其内存,通常由两种方式,标记清除法和引用计数法。
标记清除法:
- 垃圾收集器在运行时会给
所有的变量都加上一个标记
,假设内存中所有的对象都是垃圾,全部标记为0
- 然后从各个跟对象开始遍历把不是垃圾的节点改成1
- 再清除所有标记为0的垃圾,销毁并回收他们所占用的内存空间
- 最后再把内存中的对象标记修改为0,等待下一轮垃圾回收。
优点是简单,只有标记和不标记两种状态,使得使用二进制位0和1就可以为其标记。
缺点是在清除之后,由于剩余对象内存位置是不变的,会导致空闲内存空间是不连续的,会出现内存碎片的情况。
引用计数法:他的策略是记录每个变量被使用的次数。
- 当声明了一个变量并且将一个引用类型赋值给改变量的时候这个值的引用次数就为1
- 如果同一个值又被赋给了另一个变量,那么引用数加一
- 如果该变量被其他的值覆盖了,则引用次数减一
- 当这个值的引用次数变为0的时候,说明没有变量在使用,垃圾回收器在运行的时候清理掉引用次数为0的值占用的内存。
优点:比标记清楚更加清晰,在引用值为0时就回收。
缺点:需要一个计数器,因为不知道引用数量的上限,所以计数器需要很大的位置,还有就是无法解决循环引用无法回收的问题。
8.堆和栈的区别
堆和栈是计算机内存中两种常见的数据存储结构
,它们的区别如下:
内存分配方式不同
:栈
是一种自动分配和释放内存的数据结构
,由系统自动管理,程序员无需手动分配和释放内存。而堆
则是程序员手动申请和释放内存
。内存管理方式不同
:栈
采用“先进后出”
的方式管理数据,每次数据入栈时都会被压入栈顶,每次数据出栈时都会从栈顶弹出。而堆
则没有固定的管理方式
,程序员需要手动管理其内存。内存空间大小不同
:栈
的内存空间通常较小
,由系统分配的栈空间在程序运行时就已经确定,栈溢出是一种常见的错误。而堆
的内存空间通常较大
,可以在程序运行时动态分配并释放。存储内容不同
:栈
主要用于存储函数调用时的临时变量、函数参数和返回地址等数据,而堆
主要用于存储程序运行时动态分配的数据、对象和数组等。
总的来说,栈和堆的主要区别在于它们的内存分配方式、内存管理方式、内存空间大小和存储内容等方面
。
9.闭包
-
什么是闭包?
闭包就是能够读取其他函数内部变量的函数。 -
闭包有什么作用?
闭包可以实现函数的私有变量、内部方法和属性的封装,避免了变量污染和命名冲突的问题 -
如何创建闭包?
在JavaScript中,创建闭包的方式有两种:一种是使用函数嵌套,将内部函数作为外部函数的返回值返回;另一种是使用立即执行函数表达式(IIFE),将函数包裹在一个匿名函数中执行,形成一个独立的作用域。 -
闭包会不会导致内存泄漏?
闭包会导致内存泄漏的问题,因为闭包会持有其外部作用域中的变量和函数引用,在某些情况下会导致这些变量和函数无法被垃圾回收机制清除,从而占用了过多的内存空间。解决方法是在不需要使用闭包时,手动释放其引用的变量和函数。 -
闭包中的变量何时销毁
闭包中的变量何时销毁取决于是否存在对闭包的引用。当闭包被创建时,它会持有其外部作用域中的变量和函数引用,只要闭包存在,这些变量和函数就不会被垃圾回收机制清除。只有当闭包被销毁时,这些变量和函数才会被释放。
10.浅拷贝
浅拷贝是指创建一个新对象
,将原对象中的属性值复制到新对象中,如果属性值是一个引用类型数据,那么新对象中的属性值仍然是原对象中的引用,也就是说,新对象和原对象中的引用类型数据共享一份内存空间
。这意味着,如果修改了新对象中的引用类型数据,那么原对象中对应的属性值也会受到影响。
在JavaScript中,存在浅拷贝的现象有:
Object.assign
Array.prototype.slice(
),Array.prototype.concat()
- 使用
拓展运算符
实现的复制
11.深拷贝
深拷贝是指创建一个新对象,将原对象中的属性值复制到新对象中,如果属性值是一个引用类型数据,那么新对象中的属性值是原对象中属性值的一个完全独立的副本,也就是说,新对象和原对象中的引用类型数据不共享任何内存空间。这意味着,修改新对象中的引用类型数据不会影响原对象中对应的属性值。
常见的深拷贝方式有:
- JSON.stringify()
- 手写循环递归
12.bind、call、apply 区别?
call、apply、bind作用是改变函数执行时的上下文
,简而言之就是改变函数运行时的this指向
- apply
apply接受两个参数,第一个参数是this的指向
,第二个参数是函数接受的参数
,以数组
的形式传入
改变this指向后原函数会立即执行
,且此方法只是临时改变this指向一次
- call
call方法的第一个参数也是this的指向
,后面传入的是一个参数列表
跟apply一样,改变this指向后原函数会立即执行
,且此方法只是临时改变this指向一次
- bind
bind方法和call很相似,第一参数也是this的指向
,后面传入的也是一个参数列表
(但是这个参数列表可以分多次传入)
改变this指向后不会立即执行
,而是返回一个永久改变this指向的函数