2024前端JS面试题总汇
1、JS数据类型有哪些?区别?⭐⭐⭐
js数据类型分为两大类,基本数据和引用数据类型,他们两个不同的原因是存储方式不同
- 基本数据类型:number,null,string,boolean,undefined,symbol,BigInt,基本数据类型有固定大小和值,放在栈中,可直接访问,而引用数据类型不确定大小,但是其引用地址是固定的,因此,他的地址存在栈中,数据存在堆中,地址指向的是存在堆中的数据
- 引用数据类型:Object包括数组,函数,正则,日期,Math。引用数据类型是存放在堆中,在栈中保存的地址是指向堆中的数据,通过地址可以快速查找到保存在堆中的数据
- Symbol是ES6新出的一种数据类型,这种数据类型的特点就是没有重复的数据,可以做object的key
- BigInt 也是 ES6 新出的一种数据类型,BigInt 可以表示任意大的整数,能够解决解精度缺失的问题 (超过 Number 类型支持范围的数值都会失去精度)。使用方法:(1) 整数末尾直接加n:647326483767797n。(2) 调用 BigInt() 构造函数:BigInt(“647326483767797”)。
2、JS中检测数据类型的有哪些?⭐⭐⭐
-
typeof:常用于判断引用数据类型,除了null检测为object。对于引用数据类型除了function返回function,其余全部返回object
-
instanceof:主要用于检测引用数据类型,不适合用来检测基本数据类型,如果检测的类型在当前实例的原型链上,则返回true,说明这个实例属于这个类型,否则返回false。例如:A instanceof B ,判断B在不在原型链上,如果在就返回true,如果找到原型链尽头null都没有找到,就返回false。(由于原型链的指向可以随意改动,导致检测不准确)
-
constructor:获取实例的构造函数判断和某个类型是否相同,如果相同就说明该数据是符合那个数据类型的。使用方法是:“实例.constructor”。constructor 可以检测出除了 undefined 和 null 以外的其他类型,因为 undefined 和 null 没有原生构造函数。(不可靠,容易被修改)
-
**object.prototype.toString.call( ):**适用于所有类型的判断检测,检测方法是: Object.prototype.toString.call(数据) ,返回的是该数据类型的字符串。
3.JS栈和堆有什么区别⭐⭐⭐
js变量都储存在内存中,内存中开辟出来两个地方储存变量,栈和堆,栈和堆实际上是操作系统对进程占用的内存空间的两种管理方式
栈:
- 栈是一种先进后出的数据结构,由操作系统自动分配内存空间,自动释放,占固定的大小空间
- 栈储存的是基本数据类型的值以及引用数据类型的引用地址
- 栈中储存的数据的生命周期随着当前环境的执行完毕而结束
堆:
- 堆由操作系统动态分配内存空间,大小不定也不会自动释放,一般由程序员分配释放也可由垃圾回收机制回收
- 栈存储的是对象和复杂数据结构,存储的是对象的实际数据,而不是对象的引用。
- 引用数据类型只有在引用的它的变量不在时,被垃圾回收机制回收
栈和堆的区别:
- 栈是操作系统自动分配的,自己释放,堆是操作系统动态分配的,不会自动释放
- 栈取存数据快,内存少,堆取存数据较慢,内存大
- 栈自己回收,有利于内存的管理,但固定的大小,固定的生命周期,会缺乏灵活性
4.深拷贝,浅拷贝⭐⭐⭐
浅拷贝:
- 拷贝对象的第一层属性
- 如果是基本数据类型,直接将存储在栈中的值赋值新的,生产新的内存,修改值原始值不会变
- 引用数据类型,则是赋值对象的引用地址,共享一个内存,修改值,原始值会变
- 在js中可以使用Object.assign或者扩展运算符实现浅拷贝
深拷贝
- 克隆对象各个层级的属性。
- 深克隆是指创建一个与原对象完全相同的新对象 (数据源不同,数据地址已变化)。
- 可以通过递归的方式实现深克隆,也可以通过简单粗暴的方式实现 JSON.parse (JSON.stringify(obj))。但需要注意:时间对象会变成字符串的形式。RegExp、Error 对象序列化的结果将只得到空对象。函数、undefined 序列化的结果会把函数或 undefined 丢失。NaN、Infinity 序列化的结果会变成 null。如果对象中存在循环引用的情况也无法正确实现深拷贝。JSON.stringify() 只能序列化对象的可枚举的自有属性。
5、JS垃圾回收机制?⭐⭐
- **内存泄漏:**JS 代码运行时,需要分配内存空间存储变量和值,当这些变量不再作用时,需要释放内存,如果没有及时释放,就会引起内存泄漏,堆积起来会影响性能甚至造成系统崩溃。垃圾回收机制就是为了防止内存泄漏,及时释放不再使用的内存,提高程序性能。
- **垃圾回收机制:**垃圾回收机制是一种自动管理内存的机制,它会自动监测和回收不再使用的对象,从而释放内存空间。实现的原理主要有标记清除、引用计数、复制算法
- 标记清除算法:垃圾回收器会定期扫描内存中的对象,标记那些可达对象和不可达对象。可达对象指的是正在被使用的对象。不可达对象指的是不再被引用的对象。垃圾回收器会将不可达对象标记为垃圾对象,并将他们从内存中清除。该算法的优点是可以处理循环引用的情况,缺点是由于垃圾回收器的工作需要消耗一定的系统资源,因此如果程序中存在大量的内存占用或者存在频繁创建和销毁对象的操作,执行垃圾回收操作的时间会比较长,对性能造成一定的影响。
- **引用计数算法:**垃圾回收器会记录每个对象被引用的次数,当对象被引用的次数为0时,该对象就会被清除。该算法的优点是实现较为简单,但无法处理循环引用的情况,即两个对象相互引用,但是它们的引用次数都不为 0,导致内存无法被释放,可能会导致内存泄漏。
- **复制算法:**将内存空间划分为两个相等的区域,每次只使用一个区域,这个区域满时,将其中存活的对象复制到另外一个区域,再将原区域的对象全部清除。优点是可以避免由于产生大量内存碎片而引发的内存分配失败问题。
存在问题?怎么优化?
虽然浏览器可以自动的进行垃圾回收,但是当代码比较复杂时,垃圾回收对性能的消耗比较大,所以应该尽量减少垃圾回收。需要根据具体应用的需求和环境进行优化。
- 对数组进行优化。清空数组时,赋值为[ ],同时将数组的长度设置为0。
- 对象复用,尽量减少对象的创建和销毁次数。
- 不再使用的对象就设置为 null。
- 函数优化,函数功能单一化。
- 尽早释放资源。
- 使用闭包,在闭包中的变量不会被垃圾回收机制回收。
6、JS哪些操作会造成内存泄露?⭐⭐
- 意外的全局变量。由于使用未声明的变量而意外的创建了一个全局变量,而使用这个变量一直留在内存中无法被回收。
- 没有清理的DOM元素引用。获取一个DOM元素的引用,元素被删除了,由于保留了元素的引用,所以也无法被回收。
- 被遗忘的定时器或者回调函数。
- 闭包。
**7、闭包?**⭐⭐⭐
什么是闭包?
因为作用域链的存在,函数的内部可以直接读取全局变量,而函数内部无法读取另一个函数内部的局部变量,如果想读取函数内部的局部变量,可以通过闭包来实现。闭包就是在一个函数内部创建另外一个函数,让你可以在一个内层函数中访问到外层函数的局部变量。简单来说,闭包就是可以读取其他函数内部局部变量的函数,本质上,闭包是将函数内部和函数外部连接起来的桥梁。
为什么要使用闭包?
- 局部变量无法共享和长久的保存,而全局变量可能造成变量污染。
- 闭包可以读取函数内部的局部变量,且不会被垃圾回收机制回收,可以长期保存。
闭包的作用?
- 在函数外部可以访问函数内部的局部变量。
- 可以使函数内部的变量在函数执行结束之后不被销毁 ,长久保存在内存中,不会被垃圾回收机制回收。
- 使用闭包,可以封装自己的函数代码,实现模块化。
- 保护:避免命名冲突。
- 保存:解决循环绑定引发的索引问题。
闭包的缺点?
由于垃圾回收器不会将闭包中变量销毁,于是就造成了内存泄露,内存泄露积累多了就容易导致内存溢出。如何解决:在销毁函数之前,将不使用的局部变量全部删除。
闭包的应用?
- 能够模仿块级作用域。
- 设计模式中的单例模式。
- for 循环中的保留 i 的操作。
- 防抖和节流。
- 函数柯里化。
- 在构造函数中定义特权方法。
- Vue 中数据响应式 Observer 中使用闭包。
8、什么是原型链?⭐⭐⭐
- 每个函数身上都有一个 prototype 的原型对象,并且有一个__proto__的指针指向下一级原型对象,如果一个对象的属性或方法在自身中找不到,那么就会去 prototype 原型对象中查找,如果还找不到继续向上查找直到 null,当_proto_指针指向 null 时形成一个链条,这个链条叫做原型链。
- 在原型链中,对象可以继承原型对象的属性和方法。如果想在构造函数中添加属性和方法,可以将它们添加到构造函数的 prototype 属性中,这样通过该构造函数创建的对象都可以访问到这些属性和方法。
- 原型链的特点是:对象可以沿着原型链向上查找属性和方法,实现了属性和方法的共享和继承。
9、JS继承的方法有哪些?优缺点?⭐⭐
JS继承的方法有以下几种:原型链继承、构造函数继承、组合继承、原型式继承和寄生式继承,寄生组合式继承,ES6 Class实现继承。继承的目的是:重复利用另外一个对象的属性和方法。
- **原型链继承:**将父类的实例作为子类的原型,从而实现对父类属性和方法的继承。**优点:**写法方便简洁,容易理解。**缺点:**不能传递参数和共享所有继承的属性和方法,当一个发生改变另外一个随之改变。
- **构造函数继承:**在子类的构造函数中调用父类的构造函数,使用 apply() 或 call() 方法将父对象的构造函数绑定在子对象上,从而实现对父类实例属性的继承。**优点:**解决了原型链继承不能传参的问题和父类的原型共享的问题。**缺点:**方法都在构造函数中定义,因此无法实现函数复用。
- **组合继承:**将原型链继承和构造函数继承结合起来,既可以实现对父类原型属性和方法的继承,又可以实现对父类实例属性的继承。优点: 解决了原型链继承和构造函数继承造成的影响。缺点: 无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
- **原型式继承:**通过创建一个临时构造函数来实现对父类的属性和方法的继承。**优点:**不需要单独创建构造函数。**缺点:**属性中包含的引用值始终会在相关对象间共享。
- **寄生式继承:**在原型式继承的基础上,通过在临时构造函数中添加方法和属性,从而实现对父类的继承。**优点:**写法简单,不需要单独创建构造函数。**缺点:**通过寄生式继承给对象添加函数会导致函数难以重用。
- 寄生组合式继承:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。优点:高效率只调用一次父构造函数,并且因此避免了在子原型上面创建不必要,多余的属性。与此同时,原型链还能保持不变。缺点:代码复杂。
- ES6 Class实现继承:ES5 的继承,实质是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面 (Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法加到 this 上面 (所以必须先调用super方法),然后再用子类的构造函数修改 this。需要注意的是,class 关键字只是原型的语法糖,JS继承仍然是基于原型实现的。 优点:语法简单易懂,操作更方便。缺点:并不是所有的浏览器都支持 class 关键字 lass Person。
10、new操作符具体都干了什么?⭐⭐
- 创建一个新对象 obj。
- 将该对象与构造函数通过原型链连接起来(设置该对象的构造函数)。
- 将构造函数中的 this 绑定到该对象上。
- 根据构造函数返回类型作判断,如果是值类型则返回新对象 obj,如果返回对象,则返回构造函数里的对象。
javascript">// 手写 new 操作符
function mockNew(constructor, ...args) {// 1.创建一个新对象 objconst obj = {};// 2.把构造函数当参数传入,新对象指向构造函数原型对象obj.__proto__ = constructor.prototype;// 3.通过 apply 将构建函数的 this 指向新对象let result = constructor.apply(obj, args);// 4.根据返回值判断return result instanceof Object ? result : obj;
}
11、JS的几种具体异常类型(报错)
- SyntaxError:语法错误
- ReferenceError:引用错误
- RangeError:范围错误
- ypeError:类型错误
- URLError:与 url 相关参数不正确
- EvalError:全局函数 eval 执行错误
12、什么是事件冒泡?什么是事件委托?
- **事件冒泡:**在一个对象上触发某类事件,这个事件会向这个对象的的父级传播,从里到外,直至它被处理或者到达了对象层次的最顶层,即 document 对象。这个过程就是事件冒泡。
- **事件委托:**事件委托就是利用事件冒泡,指定一个事件处理程序,就可以管理某一类型的所有事件。**原理:**在元素的父级元素添加事件,点击元素时,因为事件冒泡的作用实现事件委托。简单来说,事件委托就是将子元素的事件通过冒泡的形式交由父元素来执行。**优点:**使用事件委托可以减少代码执行优化资源。
13、undefined 和 null 区别?⭐
- undefind 是全局对象的一个属性,当一个变量没有被赋值或者一个函数没有返回值或者某个对象不存在某个属性却去访问或者函数定义了形参但没有传递实参,这时候都是 undefined ,undefined 通过 typeof 判断类型是 undefined。
- null 代表空值,代表一个空对象指针,代表对象的值未设置,相当于一个对象没有设置指针地址就是 null。null 通过 typeof 判断类型是 object。
- undefined 表示一个变量初始状态值,而 null 则表示一个变量被人为的设置为空对象,而不是原始状态。当需要释放一个对象时,直接赋值为 null 即可,对象被赋值了null 以后,对象对应的堆内存中的值就是游离状态了,GC 会择机回收该值并释放内存。因此,需要释放某个对象,就将变量设置为 null,即表示该对象已经被清空,目前无效状态。
- null 是 javascript 的关键字,和其它语言一样都是代表空值, undefined 却是 javascript 才有的。是为了区分空指针对象和未初始化的变量,它是一个预定义的全局变量。
16、对于数组去重都有哪些方法?⭐⭐
- **双重 for 循环:**这是一个最笨的方法。
- **对象属性 key:**利用对象属性名 key 不可以重复这一特点,如果对象中不存在,就 push 进空数组。
- **for 循环 + indexOf:**主要是利用 indexOf 的特性,查找元素返回下标,找不到就返回-1。-1就 push 进空数组。
- **for 循环 + sort 排序:**利用数组的 sort 排序方法去重,如果第 i 项 和 i-1 项不一致,就 push 进空数组。
- **filter + indexOf:**利用 filter 过滤配合 indexOf 查找元素,判断返回元素下标和过滤数组的 index 是否相等。
- **Set:**Es6 中新增了数据类型 Set,Set 的最大一个特点就是数据不重复,可以用作数组去重。new Set 方法,返回是一个类数组,需要结合扩展运算符…,转成真实数组。
- **set + Array.from:**Set 去重结合 Array.from 转成真实数组。Array.from(new Set(原数组))
- **for 循环 + includes:**includes 用来判断一个数组是否包含一个指定的值,是就返回 true,否则返回 false。判断空数组是否包含原数组的某个元素,不包含就 push 进空数组。
- **reduce + includes:**利用 reduce 遍历结合 includes去重。
17、 数组的基本操作方法?⭐⭐
数组的基本操作方法:
push:往数组尾部添加一个元素。 返回数组的长度。
unshift:往数组头部添加一个元素。返回数组的长度。
pop:从数组尾部删除一个元素。返回删除的元素。
shift:从数组头部删除一个元素。返回删除的元素。
slice(开始位置,结束位置):截取数组一部分。只有一个元素则是开始位置,直到截取全部
splice(开始位置,长度,插入元素):删除、修改数组中的一部分元素。
reverse:反转数组。返回反转后的数组。
sort:对数组的元素进行排序。返回排序后的数组。
join:把数组变成字符串。若括号里什么都不写,则默认用逗号分隔。
toString:把数组变成字符串。
split:把字符串变成数组。
concat:合并数组,并返回结果。扩展运算符也可以合并数组。
Math.min:返回数组最小值元素。
Math.max:返回数组最大值元素。
length:获取当前数组的长度。
查找方法:
indexOf:查找数组元素,返回第一个找到的元素下标,找不到返回-1。
lastIndexOf:查找数组元素,返回最后一个找到的元素下标,找不到返回-1,从后向前搜索
includes: 查找数组是否包含某一元素,包含则返回 true,不包含返回 false。
find:查找满足函数条件的第一个值,找不到返回 undefined。
findIndex: 查找满足函数条件的第一个值得下标,找不到返回 -1。
ength:获取当前数组的长度。
查找方法:
indexOf:查找数组元素,返回第一个找到的元素下标,找不到返回-1。
lastIndexOf:查找数组元素,返回最后一个找到的元素下标,找不到返回-1,从后向前搜索
includes: 查找数组是否包含某一元素,包含则返回 true,不包含返回 false。
find:查找满足函数条件的第一个值,找不到返回 undefined。
findIndex: 查找满足函数条件的第一个值得下标,找不到返回 -1。