JavaScript真题合集(二)
★ 11. 闭包
闭包可以理解为“定义在一个函数内部的函数”,它将函数内部和函数外部连接起来,允许函数在其定义之外的环境中访问和操作函数内部的变量。
主要作用:
1.延长变量的作用域,使变量能够在非自身作用域的其他作用域内被使用。例如,一个函数内部的变量可以通过闭包被外部的函数访问和操作。
2.模仿块级作用域。在JavaScript等语言中,可以通过创建闭包来模拟块级作用域,因为JavaScript本身并不支持块级作用域。
3.创建私有变量。闭包可以用于创建私有变量,这些变量只能在闭包内部被访问和操作,从而实现了数据的封装和隐藏。
使用需要注意其作用域,因为闭包中的变量是存储在堆内存中的,而不是在栈内存中。如果闭包中的变量被外部引用,那么即使闭包执行完毕,这些变量也不会被垃圾回收机制回收,从而可能导致内存泄漏。
12. 类型转换机制
它指的是将值从一种类型转换为另一种类型。
我们在声明的时候只有一种数据类型,只有在运行期间才会确定当前类型。如果运行的类型与预期不符合,就会触发类型转换机制。
★ 常用的有:
- 强制转换
- Number(): 将参数转换为数字,如果转换失败则返回NaN。
- String() 或 toString(): 将参数转换为字符串。如果对象是自定义的,并且没有定义toString()方法,那么会调用Object.prototype.toString()。
- Boolean(): 将参数转换为布尔值。以下值会被转换为false:false、0、“”(空字符串)、null、undefined、NaN。其他所有值都会被转换为true。
- parseInt() 和 parseFloat(): 用于将字符串解析为整数或浮点数。
- 自动转换
- 算术运算符: 如+、-、*、/等,在涉及非数字值时会自动进行类型转换。
- 比较运算符: 如==、!=、<、>等,在比较不同类型的值时可能会进行类型转换。
- 逻辑运算符: 如&&(逻辑与)和||(逻辑或),当与布尔值运算时会进行类型转换。
- 一元运算符: 如+(正号运算符)和-(负号运算符),当用于非数字值时,会尝试将其转换为数字。
- 字符串连接: 当使用+运算符连接字符串和非字符串值时,非字符串值会被转换为字符串。
13. 深拷贝浅拷贝
13.1 深拷贝
深拷贝会创建一个新对象,并将原始对象的属性值复制到新对象。与浅拷贝不同的是,如果属性是引用类型,深拷贝会递归地复制这些对象及其子对象,直到最底层的基本数据类型。 这样,新对象和原始对象是完全独立的,对新对象的引用类型属性所做的任何更改都不会影响原始对象。
13.2 浅拷贝
浅拷贝会创建一个新对象,并将原始对象的属性值复制到新对象。但是,如果属性是引用类型(例如对象或数组),那么浅拷贝只会复制引用地址,而不是实际的对象。因此,新对象和原始对象会共享这些引用类型的属性。对其中一个对象的这些引用类型属性所做的任何更改都会反映到另一个对象上。
★ 13.3 实现方式
- 浅拷贝:在JavaScript中,你可以使用Object.assign()方法或扩展运算符(…)来实现浅拷贝。但请注意,这些方法对于数组和对象的嵌套结构只能进行浅拷贝。
- 深拷贝:在JavaScript中,深拷贝的实现比较复杂。你可以使用JSON.parse(JSON.stringify(obj))来实现深拷贝,但这种方法有一些限制,例如不能正确处理循环引用、函数和特殊对象(如Date、RegExp等)。为了更精确地实现深拷贝,你可能需要手动编写递归函数或使用专门的库(如lodash的_.cloneDeep()方法)。
// 浅拷贝示例
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = { ...obj1 }; // 或 Object.assign({}, obj1);
obj2.b.c = 3;
console.log(obj1.b.c); // 输出 3,因为obj1和obj2共享同一个b对象
// 深拷贝示例
let obj3 = { a: 1, b: { c: 2 } };
let obj4 = JSON.parse(JSON.stringify(obj3)); // 注意:这只是一种简单的方法,有局限性
obj4.b.c = 3;
console.log(obj3.b.c); // 输出 2,因为obj3和obj4的b对象是不同的
13.4 区别
- 复制方式
- 浅拷贝:创建一个新的对象,并将原始对象的属性值复制到新对象。但是,如果属性是引用类型(例如对象或数组),那么浅拷贝只会复制引用地址,而不是实际的对象。因此,新对象和原始对象会共享这些引用类型的属性。
- 深拷贝:也会创建一个新的对象,并将原始对象的属性值复制到新对象。但与浅拷贝不同,如果属性是引用类型,深拷贝会递归地复制这些对象及其子对象,直到最底层的基本数据类型。这样,新对象和原始对象是完全独立的。
- 对象独立性
- 浅拷贝:由于新对象和原始对象共享引用类型的属性,因此对其中一个对象的这些属性所做的任何更改都会反映到另一个对象上。这意味着它们并不是完全独立的。
- 深拷贝:新对象和原始对象是完全独立的。对新对象的引用类型属性所做的任何更改都不会影响原始对象。
- 性能
- 浅拷贝通常比深拷贝更快,因为它只需要复制引用地址,而不是整个对象结构。
- 深拷贝由于需要递归地复制对象及其子对象,因此通常比浅拷贝更慢,并且可能需要更多的内存。
- 用途
- 浅拷贝适用于那些只需要复制对象的基本数据类型属性,或者不需要修改引用类型属性的情况。
- 深拷贝适用于那些需要完全复制对象及其所有子对象,并且希望新对象和原始对象完全独立的情况。
浅拷贝示例 - 使用扩展运算符(...)或Object.assign()方法来进行浅拷贝。但请注意,这些方法只适用于一层属性的拷贝。
let obj1 = {
a: 1,
b: {
c: 2,
d: [1, 2, 3]
}
};
// 使用扩展运算符进行浅拷贝
let obj2 = { ...obj1 };
// 或者使用Object.assign()进行浅拷贝
// let obj2 = Object.assign({}, obj1);
// 修改obj2的b对象中的c属性
obj2.b.c = 3;
// 由于浅拷贝只复制了引用,所以obj1的b对象中的c属性也被修改了
console.log(obj1.b.c); // 输出 3
console.log(obj2.b.c); // 输出 3
// 修改obj2的b对象中的d数组
obj2.b.d.push(4);
// 由于浅拷贝只复制了引用,所以obj1的b对象中的d数组也被修改了
console.log(obj1.b.d); // 输出 [1, 2, 3, 4]
console.log(obj2.b.d); // 输出 [1, 2, 3, 4]
深拷贝示例 - 深拷贝需要复制对象的所有层级。在JavaScript中,我们可以使用递归的方式来实现深拷贝
function deepCopy(obj, hash = new WeakMap()) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
if (hash.has(obj)) {
return hash.get(obj);
}
let copy = Array.isArray(obj) ? [] : {};
hash.set(obj, copy);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key], hash);
}
}
return copy;
}
let obj1 = {
a: 1,
b: {
c: 2,
d: [1, 2, 3]
}
};
let obj3 = deepCopy(obj1);
// 修改obj3的b对象中的c属性
obj3.b.c = 3;
// 由于深拷贝创建了新的对象,所以obj1的b对象中的c属性没有被修改
console.log(obj1.b.c); // 输出 2
console.log(obj3.b.c); // 输出 3
// 修改obj3的b对象中的d数组
obj3.b.d.push(4);
// 由于深拷贝创建了新的数组,所以obj1的b对象中的d数组没有被修改
console.log(obj1.b.d); // 输出 [1, 2, 3]
console.log(obj3.b.d); // 输出 [1, 2, 3, 4]
14. 如何实现函数缓存?函数缓存有哪些应用场景?
它将函数运算过的结果缓存起来,以便在后续函数调用时能够更快地返回结果。常用于缓存数据计算结果和缓存对象。
14.1 实现函数缓存
主要依靠闭包、柯里化、高阶函数等技术。通常,实现函数缓存的方法是将参数和对应的结果数据存储在一个对象中。当函数调用时,首先判断参数对应的数据是否存在,如果存在则直接返回对应的结果数据,否则进行计算并存储结果。
14.2 应用场景
- 昂贵的函数调用:对于执行复杂计算或需要长时间才能完成的函数,使用函数缓存可以避免重复计算,从而提高性能。
- 具有有限且高度重复输入范围的函数:当函数的输入参数在有限范围内且重复出现时,使用函数缓存可以显著提高性能。
- 具有重复输入值的递归函数:在递归计算中,如果输入值重复出现,使用函数缓存可以避免重复计算,如斐波那契数列等。
- 纯函数:纯函数是指对于相同的输入,总是返回相同输出的函数。这类函数非常适合使用函数缓存,因为它们的结果可以被安全地缓存和重复使用。
★ 15. 字符串的常用方法有哪些
- 字符串拼接
- 使用 + 运算符:例如 String fullName = firstName + " " + lastName;
- 使用 concat() 方法:例如 String result = str1.concat(str2);
- 使用 StringBuilder:对于需要多次拼接的字符串,使用 StringBuilder 更为高效,因为它可以避免创建过多的中间字符串对象。
- 字符串分割
- 使用 split() 方法:根据指定的分隔符将字符串分割为子字符串数组。例如 String[] array = str.split(" ");
- 字符串替换
- 使用 replace() 方法:将字符串中的指定字符或子串替换为新的字符或子串。例如 String newStr = str.replace(“old”, “new”);
- 字符串截取
- 使用 substring() 方法:根据指定的起始索引和结束索引截取字符串的子串。
- 使用 slice()方法 :返回一个从开始到结束(不包括结束)选择的字符串的浅拷贝。原始字符串不会被改变。
- 字符串长度
- 使用 length() 或 length 属性(取决于编程语言)来获取字符串的长度(即字符数)。
- 字符串转换
- 使用 toUpperCase() 和 toLowerCase() 方法将字符串转换为大写或小写形式。
- 使用 trim() 方法去除字符串两端的空白字符(如空格、制表符、换行符等)。
- 字符串查找
- 使用 indexOf() 方法查找子串在字符串中首次出现的位置索引,或 lastIndexOf() 方法查找子串在字符串中最后一次出现的位置索引。
- 使用 chatAt() 返回给定索引位置的字符,由传给方法的整数参数指定
- 使用 includes() 从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值
- 字符串格式化
- 使用 String.format() 方法或类似的方法(取决于编程语言)来格式化字符串。
★ 16. 数组的常用方法有哪些
- push():将一个或多个元素添加到数组的末尾,并返回新的数组长度。
- unshift():将一个或多个元素添加到数组的开头,并返回新的数组长度。
- splice():对数组进行删除、替换或添加元素的操作。返回被删除的元素组成的数组。
- concat():用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
- pop():删除数组的最后一个元素,并返回该元素。如果数组为空,则不改变数组并返回undefined。
- shift():删除数组的第一个元素,并返回该元素。如果数组为空,则不改变数组并返回undefined。
- slice():返回一个新的数组对象,该对象是一个由开始到结束(不包括结束)选择的、由原数组的浅拷贝构成。原始数组不会被改变。
- indexOf():返回在数组中可以找到给定元素的第一个索引,如果不存在,则返回-1。
- lastIndexOf():返回指定元素在数组中最后一次出现的索引,如果不存在,则返回-1。
- includes():返回要查找的元素在数组中的位置,找到返回true,否则false。
- find():返回第一个匹配的元素。
- reverse():颠倒数组中元素的顺序。会改变原数组。
- sort():对数组的元素进行排序。默认是按照字符编码顺序进行升序排序。如果需要降序排序或其他比较规则,可以传入自定义的比较函数。
- some():对数组每一项都运行传入的测试函数,如果至少有1个元素返回 true ,则这个方法返回 true。
- every():测试数组的所有元素是否都通过了由提供的函数实现的测试。
- forEach():对数组每一项都运行传入的函数,没有返回值。
- filter():对数组每一项都运行传入的函数,函数返回true的项会组成数组之后返回。
- map():创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
- join():将数组的所有元素连接为一个字符串。元素是通过指定的分隔符进行分隔的。
- toString():将数组转换为字符串,并返回结果。与join(‘’)相似,但不会接受任何参数。
★ 17. 本地存储的方式
本地存储的方式主要有三种:cookie、localStorage和sessionStorage。以下是它们之间的区别和应用场景:
- Cookie
- 存储方式:Cookie是存储在用户浏览器上的小段文本信息,由服务器发送并在客户端存储。每次HTTP请求时,浏览器都会将cookie发送给服务器。
- 容量限制:Cookie的容量相对较小,通常最大为4KB。
- 有效期:可以设置cookie的过期时间,使其在设定的时间后失效。
- 安全性:由于cookie随HTTP请求发送,因此存在安全风险,可能被截获或篡改。
- 应用场景:主要用于保存用户登录状态、跟踪用户行为、定制页面内容等。
- localStorage
- 存储方式:localStorage是Web Storage API的一部分,用于在浏览器中存储键值对数据。数据存储在客户端,不随HTTP请求发送。
- 容量限制:localStorage的容量相对较大,通常为5MB左右。
- 有效期:数据存储在本地,除非显式删除或清除浏览器数据,否则始终有效。
- 安全性:localStorage的数据存储在本地,相对安全,但仍需要注意保护敏感信息。
- 应用场景:适合存储大量、持久化的数据,如用户偏好设置、游戏进度等。
- sessionStorage
- 存储方式:sessionStorage也是Web Storage API的一部分,与localStorage类似,但数据仅在当前浏览器窗口或标签页的生命周期内有效。
- 容量限制:与localStorage相同,通常为5MB左右。
- 有效期:当浏览器窗口或标签页关闭时,sessionStorage中的数据将被清除。
- 安全性:与localStorage相同,需要注意保护敏感信息。
- 应用场景:适合存储与当前会话相关的临时数据,如页面状态、表单输入等。
设置cookie
document.cookie = "username=John Doe; expires=Thu, 18 Dec 2023 12:00:00 UTC; path=/";
读取cookie
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
}
const username = getCookie('username');
console.log(username); // 输出 "John Doe"
设置localStorage
localStorage.setItem('myKey', 'myValue');
读取localStorage
const myValue = localStorage.getItem('myKey');
console.log(myValue); // 输出 "myValue"
删除localStorage
localStorage.removeItem('myKey');
清除所有localStorage
localStorage.clear();
设置sessionStorage
sessionStorage.setItem('myKey', 'myValue');
读取sessionStorage
const myValue = sessionStorage.getItem('myKey');
console.log(myValue); // 输出 "myValue"
删除sessionStorage
sessionStorage.removeItem('myKey');
18. 事件循环
js是一门单线程的语言,意味着同一时间内只能做一件事,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环。
以下是事件循环的基本工作原理:
-
调用栈:js引擎有一个调用栈,它用于跟踪函数执行的上下文。当一个函数被调用时,它会被推入调用栈。当函数执行完毕时,它会被从调用栈中弹出。
-
执行同步代码:当js代码开始执行时,它会从上到下同步执行。如果遇到了函数调用,那么这些函数会被推入调用栈并执行。
-
遇到异步任务:当JavaScript代码遇到异步任务(如setTimeout、setInterval、Promise、事件监听器等)时,它不会等待这些任务完成,而是立即将它们添加到任务队列或微任务队列中,并继续执行同步代码。
-
事件循环:当调用栈为空时(即没有更多的同步代码需要执行),事件循环会开始查看任务队列和微任务队列。
(1) 微任务队列:首先,事件循环会检查微任务队列。如果有微任务(如Promise的.then()或.catch()方法),它们会被依次执行,直到微任务队列为空。每个微任务的执行都会将其推入调用栈,并在执行完毕后从调用栈中弹出。
(2) 任务队列:接下来,事件循环会查看任务队列。如果有任务(如setTimeout、setInterval、事件监听器等),它们会按照先进先出的顺序被依次执行。同样地,每个任务的执行都会将其推入调用栈,并在执行完毕后从调用栈中弹出。
-
循环:这个过程会不断重复,形成一个循环。这就是所谓的“事件循环”。
常见的任务类型:
宏任务:常见的宏任务包括setTimeout、setInterval、I/O操作(如文件读写、网络请求等)和DOM事件等。
微任务:常见的微任务包括Promise的回调(.then()、.catch()等)和process.nextTick(在Node.js中)。
console.log('start'); // 同步代码,直接执行
setTimeout(function() {
console.log('setTimeout'); // 宏任务,放入宏任务队列
}, 0);
Promise.resolve().then(function() {
console.log('promise1'); // 微任务,放入微任务队列
}).then(function() {
console.log('promise2'); // 微任务,放入微任务队列
});
console.log('script end'); // 同步代码,直接执行
1.执行同步代码,首先打印 start。
2.遇到 setTimeout,这是一个宏任务,所以将其回调函数放入宏任务队列。
3.遇到 Promise.resolve().then(),这是一个微任务,所以将其回调函数放入微任务队列。由于这里有两个 .then(),它们都会被放入微任务队列,但 promise1 的回调函数会先被放入。
4.继续执行同步代码,打印 script end。
5.此时同步代码执行完毕,开始处理微任务队列。首先执行 promise1 的回调函数,打印 promise1。
6.接着执行 promise2 的回调函数,打印 promise2。
7.所有微任务执行完毕后,开始处理宏任务队列。执行 setTimeout 的回调函数,打印 setTimeout。
19. Ajax原理是什么?如何实现?
Ajax的原理主要是基于异步数据传输(HTTP请求)和浏览器端与服务器端之间的数据交互。它的实现过程大致如下:
- 1.JavaScript发送请求:通过JavaScript(具体来说,是XMLHttpRequest对象或现代的fetch API)向服务器发送HTTP请求。这个过程是异步的,意味着JavaScript不会等待服务器响应就继续执行后续的代码。
- 2.服务器处理请求:服务器接收到请求后,会根据请求的内容(如URL、请求头、请求体等)进行相应的处理。这可能包括从数据库中检索数据、处理文件或执行其他任务。
- 3.服务器返回响应:服务器处理完请求后,会生成一个HTTP响应并返回给浏览器。这个响应通常包含状态码、响应头和响应体。响应体可以包含HTML、XML、JSON或纯文本等数据。
- 4.JavaScript处理响应:JavaScript接收到服务器的响应后,会解析响应体中的数据,并根据需要执行相应的操作。例如,它可以使用DOM(Document Object Model)来更新页面的内容,而无需重新加载整个页面。
具体到Ajax的实现,以下是一个基本的步骤:
- 创建XMLHttpRequest对象:这是Ajax技术的核心对象,用于与服务器进行通信。你可以使用new XMLHttpRequest()来创建一个新的XMLHttpRequest对象。
- 配置请求:使用XMLHttpRequest对象的open()方法来配置请求。你需要指定请求的方法(如GET、POST等)、请求的URL以及是否异步发送请求。
- 发送请求:调用XMLHttpRequest对象的send()方法来发送请求。对于GET请求,你可以将查询参数附加到URL中;对于POST请求,你需要将请求体作为send()方法的参数传递。
- 处理响应:在发送请求后,你需要设置XMLHttpRequest对象的onreadystatechange事件处理器来监听响应的状态变化。当响应的状态码为200(表示成功)时,你可以使用XMLHttpRequest对象的responseText或responseXML属性来获取响应体的内容。
- 更新页面:一旦你获取了响应体的内容,你就可以使用JavaScript和DOM来更新页面的内容了。例如,你可以使用innerHTML属性来更改HTML元素的内容。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ajax 示例</title>
</head>
<body>
<button onclick="fetchData()">点击获取数据</button>
<div id="result"></div>
<script>
function fetchData() {
// 1. 创建 XMLHttpRequest 对象
var xhr = new XMLHttpRequest();
// 2. 配置请求
xhr.open('GET', 'https://api.example.com/data', true); // 假设这是从服务器获取数据的URL
// 3. 设置响应处理函数
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
// 4. 处理服务器响应
var data = JSON.parse(xhr.responseText); // 假设服务器返回的是JSON数据
document.getElementById('result').textContent = '获取到的数据: ' + data.message;
}
};
// 5. 发送请求
xhr.send();
}
</script>
</body>
</html>
20. 防抖节流
20.1 防抖
防抖的原理是在事件被触发后,延迟一定时间执行函数。如果在延迟时间内再次触发了该事件,则重新计时,直到延迟时间内没有再次触发事件,才执行函数。 这样可以确保在短时间内多次触发的事件只会被处理一次,从而避免不必要的计算和DOM操作。
防抖的实现通常使用js的定时器(setTimeout)来实现。
function debounce(fn, delay) {
let timer;
return function() {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, arguments);
}, delay);
};
}
20.2 节流
节流的原理是限制某个函数在一段时间内的执行频率。它确保函数在一段时间内只执行一次,即使在这段时间内事件被多次触发。与防抖不同,节流不会取消之前的执行计划,而是确保在一段时间内只执行一次。
节流的实现通常使用js的时间戳或定时器来实现。
function throttle(fn, delay) {
let lastCall = 0;
return function() {
const now = new Date().getTime();
if (now - lastCall < delay) return;
lastCall = now;
fn.apply(this, arguments);
};
}
20.3 区别
- 处理方式:防抖确保在短时间内只处理一次事件,而节流确保在一段时间内只处理一次事件。
- 使用场景:防抖通常用于输入框实时搜索、窗口大小调整等场景,而节流通常用于滚动、拖拽等需要持续触发的场景。