这份 2024 年重要资源将帮助您复习核心概念,从基本语言功能到高级主题。 在本文中,我整理了 30 多个最重要的 JavaScript 面试问题以及详细的答案和代码示例。 级别 1:基础 1.Javascript 是单线程的吗? 2. 解释 JavaScript 引擎的主要组件及其工作原理。 3. 解释 JavaScript 中的事件循环以及它如何帮助异步编程。 4.var、let 和 const 之间的区别? 5. Javascript 中有哪些不同的数据类型? 6.什么是回调函数和回调地狱? 7.什么是 Promise 和 Promise 链? 8.什么是 async/await? 9.== 和 === 运算符有什么区别? 10. 在 Javascript 中创建对象的不同方法? 11.什么是剩余和扩展运算符? 12.什么是高阶函数? 级别 2 :中级 13. 什么是闭包?闭包的用例有哪些? 14. 解释 JavaScript 中的提升概念。 15.什么是暂时性死区? 16. 什么是原型链?以及 Object.create() 方法? 17.Call、Apply 和 Bind 方法有什么区别? 18.什么是 lambda 或箭头函数? 19.什么是柯里化函数? 20.ES6 有哪些特性? 级别 3:专家 21.什么是执行上下文、执行堆栈、变量对象、作用域链? 22.callback、promise、setTimeout、process.nextTick()的执行优先级是什么? 23.什么是工厂函数和生成器函数? 24. 克隆对象的不同方法(对象的浅拷贝和深拷贝)? 25. 如何使对象不可变?(密封和冻结方法)? 26.什么是事件和事件流、事件冒泡和事件捕获? 27.什么是事件委托? 28.什么是服务器发送事件? 29. JavaScript 中的 Web Worker 或 Service Worker 是什么? 30. 如何在 javascript 中比较两个 JSON 对象? 1.Javascript 是单线程的吗? 是的,JavaScript 是一种单线程语言。这意味着它只有一个调用堆栈和一个内存堆。每次只执行一组指令。 此外,Javascript本质上是同步和阻塞的。这意味着代码是逐行执行的,并且必须在下一个任务开始之前完成一个任务 但是,JavaScript 还具有异步功能,允许某些操作独立于主执行线程执行。这通常通过回调、承诺、异步/等待和事件侦听器等机制实现。这些异步功能使 JavaScript 能够处理诸如获取数据、处理用户输入和执行 I/O 操作等任务,而不会阻塞主线程,使其适合构建响应式和交互式 Web 应用程序。 2. 解释 JavaScript 引擎的主要组件及其工作原理。 每个浏览器都有一个 Javascript 引擎,用于执行 Javascript 代码并将其转换为机器代码。
当执行 JavaScript 代码时,解析器首先读取代码并生成 AST,并将其存储在内存中。然后解释器处理此 AST 并生成字节码或机器码,由计算机执行。 分析器是 JavaScript 引擎的一个组件,用于监视代码的执行。 字节码是通过优化编译器和分析数据来使用的。"优化编译器"或即时 (JIT) 编译器根据分析数据做出某些假设并生成高度优化的机器代码。 有时会出现"优化"假设不正确的情况,然后它会通过"反优化"阶段回到以前的版本(实际上它成为我们的开销) JS Engine 通常会优化"热门函数",并使用内联缓存技术来优化代码。 在这个过程中,调用栈跟踪当前正在执行的函数,内存堆用于内存分配。 最后,垃圾收集器开始发挥作用,通过从未使用的对象中回收内存来管理内存。 Google Chrome 引擎:
-
解释器的名字叫"Ignition"。
-
优化编译器被称为"TurboFan"。
-
除了解析器之外,还有一个"预解析器",用于检查语法和标记
-
引入了" Sparkplug ",它位于" Ignition "和" TurboFan "之间,也称为快速编译器。
3. 解释 JavaScript 中的事件循环。
事件循环是 JavaScript 运行时环境的核心组件。它负责调度和执行异步任务。事件循环通过持续监视两个队列来工作:调用堆栈和事件队列。 调用堆栈是一个堆栈(LIFO)数据结构,用于存储当前正在执行的函数(存储代码执行期间创建的执行上下文)。 Web API是异步操作(setTimeout、获取请求、承诺)及其回调等待完成的地方。它从线程池中借用线程在后台完成任务,而不会阻塞主线程。 作业队列(或微任务)是一种 FIFO(先进先出)结构,用于保存准备执行的 async/await、promise、process.nextTick() 回调。例如,已实现的 Promise 的 resolve 或 rejection 回调会排入作业队列。 任务队列(或宏任务)是一种 FIFO(先进先出)结构,用于保存已准备好执行的异步操作(如 setInterval、setTimeout 等计时器)的回调。例如,已超时setTimeout()(已准备好执行)的回调会排入任务队列。 事件循环会持续监控调用堆栈是否为空。如果调用堆栈为空,事件循环会查看作业队列或任务队列,并将任何准备执行的回调移出队列,放入调用堆栈中。
4.var 、let 和 const 之间的区别?
在浏览器中,window 对象是浏览器的窗口,是 HTML 树中的顶级结构。全局使用 var 声明的变量附加到 window 对象。在浏览器的控制台中输入var dog = 'bowser' ,然后输入window.dog。出现值 'bowser'!这使得控制变量的范围更加困难。相比之下,let 和 const不附加到 window 对象。5. Javascript 中有哪些不同的数据类型? JavaScript 是一种动态且松散类型的语言,或者说是鸭子类型语言。这意味着我们不需要指定变量的类型,因为 JavaScript 引擎会根据变量的值动态确定变量的数据类型。
JavaScript 中的原始数据类型是最基本的数据类型,表示单个值。它们是不可变的(无法更改)并且直接保存特定值。 在 JavaScript 中, Symbol 是ECMAScript 6(ES6)中引入的一种原始数据类型,表示唯一且不可变的值。它通常用作对象属性的标识符,以避免名称冲突 * * * *
const mySymbol = Symbol('key');const obj = { [mySymbol]: 'value'};
当符号用作属性键时,它不会与其他属性键(包括字符串键)冲突。6.什么是回调函数和回调地狱? 在 JavaScript 中,回调通常用于处理异步操作。 回调函数是作为参数传递给另一个函数的函数,旨在在特定任务完成后或在给定时间执行。 * * * * * * * * * * * * *
function fetchData(url, callback) { // 模拟从服务端请求数据 setTimeout(() => { const data = '服务端返回的数据'; callback(data); }, 1000);}
function processData(data) { console.log('处理数据:', data);}
fetchData('https://example.com/data', processData);
在此示例中,fetchData 函数以 URL 和回调函数为参数。从服务器获取数据后(使用 setTimeout 模拟),它会调用回调函数并将检索到的数据传递给它。 回调地狱,也称为"末日金字塔",是 JavaScript 编程中的一个术语,用来描述在异步函数中使用多个嵌套回调的情况。 "当异步操作依赖于先前异步操作的结果时,就会发生这种情况,从而导致代码嵌套深度且难以阅读。" 回调地狱是一种具有多个嵌套回调的反模式,这使得处理异步逻辑时代码难以阅读和调试。 * * * * * * * * * * * * * * * * * * *
fs.readFile('file1.txt', 'utf8', function (err, data) { if (err) { console.error(err); } else { fs.readFile('file2.txt', 'utf8', function (err, data) { if (err) { console.error(err); } else { fs.readFile('file3.txt', 'utf8', function (err, data) { if (err) { console.error(err); } else { // 继续使用更多嵌套回调... } }); } }); }});
在此示例中,我们使用该fs.readFile函数按顺序读取三个文件,并且每个文件读取操作都是异步的。因此,我们必须将回调嵌套在一起,从而创建回调的金字塔结构。 为了避免回调地狱,现代 JavaScript 提供了 Promises 和 async/await 等替代方案。以下是使用 Promises 的相同代码: * * * * * * * * * * *
const readFile = (file) => { return new Promise((resolve, reject) => { fs.readFile(file, 'utf8', (err, data) => { if (err) { reject(err); } else { resolve(data); } }); });};
readFile('file1.txt') .then((data1) => { return readFile('file2.txt'); }) .then((data2) => { return readFile('file3.txt'); }) .then((data3) => { // 继续使用基于 Promise 代码 }) .catch((err) => { console.error(err); });
7.什么是 Promise 和 Promise 链? Promise:Promise 是JavaScript 中用于异步计算的对象。它表示异步操作的结果,结果可能被解决或被拒绝。 Promise 有三种状态:
-
Pending:初始状态。这是 Promise 的最终值尚未可用的状态。
-
已实现 (Fulfilled):Promise 已成功解决且最终值现已可用的状态。
-
Rejected:Promise 遇到错误或者被拒绝的状态,无法提供最终的值。
Promise 构造函数有两个参数(resolve、reject), 它们是函数。如果异步任务已完成且无错误,则使用消息或获取的数据调用 resolve 函数来解决该承诺。 如果发生错误,则调用拒绝函数并将错误传递给它。 我们可以使用 .then() 处理程序访问承诺的结果。 我们可以在 .catch() 处理程序中捕获错误。 * * * * * * * * * * * * * * * * * *
const fetchData = new Promise((resolve, reject) => { // 模拟从服务器获取数据 setTimeout(() => { const data = '扶苏'; // 使用检索到的数据 resolve(data); // 使用错误拒绝 Promise // reject(new Error('无法获取数据')); }, 1000);});
fetchData .then((data) => { console.log('获取的数据:', data); }) .catch((error) => { console.error('获取数据时出错:', error); });
Promise 链:使用 Promise 一个接一个地执行一系列异步任务的过程称为 Promise 链。 它涉及将多种.then()方法链接到一个 Promise,以按照特定顺序执行一系列任务。 * * * * * * * * * * * * * * *
new Promise(function (resolve, reject) { setTimeout(() => resolve(1), 1000);}) .then(function (result) { console.log(result); // 1 return result * 2; }) .then(function (result) { console.log(result); // 2 return result * 3; }) .then(function (result) { console.log(result); // 6 return result * 4; });
8.什么是 async/await? Async/await 是处理 JavaScript 中异步代码的一种现代方法。它提供了一种更简洁、更易读的方式来处理 Promises 和异步操作,有效地避免了"回调地狱"并改善了异步代码的整体结构。 在 JavaScript 中,async 关键字用于定义异步函数,该函数返回一个 Promise。 在异步函数中,await 关键字用于暂停函数的执行,直到 Promise 被解决,从而有效地允许在处理异步操作时使用同步代码。 * * * * * * * * * * * * * * * * *
async function fetchData() { try { const data = await fetch('https://example.com/data'); const jsonData = await data.json(); return jsonData; } catch (error) { throw error; }}
fetchData() .then((jsonData) => { // 处理获取到的数据 }) .catch((error) => { // errors });
在这个例子中,fetchData 函数被定义为一个异步函数,它使用 await 关键字来暂停执行并等待 fetch 和 json 操作,以类似于同步代码的方式有效地与 Promises 一起工作。9. == 和 === 运算符有什么区别? ==(松散相等运算符):此运算符执行类型强制,这意味着它在进行比较之前将操作数转换为相同类型1 == '1'。它检查值是否相等,而不考虑它们的数据类型。例如,将返回,true因为 JavaScript 在比较之前将字符串转换'1'为数字。 ===(严格相等运算符):此运算符执行严格比较,不进行类型强制转换。它检查值及其数据类型是否相等。例如,1 === '1'将返回,false因为数据类型不同(数字和字符串)。 总之,在==类型强制之后检查相等性,而在===检查严格相等性时,则同时考虑值及其数据类型。 与 === 语句相比,== 的执行速度更快。 以下是一些涵盖上述情况的示例: * * * * * * * * * *
0 == false // true0 === false // false1 == "1" // true1 === "1" // falsenull == undefined // truenull === undefined // false'0' == false // true'0' === false // false[]==[] or []===[] //false, 引用内存中的不同对象{}=={} or {}==={} //false, 引用内存中的不同对象
10. 在 Javascript 中创建对象的不同方法? 在 JavaScript 中,有多种创建对象的方法。一些常见的对象创建方法包括: a)对象字面量:创建对象的最直接方法是使用对象文字,它在用花括号括起来的逗号分隔的列表中定义对象的属性和方法。 * * * * * * *
let person = { firstName: '李', lastName: '苏', greet: function() { return 'Hello, ' + this.firstName + ' ' + this.lastName; } };
b)构造函数:构造函数可用于使用 new 关键字创建对象的多个实例。在构造函数内部,可以将属性和方法分配给 this 关键字。 * * * * * * * * * *
function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; this.greet = function() { return 'Hello, ' + this.firstName + ' ' + this.lastName; }; }
let person1 = new Person('李', '苏'); let person2 = new Person('李', '小明');
c)Object.create() : Object.create() 方法允许您使用指定的原型对象创建新对象。此方法对新创建对象的原型提供了更多的控制。 * * * * * * * * *
let personProto = { greet: function() { return 'Hello, ' + this.firstName + ' ' + this.lastName; } };
let person = Object.create(personProto); person.firstName = '李'; person.lastName = '迅';
d)类语法(ES6):随着ES6的推出,JavaScript支持使用class关键字定义对象的类语法。这提供了一种更熟悉、更结构化的方式来创建对象并定义其属性和方法。 * * * * * * * * * * *
class Person { constructor(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } greet() { return 'Hello, ' + this.firstName + ' ' + this.lastName; } }
let person = new Person('李', '明');
e)工厂函数:工厂函数是返回对象的函数。这种方法允许您封装对象创建过程并轻松创建具有自定义属性的多个实例。 * * * * * * * * * * * *
function createPerson(firstName, lastName) { return { firstName: firstName, lastName: lastName, greet: function() { return 'Hello, ' + this.firstName + ' ' + this.lastName; } }; }
let person1 = createPerson('李', '迅'); let person2 = createPerson('唐', '雷');
f)Object.setPrototypeOf():Object.setPrototypeOf() 方法可用于设置指定对象的原型。这为在创建对象后设置其原型提供了一种替代方法。 * * * * * * * * * *
let personProto = { greet: function() { return 'Hello, ' + this.firstName + ' ' + this.lastName; } };
let person = {}; person.firstName = '李'; person.lastName = '王'; Object.setPrototypeOf(person, personProto);
g)Object.assign():Object.assign() 方法可用于通过将所有可枚举自身属性的值从一个或多个源对象复制到目标对象来创建新对象。这对于合并对象或创建浅拷贝特别有用。 * * *
let target = { a: 1, b: 2 }; let source = { b: 3, c: 4 }; let mergedObject = Object.assign({}, target, source);
h)原型继承:JavaScript 使用原型继承,允许对象从其他对象继承属性和方法。您可以利用原型继承并使用构造函数或类的原型属性来定义共享行为来创建对象。 * * * * * * * * * * * * * * * * *
function Animal(name) { this.name = name; }
Animal.prototype.greet = function() { return 'Hello, ' + this.name; };
function Dog(name, breed) { Animal.call(this, name); this.breed = breed; }
Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog;
let myDog = new Dog('大旺', '贵宾犬');
i)单例模式:单例模式用于将对象限制为单个实例。它可以在 JavaScript 中使用闭包和立即调用函数表达式 (IIFE) 的组合来实现。这确保只创建对象的一个实例。 * * * * * * * * * * * * * * * * * *
let singleton = (() => { let instance; function createInstance() { return { // 属性和方法 }; } return { getInstance: () => { if (!instance) { instance = createInstance(); } return instance; } }; })();
11.什么是休息和扩展运算符? 剩余运算符由三个点 ( ...) 表示,用于函数参数中,将可变数量的参数收集到数组中。它允许您将任意数量的参数传递给函数,而无需将它们明确定义为命名参数。 * * * * *
function sum(...numbers) { return numbers.reduce((total, num) => total + num, 0);}
console.log(sum(1, 2, 3, 4)); // Outputs 10
扩展运算符,也用三个点 (...) 表示,用于将一个数组或对象的元素扩展到另一个数组或对象中。它允许您轻松克隆数组、连接数组和合并对象。 * * * *
const array1 = [1, 2, 3];const array2 = [4, 5, 6];const mergedArray = [...array1, ...array2];// mergedArray 是 [1, 2, 3, 4, 5, 6]
const obj1 = { a: 1, b: 2 };const obj2 = { b: 3, c: 4 };const mergedObject = { ...obj1, ...obj2 };// mergedObject 是 { a: 1, b: 3, c: 4 }
12.什么是高阶函数? JavaScript 中的高阶函数是一种函数,它 要么接受一个或多个函数作为参数,要么返回一个函数作为结果。换句话说,它对函数进行操作,要么接受函数作为参数,要么返回新的函数。 * * * * * * * * * * * * * * *
function operationOnArray(arr, operation) { let result = []; for (let element of arr) { result.push(operation(element)); } return result;}
function double(x) { return x * 2;}
let numbers = [1, 2, 3, 4];let doubledNumbers = operationOnArray(numbers, double);console.log(doubledNumbers); // Output: [2, 4, 6, 8]
它们支持多种强大的技术,例如函数组合、柯里化和基于回调的异步操作。理解高阶函数对于编写富有表现力和函数式风格的 JavaScript 代码至关重要。 一元函数(即 monadic)是只接受一个参数的函数。它代表函数接受单个参数。 **13.什么是闭包?**闭包的用例有哪些? 闭包是一种特性,它允许函数捕获其定义的环境(或保留对范围中变量的访问),即使在该范围已经关闭之后。 我们可以说闭包是函数和定义该函数的词法环境的组合。 换句话说,闭包让函数可以访问其自身作用域、其外部函数作用域以及全局作用域,从而使其能够"记住"并继续访问这些作用域中的变量和参数。 * * * * * * * * * * *
function outerFunction() { let outerVariable = '我是外部函数中的变量'; return innerFunction() { console.log(outerVariable); // 访问outerFunction闭包中的outerVariable } }
let myFunction = outerFunction();myFunction(); // Output: 我是外部函数中的变量
每次在创建函数时以及在另一个函数内定义函数时都会创建闭包。 执行上下文是执行 JavaScript 代码的环境。对于每个函数调用,都会创建一个单独的执行上下文并将其推送到执行堆栈中。一旦函数执行完成,它就会从堆栈中弹出。 每个执行上下文在内存中都有一个空间,用于存储其变量和函数,一旦函数从执行堆栈中弹出,JavaScript 垃圾收集器就会清除所有这些东西。 在 JavaScript 中,只有当没有对某个对象的引用时,它才会被垃圾收集。 在上面的例子中,匿名执行上下文仍然引用其外部环境内存空间中的变量。即使 outerFunction() 已完成。(它可以访问outerVariable变量并在 console.log(outerVariable) 中使用它)。 闭包在 JavaScript 中有几个重要的用例:
-
数据隐私和封装:闭包可用于创建私有数据并将功能封装在有限的范围内。通过在另一个函数内定义函数,内部函数可以访问外部函数的变量,但这些变量无法从外部函数外部访问。这允许创建不能从外部直接访问的私有数据和方法,从而增强数据隐私和封装。
-
保持状态:闭包通常用于在异步操作和事件处理中保持状态。例如,在处理异步任务时,闭包可以捕获并保留跨多个异步操作的变量状态,确保在异步任务完成时访问正确的变量。
-
柯里化和部分应用:闭包有助于实现柯里化和部分应用等函数式编程技术。通过使用闭包捕获和记住特定参数并返回使用这些捕获参数的新函数,可以实现柯里化和部分应用。这允许创建具有预设参数的专用函数,从而提供灵活性和可重用性。
-
模块模式:闭包对于在 JavaScript 中实现模块模式至关重要。通过使用闭包创建私有变量并仅公开必要的公共方法,开发人员可以创建模块化且有组织的代码,从而防止对内部模块数据的不必要的访问和修改。
-
回调函数:使用回调函数时,通常会使用闭包。闭包可用于捕获和维护异步操作上下文中的变量状态,从而确保在调用回调函数时可以访问正确的变量。
14. 解释 JavaScript 中的提升概念。 JavaScript 中的提升是默认行为,即在编译阶段(实际代码执行之前)将变量和函数声明移至其包含范围的顶部。这意味着您可以在代码中声明变量或调用函数之前使用它。 当您使用 声明一个变量时var,该声明将被提升到其包含函数或块的顶部,并使用默认值"undefined"进行初始化。 * *
console.log(x); // Outputs: undefinedvar x = 5;
let用和声明的变量const也会被提升,但有一个"暂时死区",在声明之前无法访问它们。 * *
console.log(x); // 抛出错误 (ReferenceError)let x = 5;
函数声明也会提升到其包含范围的顶部。你可以在代码中声明函数之前调用它。 * * * *
sayHello(); // Outputs: "Hello, world!"function sayHello() { console.log("Hello, world!");}
箭头函数、函数表达式或变量初始化不会发生提升。 15.什么是暂时性死区?
关于 JavaScript 中使用 let 和 const 进行变量声明的概念中有一个称为暂时性死区(Temporal Dead Zone,简称 TDZ)。 当使用 let 或 const 声明变量时,它们会被提升到其包含作用域的最顶部。但与 var 不同,使用 let 和 const 声明的变量在 TDZ 中保持未初始化状态。任何在作用域内实际声明之前尝试访问或使用该变量的尝试都会导致引用错误。这是为了防止在变量被正确定义之前使用它们。理解暂时性死区非常重要,因为它有助于防止与变量初始化前使用相关的错误,并且通过在正确使用变量之前进行声明来推广 JavaScript 编码的最佳实践。简而言之,理解暂时性死区有助于确保代码的健壮性和清晰度。 16. 什么是原型链?以及 Object.create() 方法? 在 JavaScript 中,每个函数和对象默认都有一个名为prototype的属性。 JavaScript 中的每个对象都有一个原型。原型是当前对象从其继承属性和方法的另一个对象。您可以将原型视为模板或父对象。 原型链是一种允许对象从其他对象继承属性和方法的机制 当您访问对象的属性或方法时,JavaScript 首先在对象本身上查找它。如果找不到,它会沿着原型链向上查找,直到找到该属性或方法。此过程一直持续到Object.prototype链的顶部。 17.Call、Apply 和 Bind 方法有什么区别? 调用:this call() 方法使用指定的值和以逗号分隔的值传递的各个参数来调用函数
*
*
*
*
*
*
*
*
*
const person1 = { name: '扶苏' }; const person2 = { name: '小李' };
function greet(greeting) { console.log(greeting + ' ' + this.name); }
greet.call(person1, 'Hello'); // Output: Hello 扶苏 greet.call(person2, 'Hi'); // Output: Hi 小李
使用 call() 方法,一个对象可以使用属于另一个对象的方法。 * * * * * * * * * * * *
const o1 = { name: 'China', getName: function(){ console.log(`Hello, ${this.name}`) }}
const o2 = { name: 'JavaScript'}
o1.getName.call(o2) // Hello, JavaScript
apply:使用给定值调用函数this,但以数组形式接受参数。当事先不知道要传递的参数数量或参数已在数组中时,此方法很有用。 * * * *
const numbers = [1, 2, 3, 4, 5];
const max = Math.max.apply(null, numbers); console.log(max); // Output: 5
bind:它不调用而是返回一个新函数并允许您传递任意数量的参数。bind()方法以一个对象作为第一个参数并创建一个新函数。 * * * * * * * * * *
const person = { name: '扶苏', greet: function(greeting) { return `${greeting}, 我是 ${this.name}`; }};
const greetAlice = person.greet.bind(person);
console.log(greetAlice('Hello')); // Output:Hello,我是 小明
18.什么是 lambda 或箭头函数**?** JavaScript 中有两种类型的函数
-
常规函数
-
箭头函数( ES6引入)
常规函数:我们可以通过两种方式编写常规函数,即函数声明和函数表达式。 * * * * * * * *
function add(a, b){ return a + b;}
const sum = (a, b) => a + b;
add(2,3) // 5sum(2,3) // 5
箭头函数或胖箭头函数:Lambda 函数,也称为箭头函数,是 JavaScript (ES6) 中引入的一项功能,它是一种用于编写函数表达式的更简洁的语法。与传统函数表达式相比,它们的语法更短,特别适合创建匿名函数和使用函数式编程概念。 箭头函数和普通函数之间存在一些差异,比如:
-
语法
-
没有参数(参数是类似数组的对象)
-
箭头函数没有原型对象
-
不能用 new 关键字调用(不是构造函数)
-
没有自己的 this(调用、应用和绑定不会按预期工作)
-
不能用作生成器函数
-
不允许参数重名
19.什么是柯里化函数? 柯里化是函数式编程中的一种技术,它将具有多个参数的函数转换为一系列函数,每个函数都接受一个参数。这些柯里化函数可以组合在一起以构建更复杂的函数。 在 JavaScript 中,你可以使用闭包和返回函数来实现柯里化。 * * * * * * * *
function curryAdd(x) { return function(y) { return x + y; };}
const add5 = curryAdd(5); // 创建偏函数console.log(add5(3)); // Output: 8
Currying 在函数式编程中非常有用,可以使代码更加模块化和可重用。在您想要创建具有不同数量参数的函数或构建数据转换管道的情况下,它特别有用。 20.ES6 有哪些特性? ES6(也称为 ECMAScript 2015)为 JavaScript 引入了多项新功能和增强功能,大大扩展了该语言的功能。ES6 的一些主要功能包括:
-
箭头函数
-
块级作用域变量
-
类
-
模块
-
模板文字:模板文字允许使用反引号嵌入表达式和多行字符串,提供一种更方便在JavaScript中创建复杂字符串的方式。
-
默认参数
-
剩余和展开操作符(rest and spread operators)
-
解构赋值(destructuring assignment)
-
承诺(Promises)
-
映射、集合、弱映射、弱集合:ES6引入了新的内置数据结构,如映射和集合,用于更高效地专门处理集合和键值对。
-
迭代器和生成器(iterators and generators)增强对象字面量以上是一些JavaScript ES6的新特性,它们提供了更简洁、高效的编程方式,有助于简化代码,增强可读性,并且促进了代码复用和模块化开发。通过熟练掌握这些新特性,开发者能够更有效地编写高质量的JavaScript代码。
-
增强的对象字面量
21.什么是执行上下文、执行堆栈、变量对象、作用域链? 执行上下文:执行上下文是指一段代码被执行的环境,由作用域、变量对象、this关键字的值组成。 每当执行一个函数时,就会创建一个执行上下文,它包含该函数的所有变量或属性。 JavaScript 中有三种类型的执行上下文: >全局执行上下文 >功能执行上下文 > Eval 函数执行上下文 执行堆栈:也称为"调用堆栈",是一种 LIFO(后进先出)数据结构,用于存储正在进行的函数调用的所有执行上下文。调用函数时,会创建一个新的执行上下文并将其推送到堆栈上。函数完成后,其上下文会从堆栈中弹出。 引擎执行执行上下文位于堆栈顶部的函数。当此函数完成时,其执行堆栈将从堆栈中弹出,并且控制权将到达当前堆栈中位于其下方的上下文。 执行上下文在创建阶段创建。创建阶段会发生以下事情:
-
LexicalEnvironment组件已创建。
-
VariableEnvironment组件已创建。
变量对象:它是执行上下文的一部分,包含在该上下文中定义的所有变量、函数声明和参数。 作用域链:作用域链是 JavaScript 中解析变量值的一种机制。当引用变量时,JavaScript 引擎首先在当前执行上下文的变量对象中查找该变量。如果未找到,则继续沿着作用域链查找下一个外部执行上下文,直到找到该变量或到达全局执行上下文。 22.callback、promise、setTimeout、process.nextTick()的执行优先级是什么? 我们可以根据事件循环和处理不同异步操作的顺序来理解执行的优先级:
-
process.nextTick():使用 调度的回调process.nextTick()具有最高优先级。使用 时process.nextTick(),回调将在当前操作完成后但在事件循环进入下一阶段之前立即执行。这使其成为一种确保在事件循环中尽早执行函数的方法。
-
Promise:Promise 通常在 之后执行process.nextTick()。但是,它们的优先级高于 安排的回调setTimeout()。
-
setTimeout():用 setTimeout()安排的回调被放置在事件循环的计时器阶段。它们将在当前操作、承诺和任何先前安排的回调完成setTimeout()后执行。setTimeout()
-
回调:常规回调(未使用 调度process.nextTick())具有最低优先级。它们在事件循环处理process.nextTick()、承诺和setTimeout()回调之后执行。
23.什么是工厂函数和生成器函数? JavaScript 中的工厂函数是返回对象的函数。它是一种以直接、有组织的方式创建对象的模式。不要使用构造函数和 new 关键字创建新的对象,工厂函数封装了对象创建过程并返回一个新对象。 * * * * * * * * * * * * * * *
function createPerson(name, age) { return { name: name, age: age, greet: function() { return `Hello, ${this.name} ${this.age} `; } };}
const person1 = createPerson('AA', 25);const person2 = createPerson('BB', 30);
console.log(person1.greet()); // Output: Hello, AA 25console.log(person2.greet()); // Output: Hello, BB 30
JavaScript 中的生成器函数是一种特殊类型的函数,可以在执行期间暂停和恢复。 生成器函数产生一系列结果而不是单个值。 当调用生成器函数时,它会返回一个 生成器对象,该对象可以通过调用 next() 方法来控制函数的执行。 可以使用yield关键字在函数体内暂停函数代码,稍后可以从暂停的位置重新开始执行。 * * * * * * * * * * *
function* numberGenerator() { let i = 0; while (true) { yield i++; }}
const gen = numberGenerator();console.log(gen.next().value); // Output: 0console.log(gen.next().value); // Output: 1console.log(gen.next().value); // Output: 2
24. 克隆对象的不同方法(对象的浅拷贝和深拷贝)? 浅拷贝是对象的副本,其引用与原始对象相同。这意味着,如果您更改浅拷贝中某个属性的值,它也会更改原始对象中该属性的值。 * * * * * * *
const user = { name : '金晶', age : 28, job : '芯片工程师' }; const clone = user
深层复制是指对象的引用与原始对象不同。这意味着,如果在深层复制中更改属性的值,则不会更改原始对象中该属性的值。 有不同的方法来创建对象的深层副本。 a) JSON.parse 和 JSON.stringify:对于嵌套对象也很有用。 * *
const originalObject = { name: "金三", age: 25 }; const deepCopy = JSON.parse(JSON.stringify(originalObject));
b)克隆: * *
const myDeepCopy = structuredClone(myOriginal);
c)扩展运算符(...):任何具有嵌套对象的对象都不会被深度复制。 * * * * *
const originalObject = { name: "李", age: 25 };const deepCopy = {...originalObject};
deepCopy.name = "张"console.log("originalObject", originalObject.name) // 李
d) Object.assign():该Object.assign()方法应用于深度复制没有嵌套对象的对象。 * * *
const originalObject = { name: "扶苏", age: 25 };const shallowCopy = Object.assign({}, originalObject);
e)递归: * * * * * * * * * * * * * * *
function deepCopy(obj) { if (typeof obj !== 'object' || obj === null) { return obj; } const newObj = Array.isArray(obj) ? [] : {}; for (let key in obj) { if (Object.hasOwnProperty.call(obj, key)) { newObj[key] = deepCopy(obj[key]); } } return newObj; } const originalObject = { name: "扶苏", nested: { age: 25 } }; const deepCopy = deepCopy(originalObject);
25. 如何使对象不可变?(密封和冻结方法)? 在 JavaScript 中,您可以使用 Object.seal() 和 Object.freeze() 方法使对象不可变。 Object.freeze():(完全不可变)此方法冻结对象,使其密封并将其所有属性标记为只读。冻结对象后,其属性将无法修改、添加或删除。 * * * * *
const obj = { name: '扶苏', age: 25 };Object.freeze(obj);obj.name = '小苏'; // 不允许obj.address = '亚洲...'; // 不允许delete obj.age; // 不允许
Object.seal():(部分不可变)此方法密封对象,防止添加新属性并将所有现有属性标记为不可配置。但是,您仍然可以修改可写的现有属性的值。 * * * * *
const obj = { name: '扶苏', age: 25 };Object.seal(obj);obj.name = '小苏'; // 允许obj.address = '亚洲...'; // 不允许delete obj.age; // 不允许
26.什么是事件和事件流、事件冒泡和事件捕获? 在 JavaScript 中,事件流是指网页上接收或由 Web 浏览器处理点击或按键等事件的顺序。事件流分为两个阶段:事件捕获和事件冒泡。 当您单击嵌套在其他各个元素中的元素时,在您的单击实际到达其目的地或目标元素之前,它必须首先触发其每个父元素的单击事件,从顶部的全局窗口对象开始。 * * *
<div id="parent"> <button id="child">点击!</button></div>
现在,让我们通过这个例子来解释事件流:
-
事件捕获阶段:单击按钮时,事件从顶部(文档的根)开始移动,然后向下移动到目标元素。在这种情况下,它从文档的根移动到<div>(父元素),然后移动到<button>(子元素)。这称为捕获阶段。
-
事件目标阶段:<button>事件到达目标元素。
-
事件冒泡阶段:到达目标后,事件开始向上冒泡。从<button>到<div>文档的根目录。这称为事件冒泡阶段。
下面是一段简单的 JavaScript 代码片段,用于演示此操作: * * *
document.getElementById('parent').addEventListener('click', function() { console.log('Div点击 (捕获阶段)');}, true);
document.getElementById('child').addEventListener('click', function() { console.log('Button (目标阶段)');});
document.getElementById('parent').addEventListener('click', function() { console.log('Div点击 (冒泡阶段)');});
单击按钮时,您将按以下顺序在控制台中看到这些消息:
-
"Div 点击(捕获阶段)"
-
"Button点击(目标阶段)"
-
"Div 点击(冒泡阶段)"
27.什么是事件委托? 事件委托是一种 JavaScript 编程技术,可优化多个元素的事件处理。 事件委托不是将事件监听器附加到每个单独的元素,而是将单个事件监听器附加到 DOM(文档对象模型)层次结构中较高级别的公共祖先元素。 当某个后代元素上发生事件时,它会"冒泡"到共同祖先,事件监听器正在那里等待。 事件委托是一种监听事件的技术,您可以委托父元素作为其内部发生的所有事件的监听器。 * *
var form = document.querySelector("#form");
// 监听表单字段变化form.addEventListener( "input", function (event) { // 记录改变的字段 console.log(event.target); }, false);
28.什么是服务器发送事件? 服务器发送事件 (SSE) 是一种简单而有效的技术,可通过单个 HTTP 连接实现从服务器到客户端的实时更新。 SSE 允许服务器在有新信息时立即将数据推送到 Web 客户端(通常是浏览器),这使其成为需要实时更新而无需依赖复杂协议或第三方库的场景的绝佳选择。 a)SSE 提供从服务器到客户端的单向数据流。服务器发起通信,并向客户端发送更新。 b)SSE使用基于文本的协议,这意味着从服务器发送到客户端的数据通常采用文本格式(通常是 JSON 或纯文本)。 c)SSE 自动处理重新连接。 d)SSE 在客户端和服务器之间建立持久连接,允许服务器向客户端发送事件流。每个事件都可以具有唯一的类型和与之关联的数据。 e) EventSource 对象用于接收服务器发送的事件通知。例如,您可以按如下方式接收来自服务器的消息 * * * * * *
if (typeof EventSource !== "undefined") { var source = new EventSource("generator.js"); source.onmessage = function (event) { document.getElementById("output").innerHTML += event.data + "<br>"; };}
f)这些是可用于服务器发送事件的事件列表(onopen,onmessage,onerror)。29. JavaScript 中的 Web Worker 或 Service Worker 是什么? Web Workers 和 Service Workers 是 JavaScript 中的两个不同概念, Web Workers 专为在后台并发执行 JavaScript 而设计,而Service Workers 则用于创建具有离线功能和高级功能的渐进式 Web 应用。两者都是增强 Web 应用性能和功能的必备工具。 它们在 Web 开发中都有不同的用途:
Web Workers:
-
并发性:Web Workers 是浏览器的一项功能,可让您在后台运行 JavaScript 代码,独立于浏览器主线程。这样便可并发执行任务,而不会阻塞用户界面。
-
用例:Web Workers 通常用于计算密集型或耗时的任务,例如数据处理、图像处理或复杂计算。通过在单独的线程中运行这些任务,它们不会影响网页的响应能力。
-
通信:Web Workers 可以使用消息系统与主线程进行通信。它们可以发送和接收消息,从而实现主线程和 Worker 之间的协调。
-
浏览器支持:大多数现代浏览器都支持 Web Worker。
Service Workers:
-
离线功能:Service Workers 是用于创建渐进式 Web 应用 (PWA) 的更高级功能。它们充当在后台运行的代理服务器,可以拦截和缓存网络请求。这可以实现离线功能,例如在用户离线时提供缓存内容。
-
用例:Service Worker 主要用于实现离线访问、 推送通知和后台同步等功能。它们使 Web 应用即使在没有互联网连接的情况下也能正常运行。
-
生命周期:Service Worker 有自己的生命周期,其中包含 、 和 等事件install。activate它们fetch通常在 Web 应用生命周期开始时注册。
-
浏览器支持:现代浏览器支持服务工作线程,它是创建可靠且引人入胜的 Web 应用程序的关键技术。
30. 如何在 Javascript 中比较两个 JSON 对象? a) 比较两个 JSON 对象的一种简单方法是使用 JSON.stringify 将它们转换为字符串,然后比较字符串。 * * * * * * *
function areEqual(obj1, obj2) { return JSON.stringify(obj1) === JSON.stringify(obj2);}
const obj1 = { a: 1, b: { c: 2 } };const obj2 = { a: 1, b: { c: 2 } };console.log(areEqual(obj1, obj2)); // Output: true
b) 可以使用 Ramda 库来比较两个 JSON 对象。Ramda 为此提供了一个名为 equals 的函数。 * * * * * *
const R = require('ramda');
const obj1 = { a: 1, b: { c: 2 } };const obj2 = { a: 1, b: { c: 2 } };
console.log(R.equals(obj1, obj2)); // Output: true
c) 也可以选择使用诸如 Lodash 之类的库,它提供了一种对对象进行深度比较的方法。 * * * * *
const _ = require('lodash');
const obj1 = { a: 1, b: { c: 2 } };const obj2 = { a: 1, b: { c: 2 } };console.log(_.isEqual(obj1, obj2)); // Output: true
在这篇文章中,深入探讨了JavaScript的核心概念和常见面试问题,希望这些内容能够为您提供有价值的参考和帮助。掌握这些知识不仅能提升您的编程能力,还能增强你在面试中的竞争力。无论你是正在准备面试,还是希望进一步加深对JavaScript的理解,持续学习和实践都是成功的关键。