1. 砝码称重问题(一)
**砝码称重问题(一)**的描述通常是:给定一组不同重量的砝码,每个砝码的数量可能有限,要求使用这些砝码组合出不同的重量。这个问题可以看作是一个组合问题,可以使用回溯法来解决。
以下是一个简单的JavaScript实现,假设我们有一个数组weights表示不同重量的砝码,另一个数组counts表示对应砝码的数量,我们想要找出所有可能的重量组合:
function getCombinations(weights, counts) {
let results = [];
let combination = [];
function backtrack(index, currentWeight) {
if (index === weights.length) {
results.push(currentWeight);
return;
}
// 不选择当前砝码
backtrack(index + 1, currentWeight);
// 选择当前砝码,可以选择多个
for (let i = 1; i <= counts[index]; i++) {
combination.push(weights[index]);
backtrack(index + 1, currentWeight + weights[index] * i);
}
// 回溯,撤销选择
for (let i = 1; i <= counts[index]; i++) {
combination.pop();
}
}
backtrack(0, 0);
return results;
}
// 示例
let weights = [1, 2, 3]; // 砝码的重量
let counts = [2, 3, 1]; // 砝码的数量
let combinations = getCombinations(weights, counts);
console.log(combinations);
在这个实现中:
getCombinations函数接收砝码的重量和数量,返回所有可能的重量组合。backtrack是一个递归函数,用于生成组合。它接收当前处理的砝码索引index和当前组合的重量currentWeight。- 在
backtrack中,首先考虑不选择当前砝码的情况,然后考虑选择1个、2个...直到counts[index]个当前砝码的情况。 - 使用
combination数组来记录当前选择的砝码,以便于回溯。 这个实现会输出所有可能的重量组合,包括0(即一个砝码都不选的情况)。如果需要排除0,可以在最后返回结果前进行过滤。 请注意,这个实现可能不是最高效的,特别是当砝码数量很多时。对于大规模问题,可能需要更高效的算法或优化。
2. 砝码称重问题(二)
**砝码称重问题(二)**通常指的是这样一个问题:给定一组不同重量的砝码,每个砝码只有一个,求用这些砝码能够称出的所有不同的重量。
这个问题可以通过动态规划来解决,我们维护一个数组dp,其中dp[i]表示是否可以用给定的砝码组合出重量i。初始时,dp[0]为true,表示不使用任何砝码时可以称出重量0。
以下是JavaScript实现:
function getAllWeights(weights) {
let maxWeight = weights.reduce((acc, val) => acc + val, 0);
let dp = new Array(maxWeight + 1).fill(false);
dp[0] = true;
for (let weight of weights) {
for (let j = maxWeight; j >= weight; j--) {
if (dp[j - weight]) {
dp[j] = true;
}
}
}
let result = [];
for (let i = 1; i <= maxWeight; i++) {
if (dp[i]) {
result.push(i);
}
}
return result;
}
// 示例
let weights = [1, 2, 3]; // 砝码的重量
let allWeights = getAllWeights(weights);
console.log(allWeights);
在这个实现中:
getAllWeights函数接收一个数组weights,表示不同重量的砝码。maxWeight是所有砝码重量之和,也是我们可能称出的最大重量。dp是一个布尔数组,dp[i]为true表示可以用砝码组合出重量i。- 我们遍历每个砝码,更新
dp数组。如果dp[j - weight]为true,则将dp[j]也设置为true。 - 最后,我们遍历
dp数组,收集所有为true的索引,这些索引就是可以称出的所有不同重量。 这个实现的时间复杂度是O(n * maxWeight),其中n是砝码的数量,maxWeight是砝码总重量。这种方法比回溯法更高效,特别是当砝码数量较多时。
3. 砝码称重问题(三)
**砝码称重问题(三)**可以描述为:给定一组不同重量的砝码,每个砝码有无限个,求用这些砝码能够称出的所有不同的重量。
这个问题可以通过动态规划来解决,类似于问题(二),但是因为每个砝码有无限个,所以在更新dp数组时有所不同。
以下是JavaScript实现:
function getAllWeightsWithInfiniteWeights(weights) {
let maxWeight = 1000; // 假设最大重量为1000,可以根据实际情况调整
let dp = new Array(maxWeight + 1).fill(false);
dp[0] = true;
for (let weight of weights) {
for (let j = weight; j <= maxWeight; j++) {
if (dp[j - weight]) {
dp[j] = true;
}
}
}
let result = [];
for (let i = 1; i <= maxWeight; i++) {
if (dp[i]) {
result.push(i);
}
}
return result;
}
// 示例
let weights = [1, 2, 3]; // 砝码的重量
let allWeights = getAllWeightsWithInfiniteWeights(weights);
console.log(allWeights);
在这个实现中:
getAllWeightsWithInfiniteWeights函数接收一个数组weights,表示不同重量的砝码。maxWeight是可能称出的最大重量,这里假设为1000,可以根据实际情况进行调整。dp是一个布尔数组,dp[i]为true表示可以用砝码组合出重量i。- 我们遍历每个砝码,更新
dp数组。由于每个砝码有无限个,所以我们从weight开始直到maxWeight,如果dp[j - weight]为true,则将dp[j]也设置为true。 - 最后,我们遍历
dp数组,收集所有为true的索引,这些索引就是可以称出的所有不同重量。 这个实现的时间复杂度是O(n * maxWeight),其中n是砝码的数量,maxWeight是假设的最大重量。这种方法可以有效地处理每个砝码有无限个的情况。
4. 请手写“基数排序”
基数排序(Radix Sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字)和特定格式的日期等,所以基数排序并不只适用于整数。 以下是一个JavaScript实现基数排序的示例,这里以排序整数为例:
function radixSort(arr) {
// 找到最大数,确定位数
let max = Math.max(...arr);
let digits = max.toString().length;
// 从个位开始,对每一位进行排序
for (let i = 0; i < digits; i++) {
// 使用计数排序作为基数排序的辅助排序算法
countingSort(arr, i);
}
return arr;
}
function countingSort(arr, position) {
let len = arr.length;
let count = new Array(10).fill(0); // 初始化计数数组
let output = new Array(len).fill(0); // 初始化输出数组
// 计算每个数字在当前位上的出现次数
for (let i = 0; i < len; i++) {
let digit = Math.floor(arr[i] / Math.pow(10, position)) % 10;
count[digit]++;
}
// 变更count[i],使count[i]包含位置i的数字之前所有数字的数量
for (let i = 1; i < 10; i++) {
count[i] += count[i - 1];
}
// 构建输出数组
for (let i = len - 1; i >= 0; i--) {
let digit = Math.floor(arr[i] / Math.pow(10, position)) % 10;
output[count[digit] - 1] = arr[i];
count[digit]--;
}
// 将排序后的数字赋值回原数组
for (let i = 0; i < len; i++) {
arr[i] = output[i];
}
}
// 示例
let array = [170, 45, 75, 90, 802, 24, 2, 66];
console.log(radixSort(array)); // 输出: [2, 24, 45, 66, 75, 90, 170, 802]
在这个实现中:
radixSort函数是主函数,它接收一个数组arr并返回排序后的数组。- 首先找到数组中的最大数,以确定数字的位数。
- 从个位开始,对每一位进行排序,这里使用了一个辅助函数
countingSort来进行每一位的排序。 countingSort函数实现了计数排序,它根据当前位的数字对数组进行排序。- 最后,将排序后的数组返回。 基数排序的时间复杂度是O(nk),其中n是数组的长度,k是数字的最大位数。这个算法适用于大量数据且数字范围不大的情况。
5. 请手写“桶排序”
桶排序(Bucket Sort)是一种基于计数的排序算法,它将输入数据分布到一定数量的桶中,每个桶再进行单独排序(可以是使用其他排序算法或递归地使用桶排序),最后将所有桶中的数据合并以得到排序后的结果。 以下是一个JavaScript实现桶排序的示例:
function bucketSort(arr, bucketSize = 5) {
if (arr.length === 0) {
return arr;
}
// 找到最大值和最小值
let min = Math.min(...arr);
let max = Math.max(...arr);
// 计算桶的数量
let bucketCount = Math.floor((max - min) / bucketSize) + 1;
let buckets = new Array(bucketCount);
// 初始化桶
for (let i = 0; i < bucketCount; i++) {
buckets[i] = [];
}
// 将数组中的元素分配到桶中
for (let i = 0; i < arr.length; i++) {
let bucketIndex = Math.floor((arr[i] - min) / bucketSize);
buckets[bucketIndex].push(arr[i]);
}
// 对每个桶进行排序
for (let i = 0; i < bucketCount; i++) {
buckets[i].sort((a, b) => a - b);
}
// 合并桶中的元素到原数组
let index = 0;
for (let i = 0; i < bucketCount; i++) {
for (let j = 0; j < buckets[i].length; j++) {
arr[index++] = buckets[i][j];
}
}
return arr;
}
// 示例
let array = [29, 25, 3, 49, 9, 37, 21, 43];
console.log(bucketSort(array)); // 输出: [3, 9, 21, 25, 29, 37, 43, 49]
在这个实现中:
bucketSort函数是主函数,它接收一个数组arr和一个可选的bucketSize参数,表示每个桶的大小。- 首先找到数组中的最大值和最小值,以确定桶的范围。
- 计算桶的数量,并初始化每个桶为一个空数组。
- 遍历原数组,将每个元素分配到对应的桶中。
- 对每个桶中的元素进行排序,这里使用了数组的
sort方法。 - 最后,将所有桶中的元素按顺序合并回原数组。 桶排序的时间复杂度在最佳情况下是O(n),但实际性能取决于数据的分布和桶的大小。如果数据分布均匀,桶排序可以非常高效。如果桶的大小选择不当或数据分布不均,性能可能会下降。
6. 请手写“计数排序”
计数排序(Counting Sort)是一种非比较排序算法,适用于小范围整数的排序。它的基本思想是对于每个输入元素,确定小于该元素的元素数量,然后将该元素放到输出数组中的正确位置。 以下是计数排序的JavaScript实现:
function countingSort(arr) {
// 找到数组中的最大值
let max = Math.max(...arr);
let min = Math.min(...arr);
// 初始化计数数组
let count = new Array(max - min + 1).fill(0);
// 计算每个元素的个数
for (let i = 0; i < arr.length; i++) {
count[arr[i] - min]++;
}
// 修改计数数组,使每个元素表示小于或等于该元素的元素数量
for (let i = 1; i < count.length; i++) {
count[i] += count[i - 1];
}
// 创建输出数组
let output = new Array(arr.length);
// 构建输出数组
for (let i = arr.length - 1; i >= 0; i--) {
output[count[arr[i] - min] - 1] = arr[i];
count[arr[i] - min]--;
}
// 将排序后的数组复制回原数组
for (let i = 0; i < arr.length; i++) {
arr[i] = output[i];
}
return arr;
}
// 示例
let array = [4, 2, 2, 8, 3, 3, 1];
console.log(countingSort(array)); // 输出: [1, 2, 2, 3, 3, 4, 8]
在这个实现中:
countingSort函数接收一个数组arr作为参数。- 首先找到数组中的最大值和最小值,以确定计数数组的大小。
- 初始化计数数组
count,其长度为最大值与最小值的差加一,所有元素初始为0。 - 遍历原数组,计算每个元素的出现次数,并存储在计数数组中。
- 修改计数数组,使每个元素表示小于或等于该元素的元素数量。
- 创建输出数组
output,其长度与原数组相同。 - 从后向前遍历原数组,根据计数数组将每个元素放到输出数组中的正确位置,并递减计数数组中的相应值。
- 将输出数组复制回原数组,完成排序。 计数排序的时间复杂度是O(n + k),其中n是数组的长度,k是数组中最大元素与最小元素的差。计数排序在k不是很大时非常高效。
7. 请手写“堆排序”
堆排序(Heap Sort)是一种基于堆数据结构的比较排序算法。堆是一种完全二叉树,其中每个父节点的值都小于或等于其所有子节点的值(最小堆)或者每个父节点的值都大于或等于其所有子节点的值(最大堆)。堆排序的基本思想是将待排序序列构造成一个最大堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将其余n-1个元素重新构造成一个最大堆,这样会得到n个元素的次小值。如此反复进行便能得到一个有序序列。 以下是堆排序的JavaScript实现:
function heapSort(arr) {
let n = arr.length;
// 构建最大堆
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
heapify(arr, n, i);
}
// 逐步将堆顶元素与末尾元素交换,并重新调整堆
for (let i = n - 1; i > 0; i--) {
// 将当前堆顶(最大值)与末尾元素交换
[arr[0], arr[i]] = [arr[i], arr[0]];
// 调用heapify函数,重新调整堆
heapify(arr, i, 0);
}
return arr;
}
// 调整堆的函数
function heapify(arr, n, i) {
let largest = i; // 初始化最大元素索引为i
let left = 2 * i + 1; // 左子节点索引
let right = 2 * i + 2; // 右子节点索引
// 如果左子节点大于最大元素
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
// 如果右子节点大于最大元素
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
// 如果最大元素不是当前节点,则交换它们,并继续调整子树
if (largest !== i) {
[arr[i], arr[largest]] = [arr[largest], arr[i]];
heapify(arr, n, largest);
}
}
// 示例
let array = [4, 10, 3, 5, 1];
console.log(heapSort(array)); // 输出: [1, 3, 4, 5, 10]
在这个实现中:
heapSort函数接收一个数组arr作为参数。- 首先通过循环调用
heapify函数构建一个最大堆。 - 然后通过循环将堆顶元素(最大值)与数组末尾元素交换,并重新调整剩余元素构成的堆。
heapify函数用于调整堆,确保以索引i为根的子树是一个最大堆。- 在
heapify函数中,比较当前节点与其左右子节点的大小,将最大值的索引保存在largest变量中。 - 如果最大值不是当前节点,则与当前节点交换,并递归地调整受影响的子树。 堆排序的时间复杂度是O(n log n),其中n是数组的长度。堆排序是不稳定的排序算法,但在最坏、平均和最好情况下都能提供较好的性能。
8. 请手写“快速排序”
快速排序(Quick Sort)是一种高效的排序算法,采用分而治之的策略来把一个序列分为较小和较大的两个子序列,然后递归地排序两个子序列。该算法的平均时间复杂度为O(n log n),但在最坏情况下会退化到O(n^2)。 以下是快速排序的JavaScript实现:
function quickSort(arr) {
if (arr.length <= 1) {
return arr;
}
// 选择基准值,这里选择数组中的第一个元素
const pivot = arr[0];
const left = [];
const right = [];
// 分区操作
for (let i = 1; i < arr.length; i++) {
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
// 递归排序左右子数组,并将排序好的数组合并
return [...quickSort(left), pivot, ...quickSort(right)];
}
// 示例
let array = [8, 7, 2, 1, 0, 9, 6];
console.log(quickSort(array)); // 输出: [0, 1, 2, 6, 7, 8, 9]
在这个实现中:
quickSort函数接收一个数组arr作为参数。- 如果数组长度小于或等于1,直接返回数组,因为长度为1的数组已经是有序的。
- 选择数组中的第一个元素作为基准值
pivot。 - 创建两个空数组
left和right,用于存放小于和大于基准值的元素。 - 遍历数组,将小于基准值的元素放入
left数组,将大于或等于基准值的元素放入right数组。 - 递归地对
left和right数组进行快速排序。 - 使用展开运算符
...将排序好的left数组、基准值pivot和排序好的right数组合并,返回最终排序好的数组。 请注意,这个实现是递归的,并且在每次递归时都会创建新的数组,这可能会在数组很大时导致较高的内存消耗。在实际应用中,可以采用原地排序的快速排序实现来减少内存使用。
9. 请手写“归并排序”
归并排序(Merge Sort)是一种分而治之的排序算法,它将数组分成两半,对每一半进行排序,然后将结果合并起来。归并排序的时间复杂度始终为O(n log n),这使得它非常稳定和高效。 以下是归并排序的JavaScript实现:
function mergeSort(arr) {
if (arr.length <= 1) {
return arr;
}
// 将数组分成两半
const middle = Math.floor(arr.length / 2);
const left = arr.slice(0, middle);
const right = arr.slice(middle);
// 递归地对左右两半进行排序
const sortedLeft = mergeSort(left);
const sortedRight = mergeSort(right);
// 合并排序好的两半
return merge(sortedLeft, sortedRight);
}
function merge(left, right) {
let result = [];
let indexLeft = 0;
let indexRight = 0;
// 合并两个数组
while (indexLeft < left.length && indexRight < right.length) {
if (left[indexLeft] < right[indexRight]) {
result.push(left[indexLeft]);
indexLeft++;
} else {
result.push(right[indexRight]);
indexRight++;
}
}
// 将剩余的元素添加到结果数组中
return result.concat(left.slice(indexLeft)).concat(right.slice(indexRight));
}
// 示例
let array = [8, 7, 2, 1, 0, 9, 6];
console.log(mergeSort(array)); // 输出: [0, 1, 2, 6, 7, 8, 9]
在这个实现中:
mergeSort函数接收一个数组arr作为参数。- 如果数组长度小于或等于1,直接返回数组,因为长度为1的数组已经是有序的。
- 找到数组的中间位置,将数组分成两半。
- 递归地对左右两半进行归并排序。
merge函数用于合并两个已排序的数组。它通过比较两个数组中的元素,将较小的元素依次放入结果数组中。- 如果其中一个数组先被完全合并,那么将另一个数组的剩余部分直接添加到结果数组的末尾。
mergeSort函数最终返回合并后的排序数组。 归并排序是一种稳定的排序算法,因为它在合并过程中保持了相同元素之间的原始顺序。此外,由于归并排序在递归过程中需要分配额外的空间来存储左右子数组,因此它的空间复杂度为O(n)。
10. 请手写“希尔排序”
希尔排序(Shell Sort)是一种基于插入排序的排序算法,通过允许交换距离较远的元素来改进插入排序,从而提高排序效率。希尔排序没有固定的排序顺序,因此是一种不稳定的排序算法。 以下是希尔排序的JavaScript实现:
function shellSort(arr) {
let n = arr.length;
let gap = Math.floor(n / 2); // 初始间隔
// 不断减小间隔直到为1
while (gap > 0) {
// 使用插入排序对间隔为gap的元素进行排序
for (let i = gap; i < n; i++) {
let temp = arr[i];
let j = i;
// 将arr[i]插入到正确的位置
while (j >= gap && arr[j - gap] > temp) {
arr[j] = arr[j - gap];
j -= gap;
}
arr[j] = temp;
}
// 减小间隔
gap = Math.floor(gap / 2);
}
return arr;
}
// 示例
let array = [8, 7, 2, 1, 0, 9, 6];
console.log(shellSort(array)); // 输出: [0, 1, 2, 6, 7, 8, 9]
在这个实现中:
shellSort函数接收一个数组arr作为参数。- 初始化间隔
gap为数组长度的一半。 - 使用一个循环来不断减小间隔,直到间隔为1。
- 在每个间隔下,使用插入排序的原理对间隔为
gap的元素进行排序。 - 在内部循环中,将当前元素
arr[i]与它前面间隔为gap的元素进行比较,如果前面的元素大于当前元素,则将前面的元素向后移动。 - 找到合适的位置后,将当前元素插入。
- 间隔
gap每次减小为原来的一半,直到gap为1,此时数组基本有序,最后进行一次普通的插入排序。 希尔排序的时间复杂度依赖于所选择的间隔序列,最坏情况下为O(n^2),但通常情况下要好得多。由于希尔排序没有固定的排序顺序,它是不稳定的排序算法。此外,希尔排序是原地排序,空间复杂度为O(1)。
11. 请手写“插入排序”
插入排序是一种简单的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常使用in-place排序(即只需用到O(1)的额外空间的排序)。 以下是插入排序的JavaScript实现:
function insertionSort(arr) {
let n = arr.length;
for (let i = 1; i < n; i++) {
let current = arr[i];
let j = i - 1;
// 将当前元素与已排序的元素从后向前比较,找到合适的位置插入
while (j >= 0 && arr[j] > current) {
arr[j + 1] = arr[j]; // 将元素向后移动
j = j - 1;
}
arr[j + 1] = current; // 插入当前元素
}
return arr;
}
// 示例
let array = [8, 7, 2, 1, 0, 9, 6];
console.log(insertionSort(array)); // 输出: [0, 1, 2, 6, 7, 8, 9]
在这个实现中:
insertionSort函数接收一个数组arr作为参数。- 从数组的第二个元素开始(因为第一个元素自然是有序的),遍历数组。
- 对于每个元素
arr[i],将其存储在current变量中。 - 将
current与前面的已排序元素从后向前进行比较,找到合适的位置插入。 - 如果前面的元素大于
current,则将前面的元素向后移动一位。 - 找到合适的位置后,将
current插入到该位置。 - 重复上述过程,直到数组完全排序。 插入排序的时间复杂度为O(n^2),在最佳情况下(数组已经是有序的)时间复杂度为O(n)。插入排序是稳定的排序算法,也是原地排序,空间复杂度为O(1)。由于它的简单性和在小型数组上的效率,插入排序经常被用于其他排序算法的辅助算法,例如希尔排序和快速排序中的小数组排序。
12. 请手写“选择排序”
选择排序是一种简单的排序算法,其工作原理是每次从未排序的部分中找到最小(或最大)的元素,将其与未排序部分的第一个元素交换位置。这样,每次迭代都会将一个元素放到其最终位置上。 以下是选择排序的JavaScript实现:
function selectionSort(arr) {
let n = arr.length;
for (let i = 0; i < n - 1; i++) {
// 找到从i到n-1中最小元素的索引
let minIndex = i;
for (let j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 将找到的最小元素与第i位置的元素交换
if (minIndex !== i) {
let temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
return arr;
}
// 示例
let array = [8, 7, 2, 1, 0, 9, 6];
console.log(selectionSort(array)); // 输出: [0, 1, 2, 6, 7, 8, 9]
在这个实现中:
selectionSort函数接收一个数组arr作为参数。- 外层循环从数组的第一个元素开始,遍历到倒数第二个元素(因为最后一个元素在之前的迭代中已经被放置在其最终位置上)。
- 内层循环从当前外层循环的索引
i后的下一个元素开始,遍历到数组的最后一个元素,寻找最小元素的索引。 - 如果找到的最小元素不是当前外层循环的索引
i对应的元素,则将这两个元素交换位置。 - 重复上述过程,直到数组完全排序。 选择排序的时间复杂度为O(n^2),无论最佳情况还是最坏情况。选择排序是不稳定的排序算法,因为相同元素的原始顺序可能被改变。它是原地排序,空间复杂度为O(1)。尽管选择排序的时间复杂度不是最优的,但由于其实现简单,它有时用于教学或小规模数据的排序。
13. 请手写“冒泡排序”
冒泡排序是一种简单的排序算法,它重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。 以下是冒泡排序的JavaScript实现:
function bubbleSort(arr) {
let n = arr.length;
for (let i = 0; i < n - 1; i++) {
// 标记本轮是否有交换
let swapped = false;
// 从第一个元素到第n-i-1个元素
for (let j = 0; j < n - i - 1; j++) {
// 如果当前元素大于下一个元素,交换它们
if (arr[j] > arr[j + 1]) {
let temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = true;
}
}
// 如果一轮比较中没有进行交换,说明数组已经排序完成
if (!swapped) {
break;
}
}
return arr;
}
// 示例
let array = [8, 7, 2, 1, 0, 9, 6];
console.log(bubbleSort(array)); // 输出: [0, 1, 2, 6, 7, 8, 9]
在这个实现中:
bubbleSort函数接收一个数组arr作为参数。- 外层循环表示排序的趟数,最多进行
n-1趟,其中n是数组的长度。 - 内层循环用于比较相邻的元素,如果它们的顺序错误就把它们交换过来。
swapped变量用于标记某一趟排序中是否有元素交换,如果某一趟没有交换,说明数组已经排序完成,可以提前结束排序。- 重复上述过程,直到数组完全排序。 冒泡排序的时间复杂度为O(n^2),在最佳情况下(数组已经排序),时间复杂度可以降低到O(n),因为只需要遍历一次数组即可。冒泡排序是稳定的排序算法,因为它不会改变相同元素之间的原始顺序。它是原地排序,空间复杂度为O(1)。尽管冒泡排序不是最高效的排序算法,但由于其实现简单,它经常被用于教学和简单排序任务。
14. 什么是 let 的临时性死区?
**let的临时性死区(Temporal Dead Zone,简称TDZ)**是指在使用let关键字声明变量时,从进入块级作用域开始,到变量被声明之间的区域。在这段区域内,变量不能被访问,否则会抛出错误。 具体来说,当使用let声明一个变量时,该变量在声明之前是不能被访问的,这与其他一些编程语言中的变量提升(hoisting)行为不同。在变量提升中,变量声明会被提升到作用域的顶部,但初始化(赋值)仍然留在原地。而let声明的变量在进入其作用域时就已经被创建了,但在声明之前尝试访问它会导致一个ReferenceError。 以下是一个示例,展示了let的临时性死区:
{
// TDZ开始
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 10;
// TDZ结束,a可以正常访问
console.log(a); // 10
}
在这个例子中,从块级作用域的开始到let a = 10;这一行之间,变量a处于临时性死区。在这段区域内尝试访问a会导致错误。
需要注意的是,临时性死区只适用于使用let和const声明的变量。使用var声明的变量没有这个问题,因为var声明的变量存在变量提升行为。
理解临时性死区有助于避免在代码中引入难以追踪的错误,并鼓励更清晰的变量声明和作用域管理。
15. 实现柯里化
柯里化(Currying)是一种函数转换技术,它将一个接受多个参数的函数转换成一系列接受单一参数的函数。这是一种在函数式编程中常见的模式。 下面是一个简单的JavaScript实现柯里化的例子:
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}
// 示例使用柯里化
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2, 3)); // 6
在这个例子中,curry函数接受一个函数func作为参数,并返回一个新的函数curried。这个新函数可以接受一部分参数,如果传入的参数数量足够,则直接调用原函数;如果参数数量不足,则返回一个新的函数,这个新函数可以继续接受参数,直到参数数量足够为止。
func.length属性表示函数func期望接受的参数数量。这是用来判断是否已经接收了足够参数的依据。
这种柯里化实现允许函数的参数分批传入,增加了函数的灵活性和重用性。
16. JS代码中的use strict是什么意思?
"use strict"; 是 JavaScript 中的一种指令,用于指示 JavaScript 引擎以严格模式(strict mode)运行代码。严格模式是 ECMAScript 5 引入的一种更严格的解析和错误处理机制,它改变了语义,使得代码的执行更加严格,有助于开发者编写更安全、更清晰的代码。
严格模式的主要特点包括:
- 禁止使用未声明的变量:在严格模式下,如果尝试使用一个未声明的变量,将会抛出错误。这有助于避免因变量名拼写错误而导致的问题。
- 禁止删除不可删除的属性:在严格模式下,尝试删除不可删除的属性(如
Object.prototype的属性)将会抛出错误。 - 禁止函数中的
this指向全局对象:在非严格模式下,如果函数不是作为对象的方法调用,this将指向全局对象(在浏览器中是window)。在严格模式下,这种情况下this将是undefined。 - 禁止使用
with语句:with语句在严格模式下被禁用,因为它可能会导致代码运行时的不确定性。 - 禁止使用八进制字面量:在严格模式下,以
0开头的数字字面量(如010)将被视为无效,因为它们被视为八进制数。 - 禁止使用
eval和arguments作为变量名:在严格模式下,eval和arguments不能被用作变量名或函数名。 - 函数声明中的参数名必须唯一:在严格模式下,如果函数声明中有重复的参数名,将会抛出错误。
- 增强了错误报告:严格模式会为一些在非严格模式下可能不会抛出错误的操作抛出错误。
要启用严格模式,只需在文件的顶部或函数体的开始处添加
"use strict";指令。这是一个字符串字面量,不是语句,因此不需要分号结尾,但如果它位于代码块的开头,通常建议加上分号以避免潜在的语法错误。
// 全局严格模式
"use strict";
var v = "This is a strict mode script";
function myFunction() {
// 函数内部的严格模式
"use strict";
// 函数体
}
请注意,严格模式只影响它所在的代码块或函数,不会影响其他非严格模式的代码。
17. common.js和es6中模块引入的区别?
CommonJS 和 ES6(ECMAScript 2015)模块系统是 JavaScript 中两种不同的模块引入和导出方式,它们在语法、加载方式和执行时机等方面有所区别:
1. 语法区别
CommonJS:
- 导出:使用
module.exports或exports对象。 - 引入:使用
require()函数。
// math.js
module.exports = {
add: function(a, b) {
return a + b;
}
};
// main.js
const math = require('./math');
console.log(math.add(1, 2)); // 3
ES6 Modules:
- 导出:使用
export关键字。 - 引入:使用
import关键字。
// math.js
export function add(a, b) {
return a + b;
}
// main.js
import { add } from './math';
console.log(add(1, 2)); // 3
2. 加载方式
CommonJS:
- 同步加载模块,适用于服务器端(Node.js)。
- 模块加载时,代码会被执行,模块内的代码是运行时才加载的。 ES6 Modules:
- 可以静态分析,即在代码编译阶段就可以确定模块的依赖关系。
- 支持异步加载,适用于浏览器端,可以通过
<script type="module">标签或动态import()语法实现。
3. 执行时机
CommonJS:
- 模块代码在第一次被
require时执行,并且模块内的变量会缓存,后续再次require同一模块时,不会重新执行模块代码,而是直接返回缓存的结果。 ES6 Modules: - 模块代码在导入时执行,并且每个模块只执行一次。
- ES6 模块导入的变量是只读的,并且是实时绑定的,意味着对导出变量的修改会反映到导入方。
4. this 的值
CommonJS:
- 模块内的
this指向当前模块的导出对象。 ES6 Modules: - 模块内的
this是undefined。
5. 循环依赖
CommonJS:
- 可以处理循环依赖,因为模块是按需加载的,一个模块可以在它被完全执行之前被
require。 ES6 Modules: - 也支持循环依赖,但是处理方式不同,因为模块的导入和导出是静态的,循环依赖需要更明确的处理。
6. 默认导出
CommonJS:
- 默认导出是通过
module.exports实现的。 ES6 Modules: - 支持默认导出,使用
export default语法。
// ES6 默认导出
export default function add(a, b) {
return a + b;
}
// ES6 默认导入
import add from './math';
7. 环境支持
CommonJS:
- 主要用于 Node.js 环境,但在浏览器端可以通过打包工具(如 Browserify)使用。 ES6 Modules:
- 原生支持于现代浏览器,也可以通过打包工具(如 Webpack)在旧浏览器中使用。 总的来说,ES6 模块提供了更现代、更灵活的模块系统,适用于前端和后端开发,而 CommonJS 模块则更常用于 Node.js 环境。随着 JavaScript 的发展,ES6 模块逐渐成为主流。
18. 什么是变量提升
**变量提升(Hoisting)**是JavaScript中的一个重要概念,它指的是在代码执行之前,JavaScript引擎会先进行编译阶段,在这个阶段中,变量和函数的声明会被移动到它们所在作用域的顶部。这种“移动”是概念上的,实际上是在代码解析时JavaScript引擎将这些声明放入了内存中。
变量提升的细节:
- 变量声明提升:
- 使用
var关键字声明的变量会被提升到函数或全局作用域的顶部。 - 但是,变量的赋值不会提升,只有声明会被提升。
- 例如:
在上述代码中,console.log(myVar); // undefined var myVar = 5;var myVar;被提升到了顶部,而myVar = 5;保留在原位。
- 使用
- 函数声明提升:
- 函数声明(使用
function关键字)会被完全提升,包括函数体。 - 例如:
在上述代码中,myFunction(); // "Hello, World!" function myFunction() { console.log("Hello, World!"); }function myFunction() {...}被完全提升到了顶部。
- 函数声明(使用
- 函数表达式提升:
- 函数表达式(将函数赋值给变量)的提升行为与变量声明相同,只有变量的声明被提升,函数体本身不会被提升。
- 例如:
在上述代码中,myFunction(); // TypeError: myFunction is not a function var myFunction = function() { console.log("Hello, World!"); };var myFunction;被提升到了顶部,而函数表达式本身保留在原位。
let和const:- 使用
let和const关键字声明的变量也会被提升,但它们的行为与var不同,存在“暂时性死区”(Temporal Dead Zone, TDZ)。 - 在变量声明之前访问这些变量会导致
ReferenceError。 - 例如:
在上述代码中,console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization let myLet = 5;myLet的声明被提升了,但在声明之前访问它会导致错误。
- 使用
为什么需要了解变量提升?
- 避免错误:理解变量提升有助于避免在代码中引入意外行为或错误。
- 代码组织:了解这一机制可以帮助开发者更好地组织代码,例如将函数声明放在作用域的顶部。
- 性能优化:在某些情况下,合理利用变量提升可以优化代码的执行性能。 总之,变量提升是JavaScript的一个核心特性,理解它对于深入掌握JavaScript至关重要。
19. 箭头函数和普通函数有啥区别?箭头函数能当构造函数吗?
箭头函数(Arrow Functions)是ES6(ECMAScript 2015)引入的一种新的函数表达式形式,它们与普通函数(通常指使用function关键字声明的函数)在语法和功能上有一些显著的区别。以下是箭头函数和普通函数的主要区别:
语法区别:
- 箭头函数:
- 使用
=>符号定义。 - 如果函数体只有一条语句,可以省略大括号和
return关键字。 - 如果参数只有一个,可以省略小括号。
- 例如:
const add = (a, b) => a + b; const square = x => x * x;
- 使用
- 普通函数:
- 使用
function关键字定义。 - 必须使用大括号包裹函数体,并且如果需要返回值,必须使用
return关键字。 - 参数无论多少,都需要使用小括号。
- 例如:
function add(a, b) { return a + b; } function square(x) { return x * x; }
- 使用
功能区别:
this绑定:- 箭头函数不绑定自己的
this,它会捕获其所在上下文的this值作为自己的this值。 - 普通函数有自己的
this上下文,其值取决于调用方式(如直接调用、作为对象方法调用、使用new关键字调用等)。
- 箭头函数不绑定自己的
- 构造函数:
- 箭头函数不能用作构造函数,不能使用
new关键字实例化。 - 普通函数可以当作构造函数使用,可以创建新的对象实例。
- 箭头函数不能用作构造函数,不能使用
- 原型:
- 箭头函数没有
prototype属性。 - 普通函数有
prototype属性,可以用于继承。
- 箭头函数没有
- arguments对象:
- 箭头函数没有自己的
arguments对象,但可以访问外围函数的arguments。 - 普通函数有自己的
arguments对象,包含函数调用时传入的所有参数。
- 箭头函数没有自己的
- 函数名称:
- 箭头函数没有函数名称(匿名函数),但在某些情况下可以使用
name属性获取函数名。 - 普通函数通常有函数名称。
- 箭头函数没有函数名称(匿名函数),但在某些情况下可以使用
- 可调用性:
- 箭头函数不能被
call、apply或bind方法改变this指向。 - 普通函数可以使用这些方法改变
this指向。
- 箭头函数不能被
箭头函数不能作为构造函数的原因:
箭头函数设计之初就是为了简化函数表达和解决this绑定问题,它们没有constructor方法,也没有prototype属性,因此不能使用new关键字来创建实例。如果尝试使用new关键字调用箭头函数,会抛出错误。
示例:
// 箭头函数
const arrowFunc = () => {};
// 尝试作为构造函数使用
const instance = new arrowFunc(); // TypeError: arrowFunc is not a constructor
// 普通函数
function normalFunc() {}
// 作为构造函数使用
const normalInstance = new normalFunc(); // 正常工作
在实际开发中,根据需要选择使用箭头函数或普通函数。箭头函数适合用于需要简洁语法和不需要自己this上下文的场景,而普通函数则更适合需要作为构造函数或需要独立this上下文的场景。
20. WebSocket 中的心跳是为了解决什么问题?
WebSocket中的心跳(也称为心跳机制或保活机制)主要是为了解决以下问题:
- 检测连接是否活跃:
- WebSocket连接建立后,如果长时间没有数据传输,很难判断连接是否仍然有效。心跳机制通过定期发送消息(通常是小型控制帧,如Ping帧)来检测对方是否仍然在线并响应。
- 防止连接被中间设备关闭:
- 某些网络设备(如防火墙、路由器、代理服务器)可能会在连接空闲一段时间后关闭它,以节省资源。心跳机制通过定期发送消息来保持连接的活跃状态,从而防止被这些设备关闭。
- 及时关闭无效连接:
- 如果客户端或服务器端因为某些原因(如网络故障、应用崩溃)无法正常工作,心跳机制可以帮助及时发现这些无效连接并关闭它们,释放资源。
- 维持会话状态:
- 在某些应用场景中,需要维持会话状态,即使没有数据传输。心跳机制可以确保会话状态不被意外终止。
- 同步时钟:
- 在某些实时应用中,可能需要同步客户端和服务器端的时钟。心跳消息可以携带时间戳信息,用于时钟同步。
- 负载均衡和故障转移:
- 在分布式系统中,心跳机制可以帮助负载均衡器检测后端服务器的状态,以便进行有效的负载分配和故障转移。
心跳机制的工作原理:
- Ping/Pong帧:WebSocket协议定义了Ping和Pong控制帧,用于心跳机制。客户端或服务器可以发送Ping帧,对方在收到后应立即回复Pong帧。如果发送方在指定时间内没有收到Pong帧,可以认为连接已经断开。
- 定时器:客户端和服务器可以设置定时器,定期发送Ping帧。同时,也需要设置超时时间,如果在超时时间内没有收到Pong帧,则认为连接断开。
- 应用层心跳:除了使用WebSocket内置的Ping/Pong帧,应用程序也可以实现自己的心跳机制,通过发送特定的消息格式来检测连接状态。
示例:
// 客户端示例
const socket = new WebSocket('wss://example.com/socket');
// 设置心跳间隔
const heartBeatInterval = 30000; // 30秒
let heartBeatTimer = null;
socket.onopen = function() {
// 连接打开后,开始发送心跳
sendHeartBeat();
};
socket.onclose = function() {
// 连接关闭后,清除心跳定时器
clearInterval(heartBeatTimer);
};
socket.onmessage = function(event) {
// 收到消息后,重置心跳定时器
resetHeartBeat();
};
function sendHeartBeat() {
// 发送Ping帧
socket.ping();
// 重置心跳定时器
resetHeartBeat();
}
function resetHeartBeat() {
// 清除旧的心跳定时器
clearInterval(heartBeatTimer);
// 设置新的心跳定时器
heartBeatTimer = setInterval(sendHeartBeat, heartBeatInterval);
}
在这个示例中,客户端在连接打开后开始发送心跳,并在收到消息后重置心跳定时器。如果服务器在指定时间内没有回复Pong帧,客户端可以认为连接已经断开,并采取相应措施(如重连)。 总之,心跳机制是WebSocket连接管理中的重要部分,它有助于确保连接的稳定性和可靠性。
21. 说说对 WebSocket 的了解
WebSocket是一种在单个TCP连接上提供全双工通信的协议。它建立在HTTP协议之上,旨在解决HTTP协议的无状态、单向互动带来的问题,实现客户端与服务器端之间的实时数据传输。 主要特点:
- 全双工通信:与HTTP的半双工通信不同,WebSocket提供全双工通信,允许数据在两个方向上同时传输。
- 实时性:WebSocket减少了HTTP协议的头部信息,降低了通信延迟,提高了实时性。
- 持久连接:WebSocket连接建立后,可以长时间保持开放状态,避免了HTTP连接的频繁建立和关闭。
- 二进制支持:WebSocket支持二进制数据传输,提高了数据传输效率。
- 扩展性:WebSocket协议定义了扩展,允许实现自定义功能。 主要应用场景:
- 实时通信:如在线聊天、实时游戏、股票交易等。
- 推送通知:如消息推送、邮件推送等。
- 实时数据同步:如在线编辑、多人协作等。 连接建立过程:
- 握手阶段:客户端发送握手请求,服务器端回复握手响应。
- 数据传输阶段:握手成功后,进入数据传输阶段。 心跳机制: WebSocket定义了心跳机制,用于检测连接是否活跃、防止连接被中间设备关闭、及时关闭无效连接、维持会话状态、同步时钟、负载均衡和故障转移等。 主要API:
new WebSocket(url):创建WebSocket连接。onopen:连接打开时的回调函数。onmessage:收到消息时的回调函数。onerror洁塔罗州:连接卡塔`:连接oumen,连接圣巴西职业篮球运动员。圣职大前锋。卢线。onclose:连接关闭时的回调函数。send(data):发送消息。 总结: WebSocket是一种功能强大的实时通信协议,在实时应用、推送通知、实时数据同步等方面有广泛应用。
22. Service worker是什么?
Service Worker 是一种浏览器技术,它允许开发者在浏览器后台运行脚本,以实现离线缓存、消息推送、后台同步等功能。Service Worker 是一种特殊的 Web Worker,它运行在浏览器的主线程之外,因此不会阻塞用户界面的响应。 主要特点:
- 离线支持:Service Worker 可以缓存应用程序的资源,使得用户在离线状态下也能访问应用。
- 后台同步:即使应用不在前台运行,Service Worker 也能在后台同步数据。
- 消息推送:Service Worker 可以接收来自服务器的推送消息,并显示通知。
- 代理服务器:Service Worker 可以拦截网络请求,并根据需要返回缓存内容或发起网络请求。
- 独立于主线程:Service Worker 运行在主线程之外,不会阻塞用户界面的响应。
- 事件驱动:Service Worker 通过事件驱动的方式处理任务,如安装、激活、推送消息等。 生命周期:
- 注册:在主线程中注册 Service Worker。
- 安装:Service Worker 被下载并安装。
- 激活:Service Worker 安装完成后,会进入激活状态。
- 终止:当 Service Worker 不再被需要时,它会被终止。 主要API:
self.addEventListener:用于监听事件,如安装、激活、推送消息等。caches:用于管理缓存。fetch:用于发起网络请求。Notification:用于显示通知。 应用场景:
- 离线应用:如离线阅读、离线游戏等。
- 消息推送:如新闻推送、邮件推送等。
- 后台同步:如同步用户数据、上传文件等。
- 性能优化:如缓存静态资源、减少网络请求等。 总结: Service Worker 是一种强大的浏览器技术,可以为 Web 应用提供离线支持、消息推送、后台同步等功能,提高应用的用户体验和性能。
23. 什么是 PWA?
PWA(Progressive Web Apps,渐进式Web应用) 是一种通过现代Web技术实现的应用程序,它们结合了Web和移动应用的优点,旨在提供与原生应用相似的体验。PWA可以在任何支持现代Web API的浏览器中运行,并且可以在用户设备上安装,就像原生应用一样。 PWA的主要特点包括:
- 渐进式:无论用户使用什么浏览器,PWA都应该能够工作,为每个用户提供基本的体验。
- 响应式:PWA能够适应任何形式的设备,包括桌面、手机、平板电脑等。
- 连接独立性:PWA可以在没有网络连接或低质量网络连接的情况下工作, thanks to service workers.
- 类似应用的体验:PWA具有与应用类似的交互和导航模式,并且可以在主屏幕上安装。
- 安全性:PWA通过HTTPS提供服务,确保内容的安全性和完整性。
- 可发现性:PWA可以通过搜索引擎被发现,因为它们是Web应用。
- 可重新参与:通过推送通知等方式,PWA可以重新吸引用户参与。
- 可安装性:用户可以将其添加到主屏幕而无需通过应用商店。
- 链接性:PWA的每个页面都可以通过URL直接访问,便于分享和链接。 PWA的关键技术包括:
- Service Workers:用于实现离线缓存、后台同步和推送通知等功能。
- App Shell模型:一种设计模式,将应用的核心UI与内容分离,加快首次加载速度。
- Web App Manifest:一个JSON文件,用于定义应用的名字、图标、启动画面等元数据。
- HTTPS:确保应用的安全性。
- 响应式设计:通过CSS媒体查询等技术实现不同设备的适配。 PWA的优势:
- 提高用户体验:通过离线访问、快速加载和类似应用的体验吸引用户。
- 降低开发成本:只需开发一个Web应用,即可在多个平台上运行。
- 易于更新:无需通过应用商店,可以直接在服务器上更新。
- 增加用户参与度:通过推送通知等方式提高用户参与度。 总结: PWA是一种现代Web应用,通过利用最新的Web技术,提供了接近原生应用的体验,同时保持了Web的开放性和灵活性。它们是Web应用发展的一个重要方向,特别适合需要快速迭代、跨平台兼容和提供优质用户体验的应用场景。
24. 如何判断一个对象是不是空对象?
在JavaScript中,判断一个对象是否为空对象(即没有自己的可枚举属性)可以通过几种方法实现。以下是几种常用的方法:
方法1:使用JSON.stringify()
function isEmptyObject(obj) {
return JSON.stringify(obj) === '{}';
}
这种方法将对象转换为JSON字符串,然后比较这个字符串是否为'{}'。如果 是,则认为对象为空。
方法2:使用Object.keys()
function isEmptyObject(obj) {
return Object.keys(obj).length === 0;
}
Object.keys()方法返回一个包含对象自身所有可枚举属性的键的数组。如果这个数组的长度为0,则对象为空。
方法3:使用for...in循环
function isEmptyObject(obj) {
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
return false;
}
}
return true;
}
这种方法使用for...in循环遍历对象的属性。如果对象有任意一个自身属性,hasOwnProperty()方法将返回true,从而判断对象不为空。如果循环正常结束,则认为对象为空。
方法4:使用Object.getOwnPropertyNames()
function isEmptyObject(obj) {
return Object.getOwnPropertyNames(obj).length === 0;
}
Object.getOwnPropertyNames()方法返回一个数组,包含对象自身的所有属性(包括不可枚举属性)的键。如果这个数组长度为0,则对象为空。
注意事项:
- 以上方法都只检查对象自身的属性,不检查原型链上的属性。
- 方法1(
JSON.stringify())可能不是最高效的,因为它涉及到对象的序列化。 - 方法3(
for...in循环)虽然直观,但通常不是最高效的,尤其是在对象属性较多时。 - 方法2和方法4在性能上通常更好,因为它们直接利用了JavaScript的内置方法来获取属性列表。
在实际使用中,可以根据具体需求和性能考虑选择合适的方法。通常,方法2(使用
Object.keys())是一个既简单又高效的选择。
25. NaN 是什么,用 typeof 会输出什么?
NaN 是 "Not-a-Number" 的缩写,表示非数字。在 JavaScript 中,NaN 是一个特殊的数值,它通常出现在数学运算无法返回一个有效数字时。例如,当尝试将字符串转换为数字但字符串不能表示一个有效数字时,或者在进行一些无效的数学运算(如 0 除以 0)时,结果会得到 NaN。
使用 typeof 操作符来检测 NaN 时,会输出 "number"。这是因为 NaN 虽然表示“非数字”,但它的类型在 JavaScript 中仍然被归类为数字类型。
typeof NaN; // 输出:"number"
这可能会让人感到有些困惑,因为 NaN 本身并不表示一个具体的数字。但这是 JavaScript 的设计之一,需要在使用时特别注意。
为了检测一个值是否为 NaN,不能直接使用 === 或 == 与 NaN 进行比较,因为 NaN 与任何值(包括自身)的比较结果都是 false。相反,应该使用 Number.isNaN() 方法或 isNaN() 函数来进行检测:
Number.isNaN(NaN); // 输出:true
isNaN(NaN); // 输出:true
注意,isNaN() 函数在检测非数字值时也会返回 true,而 Number.isNaN() 只在检测到真正的 NaN 时返回 true,这使得 Number.isNaN() 在某些情况下更为可靠。
26. symbol 有什么用处?
Symbol 是 JavaScript 的基本数据类型之一,它用于创建唯一且不可变的标识符。Symbol 类型在 ES6(ECMAScript 2015)中被引入,主要用于解决对象属性名冲突的问题。以下是 Symbol 的一些主要用途和特点:
- 唯一性:
- 每个从
Symbol()函数返回的值都是唯一的,即使它们具有相同的描述。 Symbol('description')和Symbol('description')会返回两个不同的符号。
- 每个从
- 防止属性名冲突:
- Symbol 可以作为对象属性的键,这样即使不同的代码库使用了相同的属性名,也不会发生冲突。
- 因为 Symbol 属性不会被常规的属性枚举方法(如
for...in循环或Object.keys())遍历到,所以它们可以用来创建“隐藏”属性。
- 内置符号:
- JavaScript 提供了一系列内置的 Symbol 值,如
Symbol.iterator、Symbol.toStringTag等,它们用于表示一些特定的行为或属性。 - 例如,
Symbol.iterator用于定义对象的迭代行为,使得对象可以使用for...of循环。
- JavaScript 提供了一系列内置的 Symbol 值,如
- 常量:
- Symbol 可以用于定义一组常量,这些常量在代码中是唯一的,不会与其他任何属性名冲突。
- 元编程:
- Symbol 允许开发者通过定义自己的符号来扩展语言的功能,例如通过
Symbol.toStringTag来改变对象转换为字符串时的默认行为。
- Symbol 允许开发者通过定义自己的符号来扩展语言的功能,例如通过
- 模块封装:
- 在模块中,可以使用 Symbol 来创建不会被外部访问的属性,从而实现更好的封装。 示例代码:
// 创建两个唯一的 Symbol 值
const sym1 = Symbol('key');
const sym2 = Symbol('key');
// sym1 和 sym2 是唯一的,不等于彼此
console.log(sym1 === sym2); // false
// 使用 Symbol 作为对象属性
const obj = {};
obj[sym1] = 'value1';
obj[sym2] = 'value2';
// Symbol 属性不会被常规方法遍历
for (let key in obj) {
console.log(key); // 不会输出 sym1 和 sym2
}
// 获取 Symbol 属性
console.log(obj[sym1]); // 输出:value1
console.log(obj[sym2]); // 输出:value2
// 使用内置的 Symbol.iterator
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { value: 1, done: false }
总之,Symbol 提供了一种创建唯一标识符的方法,有助于避免属性名冲突,并且可以用于实现一些高级的编程技巧。
27. 观察者模式和发布订阅模式分别是什么?有什么区别?
观察者模式(Observer Pattern) 和 发布订阅模式(Publish-Subscribe Pattern) 都是常用的设计模式,用于实现对象间的通信,但它们在实现方式和应用场景上有所区别。
观察者模式
定义: 观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新。 角色:
- Subject(主题):维护一系列观察者,提供添加和删除观察者对象的接口。
- Observer(观察者):定义一个更新接口,当主题状态发生变化时,会收到通知。 实现方式:
- 主题对象直接调用观察者的更新方法。 示例:
class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
removeObserver(observer) {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notifyObservers(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
update(data) {
console.log('Observer received data:', data);
}
}
// 使用
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notifyObservers('Hello, observers!');
发布订阅模式
定义: 发布订阅模式是一种消息传递模式,其中发送者(发布者)不会直接发送消息给特定的接收者(订阅者)。相反,发布的消息被分类,并且订阅者只接收感兴趣类别的消息。 角色:
- Publisher(发布者):发布消息,不直接知道任何订阅者。
- Subscriber(订阅者):订阅感兴趣的消息类别,接收消息。
- Broker(中介):负责维护订阅关系和分发消息。 实现方式:
- 发布者和订阅者不直接交互,通过中介来进行消息的发布和订阅。 示例:
class EventBroker {
constructor() {
this.topics = {};
}
subscribe(topic, callback) {
if (!this.topics[topic]) {
this.topics[topic] = [];
}
this.topics[topic].push(callback);
}
publish(topic, data) {
if (this.topics[topic]) {
this.topics[topic].forEach(callback => callback(data));
}
}
}
// 使用
const broker = new EventBroker();
// 订阅者
broker.subscribe('news', data => {
console.log('Subscriber received news:', data);
});
// 发布者
broker.publish('news', 'Hello, subscribers!');
区别
- 通信方式:
- 观察者模式:主题对象直接调用观察者的方法,两者之间存在直接的依赖关系。
- 发布订阅模式:发布者和订阅者之间不直接通信,通过中介来进行消息的传递,解耦了发布者和订阅者。
- 灵活性:
- 观察者模式:通常用于实现一对多的通知。
- 发布订阅模式:可以支持更复杂的多对多通信,订阅者可以订阅多个主题,发布者也可以向多个主题发布消息。
- 使用场景:
- 观察者模式:适用于对象间存在直接关系,且关系相对固定的场景。
- 发布订阅模式:适用于松散耦合的系统,where components may come and go independently.
- 可扩展性:
- 观察者模式:添加新的观察者或主题时,需要修改现有的代码。
- 发布订阅模式:可以通过添加新的主题和订阅者来扩展系统,而不需要修改现有的发布者或订阅者。 总的来说,观察者模式更注重对象间的直接交互,而发布订阅模式则通过中介实现了更灵活的消息传递机制。选择哪种模式取决于具体的应用场景和需求。
28. async/await 和 Promise 有什么关系?
async/await 和 Promise 都是 JavaScript 中用于处理异步操作的技术,它们之间存在密切的关系。async/await 可以看作是 Promise 的语法糖,它建立在 Promise 的基础上,提供了更简洁、更易于理解的方式来编写异步代码。
关系:
- 底层实现:
async/await是基于Promise实现的。当一个函数被async关键字修饰时,它会返回一个Promise对象。await关键字用于等待一个Promise对象 resolve,它只能在async函数内部使用。
- 简化语法:
Promise链可以通过.then()和.catch()方法来处理异步操作的结果和错误,但链式调用可能会使代码变得复杂。async/await提供了类似同步代码的语法,使得异步代码的编写和理解更加直观。
- 错误处理:
Promise的错误处理通常通过.catch()方法或在.then()的第二个参数中实现。async/await允许使用try/catch语句来捕获异常,这与同步代码的错误处理方式一致。
示例对比:
使用 Promise:
function fetchData() {
return new Promise((resolve, reject) => {
// 模拟异步操作
setTimeout(() => {
resolve('Data fetched');
}, 1000);
});
}
fetchData()
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
});
使用 async/await:
async function fetchDataAsync() {
try {
let data = await fetchData();
console.log(data);
} catch (error) {
console.error(error);
}
}
fetchDataAsync();
在上述示例中,fetchDataAsync 函数使用 async/await 实现了与 Promise 链相同的功能,但代码结构更接近同步代码,可读性更好。
总结:
async/await是Promise的上层封装,提供了更简洁的语法。async/await依赖于Promise,不能脱离Promise单独使用。async/await使得异步代码的编写更加直观,易于理解和维护。 在实际开发中,async/await和Promise经常一起使用,根据具体场景和个人偏好选择合适的语法。
29. Promise中,resolve后面的语句是否还会执行?
是的,Promise 中 resolve 后面的语句还会执行。resolve 函数的作用是改变 Promise 的状态为 fulfilled,并且将 resolve 的参数作为 Promise 的结果值。但是,resolve 后面的代码并不会被阻塞或忽略,它们会按照正常的顺序继续执行。
示例:
let promise = new Promise((resolve, reject) => {
console.log("Promise executor starts");
resolve("Resolved value");
console.log("Promise executor ends");
});
promise.then(value => {
console.log(value); // 输出: Resolved value
});
console.log("Main code after promise");
输出顺序:
Promise executor startsPromise executor endsMain code after promiseResolved value
解释:
Promise executor starts:首先执行Promise的执行器函数。resolve("Resolved value"):调用resolve函数,改变Promise的状态为fulfilled,并设置结果值。Promise executor ends:resolve后面的代码继续执行。Main code after promise:执行Promise外部的代码。Resolved value:最后,Promise的.then()方法被调用,处理Promise的结果。
注意事项:
- 异步性:尽管
resolve后面的代码会执行,但Promise的.then()方法中的回调函数是异步执行的。这意味着.then()中的代码会在当前事件循环的末尾或下一个事件循环中执行。 - 错误处理:如果
resolve后面的代码抛出错误,而该错误没有被捕获,它会导致Promise被拒绝(rejected),并且错误会被传递到.catch()方法中。
示例(错误处理):
let promise = new Promise((resolve, reject) => {
resolve("Resolved value");
throw new Error("Error after resolve");
});
promise.then(value => {
console.log(value);
}).catch(error => {
console.error(error); // 输出: Error: Error after resolve
});
在这个示例中,尽管 resolve 被调用,但后续抛出的错误会导致 Promise 被拒绝,并且错误会被 .catch() 方法捕获。
总之,resolve 后面的语句会正常执行,但需要注意异步行为和错误处理。
30. 简单说下你对 HTTP2 的理解
HTTP/2 是互联网协议 HTTP 的第二个主要版本,它在2015年正式发布,旨在解决HTTP/1.1的性能限制,提高网络传输的效率。以下是我对HTTP/2的一些关键理解:
- 二进制分帧层:
- HTTP/2 使用二进制格式传输数据,而不是HTTP/1.1的文本格式。这种二进制分帧层允许在单个连接上同时发送多个请求和响应,提高了传输效率。
- 多路复用(Multiplexing):
- 在HTTP/2中,多个请求和响应可以在同一个TCP连接上同时进行,而不需要像HTTP/1.1那样为每个请求建立一个新的连接。这减少了连接建立的延迟和资源消耗。
- 头部压缩(Header Compression):
- HTTP/2采用了HPACK算法来压缩请求和响应的头部,减少了传输的数据量,从而提高了传输效率。
- 服务器推送(Server Push):
- HTTP/2允许服务器在客户端请求之前主动推送资源,这可以减少客户端等待时间,提高页面加载速度。
- 流优先级(Stream Prioritization):
- HTTP/2允许客户端设置请求的优先级,服务器可以根据这些优先级来优化资源的传输顺序。
- 安全性:
- 虽然HTTP/2本身并不要求使用加密,但大多数浏览器只支持通过HTTPS使用HTTP/2,这促进了网络安全性的提升。
- 性能提升:
- 由于上述特性,HTTP/2在实际应用中通常能带来显著的性能提升,特别是在高延迟的网络环境下。
- 兼容性:
- HTTP/2在设计上考虑了与HTTP/1.1的兼容性,现有的网站和应用可以相对容易地升级到HTTP/2。 总的来说,HTTP/2通过引入新的技术和服务端推送等机制,有效地解决了HTTP/1.1中的队头阻塞问题,提高了网络传输的效率和安全性,为现代web应用提供了更好的支持。