提升你软件开发职业生涯:JavaScript 常见面试问题解析
将JavaScript纳入您的技能组合,无疑将显著提高您在软件开发领域获得职位的机会。接下来,让我们一同深入探讨一些常见的JavaScript面试问题。
JavaScript是网络开发领域最广泛应用的语言之一。 如今,它几乎能被用于各种应用程序的开发。
在进入面试问题之前,我们首先了解一下学习JavaScript的优势。
JavaScript是一种轻量级的、解释型或者说是即时编译型的编程语言。它构成了万维网的核心支柱之一。您是否了解www的其他两种核心语言?如果您不清楚,建议您自行查找相关信息。
JavaScript最初是为网络环境设计的,但现在其应用范围已远超网络。借助Node.js、Deno等运行时环境,我们几乎可以在任何平台上运行JavaScript代码。
接下来,我们来看看JavaScript的一些主要优点:
JavaScript的优势
- 入门简单:即使您没有任何编程经验,也可以轻松上手学习JavaScript。
- 庞大的社区支持:如果您在学习或使用过程中遇到任何困难,可以从庞大的社区中获得及时的帮助。
- 丰富的库和框架:JavaScript拥有大量可用的库和框架,这些工具可以加速应用程序的开发过程。
- 广泛的应用领域:JavaScript可用于开发前端、后端、Android、iOS等多种应用程序。虽然它在Web开发领域表现尤为出色,但几乎可以用来创建任何类型的应用程序。
JavaScript中的数据类型
数据类型用于存储不同种类的数据。每种编程语言都有其独特的数据类型。在JavaScript中,我们有8种基本的数据类型,接下来逐一介绍:
- 数字 (Number)
- 字符串 (String)
- 布尔值 (Boolean)
- 未定义 (Undefined)
- 空值 (Null)
- 大整数 (BigInt)
- 符号 (Symbol)
- 对象 (Object)
除Object之外的所有数据类型都被称为原始值,这些值是不可变的。
JavaScript中的内置方法
JavaScript的内置方法根据不同的数据类型而有所不同。我们可以通过相应的数据类型来访问这些内置方法。接下来,让我们看一些针对不同数据类型和数据结构的内置方法示例:
- 数字 (Number)
- 字符串 (String)
- toLowerCase()
- startsWith()
- charAt()
- 数组 (Array)
每种数据类型都附带大量的内置方法。您可以通过查阅相关资料来详细了解各种数据类型和数据结构的内置方法。
如何在JavaScript中创建数组?
数组是JavaScript中一种核心的数据结构。由于JavaScript的动态特性,数组可以存储任何类型的数据。接下来,我们将介绍在JavaScript中创建数组的几种方式:
使用方括号[]: 这是创建数组最简洁快速的方法。
// 空数组 const arr = []; // 包含随机值的数组 const randomArr = [1, "One", true]; console.log(arr, randomArr);
使用Array构造函数: 尽管可以使用构造函数创建数组,但在实际项目中并不常用。
// 空数组 const arr = new Array(); // 包含随机值的数组 const randomArr = new Array(1, "One", true); console.log(arr, randomArr);
JavaScript数组是可变的,这意味着我们可以在创建后修改它们。
如何在JavaScript中创建对象?
对象是JavaScript中除了数组之外另一种核心数据结构。对象以键值对的形式存储数据。键必须是不可变的值,而值可以是任何类型的数据。接下来,我们将介绍在JavaScript中创建对象的几种方式:
使用花括号{}: 这是创建对象最简洁快速的方法。
// 空对象 const object = {}; // 包含随机值的对象 const randomObject = { 1: 2, one: "Two", true: false }; console.log(object, randomObject);
使用Object构造函数: 在实际项目中,很少使用Object构造函数来创建对象。
// 空对象 const object = new Object(); // 包含随机值的对象 const randomObject = new Object(); randomObject[1] = 2; randomObject["one"] = "Two"; randomObject[true] = false; console.log(object, randomObject);
JavaScript对象是可变的,这意味着我们可以在创建后修改它们,如第二个例子所示。
如何调试JavaScript代码?
代码调试并非易事。调试方法因编程语言、项目以及具体情况而异。接下来,我们来探讨一些常用的JavaScript调试技巧。
1. 使用日志记录 (Logging)
我们可以在代码中的多个位置使用console.log
语句来追踪错误。当上一行代码存在错误时,代码将停止执行下一行。
日志记录是一种传统的调试方法,对于小型项目非常有效。它是所有编程语言中常用的调试技术。
2. 使用开发者工具 (Developer Tools)
JavaScript主要用于Web应用程序开发,因此,几乎所有浏览器都配备了开发者工具,以协助调试JavaScript代码。
在开发者工具中设置断点是最常用的调试方法之一。断点会暂停JavaScript的执行,并提供当前执行的所有信息。
我们可以在可能出现错误的地方设置多个断点,以找出问题的根源。这是调试JavaScript Web应用程序的最有效方法。
3. 使用集成开发环境 (IDEs)
我们还可以使用IDE来调试JavaScript。例如,VS Code支持断点调试。调试功能可能因您使用的IDE而异,但大多数IDE都提供了此功能。
如何在HTML文件中添加JavaScript代码?
我们可以使用<script>
标签在HTML文件中添加JavaScript代码。以下是一个示例:
<!DOCTYPE html> <html lang="en"> <head> <title>techblik.com</title> </head> <body> <h1>techblik.com</h1> <script> // JavaScript 代码在这里 console.log("这是 JavaScript 代码"); </script> </body> </html>
什么是Cookies?
Cookies是以键值对形式存储少量信息的机制。这些信息可以是任何内容。我们可以设置cookies的过期时间,过期后这些cookies会被自动删除。Cookies广泛用于存储用户相关的信息。
即使刷新页面,Cookies也不会被清除,除非我们手动删除它们或它们过期。您可以在任何浏览器的开发者工具中查看任何网络应用程序/网页的cookies。
如何读取Cookie?
我们可以使用document.cookie
在JavaScript中读取cookie。它会返回我们创建的所有cookie。
console.log("所有 cookies", document.cookie);
如果没有cookie,它将返回一个空字符串。
如何创建和删除Cookie?
我们可以通过将键值对赋值给document.cookie
来创建cookie。让我们看一个例子:
document.cookie = "one=One;";
在上面的示例中,“one”是cookie的键,“One”是它的值。我们可以为cookie添加更多属性,例如域名、路径、过期时间等。每个属性都应该用分号(;)分隔。所有属性都是可选的。
让我们看一个带有属性的例子:
document.cookie = "one=One;expires=Jan 31 2023;path=/;";
在上面的代码中,我们为cookie添加了过期日期和路径。如果没有提供过期日期,cookie将在会话结束后被删除。默认路径为文件路径。过期日期的格式应为GMT。
接下来,让我们看看如何创建多个cookie:
document.cookie = "one=One;expires=Jan 31 2023;path=/;"; document.cookie = "two=Two;expires=Jan 31 2023;path=/;"; document.cookie = "three=Three;expires=Jan 31 2023;path=/;";
如果在设置多个cookie时键或路径不同,则cookie不会被覆盖。如果键和路径相同,则会覆盖之前的cookie。查看下面的示例,它将覆盖之前设置的cookie:
document.cookie = "one=One;expires=Jan 31 2023;path=/;"; document.cookie = "one=Two;path=/;";
我们已经从cookie中删除了过期日期,并修改了它的值。
在测试代码时,请使用未来的过期日期,如果过期日期和当前日期相同,即使在过期日期之后,也不会创建cookie。
我们已经了解了如何创建和更新cookie。接下来,让我们看看如何删除cookie。
删除cookie很简单,只需将cookie的过期日期更改为过去的任何日期。请看以下示例:
// 创建cookies document.cookie = "one=One;expires=Jan 31 2023;path=/;"; document.cookie = "two=Two;expires=Jan 31 2023;path=/;"; document.cookie = "three=Three;expires=Jan 31 2023;path=/;"; // 删除最后一个cookie document.cookie = "three=Three;expires=Jan 1 2023;path=/;";
您将无法在cookies中找到最后一个cookie,因为它已在代码的最后一行被删除。这就是cookie的基本教程。
有哪些不同的JavaScript框架?
JavaScript生态系统中存在大量的框架。用于用户界面开发的React、Vue、Angular等,用于服务器端开发的Express、Koa、Nest等,用于静态站点生成的NextJS、Gatsby等,以及用于移动应用程序开发的React Native、Ionic等。这里我们仅列举了一些JavaScript框架,您可以进一步探索更多框架。在有需要的时候再进行深入研究。
JavaScript中的闭包
闭包是一个函数,它与其词法作用域以及父级词法环境捆绑在一起。利用闭包,我们可以访问外部作用域的数据。闭包在函数被创建时形成。
function outer() { const a = 1; function inner() { // 我们可以在这里访问外部函数作用域的所有数据 // 即使我们在外部函数之外执行此函数, // 数据仍然可用,因为内部函数在创建时形成了闭包 console.log("在内部函数中访问 a", a); } return inner; } const innerFn = outer(); innerFn();
闭包在JavaScript应用程序中广泛使用。您可能在不知不觉中就已经使用过它们。关于闭包,还有很多需要了解的地方,请务必充分掌握这个概念。
JavaScript中的提升
提升是JavaScript中的一个过程,变量、函数和类的声明在代码执行之前会被提升到作用域的顶部。
// 在声明之前访问`name` console.log(name); // 声明并初始化`name` var name = "techblik.com";
如果运行上面的代码,不会看到任何错误。但在大多数编程语言中,你会得到错误。输出将是undefined,因为提升只会将声明移动到顶部,直到第3行才会进行初始化。
请将上面的代码中的var更改为let或const,然后再次运行:
// 在声明之前访问`name` console.log(name); // 声明并初始化`name` const name = "techblik.com";
现在,你会收到一个引用错误,指出我们无法在变量初始化之前访问该变量。
ReferenceError: Cannot access 'name' before initialization
所以,ES6引入了let和const,在变量初始化之前无法访问,否则会抛出错误。这是因为使用let或const声明的变量会存在于临时死区 (TDZ) 中,直到该行被初始化。我们无法从TDZ访问变量。
JavaScript中的柯里化
柯里化是一种将具有多个参数的函数转换为具有多个可调用对象的较少参数的技术。通过柯里化,我们可以将可调用的函数add(a, b, c, d) 转换为可调用的 add(a)(b)(c)(d)。让我们看一个如何实现的例子:
function getCurryCallback(callback) { return function (a) { return function (b) { return function (c) { return function (d) { return callback(a, b, c, d); }; }; }; }; } function add(a, b, c, d) { return a + b + c + d; } const curriedAdd = getCurryCallback(add); // 调用柯里化的函数 console.log(curriedAdd(1)(2)(3)(4));
我们可以将getCurryCallback函数通用化,以便将其用于不同的函数,从而转换为柯里化的函数调用。您可以参考JavaScript Info了解更多详情。
文档(Document)和窗口(Window)的区别
窗口是浏览器中最顶层的对象。它包含有关浏览器窗口的所有信息,例如历史记录、位置、导航器等。在JavaScript中,窗口对象是全局可用的。我们可以在代码中直接使用它,而无需任何导入操作。在不使用window的情况下,我们依然可以访问window对象的属性和方法。
文档是窗口对象的一部分。网页上加载的所有HTML都会被转换为文档对象。文档对象指的是特定的HTMLDocument元素,它和所有的HTML元素一样,具有不同的属性和方法。
简而言之,window对象表示浏览器窗口,而document表示在该浏览器窗口中加载的HTML文档。
客户端(Client-side)和服务器端(Server-side)的区别
客户端指的是使用应用程序的最终用户。服务器端指的是部署应用程序的Web服务器。
在前端术语中,我们可以将用户计算机上的浏览器称为客户端,而将云服务称为服务器端。
innerHTML和innerText的区别
innerHTML和innerText都是HTML元素的属性,我们可以使用这些属性来更改HTML元素的内容。
我们可以将HTML字符串赋值给innerHTML属性,该字符串会像普通的HTML一样被渲染。请看下面的示例:
const titleEl = document.getElementById("title"); titleEl.innerHTML = '<span style="color:orange;">techblik.com</span>';
请在HTML中添加一个id为title的元素,并将上述脚本添加到JavaScript文件中。运行代码并查看输出,您会看到橙色的techblik.com。如果您检查该元素,它会被包含在span标签中。因此,innerHTML会获取HTML字符串并将其渲染为普通的HTML。
另一方面,innerText会获取普通字符串,并按原样渲染。它不会像innerHTML那样渲染任何HTML。请将上面代码中的innerHTML更改为innerText并查看输出:
const titleEl = document.getElementById("title"); titleEl.innerText='<span style="color:orange;">techblik.com</span>';
现在,您会在网页上看到我们提供的确切字符串。
let和var的区别
let和var关键字用于在JavaScript中创建变量。let关键字是在ES6中引入的。
let具有块级作用域,而var具有函数级作用域。
{ let a = 2; console.log("在块内部", a); } console.log("在块外部", a);
运行上述代码,您将在最后一行收到错误消息,因为我们无法访问块外部的let变量a,因为它是块级作用域的。现在,请将let更改为var并再次运行代码:
{ var a = 2; console.log("在块内部", a); } console.log("在块外部", a);
您不会收到任何错误,因为我们也可以访问块外部的变量。接下来,让我们用函数替换块:
function sample() { var a = 2; console.log("在函数内部", a); } sample(); console.log("在函数外部", a);
如果运行上面的代码,会得到一个引用错误,因为我们不能在函数外部访问var变量a,因为它是函数作用域的。
我们可以使用var关键字重新声明变量,但不能使用let关键字重新声明变量。请看下面的示例:
var a = "techblik.com"; var a = "Chandan"; console.log(a);
let a = "techblik.com"; let a = "Chandan"; console.log(a);
第一段代码不会抛出任何错误,变量a的值将更改为最近赋的值。第二段代码将抛出一个错误,因为我们不能使用let重新声明变量。
会话存储(Session Storage)和本地存储(Local Storage)的区别
会话存储和本地存储用于在用户计算机上存储信息,这些信息可以在没有网络的情况下被访问。我们可以在会话存储和本地存储中存储键值对。如果您提供了任何其他数据类型或数据结构,键和值都会转换为字符串。
会话存储在会话结束后(浏览器关闭时)会被清除。本地存储则会一直保存信息,除非我们手动清除。
我们可以分别使用sessionStorage
和localStorage
对象来访问、更新和删除会话存储和本地存储。
JavaScript中的NaN是什么?
NaN是Not-a-Number(非数字)的缩写,它表示JavaScript中某个值不是合法的数字。在某些情况下,我们会得到NaN作为输出,例如0/0、undefined * 2、1 + undefined、null * undefined等。
什么是词法作用域?
词法作用域是指从其父作用域访问变量的能力。假设我们有一个函数,它包含两个内部函数。最内层的函数可以访问其两个父函数的作用域变量。同样,第二个函数可以访问最外层函数的作用域变量。让我们看一个例子:
function outermost() { let a = 1; console.log(a); function middle() { let b = 2; // `a`在这里可访问 console.log(a, b); function innermost() { let c = 3; // `a`和`b`在这里都可访问 console.log(a, b, c); } innermost(); } middle(); } outermost();
当我们在代码中的某处访问变量时,JavaScript会使用作用域链来查找变量。首先,它会检查当前作用域中的变量,然后是父作用域,直到全局作用域。
什么是按值传递和按引用传递?
按值传递和按引用传递是在JavaScript中将参数传递给函数的两种方式。
按值传递:它会创建原始数据的副本并将其传递给函数。因此,当我们在函数内部修改该值时,不会影响原始数据。请看以下示例:
function sample(a) { // 修改 `a` 的值 a = 5; console.log("函数内部", a); } let a = 3; sample(a); console.log("函数外部", a);
您会看到a的原始值没有改变,即使我们在函数内部修改了它。
按引用传递:它会将数据的引用传递给函数。因此,当我们在函数内部修改该值时,原始数据也会被修改。
function sample(arr) { // 向数组添加一个新值 arr.push(3); console.log("函数内部", arr); } let arr = [1, 2]; sample(arr); console.log("函数外部", arr);
您会看到,当我们在函数内部修改数组时,arr的原始值也发生了变化。
注意:所有原始数据类型都是按值传递,而非原始数据类型是按引用传递的。
什么是记忆化?
记忆化是一种将计算结果存储在缓存中,并在再次需要它们时直接使用缓存结果,而无需再次计算它们的技术。如果计算量非常大,记忆化将加快代码的执行速度。虽然这会占用一定的存储空间,但与时间相比,这不是一个大问题。
const memo = {}; function add(a, b) { const key = `${a}-${b}`; // 检查是否已计算过该值 if (memo[key]) { console.log("不再重新计算"); return memo[key]; } // 将新计算的值添加到缓存 // 在这里,缓存是一个简单的全局对象 memo[key] = a + b; return memo[key]; } console.log(add(1, 2)); console.log(add(2, 3)); console.log(add(1, 2));
这是一个演示记忆化的简单示例。在此示例中,将两个数字相加并不是一项计算量大的操作,只是为了演示目的。
什么是剩余参数?
剩余参数用于收集函数中所有剩余的参数。假设我们有一个函数,它至少接受2个参数,并且最多可以接受任意数量的参数。由于我们没有参数的最大数量,我们可以使用普通变量收集前2个参数,并使用剩余参数收集所有其他参数。
function sample(a, b, ...rest) { console.log("剩余参数", rest); } sample(1, 2, 3, 4, 5);
在上面的示例中,剩余参数将是最后三个参数的数组。有了它,我们可以为一个函数设置任意数量的参数。
一个函数只能有一个剩余参数,并且剩余参数必须位于参数顺序的最后一位。
什么是对象解构?
对象解构用于从对象中访问变量,并将它们赋值给与对象键同名的变量。让我们看一个例子:
const object = { a: 1, b: 2, c: 3 }; // 对象解构 const { a, b, c } = object; // 现在,a,b,c可以像普通变量一样使用 console.log(a, b, c);
我们可以在同一行中更改解构变量的名称,如下所示:
const object = { a: 1, b: 2, c: 3 }; // 更改 `a` 和 `b` 的名称 const { a: changedA, b: changedB, c } = object; // 现在,changedA、changedB和c可以像普通变量一样使用 console.log(changedA, changedB, c);
什么是数组解构?
数组解构用于从数组中访问变量,并将它们赋值给变量。让我们看一个例子:
const array = [1, 2, 3]; // 数组解构 // 它基于数组的索引 const [a, b, c] = array; // 现在,我们可以像普通变量一样使用a,b,c console.log(a, b, c);
什么是事件捕获和事件冒泡?
事件捕获和事件冒泡是HTML DOM中事件传播的两种方式。假设有两个HTML元素,一个嵌套在另一个元素内部。如果内部元素触发一个事件,那么事件传播模式将决定这些事件的执行顺序。
事件冒泡:它会首先在目标元素上运行事件处理程序,然后是它的父元素,一直运行到最顶层的元素。这是所有事件的默认行为。
事件捕获:我们需要在事件中指定我们需要使用这种类型的事件传播。我们可以在添加事件侦听器时指定。如果我们启用了事件捕获,事件将按照以下顺序执行:
- 事件从最顶层的元素开始执行,向下直到目标元素。
- 目标元素上的事件将再次执行。
- 冒泡事件传播将再次发生,直至最顶层元素开始。
我们可以通过在事件处理程序中调用event.stopPropagation
方法来停止事件传播。
JavaScript中的Promise是什么?
Promise对象用于处理异步操作,这些操作将在未来以成功或失败状态完成。
Promise可以处于以下状态之一:
- pending(等待中)——操作仍在进行中。
- fulfilled(已完成)——当操作成功完成时。我们将在成功状态下获得结果(如果有)。
- rejected(已拒绝)——当操作失败完成时。我们会知道它失败的原因(错误)。
接下来,我们看两个成功和失败案例的示例:
// 将成功完成的 Promise const successPromise = new Promise((resolve, reject) => { setTimeout(() => { resolve({ message: "成功完成" }); }, 300); }); successPromise .then((data) => { console.log(data); }) .catch((error) => { console.log(error); }); // 将以失败状态完成的 Promise const failurePromise = new Promise((resolve, reject) => { setTimeout(() => { reject(new Error("为了测试,Promise失败")); }, 300); }); failurePromise .then((data) => { console.log(data); }) .catch((error) => { console.log(error); });
您可以根据需要链接多个then。前一个then返回的数据将在下一个then回调中被接收。
解释JavaScript中不同类型的作用域
JavaScript中有两种类型的作用域:全局作用域和局部作用域。
您可能还听说过函数作用域和块作用域。它们分别是var和let/const的局部作用域。
什么是自调用函数?
自调用函数是匿名函数,在创建后会立即执行。让我们看一些例子:
// 不带任何参数 (function sayHello() { console.log("你好,世界!"); })(); // 带参数 (function add(a, b) { console.log("和", a + b); })(1, 2);
我们甚至可以将参数传递给您在示例中看到的自调用函数。
什么是箭头函数?
箭头函数是普通函数的语法糖,但有一些变化。它们在一般用例中表现得像普通函数。当我们必须使用回调时,箭头函数就派上用场了。接下来让我们看一下它的语法:
// 箭头函数如果没有大括号,默认返回 let add = (a, b) => a + b; console.log(add(1, 2));
箭头函数和普通函数之间存在一些差异:
- 箭头函数没有自己的this绑定。箭头函数中的this关键字引用其父作用域的this。
- 箭头函数不能用作构造函数。
什么是回调?
回调是传递给另一个函数并在该函数内部调用的函数。在JavaScript中使用回调非常常见。让我们看一个例子:
function sample(a, b, callback) { const result = a + b; callback(result); } function finished(result) { console.log("已完成,结果为", result); } sample(1, 2, finished);
在这里,finished函数作为回调传递给sample。finished函数在执行一些操作后使用结果被调用。您会看到回调主要用于异步操作,例如Promise、setTimeout等。
有哪些不同类型的错误?
接下来我们检查一下JavaScript中的一些错误类型:
ReferenceError:当访问的变量不存在时会发生此错误。
TypeError:当错误与其他类型的错误不匹配时,JavaScript会抛出此错误。当我们尝试执行与数据类型不兼容的操作时,也会发生这种情况。
SyntaxError:如果JavaScript语法不正确,则会发生此错误。
还有其他类型的错误,但这些是JavaScript中常见的错误类型。
JavaScript中变量的不同作用域是什么?
JavaScript中有两种变量作用域。使用var关键字声明的变量将具有函数作用域,而使用let和const声明的变量将具有块级作用域。
关于这些变量作用域的更多详细信息,请参考第17个问题。
什么是JavaScript中的转义字符?
反斜杠是JavaScript中的转义字符。它用来打印一些我们通常无法直接打印的特殊字符。例如,如果我们想在一个字符串中打印撇号(‘),但通常情况下,字符串会在第二个撇号处结束。在这种情况下,我们将使用转义字符来避免字符串在这一点结束。
const message="Hi, I'm techblik.com"; console.log(message);
我们可以在不使用转义字符的情况下,通过将外部单撇号替换为双撇号来实现上述输出。但这只是一个如何使用转义字符的示例。还有其他一些字符我们绝对需要使用转义字符,例如\n、\t等。
什么是BOM和DOM?
浏览器对象模型 (BOM):所有浏览器都有一个BOM来代表当前浏览器窗口。它包含我们最顶层的窗口对象,用于操作浏览器窗口。
文档对象模型 (DOM):当浏览器在树结构中加载HTML时,会创建DOM。我们可以使用DOM API来操作HTML元素。
什么是屏幕对象?
屏幕对象是全局窗口对象的属性之一。它包含表示当前浏览器窗口的屏幕的各种属性。一些属性包括宽度、高度、方向、pixelDepth等。
结论
上述所有问题都可能会引发后续问题。因此,您需要围绕上述所有问题准备概念。
您还可以探索一些常见的Java面试问题和答案。
祝您学习愉快!🙂