JS相关面试题
1、javaScript中的数据类型?存储上的差别?
JS中的数据类型主要分为基本数据类型和引用数据类型
基本数据类型:
- Number
- String
- Boolean
- Undefined
- null
- symbol
引用数据类型:
- Object
- Array
- Function
区别:
1、基本数据类型存储在栈中;引用数据类型存储在堆中,值是存储在堆中,每个堆内存对象都有对应的引用地址指向它,引用地址存放再栈中。
2、再赋值变量的时候,基本数据类型是生成相同的值,两个对象对应不同的地址;引用数据类型是将保存对象的内存地址赋值给另一个变量,也就是两个变量指向堆内存中的同一个对象。
2、数组的常用方法?
添加:
push()
接受任意数量的参数,并且将它们添加至数组的末尾,返回数组的最新长度unshift()
在数组开头添加任意多个值,然后返回新的数组长度splice()
传入三个参数,分别是开始位置、0(要删除的元素数量)、插入的元素,返回空数组concat()
首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组删除:
pop()
方法用于删除数组的最后一项,同时减少数组的length
值,返回被删除的项shift()
方法用于删除数组的第一项,同时减少数组的length
值,返回被删除的项splice()
传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组slice()
用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组修改:
splice()
传入三个参数,分别是开始位置,要删除元素的数量,要插入的任意多个元素,返回删除元素的数组,对原数组产生影响查找:
indexOf()
返回要查找的元素在数组中的位置,如果没找到则返回 -1includes()
返回要查找的元素在数组中的位置,找到返回true
,否则false
find()
返回第一个匹配的元素排序:
reverse()
顾名思义,将数组元素方向反转sort()
方法接受一个比较函数,用于判断哪个值应该排在前面转换:
join()
方法接收一个参数,即字符串分隔符,返回包含所有项的字符串迭代方法:
some()
对数组每一项都运行传入的测试函数,如果至少有1个元素返回 true ,则这个方法返回 trueevery()
对数组每一项都运行传入的测试函数,如果所有元素都返回 true ,则这个方法返回 trueforEach()
对数组每一项都运行传入的函数,没有返回值filter()
对数组每一项都运行传入的函数,函数返回true
的项会组成数组之后返回map()
对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组
3、字符串中的常用方法?
添加:
concat
用于将一个或多个字符串拼接成一个新字符串删除:
slice()
substr()
substring()
- 这三个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。
修改:
这里改的意思也不是改变原字符串,而是创建字符串的一个副本,再进行操作
常见的有:
trim()、trimLeft()、trimRight()
:删除前、后或前后所有空格符,再返回新的字符串repeat()
:接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果padStart()、padEnd()
:复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件toLowerCase()、 toUpperCase()
:大小写转化查找:
chatAt()
:返回给定索引位置的字符,由传给方法的整数参数指定indexOf()
:从字符串开头去搜索传入的字符串,并返回位置(如果没找到,则返回 -1 )startWith()
:从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值includes()
:从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值转换方法:
split
:把字符串按照指定的分割符,拆分成数组中的每一项模板匹配:
match()
:接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp
对象,返回数组search()
:接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp
对象,找到则返回匹配索引,否则返回 -1replace()
:接收两个参数,第一个参数为匹配的内容,第二个参数为替换的元素(可用函数)
4、JavaScript中的类型转换机制?
- JS中的类型转换机制常见的有显示转换(强制转换)和隐式转换(自动转换)
- 显示转换:
- Number():将任意类型的值转化为数值
- parseInt():
parseInt
相比Number
,就没那么严格了,parseInt
函数逐个解析字符,遇到不能转换的字符就停下来- String():可以将任意类型的值转化成字符串
- Boolean():可以将任意类型的值转为布尔值
- 隐式转换
- 比较运算(
==
、!=
、>
、<
)、if
、while
需要布尔值地方- 算术运算(
+
、-
、*
、/
、%
)
5、==
和===
有什么区别?
- 等于==
- 等于操作符用两个等于号( == )表示,如果操作数相等,则会返回
true
;在JavaScript
中存在隐式转换。等于操作符(==)在比较中会先进行类型转换,再确定操作数是否相等- 全等===
- 全等操作符由 3 个等于号( === )表示,只有两个操作数在不转换的前提下相等才返回
true
。即类型相同,值也需相同- 最主要的区别就是等于操作符存在类型转换,全等操作符不会做类型转换
6、深拷贝和浅拷贝的区别?如何实现一个深拷贝?
浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址
即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址
在
JavaScript
中,存在浅拷贝的现象有:
Object.assign
Array.prototype.slice()
,Array.prototype.concat()
- 使用拓展运算符实现的复制
//实现一个浅拷贝
function shallowClone(obj) {
const newObj = {};
for(let prop in obj) {
if(obj.hasOwnProperty(prop)){
newObj[prop] = obj[prop];
}
}
return newObj;
}
深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
常见的深拷贝方式有:
- _.cloneDeep()
- jQuery.extend()
- JSON.stringify()
- 手写循环递归
//递归循环实现深拷贝
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
区别:
- 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址
- 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址
7、谈一谈对于闭包的理解?以及闭包的使用场景?
- 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包。也就是说,
闭包让你可以在一个内层函数中访问到其外层函数的作用域
-使用场景:
- 创建私有变量
- 延长变量生命周期
- 函数柯里化
- 模拟闭包私有方法
如何实现一个闭包?
- 在一个函数内再生成一个函数
- 将一个函数作为另一个函数的返回值调用
- 将一个函数当作参数传递给另一个函数
8、说说对于作用域链的理解?
1、当在
Javascript
中使用一个变量的时候,首先Javascript
引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错
9、JavaScript中原型、原型链是什么?有什么特点?
- 在JavaScript中,每个对象都有一个原型(prototype),
原型是一种用于实现对象属性继承的机制。每个JavaScript对象都有一个原型对象,它充当了对象的模板,并提供了共享属性和方法。
- 当我们访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript会去它的原型中查找,如果原型中也没有,则会继续查找原型的原型,直到找到或者到达原型链的末端。
原型链
是由对象的原型组成的链式结构,它描述了对象之间的继承关系。每个对象都有一个隐式的原型指针(proto),指向它的原型对象。通过原型链,我们可以实现对象的属性和方法的继承。- 原型链的特点包括:
- 对象之间通过原型链实现属性和方法的继承,可以减少重复代码,提高代码的复用性。
- 原型链是一个单向的链式结构,子对象可以访问父对象的属性和方法,但父对象不能访问子对象的属性和方法。
- 原型链的末端是Object.prototype,它是所有对象的最顶层原型,包含了一些常用的方法,如toString()、valueOf()等
- 如果在原型链上的某个对象上找到了需要的属性或方法,就会停止查找,不会继续向上查找。
10、JavaScript如何实现继承?
下面给出
JavaScripy
常见的继承方式:-
原型链继承:
原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针function Parent() { this.name = 'parent1'; this.play = [1, 2, 3] } function Child() { this.type = 'child2'; } Child1.prototype = new Parent(); console.log(new Child())
构造函数继承(借助 call)
:相比第一种原型链继承方式,父类的引用属性不会被共享,优化了第一种继承方式的弊端,但是只能继承父类的实例属性和方法,不能继承原型属性或者方法function Parent(){ this.name = 'parent1'; } Parent.prototype.getName = function () { return this.name; } function Child(){ Parent1.call(this); this.type = 'child' } let child = new Child(); console.log(child); // 没问题 console.log(child.getName()); // 会报错
-
组合继承:
组合继承则将前两种方式继承起来function Parent3 () { this.name = 'parent3'; this.play = [1, 2, 3]; } Parent3.prototype.getName = function () { return this.name; } function Child3() { // 第二次调用 Parent3() Parent3.call(this); this.type = 'child3'; } // 第一次调用 Parent3() Child3.prototype = new Parent3(); // 手动挂上构造器,指向自己的构造函数 Child3.prototype.constructor = Child3; var s3 = new Child3(); var s4 = new Child3(); s3.play.push(4); console.log(s3.play, s4.play); // 不互相影响 console.log(s3.getName()); // 正常输出'parent3' console.log(s4.getName()); // 正常输出'parent3'
原型式继承
:这里主要借助Object.create
方法实现普通对象的继承- 缺点也很明显,因为
Object.create
方法实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能let parent4 = { name: "parent4", friends: ["p1", "p2", "p3"], getName: function() { return this.name; } }; let person4 = Object.create(parent4); person4.name = "tom"; person4.friends.push("jerry"); let person5 = Object.create(parent4); person5.friends.push("lucy"); console.log(person4.name); // tom console.log(person4.name === person4.getName()); // true console.log(person5.name); // parent4 console.log(person4.friends); // ["p1", "p2", "p3","jerry","lucy"] console.log(person5.friends); // ["p1", "p2", "p3","jerry","lucy"]
寄生式继承
:寄生式继承在上面继承基础上进行优化,利用这个浅拷贝的能力再进行增强,添加一些方法let parent5 = { name: "parent5", friends: ["p1", "p2", "p3"], getName: function() { return this.name; } }; function clone(original) { let clone = Object.create(original); clone.getFriends = function() { return this.friends; }; return clone; } let person5 = clone(parent5); console.log(person5.getName()); // parent5 console.log(person5.getFriends()); // ["p1", "p2", "p3"]
寄生组合式继承
:寄生组合式继承,借助解决普通对象的继承问题的Object.create
方法,在前面几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式function clone (parent, child) { // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程 child.prototype = Object.create(parent.prototype); child.prototype.constructor = child; } function Parent6() { this.name = 'parent6'; this.play = [1, 2, 3]; } Parent6.prototype.getName = function () { return this.name; } function Child6() { Parent6.call(this); this.friends = 'child5'; } clone(Parent6, Child6); Child6.prototype.getFriends = function () { return this.friends; } let person6 = new Child6(); console.log(person6); //{friends:"child5",name:"child5",play:[1,2,3],__proto__:Parent6} console.log(person6.getName()); // parent6 console.log(person6.getFriends()); // child5
11、谈谈对this对象的理解?
在 JavaScript 中,关键字
this
指向当前执行上下文中的对象。它是动态绑定的,具体的指向取决于函数被调用的方式和位置。
this
的指向有以下几种情况:
默认绑定:当一个函数独立调用时,
this
默认绑定到全局对象(在浏览器环境中为window
对象,在 Node.js 环境中为global
对象)。function foo() { console.log(this); } foo(); // 在浏览器环境中输出 window,在 Node.js 环境中输出 global
隐式绑定:当函数作为对象的方法调用时,
this
绑定到调用该方法的对象。const obj = { name: 'Alice', sayHello: function() { console.log('Hello, ' + this.name); } }; obj.sayHello(); // 输出 "Hello, Alice"
显式绑定:通过
call()
、apply()
或bind()
方法显式指定一个对象作为函数执行时的this
值。function greet() { console.log('Hello, ' + this.name); } const person = { name: 'Bob' }; greet.call(person); // 输出 "Hello, Bob"
new 绑定:当使用
new
关键字调用构造函数创建一个新对象时,this
绑定到新创建的对象。function Person(name) { this.name = name; } const john = new Person('John'); console.log(john.name); // 输出 "John"
箭头函数:箭头函数没有自己的
this
,它继承外部作用域的this
值。javascript复制代码const obj = { name: 'Alice', sayHello: function() { const greet = () => { console.log('Hello, ' + this.name); }; greet(); } }; obj.sayHello(); // 输出 "Hello, Alice"
需要注意的是,在某些特殊情况下(如事件处理程序、定时器回调等),
this
的指向可能会发生变化,也可以使用箭头函数来维持所需的this
值。此外,使用严格模式('use strict'
)时,全局对象不能默认绑定到this
,而是绑定到undefined
。
12、typeof和instanceof有什么区别?
typeof
运算符和instanceof
运算符在JavaScript中用于检测变量的类型,但它们有不同的应用场景和功能。
typeof
运算符:
typeof
用于判断一个值的基本类型(原始类型)。- 它返回一个表示值类型的字符串。常见的返回值包括:“number”、“string”、“boolean”、“undefined”、“object”、"function"以及 “symbol”(ES6新增)。
typeof null
返回 “object” 是因为历史原因造成的错误,实际上 null 是一个特殊的原始值,而不是对象。示例:
typeof 42; // "number" typeof "Hello"; // "string" typeof true; // "boolean" typeof undefined; // "undefined" typeof null; // "object" (错误的结果)
instanceof
运算符:
instanceof
用于判断一个对象是否是某个构造函数的实例。- 它基于原型链进行判断,即判断对象的原型链中是否出现了指定构造函数的原型对象。
- 如果对象是指定构造函数的实例,则返回
true
,否则返回false
。示例:
const obj = {}; obj instanceof Object; // true function Person() {} const person = new Person(); person instanceof Person; // true const arr = []; arr instanceof Array; // true arr instanceof Object; // true,因为Array继承自Object const str = "Hello"; str instanceof String; // false,因为str是基本类型而不是字符串对象
总结:
typeof
用于判断基本类型。instanceof
用于判断对象的构造函数实例。- 两者针对的对象范围不同,
typeof
适用于原始类型和函数(也是对象),instanceof
适用于对象及其派生对象。
13、什么是事件代理?应用场景?
事件代理(Event delegation)
是一种常用的JavaScript设计模式,它利用事件冒泡机制将事件处理程序添加到父元素上,从而代理处理其子元素触发的事件。换句话说,事件处理被委托给父元素来处理。
工作原理:
当一个事件在DOM树中的元素上触发时,该事件会在触发元素上产生,并且自动向上层传播,即向祖先元素逐级冒泡。通过将事件处理程序添加到父元素或更高层次的元素上,我们可以捕获到子元素触发的事件,然后根据需要进行处理。
应用场景:
- 效率优化:对于包含大量子元素的列表、表格等结构,如果为每个子元素都绑定事件处理程序,会消耗大量内存和性能。使用事件代理,只需在父元素上绑定一个事件处理程序,减少了内存占用和DOM操作次数。
- 动态添加元素:当通过JavaScript动态添加新的子元素时,无需再次绑定事件处理程序,因为事件冒泡机制已经将事件传递给父元素,父元素上的事件处理程序会自动处理新添加的子元素。
- 处理未来元素:如果需要在页面加载后处理尚不存在的元素的事件,例如异步加载内容或延迟执行脚本,事件代理可以派上用场。通过将事件处理程序添加到已存在的父元素上,可以捕获尚不存在的元素触发的事件。
14、说一下new操作符具体干了什么?
new
操作符用于创建一个对象实例,它执行了以下几个具体的步骤:
创建一个空对象:
首先,new
操作符会创建一个新的空对象。设置原型链:
接下来,通过将新对象的[[Prototype]]
属性设置为构造函数的prototype
属性,建立对象与原型之间的连接。这样新对象就可以访问构造函数原型上的属性和方法。绑定this指向
:将构造函数内部的this
关键字绑定到新创建的对象上,使构造函数内部的代码可以操作该对象的属性和方法。执行构造函数代码:
接着,执行构造函数体内的代码,给新对象添加属性和方法。通过在构造函数内使用this
关键字,可以将新对象的属性和方法绑定到该对象上。返回新对象
:如果构造函数没有显式返回其他对象,则返回新创建的对象实例;否则,返回构造函数显式返回的对象。
15、ajax的原理?如何实现?
Ajax(Asynchronous JavaScript and XML)是一种在客户端和服务器之间进行异步数据交互的技术,它允许通过在后台发送HTTP请求来更新网页内容,而无需刷新整个页面。下面是Ajax的原理和实现方式:
原理:
- 前端代码使用JavaScript创建XMLHttpRequest对象,它是浏览器提供的内置对象,用于发送HTTP请求和接收响应。
- 通过XMLHttpRequest对象发送异步请求到服务器,可以是GET、POST等HTTP请求。
- 服务器接收到请求后处理,并将需要返回的数据以合适的格式(如JSON、XML)发送回前端。
- 前端通过XMLHttpRequest对象接收到服务器返回的数据。
- 根据接收到的数据,前端进行相应的处理,如更新页面内容、修改DOM元素等。
实现:以下是一个基本的Ajax实现步骤:
1、创建XMLHttpRequest对象:
var xhr = new XMLHttpRequest();
2、设置请求参数和回调函数:
xhr.open('GET', 'url'); // 设置请求方法和URL xhr.setRequestHeader('Content-Type', 'application/json'); // 设置请求头 xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { // 请求完成且成功返回时的回调处理 var responseData = JSON.parse(xhr.responseText); // 处理responseData } };
3、发送请求:
xhr.send();
16、bind、call、apply区别?
bind、call和apply都是用于改变函数的执行上下文(即函数内部的this指向)的方法,它们的区别如下:
bind:
bind() 方法创建一个新的函数,并将指定的值作为新函数的 this 值。bind() 方法并不会立即调用函数,而是返回一个绑定了指定上下文的新函数。示例:
var obj = { name: 'Alice' }; function sayHello() { console.log('Hello, ' + this.name); } var boundFn = sayHello.bind(obj); // 创建一个绑定了obj作为this的新函数 boundFn(); // 输出: Hello, Alice
call:
call() 方法调用一个函数,并将指定的对象作为函数执行时的上下文环境(即this指向),同时可以传递多个参数给函数。示例:
var obj = { name: 'Alice' }; function sayHello() { console.log('Hello, ' + this.name); } sayHello.call(obj); // 将obj作为上下文调用sayHello函数,输出: Hello, Alice
apply:
apply() 方法调用一个函数,并将指定的对象作为函数执行时的上下文环境(即this指向),同时接收一个数组或类数组对象作为参数列表。示例:
var obj = { name: 'Alice' }; function sayHello(greeting) { console.log(greeting + ', ' + this.name); } sayHello.apply(obj, ['Hello']); // 将obj作为上下文调用sayHello函数,并传递参数数组,输出: Hello, Alice
相同点:bind、call和apply都可以改变函数的执行上下文,即this的指向。
不同点:bind在绑定上下文后返回一个新函数,而call和apply是立即调用函数。另外,call接收多个参数列表,而apply接收一个参数数组。
17、说一下对事件循环的理解?
事件循环是
JavaScript 中用于处理异步代码执行的机制,它负责管理和调度代码的执行顺序,确保在单线程环境下能够处理异步任务。
事件循环的主要流程如下:
- 执行同步代码:从程序的入口开始执行同步代码,按照顺序执行函数调用、变量赋值等操作。
- 处理微任务队列:在同步代码执行过程中,如果遇到微任务(Promise、MutationObserver等),则将其添加到微任务队列中。同步代码执行完毕后,在下一个事件循环迭代开始前,会检查并按顺序执行微任务队列中的任务,直到微任务队列为空。
- 处理宏任务队列:检查是否有宏任务(setTimeout、setInterval、I/O等)需要执行,如果有,则按顺序执行宏任务队列中的任务。每次只执行一个宏任务,直到宏任务队列为空。
- 进入下一轮事件循环:重复上述步骤,不断处理微任务队列和宏任务队列,直到程序结束或手动停止事件循环。
18、DOM常见的操作有哪些?
DOM(Document Object Model)是指文档对象模型,它是HTML或XML文档的编程接口,提供了对文档结构、内容和样式的访问和操作。DOM 提供了一系列常见的操作方法,其中包括:
访问元素:
getElementById(id)
:通过元素的唯一ID获取元素。getElementsByClassName(className)
:通过类名获取一组元素。getElementsByTagName(tagName)
:通过标签名获取一组元素。querySelector(selector)
:通过选择器获取匹配的第一个元素。querySelectorAll(selector)
:通过选择器获取所有匹配的元素。操作元素属性和内容:
getAttribute(name)
:获取指定属性的值。setAttribute(name, value)
:设置指定属性的值。innerHTML
:获取或设置元素的 HTML 内容。textContent
:获取或设置元素的纯文本内容。classList
:用于添加、移除和切换元素的 CSS 类。操作元素样式:
style.property
:直接设置元素的行内样式属性。classList.add(className)
:向元素添加类名。classList.remove(className)
:从元素移除类名。classList.toggle(className)
:切换元素的类名状态。创建和修改元素节点:
createElement(tagName)
:创建新的元素节点。appendChild(node)
:向父节点添加子节点。removeChild(node)
:从父节点移除子节点。replaceChild(newNode, oldNode)
:用新节点替换旧节点。添加事件监听器:
addEventListener(event, listener)
:向元素添加事件监听器。removeEventListener(event, listener)
:从元素移除事件监听器。
19、常见的BOM对象有哪些?对于BOM的理解?
BOM(Browser Object Model)指的是浏览器对象模型,它是一组浏览器提供的对象和方法,用于与浏览器窗口进行交互。常见的BOM对象包括:
window
对象:表示浏览器窗口或框架。navigator
对象:提供关于浏览器的信息。screen
对象:提供关于用户屏幕的信息。location
对象:提供当前文档的 URL 信息。history
对象:提供浏览器访问历史记录的功能。document
对象:表示当前文档的内容。console
对象:提供控制台用于调试和输出信息。BOM 是 JavaScript 和浏览器之间的接口,通过 BOM 对象可以实现对浏览器窗口、导航、屏幕等进行操作和获取信息。BOM 提供了一系列对象和方法,使开发者可以与浏览器进行交互,例如修改页面 URL、执行页面跳转、操作浏览器历史记录、显示警告或错误信息等。
20、js中内存泄漏的情况有哪些?
JavaScript 中的内存泄漏是
指应用程序中已不再使用的内存仍然被占用,导致内存资源浪费
。以下是一些常见的 JavaScript 内存泄漏情况:
- 未正确释放引用:当对象不再需要时,仍然保留对它的引用。这可能发生在全局变量、闭包、事件监听器或定时器等方面。
- 循环引用:两个或更多的对象之间存在相互引用,导致无法通过垃圾回收机制释放这些对象。特别是在使用闭包时容易出现循环引用的问题。
- 被遗忘的定时器或回调函数:如果定时器或回调函数没有被正确清理和取消,它们会继续持有对对象的引用,阻止它们被垃圾回收。
- DOM 引用未释放:删除了 DOM 元素,但相关的 JavaScript 对象(如事件处理程序)仍然存在引用,导致内存泄漏。
- 大量数据存储:在客户端中存储大量数据(如缓存数据)而不进行及时清理,会导致内存占用过高。
- 不合理的缓存使用:缓存的使用需要谨慎,如果缓存对象长时间不被清理或无法回收,将导致内存泄漏。
- 第三方库的问题:某些第三方库可能存在内存泄漏的问题,需要仔细评估和处理。
为避免内存泄漏,可以采取以下几种方法:
- 及时释放不再使用的对象引用。
- 注意闭包中的变量引用,确保正确地释放资源。
- 使用合适的定时器和事件监听器,在不需要时取消它们。
- 确保删除 DOM 元素后,相关的 JavaScript 对象也被正确清理。
- 合理管理数据缓存,并定期进行清理。
- 使用浏览器开发者工具进行性能分析和内存
21、什么是防抖和节流?有什么区别?
防抖(Debouncing)和节流(Throttling)是两种常用的优化 JavaScript 函数执行的技术,用于限制函数的调用频率。它们的主要区别在于对函数的执行时机进行控制的方式不同。
防抖(Debouncing)
是指在连续触发某个事件时,在规定的延迟时间内只执行一次函数。如果在延迟时间内再次触发了该事件,则重新计时延迟时间。
应用场景:
当用户连续输入搜索关键字时,可以使用防抖技术来限制搜索请求的发送频率。只有在用户停止输入一段时间后,才会实际发送搜索请求。
节流(Throttling)
是指按照一定的时间间隔固定执行某个函数。无论事件触发频率多高,都会按照设定的时间间隔执行一次函数。
应用场景:
在处理页面滚动事件时,可以使用节流技术来限制滚动事件的处理频率。每隔一定时间执行一次滚动事件的处理逻辑,避免频繁触发事件导致性能问题。
区别:
- 触发时机:防抖在连续触发事件时,只在规定延迟时间内最后一次触发后执行,而节流则是按照固定的时间间隔进行执行。
- 执行次数:防抖只执行最后一次触发的函数,节流会按照固定间隔时间执行多次函数,但频率有限制。
- 响应速度:防抖在延迟时间内只执行一次函数,相对会更快响应最新的触发事件,而节流则是按照固定时间间隔执行函数。
22、javaScript本地存储方式有哪些?有什么区别以及应用场景?
JavaScript提供了几种本地存储方式,包括以下三种主要方式:
Cookie(Cookie)
:是浏览器端保存少量数据的一种机制。可以通过document.cookie
属性读取和设置Cookie的值。Cookie的大小有限制(一般4KB),并且会随着每次请求都携带到服务器,因此对于大量或敏感数据不适合使用。常用于存储用户登录状态、记住用户偏好设置等场景。Web Storage(Web 存储)
:包括 localStorage 和 sessionStorage 两种机制。localStorage 可以用来保存持久化的数据,而 sessionStorage 则只在当前会话有效,浏览器关闭或页面刷新后会清除。它们可以通过localStorage
和sessionStorage
对象进行读写操作。这两种机制支持存储较大的数据(一般几MB),并且只在浏览器与网站之间传输,不会自动发送到服务器。适用于长期保存数据,实现离线缓存等场景。IndexedDB(索引数据库)
:是一个功能强大的客户端数据库,允许存储大量结构化数据。它使用异步 API 进行操作,并支持事务控制。IndexedDB 是在浏览器中创建和维护的本地数据库,适用于需要高级查询或复杂数据结构的应用,比如存储大量离线数据、缓存数据等。
这些本地存储方式有以下区别和应用场景:
- Cookie 适合存储少量数据,并且需要在每次请求中发送到服务器。常用于用户身份认证、记住登录状态等。
- Web Storage(localStorage 和 sessionStorage)适合存储较大的数据,不需要频繁与服务器交互,并且希望在浏览器关闭后仍然可用。适用于缓存数据、保存用户偏好设置等场景。
- IndexedDB 适合存储大量结构化数据,并提供复杂查询和事务支持。适用于需要离线数据、高级查询或复杂数据操作的应用。