1.数组⾥⾯有10万个数据,取第⼀个元素和第10万个元素的时间相差多少
在JavaScript中,访问数组元素的时间复杂度是O(1),也就是说无论数组有多大,访问任何一个元素所需的时间都是常数。因此,取第一个元素和第10万个元素的时间理论上是相同的,没有明显的差异。
但是,实际的访问时间可能会受到其他因素的影响,比如CPU的调度、内存的速度等。这些因素可能导致实际的访问时间有所不同,但这种差异通常非常小,对于我们来说几乎是无法感知的。
这段代码会分别计算访问第一个元素和第10万个元素所需的时间,并打印出来。
let arr = new Array(100000).fill(0); // 创建一个包含10万个元素的数组
let startTime = performance.now(); // 记录开始时间
arr[0]; // 访问第一个元素
let endTime = performance.now(); // 记录结束时间
console.log("访问第一个元素所需时间:" + (endTime - startTime) + "ms");
startTime = performance.now(); // 重新记录开始时间
arr[99999]; // 访问第10万个元素
endTime = performance.now(); // 记录结束时间
console.log("访问第10万个元素所需时间:" + (endTime - startTime) + "ms");
2.栈和堆具体怎么存储
栈(Stack)和堆(Heap)是两种不同的内存分配方式,它们在存储数据时具有不同的特点。
-
栈(Stack):
栈是一种线性数据结构,遵循后进先出(LIFO)的原则。它在内存中以连续的块来存储数据。当一个函数被调用时,系统会在栈中为该函数分配一块连续的内存空间,用于存储函数的局部变量、参数以及返回地址等信息。当函数执行完毕后,这块内存空间会被自动释放。栈的优点是分配和释放内存速度快,但缺点是空间大小有限,容易产生栈溢出。 -
堆(Heap):
堆是一种非线性数据结构,遵循先进先出(FIFO)的原则。它在内存中以不连续的块来存储数据。程序员可以手动申请和释放堆内存。堆内存的大小只受限于操作系统可用内存的大小,因此可以动态地分配大量内存。但相对于栈,堆的分配和释放内存速度较慢。
以下简单的代码,展示了如何在C语言中使用栈和堆:
#include <stdio.h>
#include <stdlib.h>
int main() {
// 使用栈存储数据
int stack_data[5] = {1, 2, 3, 4, 5};
printf("栈中的数据:");
for (int i = 0; i < 5; i++) {
printf("%d ", stack_data[i]);
}
printf("
");
// 使用堆存储数据
int *heap_data = (int *)malloc(5 * sizeof(int));
if (heap_data == NULL) {
printf("内存分配失败!
");
return -1;
}
heap_data[0] = 1;
heap_data[1] = 2;
heap_data[2] = 3;
heap_data[3] = 4;
heap_data[4] = 5;
printf("堆中的数据:");
for (int i = 0; i < 5; i++) {
printf("%d ", heap_data[i]);
}
printf("
");
// 释放堆内存
free(heap_data);
return 0;
}
总结:栈和堆是两种不同的内存分配方式,栈以连续的块存储数据,分配和释放内存速度快,但空间大小有限;堆以不连续的块存储数据,可以动态分配大量内存,但分配和释放内存速度较慢。在实际编程中,根据需要选择合适的内存分配方式。
3.介绍闭包以及闭包为什么没清除
闭包(Closure)是一种特殊的函数,它可以捕获并记住其外部作用域中的变量,即使函数在其外部作用域之外被调用。在JavaScript中,闭包是通过创建一个函数内部的子函数来实现的。子函数可以访问其父函数的局部变量、参数以及其他子函数。当父函数执行完毕后,其局部变量和参数会被清除,但由于子函数仍然保持对它们的引用,因此它们不会被立即回收,这就是闭包。
闭包的一个常见用途是创建私有变量,这些变量不能从外部访问,只能通过特定的公共方法访问。这有助于保护数据的完整性,防止意外修改。
这是一个闭包的简单示例:
function createCounter() {
let count = 0;
return function() {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 输出 1
console.log(counter()); // 输出 2
在这个例子中,createCounter
函数返回一个匿名函数,这个匿名函数可以访问其父函数 createCounter
的局部变量 count
。当我们调用 counter()
时,实际上是在调用这个匿名函数,它会增加 count
的值并返回。由于匿名函数仍然保持对 count
的引用,所以 count
不会被清除。
闭包没有被清除的原因是因为闭包中的函数仍然保持对其外部作用域中的变量的引用。只要这些引用仍然存在,垃圾回收器就不会回收这些变量,因为它们仍在使用中。这有助于确保数据的持久性和一致性,但也可能导致内存占用过高的问题。为了避免这种情况,我们需要在不再需要闭包时手动解除对外部变量的引用,或者使用其他内存管理技术。
4.闭包的使⽤场景
闭包在前端开发中有很多实用的场景,以下是一些常见的使用场景:
- 创建私有变量:闭包可以用于创建私有变量,这些变量不能从外部访问,只能通过特定的公共方法访问。这有助于保护数据的完整性,防止意外修改。
function createCounter() {
let count = 0;
return {
increment: function() {
count += 1;
},
getCount: function() {
return count;
}
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 输出 1
- 模块化和封装:闭包可以帮助我们将代码模块化,将相关的功能封装在一个函数内部,避免全局作用域的污染。
const module = (function() {
const privateVar = "I'm private";
function privateMethod() {
console.log("Accessed private variable:", privateVar);
}
return {
publicMethod: function() {
console.log("Called public method");
privateMethod();
}
};
})();
module.publicMethod(); // 输出 "Called public method" 和 "Accessed private variable: I'm private"
- 实现高阶函数:闭包可以用于实现高阶函数,即接受其他函数作为参数或返回一个函数的函数。
function higherOrderFunction(callback) {
return function(value) {
return callback(value);
};
}
const double = higherOrderFunction(function(x) {
return x * 2;
});
console.log(double(5)); // 输出 10
- 实现函数柯里化(Currying):闭包可以用于实现函数柯里化,即把一个接受多个参数的函数转换为一系列使用闭包的嵌套函数。
function curry(fn) {
const arity = fn.length;
return function(...args) {
if (args.length >= arity) {
return fn.apply(null, args);
} else {
return function(...rest) {
return curry(fn).apply(null, args.concat(rest));
};
}
};
}
const sum = curry((a, b, c) => a + b + c);
console.log(sum(1)(2)(3)); // 输出 6
这些场景只是闭包的一部分应用,实际上闭包在前端开发中还有很多其他用途,可以根据实际需求灵活运用。
5.JS怎么实现异步
JavaScript中实现异步的常见方法有:
- 回调函数(Callback):通过将一个函数作为参数传递给另一个函数,在异步操作完成后调用该回调函数。
function asyncFunction(callback) {
setTimeout(() => {
const result = '异步操作结果';
callback(result);
}, 1000);
}
asyncFunction((result) => {
console.log(result); // 输出 "异步操作结果"
});
- Promise:Promise是一种用于处理异步操作的对象,它有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。可以使用then()和catch()方法来处理异步操作的结果。
function asyncFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const result = '异步操作结果';
resolve(result);
}, 1000);
});
}
asyncFunction().then((result) => {
console.log(result); // 输出 "异步操作结果"
}).catch((error) => {
console.error(error);
});
- async/await:async/await是基于Promise的一种更简洁的异步编程方式,使用async关键字声明一个异步函数,并在其中使用await关键字等待Promise的结果。
async function asyncFunction() {
const result = await new Promise((resolve, reject) => {
setTimeout(() => {
const result = '异步操作结果';
resolve(result);
}, 1000);
});
console.log(result); // 输出 "异步操作结果"
}
asyncFunction();
这些方法可以根据实际需求选择使用,以实现JavaScript中的异步操作。
6.异步整个执行周期
异步执行是指JavaScript代码在不阻塞主线程的情况下执行。这是因为JavaScript是单线程的,如果一个操作(如网络请求、文件读写或定时器)需要花费较长时间完成,那么它会阻塞后续代码的执行,导致页面无响应。为了避免这种情况,JavaScript引入了异步编程模型。
整个异步执行周期可以分为以下几个阶段:
-
事件循环(Event Loop):JavaScript运行时环境通过事件循环来处理异步任务。事件循环包括一个任务队列和一个微任务队列。当异步任务完成时,它们会被添加到相应的队列中。
-
任务队列(Task Queue):任务队列用于存放宏任务(macro-task),如setTimeout、setInterval和XMLHttpRequest等。
-
微任务队列(Microtask Queue):微任务队列用于存放微任务(micro-task),如Promise的then方法。
-
执行栈(Call Stack):执行栈用于存放当前正在执行的任务。当执行栈为空时,事件循环会从任务队列中取出一个任务放入执行栈执行。
下面是一个示例,展示了异步执行周期的过程:
console.log('script start');
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
console.log('script end');
输出结果如下:
script start
script end
promise
timeout
解释:
- 首先,输出"script start"和"script end",因为它们是同步任务,直接在执行栈中执行。
- 然后,Promise的then方法将微任务放入微任务队列。由于执行栈为空,事件循环将微任务放入执行栈执行,输出"promise"。
- 最后,setTimeout将宏任务放入任务队列。当执行栈再次为空时,事件循环将宏任务放入执行栈执行,输出"timeout"。
通过这个示例,我们可以看到异步执行周期是如何工作的,以及事件循环如何在任务队列和微任务队列之间切换,以实现非阻塞的异步执行。
7.Promise的三种状态
Promise在JavaScript中是一种用于处理异步操作的对象,它有三种状态:
-
pending(待定):这是Promise的初始状态。当一个Promise被创建时,它处于pending状态。这意味着Promise尚未完成,也没有失败,仍然在进行中。
-
fulfilled(已实现):当Promise成功完成时,它的状态会从pending变为fulfilled。这意味着Promise的操作已经成功完成,并且有一个结果值。
-
rejected(已拒绝):当Promise失败时,它的状态会从pending变为rejected。这意味着Promise的操作失败,并且有一个原因(reason)。
下面是一个示例,展示了Promise的三种状态:
const promise = new Promise((resolve, reject) => {
// 初始状态为pending
console.log('Promise state:', typeof promise.state); // 输出 "Promise state: undefined"
setTimeout(() => {
const randomNumber = Math.random();
if (randomNumber > 0.5) {
resolve('Promise fulfilled');
// 当成功完成时,状态变为fulfilled
console.log('Promise state:', promise.state); // 输出 "Promise state: resolved"
} else {
reject('Promise rejected');
// 当失败时,状态变为rejected
console.log('Promise state:', promise.state); // 输出 "Promise state: rejected"
}
}, 1000);
});
promise
.then((value) => {
console.log('Success:', value);
})
.catch((error) => {
console.log('Error:', error);
});
这个示例中,我们创建了一个Promise,它的初始状态是pending。然后,我们使用setTimeout模拟一个异步操作,根据随机数决定Promise是成功还是失败。如果成功,我们调用resolve将状态更改为fulfilled;如果失败,我们调用reject将状态更改为rejected。最后,我们使用then和catch方法处理Promise的结果。
8.Async/Await怎么实现
Async/Await是JavaScript中的一种异步编程方式,它是基于Promise实现的。通过使用async关键字声明一个异步函数,并在其中使用await关键字等待Promise的结果。
一个代码示例:
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Error:', error);
}
}
fetchData();
在这个示例中,我们定义了一个名为fetchData的异步函数。在函数内部,我们使用await关键字等待fetch函数返回的Promise对象。当Promise成功完成时,我们可以获取到数据并进行处理。如果发生错误,我们可以使用try-catch语句捕获异常并进行相应的处理。
需要注意的是,await关键字只能在async函数中使用。此外,await会阻塞代码执行,直到Promise完成。因此,在使用await时,需要确保不会阻塞主线程,以免影响用户体验。
9. Promise和setTimeout执⾏先后的区别
在JavaScript中,Promise和setTimeout都用于处理异步操作,但它们在执行顺序上存在显著差异。具体来说,Promise属于微任务(microtask),而setTimeout则属于宏任务(macrotask)。
首先,让我们了解什么是微任务和宏任务:
- 微任务(Microtask):包括了Promise的回调函数(如
then
和catch
),以及process.nextTick
(Node.js环境)等。这些任务会被添加到一个名为微任务队列的特殊队列中,并在当前执行栈为空时立即执行。 - 宏任务(Macrotask):包括了如
setTimeout
、setInterval
、用户交互事件、网络请求等。这些任务会被放入任务队列,等待当前的执行栈和所有微任务完成后,才会被执行。
现在,我们通过代码示例来说明两者的执行先后顺序:
console.log(1); // 输出1
setTimeout(() => {
console.log(2); // 输出2
});
new Promise((resolve) => {
console.log(3); // 输出3
resolve();
}).then(() => {
console.log(4); // 输出4
});
console.log(5); // 输出5
在上述代码中,数字的打印顺序是:1、3、5、4、2。这是因为:
console.log(1)
是同步任务,直接执行。setTimeout
将回调函数放入宏任务队列。new Promise
立即执行,打印3
,然后resolve
将then
方法中的回调函数放入微任务队列。console.log(5)
是同步任务,直接执行。- 当执行栈为空,事件循环检查微任务队列,发现有
then
方法的回调,执行并打印4
。 - 最后,当微任务队列也为空,事件循环再次检查宏任务队列,找到
setTimeout
的回调,执行并打印2
。
综上所述,Promise的回调(即微任务)总是在setTimeout(即宏任务)之前执行,这是由于JavaScript的事件循环机制决定的。
10.JS为什么要区分微任务和宏任务
在JavaScript中,区分微任务(microtask)和宏任务(macrotask)是为了更有效地管理异步行为和优化用户体验。这种设计允许开发者更好地理解事件循环(Event Loop)机制,从而更好地控制任务的执行顺序和时机。
-
提高响应性:通过微任务队列,可以确保某些重要的、高优先级的异步操作能够在主线程(执行同步任务的地方)空闲时立即得到执行。这有助于保持页面或应用的响应性。
-
避免阻塞:将任务分为宏任务和微任务,可以避免长时间运行的任务(如复杂的计算或网络请求)阻塞主线程,导致用户界面无响应。
-
优化执行顺序:有些情况下,我们希望某个异步任务在其它一系列异步任务之后立即执行。通过使用微任务,可以实现这一点,因为微任务会在所有当前宏任务完成后立即执行。
-
处理依赖关系:有时,异步任务之间存在依赖关系。通过微任务,可以确保依赖的任务按照预期的顺序执行。
下面是一个代码示例,展示了微任务和宏任务的区别以及它们如何影响执行顺序:
console.log('Start'); // 输出 "Start"
setTimeout(() => {
console.log('Timeout'); // 输出 "Timeout"
}, 0);
Promise.resolve().then(() => {
console.log('Promise'); // 输出 "Promise"
});
console.log('End'); // 输出 "End"
在这个例子中:
console.log('Start')
和console.log('End')
是同步任务,直接执行。setTimeout
是一个宏任务,被放入任务队列等待执行。Promise.resolve().then()
中的then
方法是一个微任务,被放入微任务队列。
执行顺序如下:
Start
- 同步任务直接执行。End
- 同步任务直接执行。Promise
- 当同步任务执行完毕后,事件循环检查微任务队列,发现有then
方法的回调,执行并打印。Timeout
- 当微任务队列为空,事件循环再次检查宏任务队列,找到setTimeout
的回调,执行并打印。
通过这个例子,我们可以看到,区分微任务和宏任务可以让开发者更精确地控制任务的执行顺序,从而更好地管理异步行为。
需要的同学转发本文+关注+【点击此处】即可无偿获取!