无论你是经验丰富的开发人员,还是刚开始你的技术生涯,这份 2024 年 JavaScript 面试宝典都将帮助你温习核心概念,从基本的语言特性到高级主题,应有尽有。
本文汇总了 30 多个最关键的 JavaScript 面试问题,并附有详细的解答和代码示例,帮助你轻松应对面试,在竞争激烈的科技行业立于不败之地。
1 级:基础
- JavaScript 是单线程语言吗?
- 解释 JavaScript 引擎的主要组成部分及其工作原理。
- 解释 JavaScript 中的事件循环,以及它如何帮助异步编程。
var
、let
和const
之间的区别是什么?- JavaScript 中有哪些不同的数据类型?
- 什么是回调函数和回调地狱?
- 什么是 Promise 和 Promise 链式调用?
- 什么是
async/await
? ==
和===
运算符之间的区别是什么?- JavaScript 中创建对象的几种方法有哪些?
- 什么是剩余运算符和展开运算符?
- 什么是高阶函数?
2 级:中级
- 什么是闭包?闭包有哪些应用场景?
- 解释 JavaScript 中的提升概念。
- 什么是暂时性死区?
- 什么是原型链?
Object.create()
方法的作用是什么? call
、apply
和bind
方法之间的区别是什么?- 什么是 Lambda 函数或箭头函数?
- 什么是柯里化函数?
- ES6 的特性有哪些?
3 级:专家
- 什么是执行上下文、执行栈、变量对象和作用域链?
- 回调函数、Promise、
setTimeout
和process.nextTick()
的执行优先级是什么? - 什么是工厂函数和生成器函数?
- 克隆对象(浅拷贝和深拷贝)的几种方法有哪些?
- 如何使对象不可变?(
seal
和freeze
方法) - 什么是事件和事件流,事件冒泡和事件捕获?
- 什么是事件委托?
- 什么是服务器发送事件?
- 什么是 JavaScript 中的 Web Worker 或 Service Worker?
- 如何比较 JavaScript 中的两个 JSON 对象?
1. JavaScript 是单线程语言吗?
是的,JavaScript 是单线程语言。这意味着它只有一个调用栈和一个内存堆。一次只能执行一组指令。
此外,JavaScript 本质上是同步和阻塞的。这意味着代码是逐行执行的,一项任务必须完成才能开始下一项任务。
然而,JavaScript 也有异步能力,允许某些操作独立于主执行线程执行。这通常通过回调、Promise、async/await
和事件监听器等机制实现。这些异步功能使 JavaScript 能够处理数据获取、用户输入处理和 I/O 操作等任务,而不会阻塞主线程,使其适合构建响应式和交互式 Web 应用程序。
2. 解释 JavaScript 引擎的主要组成部分及其工作原理。
每个浏览器都拥有一个 JavaScript 引擎,它负责执行 JavaScript 代码并将代码转换为机器代码。
当执行 JavaScript 代码时,解析器首先读取代码并生成一个抽象语法树(AST),然后将其存储在内存中。解释器随后处理这个 AST 并生成字节码或机器代码,由计算机执行。
分析器是 JavaScript 引擎的一个组件,负责监控代码的执行。
字节码与分析数据一起被优化编译器使用。"优化编译器" 或即时 (JIT) 编译器根据分析数据做出某些假设并生成高度优化的机器代码。
有时,假设不正确,此时就会通过 "反优化" 阶段回到之前的版本(实际上对我们来说会造成开销)。
JS 引擎通常优化 "热点函数" 并使用内联缓存技术来优化代码。
在此过程中,调用栈跟踪当前正在执行的函数,内存堆用于内存分配。
最后,垃圾回收器发挥作用,通过回收未使用的对象的内存来管理内存。
Google Chrome V8 引擎:
- 解释器称为 "Ignition"。
- 优化编译器称为 "TurboFan"。
- 除了解析器之外,还有一个 "预解析器",它检查语法和标记。
- "Sparkplug" 被引入,它介于 "Ignition" 和 "TurboFan" 之间,也称为快速编译器。
3. 解释 JavaScript 中的事件循环。
事件循环是 JavaScript 运行时环境的核心组件。它负责调度和执行异步任务。事件循环通过持续监控两个队列:调用栈和事件队列来工作。
调用栈 是一种栈(后进先出)数据结构 ,它存储当前正在执行的函数(存储代码执行期间创建的执行上下文)。
Web API 是异步操作(setTimeout
、fetch
请求、Promise)及其回调等待完成的地方。它从线程池中借用线程来完成后台任务,而不会阻塞主线程。
作业队列(或微任务) 是一个 FIFO(先进先出)结构,它保存 async/await
、Promise、process.nextTick()
的回调 ,这些回调已准备好执行。例如,已完成的 Promise 的 resolve
或 reject
回调会排队到作业队列中。
任务队列(或宏任务) 是一个 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)中引入的原始数据类型 ,它表示唯一且不可变的值 。它通常用作对象属性的标识符,以避免名称冲突。
javascript
const mySymbol = Symbol('key');
const obj = {
[mySymbol]: 'value'
};
当使用 Symbol
作为属性键时,它不会与其他属性键发生冲突,包括字符串键。
6. 什么是回调函数和回调地狱?
在 JavaScript 中,回调通常用于处理异步操作。
回调函数 是一个函数,作为参数传递给另一个函数 ,并且它将在完成特定任务或在给定时间后执行。
javascript
function fetchData(url, callback) {
// 模拟从服务器获取数据
setTimeout(() => {
const data = 'Some data from the server';
callback(data);
}, 1000);
}
function processData(data) {
console.log('Processing data:', data);
}
fetchData('https://example.com/data', processData);
在此示例中,fetchData
函数接受一个 URL 和一个回调函数作为参数 。从服务器获取数据后(使用 setTimeout
模拟),它会调用回调函数并将检索到的数据传递给它。
回调地狱 ,也称为**"金字塔地狱",是 JavaScript 编程中使用的一个术语,用于描述在异步函数中使用多个嵌套回调**的情况。
"当异步操作依赖于先前异步操作的结果时,会导致深度嵌套,并且通常难以阅读的代码。"
回调地狱是一种反模式,它使用多个嵌套回调,这使得在处理异步逻辑时,代码难以阅读和调试。
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 提供了 Promise 和 async/await
等替代方案。 以下是使用 Promise 的相同代码:
javascript
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 有三种状态:
- 等待中: 初始状态。这是 Promise 的最终值尚未可用的状态。
- 已完成: Promise 已成功解决,最终值现在可用的状态。
- 已拒绝: Promise 遇到错误或被拒绝,最终值无法提供。
Promise 构造函数 有两个参数 (resolve
、reject
) ,它们是函数 。如果异步任务已在没有错误的情况下完成,则使用消息或获取的数据调用 resolve
函数来解决 Promise。
如果发生错误,则调用 reject
函数并将错误传递给它。
我们可以使用 .then()
处理程序访问 Promise 的结果。
我们可以使用 .catch()
处理程序捕获错误。
javascript
// 创建一个 Promise
const fetchData = new Promise((resolve, reject) => {
// 模拟从服务器获取数据
setTimeout(() => {
const data = 'Some data from the server';
// 使用检索到的数据解决 Promise
resolve(data);
// 使用错误拒绝 Promise
// reject(new Error('Failed to fetch data'));
}, 1000);
});
// 使用 Promise
fetchData
.then((data) => {
console.log('Data fetched:', data);
})
.catch((error) => {
console.error('Error fetching data:', error);
});
Promise 链式调用: 使用 Promise 一次一个地执行一系列异步任务的过程称为 Promise 链式调用。
它涉及将多个**.then()
** 方法链接到一个 Promise 上,以便按特定顺序执行一系列任务。
javascript
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 中处理异步代码的现代方法。它提供了更简洁、更易读的方式来处理 Promise 和异步操作,有效地避免了"回调地狱",并改进了异步代码的整体结构。
在 JavaScript 中,async
关键字用于定义异步函数,该函数返回一个 Promise。
在异步函数中,await
关键字用于暂停函数的执行,直到 Promise 解决,有效地允许在处理异步操作时使用类似同步的代码。
javascript
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) => {
// 处理错误
});
在此示例中,fetchData
函数被定义为异步函数,它使用 await
关键字来暂停执行,并等待 fetch
和 json
操作,有效地以类似同步的方式处理 Promise。
9. ==
和 ===
运算符之间的区别是什么?
==
(松散相等运算符): 此运算符执行类型强制 ,这意味着在进行比较之前,它会将操作数转换为相同的类型 。它检查值是否相等,而不考虑其数据类型。例如,1 == '1'
将返回 true
,因为 JavaScript 会将字符串 '1'
转换为数字,然后再进行比较。
===
(严格相等运算符): 此运算符执行严格比较**,不进行类型强制**。它检查值和其数据类型是否相等。例如,1 === '1'
将返回 false
,因为数据类型不同(数字和字符串)。
总之,==
在进行类型强制后检查相等性,而 ===
检查严格相等性,同时考虑值及其数据类型。
==
的执行速度比 ===
语句快。
以下是一些涵盖上述情况的示例:
javascript
0 == false // true
0 === false // false
1 == "1" // true
1 === "1" // false
null == undefined // true
null === undefined // false
'0' == false // true
'0' === false // false
[]==[] or []===[] //false, refer different objects in memory
{}=={} or {}==={} //false, refer different objects in memory
10. JavaScript 中创建对象的几种方法有哪些?
在 JavaScript 中,创建对象的方法有很多种。一些常用的创建对象的方法包括:
a) 对象字面量: 创建对象的最直接的方法是使用对象字面量,它在花括号中以逗号分隔的列表形式定义对象的属性和方法。
javascript
let person = {
firstName: 'John',
lastName: 'Doe',
greet: function() {
return 'Hello, ' + this.firstName + ' ' + this.lastName;
}
};
b) 构造函数: 构造函数可用于使用 new
关键字创建对象的多个实例。在构造函数中,可以使用 this
关键字分配属性和方法。
javascript
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.greet = function() {
return 'Hello, ' + this.firstName + ' ' + this.lastName;
};
}
let person1 = new Person('John', 'Doe');
let person2 = new Person('Jane', 'Smith');
c) Object.create()
: Object.create()
方法允许你使用指定的原型对象创建一个新对象。此方法提供了对新创建对象原型的更多控制。
javascript
let personProto = {
greet: function() {
return 'Hello, ' + this.firstName + ' ' + this.lastName;
}
};
let person = Object.create(personProto);
person.firstName = 'John';
person.lastName = 'Doe';
d) 类语法 (ES6) :随着 ES6 的引入,JavaScript 支持使用 class
关键字定义对象的类语法。这提供了更熟悉、更结构化的方式来创建对象并定义其属性和方法。
javascript
class Person {
constructor(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
greet() {
return 'Hello, ' + this.firstName + ' ' + this.lastName;
}
}
let person = new Person('John', 'Doe');
e) 工厂函数: 工厂函数是返回对象的函数。这种方法允许你封装对象创建过程,并轻松创建具有自定义属性的多个实例。
javascript
function createPerson(firstName, lastName) {
return {
firstName: firstName,
lastName: lastName,
greet: function() {
return 'Hello, ' + this.firstName + ' ' + this.lastName;
}
};
}
let person1 = createPerson('John', 'Doe');
let person2 = createPerson('Jane', 'Smith');
f) Object.setPrototypeOf()
: Object.setPrototypeOf()
方法可用于设置指定对象的原型。这提供了一种在对象创建后设置对象原型的替代方法。
javascript
let personProto = {
greet: function() {
return 'Hello, ' + this.firstName + ' ' + this.lastName;
}
};
let person = {};
person.firstName = 'John';
person.lastName = 'Doe';
Object.setPrototypeOf(person, personProto);
g) Object.assign()
: Object.assign()
方法可用于通过将一个或多个源对象的枚举自身属性的值复制到目标对象来创建一个新对象。这对于合并对象或创建浅拷贝特别有用。
javascript
let target = { a: 1, b: 2 };
let source = { b: 3, c: 4 };
let mergedObject = Object.assign({}, target, source);
h) 原型继承: JavaScript 使用原型继承,允许对象从其他对象继承属性和方法。你可以通过利用原型继承和使用构造函数或类的原型属性来定义共享行为来创建对象。
javascript
function Animal(name) {
this.name = name;
}
Animal.prototype.greet = function() {
return 'Hello, I am ' + 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('Max', 'Poodle');
i) 单例模式: 单例模式用于将对象限制为单个实例。它可以使用闭包和立即调用函数表达式 (IIFE) 的组合在 JavaScript 中实现。这确保了只创建对象的单个实例。
javascript
let singleton = (() => {
let instance;
function createInstance() {
return {
// 属性和方法
};
}
return {
getInstance: () => {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
11. 什么是剩余运算符和展开运算符?
剩余运算符由三个点 (...
) 表示,用于函数参数中将可变数量的参数收集到一个数组中。它允许你将任意数量的参数传递给函数,而无需将其明确定义为命名参数。
javascript
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4)); // 输出 10
展开运算符 也由三个点 (...
) 表示,用于将数组或对象中的元素展开到另一个数组或对象中。它允许你轻松克隆数组、连接数组和合并对象。
javascript
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 中的高阶函数要么接受一个或多个函数作为参数,要么返回一个函数作为其结果。换句话说,它对函数进行操作,通过将函数作为参数、返回函数,或者两者兼而有之。
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); // 输出: [2, 4, 6, 8]
它们支持强大的技术,如函数组合、柯里化和基于回调的异步操作。了解高阶函数对于编写富有表现力的函数式 JavaScript 代码至关重要。
一元函数(即单子)是一个接受恰好一个参数的函数。 它代表函数接受的单个参数。
13. 什么是闭包?闭包有哪些应用场景?
闭包 是一种功能,它允许函数捕获定义它的环境(或保留对该作用域中变量的访问权限),即使该作用域已关闭。
我们可以说闭包是函数及其定义函数的词法环境的组合。
换句话说,闭包允许函数访问它自己的作用域、外部函数的作用域和全局作用域,从而使其能够"记住"并继续访问这些作用域中的变量和参数。
javascript
function outerFunction() {
let outerVariable = 'I am from the outer function';
return innerFunction() {
console.log(outerVariable); // 从外部函数的作用域访问 `outerVariable`
}
}
let myFunction = outerFunction();
myFunction(); // 输出:I am from the outer function
闭包在函数创建时,以及在另一个函数内部定义函数时创建。
执行上下文是执行 JavaScript 代码的环境。每次调用函数时,都会创建一个单独的执行上下文并将其推入执行栈。当函数执行完成后,它会从栈中弹出。
每个执行上下文在内存中都有一块空间用于存储其变量和函数,一旦函数从执行栈中弹出,JavaScript 垃圾回收器就会清除所有这些东西。
在 JavaScript 中,只有当没有对它的引用时,任何东西才会被垃圾回收。
在上面的示例中,匿名执行上下文仍然引用了其外部环境的内存空间中的变量。即使 outerFunction()
已完成。(它可以访问 outerVariabl
e 变量并在 console.log(outerVariable)
中使用它)。
闭包在 JavaScript 中有几个重要的应用场景:
- 数据隐私和封装: 闭包可用于创建私有数据,并将功能封装在有限的作用域内。通过在另一个函数中定义函数,内部函数可以访问外部函数的变量,但这些变量无法从外部函数之外访问。这允许创建私有数据和方法,这些数据和方法无法从外部直接访问,从而增强数据隐私和封装。
- 维护状态: 闭包通常用于在异步操作和事件处理中维护状态。例如,在处理异步任务时,闭包可以捕获和保留变量在多个异步操作中的状态,确保在异步任务完成后访问正确的变量。
- 柯里化和偏应用: 闭包有助于实现函数式编程技术,如柯里化和偏应用。通过使用闭包来捕获和记住特定参数,并返回使用这些捕获参数的新函数,可以实现柯里化和偏应用。这允许创建具有预设参数的专用函数,从而提供灵活性并提高可重用性。
- 模块模式: 闭包在 JavaScript 中实现模块模式至关重要。通过使用闭包来创建私有变量并仅公开必要的方法,开发人员可以创建模块化和组织良好的代码,防止意外访问和修改内部模块数据。
- 回调函数: 在使用回调函数时,经常会用到闭包。闭包可用于捕获和维护异步操作上下文中的变量状态,确保在调用回调函数时访问正确的变量。
14. 解释 JavaScript 中的提升概念。
JavaScript 中的提升是默认行为 ,它在编译阶段 将变量和函数声明提升到其包含的作用域的顶部,在实际代码执行之前。这意味着你可以在代码中使用变量或调用函数,即使它是在代码中声明的。
当你使用 var
声明变量时,声明会被提升到其包含的函数或块的顶部,并且初始化为"undefined
"的默认值。
javascript
console.log(x); // 输出:undefined
var x = 5;
使用 let
和 const
声明的变量也会被提升,但它们有一个暂时性死区,在其中它们无法在声明之前访问。
javascript
console.log(x); // 抛出错误(ReferenceError)
let x = 5;
函数声明也会被提升到其包含的作用域的顶部。你可以在代码中声明函数之前调用它。
javascript
sayHello(); // 输出:"Hello, world!"
function sayHello() {
console.log("Hello, world!");
}
箭头函数、函数表达式或变量初始化都不会发生提升。
15. 什么是暂时性死区?
暂时性死区 (TDZ) 是 JavaScript 中与使用 let
和 const
声明变量相关的概念。
当你使用 let
或 const
声明变量时,它会被提升到其包含的作用域的顶部,但是,与 var
不同,使用 let
和 const
声明的变量在 TDZ 中仍然未初始化。
在作用域内,在实际声明之前访问或使用变量的任何尝试都会导致 ReferenceError
。这是为了防止在变量正确定义之前使用变量。
了解暂时性死区很重要,因为它有助于防止与在初始化之前使用变量相关的错误。它还鼓励在 JavaScript 编码中采用最佳实践,即在使用变量之前进行正确的变量声明。
16. 什么是原型链?Object.create()
方法的作用是什么?
在 JavaScript 中,每个函数和对象默认都有一个名为原型的属性。
JavaScript 中的每个对象都有一个原型。原型是另一个对象,当前对象从中继承属性和方法。你可以将原型视为模板或父对象。
原型链是一种机制,它允许对象从其他对象继承属性和方法。
当你访问对象上的属性或方法时,JavaScript 首先会在对象本身中查找它。如果找不到,它会向上查找原型链,直到找到属性或方法。这个过程会一直持续到它到达链顶部的 Object.prototype
。
17. call
、apply
和 bind
方法之间的区别是什么?
call
: call()
方法使用指定的 this
值调用函数,并将作为逗号分隔的值传递的单个参数。
javascript
const person1 = { name: 'John' };
const person2 = { name: 'Jane' };
function greet(greeting) {
console.log(greeting + ' ' + this.name);
}
greet.call(person1, 'Hello'); // 输出:Hello John
greet.call(person2, 'Hi'); // 输出:Hi Jane
使用 call()
方法,一个对象可以使用属于另一个对象的方法。
javascript
const o1 = {
name: 'ravi',
getName: function(){
console.log(`Hello, ${this.name}`)
}
}
const o2 = {
name: 'JavaScript Centric'
}
o1.getName.call(o2) // Hello, JavaScript Centric
apply
: 调用函数,使用给定的 this
值,但它接受参数作为数组。当传递的参数数量未知,或者参数已在数组中时,它很有用。
javascript
const numbers = [1, 2, 3, 4, 5];
const max = Math.max.apply(null, numbers);
console.log(max); // 输出:5
bind
: 而不是调用它,它会返回一个新函数,并允许你传递任意数量的参数。bind()
方法将一个对象作为第一个参数,并创建一个新函数。
javascript
const module = {
x: 42,
getX: function() {
return this.x;
}
};
const boundGetX = unboundGetX.bind(module);
console.log(boundGetX()); // 输出:42
18. 什么是 Lambda 函数或箭头函数?
JavaScript 中有两种类型的函数:
- 常规函数
- 箭头函数(在 ES6 中引入)
常规函数: 我们可以通过两种方式编写常规函数,即函数声明 和函数表达式。
箭头函数或胖箭头函数:Lambda 函数,也称为箭头函数 ,是 JavaScript(ES6)中引入的一项功能,它提供了一种更简洁的语法来编写函数表达式 。与传统的函数表达式相比,它们具有更短的语法,并且在创建匿名函数和处理函数式编程概念方面特别有用。
这里没有声明方法,我们只能使用函数表达式来编写。
箭头函数和常规函数之间存在一些差异,例如:
- 语法
- 没有参数 (参数是类似数组的对象)
- 箭头函数 没有原型对象
- 不能使用
new
关键字调用 (不是构造函数) - 没有自己的
this
(call
、apply
和bind
的效果将与预期不同) - 它不能用作生成器函数
- 不允许重复命名参数
19. 什么是柯里化函数?
柯里化 是函数式编程中的一种技术,它将一个接受多个参数的函数转换为一系列函数,每个函数都接受单个参数。这些柯里化函数可以组合在一起以构建更复杂的函数。
在 JavaScript 中,你可以使用闭包和返回函数来实现柯里化。
javascript
// 接受两个参数的常规函数
function add(x, y) {
return x + y;
}
// 函数的柯里化版本
function curryAdd(x) {
return function(y) {
return x + y;
};
}
const add5 = curryAdd(5); // 偏应用,创建一个新函数
console.log(add5(3)); // 输出:8
柯里化在函数式编程中很有益,可以使代码更模块化、更可重用。它在你想创建具有可变数量参数的函数或构建数据转换管道时特别有用。
20. ES6 的特性有哪些?
ES6,也称为 ECMAScript 2015,为 JavaScript 引入了许多新功能和增强功能,极大地扩展了该语言的功能。ES6 的一些关键特性包括:
- 箭头函数
- 块级作用域变量
- 类
- 模块
- 模板字面量: 模板字面量允许使用反引号嵌入表达式和多行字符串,为在 JavaScript 中创建复杂字符串提供了一种更便捷的方式。
- 默认参数
- 剩余运算符和展开运算符
- 解构赋值
- Promise
Map
、Set
、WeakMap
、WeakSet
: ES6 引入了新的内置数据结构,如Map
和Set
,用于更有效地专门处理集合和键值对。- 迭代器和生成器
- 增强的对象字面量
21. 什么是执行上下文、执行栈、变量对象和作用域链?
执行上下文: 执行上下文是指执行代码片段的环境。它包括作用域、变量对象和 "this" 关键字的值。
每当一个函数被执行时,就会创建一个执行上下文,它包含该函数的所有变量或属性。
JavaScript 中有三种类型的执行上下文:
全局执行上下文
函数执行上下文
eval
函数执行上下文
执行栈: 也称为"调用栈",它是一种后进先出的数据结构,它存储正在进行的所有函数调用的执行上下文。当调用一个函数时,就会创建一个新的执行上下文并将其推入栈中。当函数完成时,它的上下文会从栈中弹出。
引擎执行栈顶的函数的执行上下文。当此函数完成时,它的执行栈会从栈中弹出,控制权会到达栈中当前位置的下方上下文。
执行上下文在创建阶段创建。在创建阶段会发生以下情况:
LexicalEnvironment
组件被创建。VariableEnvironment
组件被创建。
变量对象: 它是一个执行上下文的一部分,它包含在该上下文中定义的所有变量、函数声明和参数。
作用域链: 作用域链是 JavaScript 中解析变量值的一种机制。当引用一个变量时,JavaScript 引擎首先会在当前执行上下文的变量对象中查找该变量。如果在那里找不到,它会继续到下一个外部执行上下文,按照作用域链查找,直到找到变量或到达全局执行上下文。
22. 回调函数、Promise、setTimeout
和 process.nextTick()
的执行优先级是什么?
22. 回调函数、Promise、setTimeout
和 process.nextTick()
的执行优先级是什么?
可以根据事件循环以及不同异步操作的处理顺序来理解执行优先级:
process.nextTick()
: 使用process.nextTick()
调度的回调具有最高优先级。当你使用process.nextTick()
时,回调会在当前操作完成后立即执行,但在事件循环进入下一阶段之前。这使其成为一种确保函数在事件循环中尽快执行的方法。- Promise: Promise 通常在
process.nextTick()
之后执行。然而,它们比使用setTimeout()
调度的回调优先级更高。 setTimeout()
: 使用setTimeout()
调度的回调被放置在事件循环的计时器阶段。它们将在当前操作、Promise 和任何先前调度的setTimeout()
回调完成后执行。- 回调: 常规回调(不是使用
process.nextTick()
调度的)具有最低优先级。它们在事件循环处理process.nextTick()
、Promise 和setTimeout()
回调之后执行。
23. 什么是工厂函数和生成器函数?
工厂函数 在 JavaScript 中是一个返回对象的函数 。它是一种用于创建对象的模式,简单明了,结构清晰。与使用构造函数和 new
关键字创建新对象不同 ,工厂函数封装了对象创建过程并返回一个新对象。
javascript
function createPerson(name, age) {
return {
name: name,
age: age,
greet: function() {
return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
}
};
}
const person1 = createPerson('Alice', 25);
const person2 = createPerson('Bob', 30);
console.log(person1.greet()); // 输出:Hello, my name is Alice and I am 25 years old.
console.log(person2.greet()); // 输出:Hello, my name is Bob and I am 30 years old.
生成器函数 是 JavaScript 中的一种特殊类型的函数,它可以在执行过程中暂停和恢复。
生成器函数会生成一系列结果,而不是单个值。
当调用一个生成器函数时,它会返回一个生成器对象 ,该对象可用于通过调用 next()
方法来控制函数的执行。
函数的代码可以使用**yield
关键字**在主体中暂停,并且可以稍后从它暂停的精确位置恢复。
javascript
function* numberGenerator() {
let i = 0;
while (true) {
yield i++;
}
}
const gen = numberGenerator();
console.log(gen.next().value); // 输出:0
console.log(gen.next().value); // 输出:1
console.log(gen.next().value); // 输出:2
这提供了一种强大的机制来创建迭代器和处理异步代码。
24. 克隆对象(浅拷贝和深拷贝)的几种方法有哪些?
浅拷贝 是对象的副本,其引用与原始对象相同。这意味着如果你更改浅拷贝中属性的值,也会更改原始对象中该属性的值。
javascript
const user = {
name: "Kingsley",
age: 28,
job: "Web Developer"
}
const clone = user;
深拷贝 是对象的副本,其引用与原始对象不同。这意味着如果你更改深拷贝中属性的值,不会更改原始对象中该属性的值。
有多种方法可以创建对象的深拷贝。
a) JSON.parse
和 JSON.stringify
: 对于嵌套对象也很有用。
javascript
const originalObject = { name: "Alice", age: 25 };
const deepCopy = JSON.parse(JSON.stringify(originalObject));
b) structuredClone
:
javascript
const myDeepCopy = structuredClone(myOriginal);
c) 展开运算符 (...): 任何具有嵌套对象的深拷贝将不会被深拷贝。
javascript
const originalObject = { name: "Alice", age: 25 };
const deepCopy = {...originalObject};
deepCopy.name = "ravi";
console.log("originalObject", originalObject.name); // Alice
d) Object.assign()
: Object.assign()
方法应用于深拷贝没有嵌套对象的深拷贝。
javascript
const originalObject = { name: "Alice", age: 25 };
const shallowCopy = Object.assign({}, originalObject);
e) 递归:
javascript
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: "Alice", nested: { age: 25 } };
const deepCopy = deepCopy(originalObject);
25. 如何使对象不可变?(seal
和 freeze
方法)
在 JavaScript 中,你可以使用 Object.seal()
和 Object.freeze()
方法来使对象不可变。
Object.freeze()
: (完全不可变)此方法会冻结一个对象,使其同时被密封,并将所有属性标记为只读。冻结对象后,不能修改、添加或删除其属性。
javascript
const obj = { name: 'Alice', age: 25 };
Object.freeze(obj);
obj.name = 'Bob'; // 不允许
obj.address = '123 Street'; // 不允许
delete obj.age; // 不允许
Object.seal()
: (部分不可变)此方法会密封一个对象,防止添加新属性,并将所有现有属性标记为不可配置。但是,你仍然可以修改可写的现有属性的值。
javascript
const obj = { name: 'Alice', age: 25 };
Object.seal(obj);
obj.name = 'Bob'; // 允许
obj.address = '123 Street'; // 不允许(不能添加新属性)
delete obj.age; // 不允许(不能删除现有属性)
26. 什么是事件和事件流,事件冒泡和事件捕获?
在 JavaScript 中,事件流是指事件(如点击或按键)在网页上或由网页浏览器处理的顺序。事件流包含两个阶段:事件捕获和事件冒泡。
当你点击嵌套在其他各种元素中的元素时,在点击实际到达其目标元素或目标元素之前,它必须首先触发每个父元素的点击事件 ,从顶部(全局 window
对象)开始。
现在,让我们通过这个示例来解释事件流:
- 事件捕获阶段: 当你点击按钮时,事件从顶部(文档的根节点)开始,向下移动到目标元素。在这种情况下,它从文档的根节点移动到(父元素),然后移动到(子元素)。这称为捕获阶段。
- 事件目标阶段: 事件到达目标元素,在本例中是。
- 事件冒泡阶段: 到达目标元素后,事件开始冒泡。它从向后移动到,最终到达文档的根节点。这称为冒泡阶段。
以下是一个简单的 JavaScript 代码片段,用于演示此操作:
javascript
document.getElementById('parent').addEventListener('click', function() {
console.log('Div clicked (capturing phase)');
}, true); // `true` 表示捕获阶段。
document.getElementById('child').addEventListener('click', function() {
console.log('Button clicked (target phase)');
});
document.getElementById('parent').addEventListener('click', function() {
console.log('Div clicked (bubbling phase)');
});
当你点击按钮时,你将在控制台中看到以下顺序的消息:
- "Div clicked (capturing phase)"
- "Button clicked (target phase)"
- "Div clicked (bubbling phase)"
27. 什么是事件委托?
事件委托是一种 JavaScript 编程技术,用于优化针对多个元素的事件处理。
与为每个单独的元素都附加一个事件监听器不同,事件委托涉及将单个事件监听器附加到 DOM(文档对象模型)层次结构中较高位置的公共祖先元素。
当事件发生在一个子元素上时,它会"冒泡"到公共祖先,事件监听器在那里等待。
事件委托是一种监听事件的技术,你将父元素委托为所有发生在其内部的事件的监听器。
javascript
var form = document.querySelector("#registration-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
对象用于接收服务器发送的事件通知。例如,你可以像下面这样接收来自服务器的消息:
javascript
if (typeof EventSource !== "undefined") {
var source = new EventSource("sse_generator.js");
source.onmessage = function (event) {
document.getElementById("output").innerHTML += event.data + "";
};
}
f) 以下是服务器发送事件可用的事件列表(onopen
、onmessage
、onerror
)。
29. 什么是 JavaScript 中的 Web Worker 或 Service Worker?
Web Worker 和 Service Worker 是 JavaScript 中的两个不同概念,
Web Worker 用于在后台并发执行 JavaScript 代码 ,而Service Worker 用于创建具有离线功能和高级功能的渐进式 Web 应用程序。两者都是增强 Web 应用程序性能和功能的重要工具。
它们在 Web 开发中各司其职:
Web Worker:
- 并发性: Web Worker 是一种浏览器功能 ,允许你在后台运行 JavaScript 代码,独立于主浏览器线程。这使得能够并发执行任务,而不会阻塞用户界面。
- 应用场景: Web Worker 通常用于执行计算量大或耗时的任务,如数据处理、图像处理或复杂计算。通过在单独的线程中运行这些任务,它们不会影响网页的响应速度。
- 通信: Web Worker 可以使用消息系统与主线程通信。它们可以发送和接收消息,允许在主线程和 Worker 之间进行协调。
- 浏览器支持: Web Worker 在大多数现代浏览器中得到支持。
Service Worker:
- 离线功能: Service Worker 是一种更高级的功能,用于创建渐进式 Web 应用程序 (PWA)。它们充当代理服务器,在后台运行,可以拦截和缓存网络请求。这使得离线功能成为可能,例如,在用户离线时提供缓存内容。
- 应用场景: Service Worker 主要用于实现离线访问、推送通知和后台同步等功能。它们使 Web 应用程序即使在没有互联网连接的情况下也能正常运行。
- 生命周期: Service Worker 具有自己的生命周期,包括
install
、activate
和fetch
等事件。它们通常在 Web 应用程序生命周期的开始时注册。 - 浏览器支持: Service Worker 在现代浏览器中得到支持,是创建可靠且引人入胜的 Web 应用程序的关键技术。
30. 如何比较 JavaScript 中的两个 JSON 对象?
a) 比较两个 JSON 对象的一种简单方法是使用 JSON.stringify
将它们转换为字符串,然后比较字符串。
javascript
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)); // 输出:true
b) 你也可以使用 Ramda 库来比较两个 JSON 对象。Ramda 提供了一个名为 equals
的函数来实现此目的。
javascript
const R = require('ramda');
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
console.log(R.equals(obj1, obj2)); // 输出:true
c) 另一种选择是使用 Lodash 等库,它提供了一种方法用于深度比较对象。
javascript
const _ = require('lodash');
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = { a: 1, b: { c: 2 } };
console.log(_.isEqual(obj1, obj2)); // 输出:true
希望你喜欢这篇文章。非常感谢你的阅读。