导读:在本系列文章中,我整理了大量常见前端面试问题,以及详细的答案和代码示例。本文是本系列文章第一篇。
===========================================================
1、 JavaScript是单线程的吗?
是的,JavaScript是一种单线程语言。这意味着它只有一个调用栈和一个内存堆。一次只能执行一组指令。
此外,JavaScript在本质上是同步和阻塞的,这意味着代码是按行执行的,一个任务必须在下一个任务开始之前完成。
然而,JavaScript也有异步能力,允许某些操作独立于主执行线程执行。这通常通过回调、Promise、async/await和事件监听器等机制来实现。这些异步功能使JavaScript能够处理诸如获取数据、处理用户输入和执行I/O操作等任务,而不会阻塞主线程,使其适合构建响应式和交互式Web应用程序。
2、请介绍JavaScript引擎的主要构成以及引擎是如何工作的。
每个浏览器都有一个 JavaScript 引擎,用于执行 JavaScript 代码并将其转换为机器代码。
当 JavaScript 代码被执行时,解析器(Parser)首先读取代码并生成一个 AST(抽象语法树),并将其存储在内存中。然后解释器(Interpreter)处理这个 AST 并生成字节码或机器代码,由计算机执行。
分析器(Profiler)是 JavaScript 引擎的一个组件,用于监控代码的执行。字节码与性能分析数据一起被优化编译器使用。“优化编译器”或即时 (JIT) 编译器基于性能分析数据做出某些假设,并生成高度优化的机器代码。
有时候,存在“优化”假设错误的情况,然后它会通过“去优化”(Deoptimization)阶段回到之前版本(有负面开销)。JS 引擎通常优化“热函数”,并使用内联缓存技术优化代码。
在此过程中,调用栈跟踪当前正在执行的功能,内存堆用于内存分配。最后,垃圾回收器发挥作用,通过从未使用对象中回收内存来管理内存。
Google Chrome v8 引擎里的“术语”:
-
解释器(Interpreter)被称为“Ignition”。
-
优化编译器(Optimizing)被称为“TurboFan”。
-
除了解析器,还有一个“预解析器”用于检查语法和令牌。
-
“Sparkplug”,位于“Ignition”和“TurboFan”之间,也称为快速编译器。
(注:对V8原理有兴趣的同学可通过这门课系统学习:图解谷歌V8;对浏览器工作原理有兴趣的同学可通过这门课系统学习:浏览器工作原理与实践。)
3、请介绍JavaScript的事件循环并讲解它对异步编程的意义。
事件循环是JavaScript运行时环境的核心组件。它负责调度和执行异步任务。事件循环通过不断监视两个队列来工作:调用栈和事件队列(包括微任务队列和宏任务队列)。
调用栈是一种栈(LIFO)数据结构,用于存储当前正在执行的功能(存储代码执行期间创建的执行上下文)。
setTimeout、fetch 请求、Promise等Web API 负责触发异步操作及其处理回调。它们从线程池“借用”线程,在后台完成任务,而不会阻塞主线程。 这些异步操作被保存在微任务队列和宏任务队列里。
微任务队列是一个 FIFO(先进先出)结构,用于存储 async/await、Promise、process.nextTick() 的回调。例如,一个已完成的 Promise 的 resolve 或 reject 回调会被排入微任务队列。
宏任务队列也是一个FIFO(先进先出)结构,用于存储setInterval、setTimeout、requestAnimationFrame的回调。
事件循环永久性地监视调用栈是否为空。如果调用栈为空,事件循环会查看两个任务队列。首先从宏任务队列取出一个任务执行,然后如果微任务里有任务,则先把整个微任务队列执行完,然后再检查宏任务队列的下个任务。
4、请解释下JavaScript里的“提升”概念?
提升(hoisting)是一种 JavaScript 机制,其中变量和函数声明在代码执行之前被移动到其作用域的顶部。这意味着,如果我们这样做:
console.log (greeter);
var greeter = "say hello"
它被解释为:
var greeter;
console.log(greeter); // greeter is undefined
greeter = "say hello"
即 greeter 变量的声明被提升到其作用域的顶部,并使用 undefined 值进行初始化。
5、请讲下var、let和const的差异?
首先讲下JavaScript里的作用域。在ES6之前,JavaScript里只有全局作用域和函数作用域,没有块级作用域。ES6中新增了块级作用域。块作用域由 { } 包括,if语句和for语句里面的{ }也属于块作用域。
| var | let | const | |
|---|---|---|---|
| 作用域 | var 声明的作用域是全局的或函数/局部的。当 var 变量在函数外部声明时,作用域是全局的。这意味着在函数体外用 var 声明的任何变量都可以在整个窗口中使用。var 在函数中声明时,它的作用域是在函数体内。这意味着它只能在该函数中被访问。 | let 是块作用域。块是由 {} 界定的代码块。一个块存在于花括号中。花括号内的任何内容都是一个块。因此,在带有 let 的块中声明的变量只能在该块中使用。 | const 声明是块作用域。与 let 声明一样,const 声明只能在它们声明的块内访问。 |
| 提升(hoisting) | 提升到其作用域的顶部,并使用 undefined 值进行初始化。 | 就像 var 一样,let 声明被提升到顶部。但与初始化为 undefined 的 var 不同,let 关键字未初始化。所以如果你在声明之前尝试使用 let 变量,你会得到一个 Reference Error。 | 就像 let 一样,const 声明被提升到顶部但没有被初始化。 |
| 可变性 | 可更新、可在同作用域重复声明。 | 可更新、不可在同作用域重复声明。 | 不可更新、不可在同作用域重复声明。 |
6、JavaScript有哪些数据类型?
JavaScript是一种动态且松散类型(或称为鸭子类型)的语言。这意味着我们不需要指定变量的类型,因为JavaScript引擎会根据变量的值动态确定数据类型。
除了 Object 以外,其它类型都定义了表示在语言最低层面的不可变值。我们将这些值称为原始值。这些类型如下:
| 类型 | typeof 返回值 | 对象包装器 |
|---|---|---|
| Null | "object" | 不适用 |
| Undefined | "undefined" | 不适用 |
| Boolean | "boolean" | Boolean |
| Number | "number" | Number |
| BigInt | "bigint" | BigInt |
| String | "string" | String |
| Symbol | "symbol" | Symbol |
除了 null 和 undefined,所有原始类型都有它们相应的对象包装类型,这为处理原始值提供可用的方法。例如,Number 对象提供像 toExponential() 这样的方法。当在原始值上访问属性时,JavaScript 会自动将值包装到相应的包装对象中,并访问对象上的属性。然而,在 null 或 undefined 上访问属性时,会抛出 TypeError 异常。
Null 类型只有一个值:null。Undefined 类型只有一个值:undefined。从概念上讲,undefined 表示值的缺失,null 表示对象的缺失(这也可以说明 typeof null === "object" 的原因)。
在计算机科学中,对象(object)是指内存中的可以被标识符引用的一块区域。在 JavaScript 中,object是唯一可变的值。事实上,JavaScript里函数本质上也是对象。
7、什么是回调函数和回调地狱?
在JavaScript中,回调通常用于处理异步操作。回调函数是一个作为另一个函数的参数传递的函数,并计划在特定任务完成后或在给定时间执行。
function fetchData(url, callback) {
// Simulate fetching data from a server
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模拟)获取数据后,它调用回调函数并将检索到的数据传递给它。
回调地狱(Callback Hell),是在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 {
// Continue with more nested callbacks...
}
});
}
});
}
});
在如上示例中,我们使用fs.readFile函数按顺序读取三个文件,每个文件读取操作都是异步的。因此,我们必须将回调相互嵌套,从而创建一个回调金字塔结构。
为避免回调地狱,现代JavaScript提供了Promises和async/await等替代方案。 示例代码如下:
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) => {
// Continue with more promise-based code...
})
.catch((err) => {
console.error(err);
});
8、请介绍下Promise?
Promise:Promise 是 JavaScript 中用于异步计算的对象。它表示异步操作的结果,该结果可能被解析或拒绝。
Promise 有三种状态:
-
Pending:初始状态。这是 Promise 的最终值尚不可用的状态。
-
Fulfilled:已成功解析 Promise 的状态,并且最终值现在可用。
-
Rejected:Promise 遇到错误或被拒绝的状态,并且最终值无法提供。
Promise 构造函数有两个参数(resolve,reject),它们都是函数。如果异步任务已完成且没有错误,则调用 resolve 函数并以消息或获取的数据来解决 Promise。如果发生错误,则调用 reject 函数并将错误传递给它。我们可以使用 .then() 处理程序访问 Promise 的结果、使用 .catch() 处理程序捕获错误。
// Creating a Promise
const fetchData = new Promise((resolve, reject) => {
// Simulate fetching data from a server
setTimeout(() => {
const data = 'Some data from the server';
// Resolve the Promise with the retrieved data
resolve(data);
// Reject the Promise with an error
// reject(new Error('Failed to fetch data'));
}, 1000);
});
// Consuming the Promise
fetchData
.then((data) => {
console.log('Data fetched:', data);
})
.catch((error) => {
console.error('Error fetching data:', error);
});
9、请介绍下async/await?
Async/await 是 JavaScript 中处理异步代码的一种现代方法。它提供了一种更简洁、更易读的方式来处理 Promises 和异步操作,有效地避免了“回调地狱”并改进了异步代码的整体结构。
在 JavaScript 中,async 关键字用于定义一个异步函数,该函数返回一个 Promise。
在一个 async 函数中,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;
}
}
// Using the async function
fetchData()
.then((jsonData) => {
// Handle the retrieved data
})
.catch((error) => {
// Handle errors
});
在这个例子中,fetchData 函数被定义为一个 async 函数,它使用 await 关键字来暂停执行并等待 fetch 和 json 操作,从而有效地以类似于同步代码的方式处理 Promises。
(备注:这个题目还可以引申出“请讲下async/await原理”问题。)
10、== 和 === 操作符的差别?
==(松散相等操作符):此操作符执行类型强制转换,这意味着它在进行比较之前将操作数转换为相同的类型。它检查值是否相等,而不考虑它们的数据类型。例如,1 == '1' 将返回 true,因为 JavaScript 在比较之前将字符串 '1' 转换为数字。
===(严格相等操作符):此操作符执行严格比较而不进行类型强制转换。它检查值及其数据类型是否相等。例如,1 === '1' 将返回 false,因为数据类型不同(数字和字符串)。
总之,== 在类型强制转换后检查相等性,而 === 检查严格相等性,同时考虑值及其数据类型。
以下是一些涵盖上述情况的示例:
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
11、Javascript 里有哪些创建对象的方法?
在JavaScript中,有多种创建对象的方法。一些常见的对象创建方法包括:
a) 对象字面量:创建对象的最直接方法是使用对象字面量,它使用大括号包围的逗号分隔列表定义对象的属性和方法。
let person = {
firstName: 'John',
lastName: 'Doe',
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('John', 'Doe');
let person2 = new Person('Jane', 'Smith');
c) Object.create():Object.create()方法允许你使用指定的原型对象创建新对象。此方法为新创建的对象提供更多的原型控制。
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关键字定义对象的类语法。这为创建对象和定义它们的属性和方法提供了更熟悉和结构化的方式。
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) 工厂函数:工厂函数是返回对象的函数。这种方法允许你封装对象创建过程并轻松创建具有自定义属性的多个实例。
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()方法可用于设置指定对象的原型。这为在创建对象后设置其原型提供了另一种方法。
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()方法可用于通过从一个或多个源对象复制所有可枚举自有属性的值到目标对象来创建新对象。这对于合并对象或创建浅拷贝非常有用。
let target = { a: 1, b: 2 };
let source = { b: 3, c: 4 };
let mergedObject = Object.assign({}, target, source);
h) 原型继承:JavaScript使用原型继承,允许对象从其他对象继承属性和方法。你可以使用原型继承并使用方法或构造函数的prototype属性定义共享行为来创建对象。
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) 单例模式:单例模式用于将对象限制为单个实例。在JavaScript中,可以使用闭包和立即调用的函数表达式(IIFE)的组合实现单例模式。这样可以确保仅创建一个对象实例。
let singleton = (() => {
let instance;
function createInstance() {
return {
// properties and methods
};
}
return {
getInstance: () => {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
12、介绍下...操作符?
剩余操作符(rest operator),由三个点(...)表示,用于函数参数中以将可变数量的参数收集到数组中。它允许你将任意数量的参数传递给函数,而无需显式地将它们定义为命名参数。
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 is [1, 2, 3, 4, 5, 6]
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const mergedObject = { ...obj1, ...obj2 };
// mergedObject is { a: 1, b: 3, c: 4 }
13、什么是事件流、事件冒泡、事件捕获和事件委托?
在JavaScript中,事件流是指在网页上接收诸如点击或按键等事件或由Web浏览器处理的顺序。事件流有两个阶段:事件捕获和事件冒泡。当你点击一个嵌套在其他元素中的元素时,在点击实际到达目标元素之前,它必须首先为其每个父元素触发点击事件,从全局窗口对象开始。
<div id="parent">
<button id="child">Click me!</button>
</div>
现在,让我们用上面的代码示例来解释事件流:
-
事件捕获阶段:当你点击按钮时,事件从顶部(文档的根)开始,向下移动到目标元素。在这种情况下,它从文档的根移动到
(父元素),然后到(子元素)。这就是所谓的捕获阶段。 -
事件目标阶段:事件到达目标元素,这里是。
-
事件冒泡阶段:在到达目标后,事件开始冒泡。它从回到
,最后到达文档的根。这就是所谓的冒泡阶段。
以下是一个简单的JavaScript代码片段,可以看到这个动作:
document.getElementById('parent').addEventListener('click', function() {
console.log('Div clicked (capturing phase)');
}, true); // The 'true' here indicates capturing phase.
document.getElementById('child').addEventListener('click', function() {
console.log('Button clicked (target phase)');
});
document.getElementById('parent').addEventListener('click', function() {
console.log('Div clicked (bubbling phase)');
});
当你点击按钮时,你会在控制台中按照以下顺序看到这些消息:
1、 “Div clicked (capturing phase)”
2、 “Button clicked (target phase)”
3、 “Div clicked (bubbling phase)”
事件委托是一种JavaScript编程技术,它优化了多个元素的事件处理。事件委托不是将事件监听器附加到每个单独的元素上,而是将单个事件监听器附加到DOM(文档对象模型)层次结构中更高的通用祖先元素上。当其中一个后代元素上发生事件时,它会“冒泡”到通用祖先元素,在那里等待事件监听器。事件委托是一种倾听事件的技术,你将父元素作为发生在其中的所有事件的监听者进行委托。
var form = document.querySelector("#registration-form");
// Listen for changes to fields inside the form
form.addEventListener(
"input",
function (event) {
// Log the field that was changed
console.log(event.target);
},
false
);
Note:本文主体翻译自《Top 30 JavaScript Interview Questions and Answers for 2024》,笔者完善、补充了部分内容。
===========================================================
【自我介绍】
大家好,我是意滔,大厂资深工程师,擅长前端开发和C++开发,是一名终身学习者。我每周都会更新技术文章,⭐️欢迎关注⭐️,一起成长~
【⭐️优质资料推荐⭐️】
想系统学习前端知识的话,笔者推荐以下课程:
● 对V8原理有兴趣的同学可学习:图解谷歌V8;
● 对浏览器工作原理有兴趣的同学可学习:浏览器工作原理与实践;
● 对React实践有兴趣的同学可学习:现代 React Web 开发实战;
● 对React Hooks原理有兴趣的同学可学习:React Hooks核心原理与实战;
● 对设计模式有兴趣的同学可学习:设计模式之美。