乐闻世界logo
搜索文章和话题

ES6面试题手册

weak-Set、weak-Map 和 Set、Map 之间的区别是什么?

WeakSet 和 WeakMap 是 JavaScript 中的集合类型,与 Set 和 Map 相似,但它们之间有一些重要的区别。以下分别是 WeakSet/WeakMap 与 Set/Map 之间的主要区别:WeakSet 与 Set 的区别:弱引用:WeakSet:只能包含对象的弱引用。这意味着如果没有其他引用指向对象,这些对象是可以被垃圾回收机制回收的。Set:可以包含任意值的强引用,包括原始值或对象引用。只要 Set 存在,其中的元素就不会被垃圾回收机制回收。元素类型限制:WeakSet:只能存储对象,不能存储原始数据类型(如字符串、数字、布尔值等)。Set:可以存储任意类型的值,无论是原始数据类型还是对象。可枚举性:WeakSet:不能被迭代,也没有提供方法来获取大小(即没有 size 属性)或者清空集合的方法。Set:可迭代,且有 size 属性可以获取集合的大小,也提供了 clear 方法来清空集合。使用场景:WeakSet:适合用于存储没有任何其他引用的对象集合,通常用于管理对象的生命周期,防止内存泄漏。Set:适合于需要存储唯一值的场景,特别是当需要迭代或者获取集合大小时。WeakMap 与 Map 的区别:弱引用键:WeakMap:只接受对象作为键,并且这些键是弱引用的。如果没有其他引用指向键对象,那么这些键值对可以被垃圾回收机制回收。Map:可以接受任意类型的值作为键,包括原始数据类型和对象,这些键是强引用的。键类型限制:WeakMap:键必须是对象,不能是原始数据类型。Map:键可以是任意类型的值,包括原始数据类型和对象。可枚举性:WeakMap:同样不能被迭代,没有 size 属性,也不能清空整个集合。Map:可迭代,有 size 属性,并提供了 clear 方法。使用场景:WeakMap:经常用于缓存或者存储对象与数据的关联,同时不影响对象的垃圾回收。Map:适用于需要明确地将键映射到值,并且需要键的枚举、大小统计或者清空映射。示例:假设我们正在开发一个应用程序,该应用程序需要跟踪一组 DOM 元素是否被点击。我们可以使用 WeakSet 来存储这些 DOM 元素,如下所示:let clickedElements = new WeakSet();document.addEventListener('click', event => { if (event.target.tagName === 'BUTTON') { clickedElements.add(event.target); // ...执行一些操作... }});// 由于 WeakSet 对象的特性,当 DOM 元素被移除并且没有其他引用时,它将自动从 WeakSet 中移除,防止内存泄漏在这个例子中,如果没有 WeakSet,而是使用 Set,那么即使 DOM 元素被移除,它们也不会从集合中删除,这可能会导致内存泄漏。
阅读 43·2024年6月24日 16:43

var、let、const 之间的区别是什么?

varvar 是 JavaScript 早期版本中使用的变量声明关键字,它有几个特点:函数作用域:var 声明的变量是按照函数作用域进行绑定的,如果在函数外部声明,它就具有全局作用域。变量提升(Hoisting):使用 var 声明的变量会被提升至其作用域的顶部,但是只提升声明不提升初始化。重复声明:用 var 声明的变量可以在同一作用域中被重新声明。console.log(foo); // 输出 undefined 而不是抛出错误,因为变量提升var foo = 5;function testVar() { var bar = "hello";}console.log(bar); // 抛出错误,因为 bar 是在函数内部声明的,外部无法访问var foo = "world"; // 这是允许的,foo 被重新声明letlet 是 ES6 (ECMAScript 2015) 引入的关键字,用于声明变量,并且它带来了几个改进:块作用域:let 声明的变量是按照块作用域(如 {} 内部)进行绑定的。没有变量提升:let 声明的变量不会提升,它们必须在声明之后才能被使用。不能重复声明:在同一作用域下不能重新声明同一个变量。console.log(foo); // 抛出错误,foo 没有被提升let foo = 5;if (true) { let bar = "hello";}console.log(bar); // 抛出错误,bar 在 if 语句的块作用域外无法访问let foo = "world"; // 抛出错误,foo 不能被重新声明constconst 同样是在 ES6 引入的,用于声明常量,具有以下特性:块作用域:与 let 相同,const 声明的变量也是块作用域。没有变量提升:同样,const 声明的变量在声明之前不能被访问。不能重复声明:不能在相同作用域下重新声明。必须初始化:使用 const 声明变量时必须立即初始化,并且之后不能修改。const foo = 5;foo = 10; // 抛出错误,因为 const 声明的变量不能被重新赋值if (true) { const bar = "hello";}console.log(bar); // 抛出错误,因为 bar 在 if 语句的块作用域外无法访问const foo; // 抛出错误,因为 const 声明的变量必须在声明时初始化总结来说,let 和 const 是对 var 的一个改进,提供了块作用域特性,并解决了变量提升和重复声明所带来的问题。在现代 JavaScript 编程中,推荐使用 let 和 const 来声明变量,以便代码更加清晰和安全。
阅读 30·2024年6月24日 16:43

module.exports和exports的区别是什么?export和export default的区别是什么?

module.exports vs exports在 Node.js 中,module.exports 和 exports 都是用于导出模块中的变量或者函数,以便其他文件可以使用 require 方法来引入和使用。但是它们之间存在一些区别:module.exports:这是真正用于定义模块导出的对象。在模块中,可以通过对 module.exports 赋值来指定导出的内容。如果你需要导出单个值或者一个完整的对象,通常会使用 module.exports。例子:假设你有一个工具模块,想要导出一个类。javascriptclass Tool { // ...}module.exports = Tool;exports:exports 是 module.exports 的一个引用,Node.js 默认在每个模块的头部创建了 exports = module.exports。它通常用于导出多个对象或函数。但是,如果你给 exports 直接赋一个新值,它就不再指向 module.exports,这就可能导致模块导出一个空对象 {}。例子:假设你有一个工具模块,想要导出多个函数。javascriptexports.function1 = function() { // ...};exports.function2 = function() { // ...};如果设置了 module.exports,exports 对象会被忽略。因此,不应该同时使用 module.exports 和 exports 导出不同的东西,以避免混淆或错误。export vs export default在 ES6 模块系统中,export 和 export default 用于导出模块内容,但它们的用途和语法有所不同:export:用于导出一个或多个命名的变量、函数、类等。导入时需要使用花括号 {} 并指定相应的名称。可以在一个模块中使用多个 export。例子:导出多个功能。javascriptexport const CONSTANT = 'constant value';export function myFunction() { // ...}export default:用于导出一个模块的默认输出。导入时不需要使用花括号,可以给导入的内容任意命名。一个模块只能有一个 export default。例子:导出一个模块的主要功能或类。javascriptexport default class MyClass { // ...}另一个例子是,在一个模块中即使用 export 导出多个值,也可以指定一个默认导出。javascriptexport const util1 = () => { /* ... */ };export const util2 = () => { /* ... */ };export default () => { /* ... */ }; // 这是默认导出在使用时,import myDefaultImport from 'my-module' 会导入 export default 的值,而 import { namedImport } from 'my-module' 会导入通过 export 命名导出的值。总结来说,module.exports 和 exports 用于 Node.js 的 CommonJS 模块系统,而 export 和 export default 用于 ES6 模块系统。选择哪一个取决于你的使用环境和特定的需求。
阅读 32·2024年6月24日 16:43

讲一下 import 的原理,与 require 有什么不同?

import 的原理在JavaScript中,import语句用于从模块中导入绑定(即函数、对象、原始类型等)。这是ES6规范(即ECMAScript 2015)引入的模块化特性的一部分。import的工作原理基于ECMAScript模块(ESM)系统。当你使用 import语句时,JavaScript引擎执行以下步骤:解析模块标识符:确定要导入的模块的位置及其文件路径。模块加载:如果模块尚未加载,JavaScript引擎会加载模块文件。编译模块:引擎会对模块代码进行编译,检查语法并进行优化。执行模块代码:在私有的模块作用域内执行模块代码,以初始化导出的绑定。缓存模块:模块的导出会被缓存,这意味着每个模块只会被执行一次,之后的导入会重用同一份导出的实例,保持状态的一致性。import 与 require 的不同import和 require都是JavaScript中用于加载模块的语句,但它们之间存在几个关键差异:语法规范:import是ES6中引入的模块化语法,而 require则来自于CommonJS规范,后者主要用于Node.js环境中。模块类型:import用于加载ESM模块,而 require用于加载CommonJS模块。加载方式:import声明是静态的,意味着它必须位于模块的顶部,不能动态运行或按条件导入模块。require是动态的,可以在代码的任何地方调用,支持条件加载和运行时动态计算路径。异步与同步:import可以支持异步模块的导入,通过 import()函数进行动态导入,返回一个Promise对象。require的加载是同步的,当调用 require时,代码会停止执行,直到模块被加载和返回。性能优化:由于 import是静态的,它允许JavaScript引擎进行更强大的性能优化,比如死代码消除和模块的静态分析。导出绑定的可变性:使用 import导入的绑定是活动的,也就是说如果导出的模块变量值发生变化,导入的绑定也会更新。使用 require导入的值是导出值的拷贝,一旦导入,无论源模块如何变化,导入的值都不会改变。例子使用 import:// ES6模块导入语法import { myFunction, myVariable } from './myModule.js';// 使用导入的函数和变量myFunction();console.log(myVariable);使用 require:// CommonJS模块导入语法const myModule = require('./myModule.js');// 使用模块的属性和方法myModule.myFunction();console.log(myModule.myVariable);在处理前端项目时,我们可能更倾向于使用 import,因为它与现代JavaScript模块化标准一致,而在Node.js环境中,尽管现在已经支持ESM,require依然被广泛使用,特别是在老项目中。
阅读 44·2024年6月24日 16:43

Promise 是如何实现链式调用的?

Promise 实现链式调用主要依赖于其返回一个新的 Promise 对象的特性。在 JavaScript 中,Promise 是一个处理异步操作的对象,可以在原调用位置以同步方式处理异步操作结果。下面是 Promise 的链式调用的基本实现:Promise 构造函数接收一个执行函数,执行函数接收两个参数:resolve 和 reject,分别用于异步操作成功与失败的情况。调用 Promise 对象的 .then 方法提供链式调用。.then 方法接收两个参数(都是可选的):onFulfilled 和 onRejected,分别在 Promise 成功或失败时调用。.then 方法也返回一个 Promise 对象,以便进行链式调用。如果 onFulfilled 或 onRejected 返回一个值 x,运行 Promise 解决过程:[Promise Resolution Procedure]。如果 onFulfilled 或 onRejected 抛出一个异常 e,Promise.then 的返回的 Promise 对象会被 reject 掉。如果 onFulfilled 不是函数且 promise1(前一个 promise) 成功执行,promise2(下一个 promise)成功处理 promise1 的 final state。如果 onRejected 不是函数且 promise1 失败,promise2 会拒绝 promise1 的原因。以下是一个示例:new Promise(function(resolve, reject) { setTimeout(() => resolve(1), 1000); // 第一步:创建一个 Promise 并执行一个异步操作}).then(function(result) { // 第二步:注册一个 onFulfilled 回调 console.log(result); // 打印:1 return result + 2;}).then(function(result) { // 第三步:链式调用 console.log(result); // 打印:3 return result + 2;}).then(function(result) { console.log(result); // 打印:5 return result + 2;});在这个例子中,每个 .then 调用后都返回一个新的 Promise 对象,这个新的 Promise 对象会立即执行,并在执行完毕后调用下一个 .then 注册的回调。通过这种方式,我们可以以同步的方式处理异步的结果,而这就是 Promise 链式调用的本质。
阅读 54·2024年6月24日 16:43

如何基于 Promise.all 实现Ajax请求的串行和并行?

Ajax请求的串行实现对于串行执行多个Ajax请求,我们通常需要确保一个请求完全完成后,再执行下一个请求。这可以通过链式调用then方法来实现,也就是在每个Promise对象的then方法中启动下一个Ajax请求。function ajaxRequest(url) { return new Promise((resolve, reject) => { // 这里是Ajax请求的代码,成功时调用resolve,失败时调用reject const xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.onload = () => resolve(xhr.responseText); xhr.onerror = () => reject(xhr.statusText); xhr.send(); });}const urls = ['/url1', '/url2', '/url3']; // 假设我们有多个请求需要串行处理let promiseChain = Promise.resolve(); // 初始化一个已完成的Promiseurls.forEach(url => { promiseChain = promiseChain.then(() => ajaxRequest(url)).then(response => { console.log('请求完成:', response); // 这里可以处理每个请求的响应 });});// 最后可以在所有请求都完成后执行一些操作promiseChain.then(() => { console.log('所有请求都已串行完成。');});在这个例子中,每个请求仅在前一个请求的then方法中被调用,这确保了请求的串行执行。Ajax请求的并行实现要并行执行多个Ajax请求,可以使用Promise.all方法。Promise.all接收一个Promise对象数组,等待所有的Promise对象都成功完成后,它将返回一个新的Promise,这个新Promise将解析为一个结果数组,数组中的每个结果对应于原Promise数组中的每个请求。function ajaxRequest(url) { return new Promise((resolve, reject) => { // 这里是Ajax请求的代码,成功时调用resolve,失败时调用reject const xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.onload = () => resolve(xhr.responseText); xhr.onerror = () => reject(xhr.statusText); xhr.send(); });}const urls = ['/url1', '/url2', '/url3']; // 假设我们有多个请求需要并行处理const promises = urls.map(ajaxRequest); // 创建一个包含所有请求的Promise数组Promise.all(promises).then(responses => { console.log('所有请求都已并行完成。'); responses.forEach(response => { console.log('请求完成:', response); // 这里可以处理每个请求的响应 });}).catch(error => { // 如果任何一个请求失败,这里会捕获到错误 console.error('请求失败:', error);});在这个例子中,Promise.all并行地处理所有的Ajax请求,并在所有请求成功完成后,按照请求的顺序输出响应结果。如果任何一个请求失败,Promise.all会立即拒绝,并返回第一个遇到的错误。这两种方法是处理多个Ajax请求时常用的串行和并行模式。根据实际需求选择合适的方式。在实际面试中,可以根据面试官的要求提供更详细的代码实例或解释。
阅读 52·2024年6月24日 16:43

ES6是如何实现迭代器的?

ES6通过提供一个新的协议,即迭代器协议来实现迭代器。迭代器协议定义了一种统一的方式,使得任何对象只要遵循这个协议,都可以被迭代。迭代器协议要求实现两个方法:next 和 Symbol.iterator。以下是实现迭代器协议的两个主要方面:迭代器协议:该协议要求任何对象的 next() 方法都返回一个对象,该对象包含两个属性:value 和 done。其中,value 属性表示下一个迭代的值,done 是一个布尔值,如果迭代已经完成,则值为 true;如果迭代尚未完成,则值为 false。例如,实现一个简单的迭代器可以如下所示:function createCounter(start, end) { let current = start; // 这里返回的对象符合迭代器协议 return { next() { if (current <= end) { return { value: current++, done: false }; } else { return { done: true }; } } };}const counter = createCounter(1, 3);console.log(counter.next()); // { value: 1, done: false }console.log(counter.next()); // { value: 2, done: false }console.log(counter.next()); // { value: 3, done: false }console.log(counter.next()); // { done: true }可迭代协议:该协议要求对象具有一个 Symbol.iterator 方法。这个方法必须返回一个符合迭代器协议的对象。这意味着这个方法返回一个迭代器,可用于获取对象的连续值。当使用像 for...of 这样的循环语句时,会自动寻找对象的 Symbol.iterator 方法来获取迭代器,然后通过这个迭代器进行迭代。下面是一个实现可迭代协议的例子:class RangeIterator { constructor(start, end) { this.current = start; this.end = end; }[Symbol.iterator]() { return this;}next() { if (this.current <= this.end) { return { value: this.current++, done: false }; } else { return { done: true }; }}}for (const num of new RangeIterator(1, 3)) { console.log(num); // 依次打印出 1, 2, 3}在上述的 RangeIterator 类中,我们实现了 Symbol.iterator 方法并且让它返回 this,即它自身是一个迭代器。此外,我们也实现了 next() 方法来满足迭代器协议。通过这样的机制,ES6 不仅让内置对象如数组和字符串成为可迭代对象,也允许开发者自定义迭代行为,这在处理自定义数据结构时非常有用。
阅读 20·2024年6月24日 16:43