关于高级前端的 JavaScript 面试题
myzbx 2025-01-09 14:47 17 浏览
JavaScript 是一种功能强大的语言,是网络的主要构建块之一。这种强大的语言也有一些怪癖。例如,您是否知道 0 === -0 的计算结果为 true,或者 Number("") 的结果为 0?
问题是,有时这些怪癖会让你摸不着头脑,甚至质疑 Brendon Eich 发明 JavaScript 的那一天。好吧,重点不在于 JavaScript 是一种糟糕的编程语言,或者像它的批评者所说的那样它是邪恶的。所有编程语言都有某种与之相关的奇怪之处,JavaScript 也不例外。
因此,在今天这篇文章中,我们将会看到一些重要的 JavaScript 面试问题的深入解释。我的目标是彻底解释这些面试问题,以便我们能够理解基本概念,并希望在面试中解决其他类似问题。
1、仔细观察 + 和 - 运算符
console.log(1 + '1' - 1);
您能猜出 JavaScript 的 + 和 - 运算符在上述情况下的行为吗?
当 JavaScript 遇到 1 + '1' 时,它会使用 + 运算符处理表达式。+ 运算符的一个有趣的属性是,当操作数之一是字符串时,它更喜欢字符串连接。在我们的例子中,“1”是一个字符串,因此 JavaScript 隐式地将数值 1 强制转换为字符串。因此,1 + '1' 变为 '1' + '1',结果是字符串 '11'。
现在,我们的等式是 '11' - 1。- 运算符的行为恰恰相反。无论操作数的类型如何,它都会优先考虑数字减法。当操作数不是数字类型时,JavaScript 会执行隐式强制转换,将其转换为数字。在本例中,“11”被转换为数值 11,并且表达式简化为 11 - 1。
把它们放在一起:
'11' - 1 = 11 - 1 = 10
2、复制数组元素
考虑以下 JavaScript 代码并尝试查找此代码中的任何问题:
function duplicate(array) {
for (var i = 0; i < array.length; i++) {
array.push(array[i]);
}
return array;
}
const arr = [1, 2, 3];
const newArr = duplicate(arr);
console.log(newArr);
在此代码片段中,我们需要创建一个包含输入数组的重复元素的新数组。初步检查后,代码似乎通过复制原始数组 arr 中的每个元素来创建一个新数组 newArr。然而,重复函数本身出现了一个关键问题。
重复函数使用循环来遍历给定数组中的每个项目。但在循环内部,它使用 push() 方法在数组末尾添加一个新元素。这使得数组每次都变得更长,从而产生循环永远不会停止的问题。循环条件 (i < array.length) 始终保持为 true,因为数组不断变大。这使得循环永远持续下去,导致程序卡住。
为了解决数组长度不断增长导致无限循环的问题,可以在进入循环之前将数组的初始长度存储在变量中。
然后,您可以使用该初始长度作为循环迭代的限制。这样,循环将仅针对数组中的原始元素运行,并且不会因添加重复项而受到数组增长的影响。这是代码的修改版本:
function duplicate(array) {
var initialLength = array.length; // Store the initial length
for (var i = 0; i < initialLength; i++) {
array.push(array[i]); // Push a duplicate of each element
}
return array;
}
const arr = [1, 2, 3];
const newArr = duplicate(arr);
console.log(newArr);
输出将显示数组末尾的重复元素,并且循环不会导致无限循环:
[1, 2, 3, 1, 2, 3]
3、原型和__proto__之间的区别
原型属性是与 JavaScript 中的构造函数相关的属性。构造函数用于在 JavaScript 中创建对象。定义构造函数时,还可以将属性和方法附加到其原型属性。
然后,从该构造函数创建的对象的所有实例都可以访问这些属性和方法。因此,prototype 属性充当在实例之间共享的方法和属性的公共存储库。
考虑以下代码片段:
// Constructor function
function Person(name) {
this.name = name;
}
// Adding a method to the prototype
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}.`);
};
// Creating instances
const person1 = new Person("Haider Wain");
const person2 = new Person("Omer Asif");
// Calling the shared method
person1.sayHello(); // Output: Hello, my name is Haider Wain.
person2.sayHello(); // Output: Hello, my name is Omer Asif.
在此示例中,我们有一个名为 Person 的构造函数。通过使用 sayHello 之类的方法扩展 Person.prototype,我们将此方法添加到所有 Person 实例的原型链中。这允许 Person 的每个实例访问和利用共享方法。而不是每个实例都有自己的方法副本。
另一方面, __proto__ 属性(通常发音为“dunder proto”)存在于每个 JavaScript 对象中。在 JavaScript 中,除了原始类型之外,所有东西都可以被视为对象。这些对象中的每一个都有一个原型,用作对另一个对象的引用。__proto__ 属性只是对此原型对象的引用。当原始对象不具备属性和方法时,原型对象用作属性和方法的后备源。默认情况下,当您创建对象时,其原型设置为 Object.prototype。
当您尝试访问对象的属性或方法时,JavaScript 会遵循查找过程来查找它。这个过程涉及两个主要步骤:
对象自己的属性:JavaScript 首先检查对象本身是否直接拥有所需的属性或方法。如果在对象中找到该属性,则直接访问和使用它。
原型链查找:如果在对象本身中找不到该属性,JavaScript 将查看该对象的原型(由 __proto__ 属性引用)并在那里搜索该属性。此过程在原型链上递归地继续,直到找到属性或查找到达 Object.prototype。
如果即使在 Object.prototype 中也找不到该属性,JavaScript 将返回 undefined,表明该属性不存在。
4、范围
编写 JavaScript 代码时,理解作用域的概念很重要。范围是指代码不同部分中变量的可访问性或可见性。在继续该示例之前,如果您不熟悉提升以及 JavaScript 代码的执行方式,可以从此链接了解它。这将帮助您更详细地了解 JavaScript 代码的工作原理。
让我们仔细看看代码片段:
function foo() {
console.log(a);
}
function bar() {
var a = 3;
foo();
}
var a = 5;
bar();
该代码定义了 2 个函数 foo() 和 bar() 以及一个值为 5 的变量 a。所有这些声明都发生在全局范围内。在 bar() 函数内部,声明了一个变量 a 并赋值为 3。那么当调用 thebar() 函数时,你认为它会打印 a 的值是多少?
当 JavaScript 引擎执行此代码时,声明全局变量 a 并为其赋值 5。然后,调用 bar() 函数。在 bar() 函数内部,声明了一个局部变量 a 并赋值为 3。该局部变量 a 与全局变量 a 不同。之后,从 bar() 函数内部调用 foo() 函数。
在 foo() 函数内部,console.log(a) 语句尝试记录 a 的值。由于 foo() 函数的作用域内没有定义局部变量 a,JavaScript 会查找作用域链以找到最近的名为 a 的变量。作用域链是指函数在尝试查找和使用变量时可以访问的所有不同作用域。
现在,我们来解决 JavaScript 将在哪里搜索变量 a 的问题。它会在 bar 函数的范围内查找,还是会探索全局范围?事实证明,JavaScript 将在全局范围内进行搜索,而这种行为是由称为词法范围的概念驱动的。
词法作用域是指函数或变量在代码中编写时的作用域。当我们定义 foo 函数时,它被授予访问其自己的本地作用域和全局作用域的权限。无论我们在哪里调用 foo 函数,无论是在 bar 函数内部还是将其导出到另一个模块并在那里运行,这个特征都保持一致。词法范围不是由我们调用函数的位置决定的。
这样做的结果是输出始终相同:在全局范围内找到的 a 值,在本例中为 5。
但是,如果我们在 bar 函数中定义了 foo 函数,则会出现不同的情况:
function bar() {
var a = 3;
function foo() {
console.log(a);
}
foo();
}
var a = 5;
bar();
在这种情况下, foo 的词法作用域将包含三个不同的作用域:它自己的局部作用域、 bar 函数的作用域和全局作用域。词法范围由编译时将代码放置在源代码中的位置决定。
当此代码运行时,foo 位于 bar 函数内。这种安排改变了范围动态。现在,当 foo 尝试访问变量 a 时,它将首先在其自己的本地范围内进行搜索。由于它在那里找不到 a,因此它将搜索范围扩大到 bar 函数的范围。你瞧,a 存在,其值为 3。因此,控制台语句将打印 3。
5、对象强制
const obj = {
valueOf: () => 42,
toString: () => 27
};
console.log(obj + '');
值得探索的一个有趣的方面是 JavaScript 如何处理对象到原始值(例如字符串、数字或布尔值)的转换。这是一个有趣的问题,测试您是否知道强制转换如何与对象一起使用。
在字符串连接或算术运算等场景中处理对象时,这种转换至关重要。为了实现这一点,JavaScript 依赖于两个特殊的方法:valueOf 和 toString。
valueOf 方法是 JavaScript 对象转换机制的基本部分。当在需要原始值的上下文中使用对象时,JavaScript 首先在对象中查找 valueOf 方法。
如果 valueOf 方法不存在或未返回适当的原始值,JavaScript 将回退到 toString 方法。该方法负责提供对象的字符串表示形式。
回到我们原来的代码片段:
const obj = {
valueOf: () => 42,
toString: () => 27
};
console.log(obj + '');
当我们运行此代码时,对象 obj 被转换为原始值。在本例中,valueOf 方法返回 42,然后,由于与空字符串连接而隐式转换为字符串。因此,代码的输出将为 42。
但是,如果 valueOf 方法不存在或未返回适当的原始值,JavaScript 将回退到 toString 方法。让我们修改一下之前的例子:
const obj = {
toString: () => 27
};
console.log(obj + '');
这里,我们删除了 valueOf 方法,只留下 toString 方法,该方法返回数字 27。在这种情况下,JavaScript 将诉诸 toString 方法进行对象转换。
6、理解对象键
在 JavaScript 中使用对象时,了解如何在其他对象的上下文中处理和分配键非常重要。考虑以下代码片段并花一些时间猜测输出:
let a = {};
let b = { key: 'test' };
let c = { key: 'test' };
a[b] = '123';
a[c] = '456';
console.log(a);
乍一看,这段代码似乎应该生成一个具有两个不同键值对的对象 a。然而,由于 JavaScript 对对象键的处理方式,结果完全不同。
JavaScript 使用默认的 toString() 方法将对象键转换为字符串。但为什么?在 JavaScript 中,对象键始终是字符串(或符号),或者它们通过隐式强制转换自动转换为字符串。当您使用字符串以外的任何值(例如数字、对象或符号)作为对象中的键时,JavaScript 会在将该值用作键之前在内部将该值转换为其字符串表示形式。
因此,当我们使用对象 b 和 c 作为对象 a 中的键时,两者都会转换为相同的字符串表示形式:[object Object]。由于这种行为,第二个赋值 a[b] = '123'; 将覆盖第一个赋值 a[c] = '456';。
现在,让我们逐步分解代码:
- let a = {};:初始化一个空对象a。
- let b = { key: 'test' };: 创建一个对象 b,其属性键值为 'test'。
- let c = { key: 'test' };: 定义另一个与 b 结构相同的对象 c。
- a[b] = '123';:将对象a中键为[object Object]的属性设置为值'123'。
- a[c] = '456';:将对象 a 中键 [object Object] 相同属性的值更新为 '456',替换之前的值。
两个分配都使用相同的键字符串 [object Object]。结果,第二个赋值会覆盖第一个赋值设置的值。
当我们记录对象 a 时,我们观察到以下输出:
{ '[object Object]': '456' }
7、==运算符
console.log([] == ![]);
这个有点复杂。那么,您认为输出会是什么?让我们逐步评估一下。让我们首先看看两个操作数的类型:
typeof([]) // "object"
typeof(![]) // "boolean"
对于[]来说它是一个对象,这是可以理解的。JavaScript 中的一切都是对象,包括数组和函数。但是操作数![]如何具有布尔类型呢?让我们试着理解这一点。当你使用 ! 对于原始值,会发生以下转换:
假值:如果原始值是假值(例如 false、0、null、undefined、NaN 或空字符串 ''),则应用 ! 会将其转换为 true。
真值:如果原始值是真值(任何非假值),则应用!会将其转换为 false。
在我们的例子中,[] 是一个空数组,它是 JavaScript 中的真值。由于 [] 为真,所以 ![] 变为假。所以,我们的表达式就变成了:
[] == ![]
[] == false
现在让我们继续了解 == 运算符。每当使用 == 运算符比较 2 个值时,JavaScript 就会执行抽象相等比较算法。
该算法有以下步骤:
正如您所看到的,该算法考虑了比较值的类型并执行必要的转换。
对于我们的例子,我们将 x 表示为 [],将 y 表示为 ![]。我们检查了 x 和 y 的类型,发现 x 是对象,y 是布尔值。由于 y 是布尔值,x 是对象,因此应用抽象相等比较算法中的条件 7:
如果 Type(y) 为 Boolean,则返回 x == ToNumber(y) 的比较结果。
这意味着如果其中一种类型是布尔值,我们需要在比较之前将其转换为数字。ToNumber(y) 的值是多少?正如我们所看到的,[] 是一个真值,否定则使其为假。结果,Number(false)为0。
[] == false
[] == Number(false)
[] == 0
现在我们有了比较 [] == 0,这次条件 8 开始发挥作用:
如果 Type(x) 是 String 或 Number 并且 Type(y) 是 Object,则返回比较结果 x == ToPrimitive(y)。
基于这个条件,如果其中一个操作数是对象,我们必须将其转换为原始值。这就是 ToPrimitive 算法发挥作用的地方。我们需要将 [] x 转换为原始值。数组是 JavaScript 中的对象。正如我们之前所看到的,当将对象转换为基元时,valueOf 和 toString 方法就会发挥作用。
在这种情况下, valueOf 返回数组本身,它不是有效的原始值。因此,我们转向 toString 进行输出。将 toString 方法应用于空数组会得到一个空字符串,这是一个有效的原语:
[] == 0
[].toString() == 0
"" == 0
将空数组转换为字符串会得到一个空字符串“”,现在我们面临比较:“”== 0。
现在,其中一个操作数是字符串类型,另一个操作数是数字类型,则条件 5 成立:
如果 Type(x) 是 String 并且 Type(y) 是 Number,则返回比较结果 ToNumber(x) == y。
因此,我们需要将空字符串“”转换为数字,即为 0。
"" == 0
ToNumber("") == 0
0 == 0
最后,两个操作数具有相同的类型并且条件 1 成立。由于两者具有相同的值,因此,最终输出为:
0 == 0 // true
到目前为止,我们在探索的最后几个问题中使用了强制转换,这是掌握 JavaScript 和在面试中解决此类问题的重要概念,这些问题往往会被问到很多。
8、闭包
这是与闭包相关的最著名的面试问题之一:
const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log('Index: ' + i + ', element: ' + arr[i]);
}, 3000);
}
如果您知道输出,那就好了。那么,让我们尝试理解这个片段。从表面上看,这段代码片段将为我们提供以下输出:
Index: 0, element: 10
Index: 1, element: 12
Index: 2, element: 15
Index: 3, element: 21
但这里的情况并非如此。由于闭包的概念以及 JavaScript 处理变量作用域的方式,实际的输出会有所不同。当延迟 3000 毫秒后执行 setTimeout 回调时,它们都将引用同一个变量 i,循环完成后该变量的最终值为 4。结果,代码的输出将是:
Index: 4, element: undefined
Index: 4, element: undefined
Index: 4, element: undefined
Index: 4, element: undefined
出现此行为的原因是 var 关键字没有块作用域,并且 setTimeout 回调捕获对同一 i 变量的引用。当回调执行时,它们都会看到 i 的最终值,即 4,并尝试访问未定义的 arr[4]。
为了实现所需的输出,您可以使用 let 关键字为循环的每次迭代创建一个新范围,确保每个回调捕获 i 的正确值:
const arr = [10, 12, 15, 21];
for (let i = 0; i < arr.length; i++) {
setTimeout(function() {
console.log('Index: ' + i + ', element: ' + arr[i]);
}, 3000);
}
通过此修改,您将获得预期的输出:
Index: 0, element: 10
Index: 1, element: 12
Index: 2, element: 15
Index: 3, element: 21
使用 let 在每次迭代中为 i 创建一个新的绑定,确保每个回调引用正确的值。
通常,开发人员已经熟悉涉及 let 关键字的解决方案。然而,面试有时会更进一步,挑战你在不使用 let 的情况下解决问题。在这种情况下,另一种方法是通过立即调用循环内的函数(IIFE)来创建闭包。这样,每个函数调用都有自己的 i 副本。您可以这样做:
const arr = [10, 12, 15, 21];
for (var i = 0; i < arr.length; i++) {
(function(index) {
setTimeout(function() {
console.log('Index: ' + index + ', element: ' + arr[index]);
}, 3000);
})(i);
}
在此代码中,立即调用的函数 (function(index) { ... })(i); 为每次迭代创建一个新范围,捕获 i 的当前值并将其作为索引参数传递。这确保每个回调函数都有自己单独的索引值,防止与闭包相关的问题并为您提供预期的输出:
Index: 0, element: 10
Index: 1, element: 12
Index: 2, element: 15
Index: 3, element: 21
来源:前端达人
相关推荐
- 一键生成高颜值图表!让你的文字瞬间有画面感,职场人必备!
-
哈喽,打工人们!忙碌的周中,大熊又来给你们带来一个超实用的效率神器啦!这次的宝藏网站绝对是那种用过就离不开的"真香"型产品!假设你明天就要做重要汇报,可面对一大堆密密麻麻的文字材料,你...
- 批量将 Word 转换为 PDF/Excel/Txt/图片等多种格式
-
Word文档是我们工作中经常会打交道的一种文档格式,我们也经常会有需要对Word文档进行格式转换的需求,比如将Word格式转换为PDF、将Word文档转换为Excel、将Word...
- 绝了!一键用AI生成高颜值动态PPT(附详细步骤+Prompt)
-
大家好,我是一名酷爱研究AI的产品经理,最近我有个新发现:那些花了你3天做出来的PPT,现在用AI可以1小时搞定!而且颜值还高!为什么AI做PPT比传统方式效率高10倍?我用一张图就能告诉你:AI生成...
- ztext - 简单几行代码创建酷炫3D特效文字的开源JS库
-
把网页上的文字变成酷炫的3D风格,还能制作旋转动效,有了ztext.js,只需要几行代码。ztext能做什么ztext.js是一个能把常规的平面文字变成3D样式的前端开源代码库,让开发者...
- 文字内插入小图片,也太可爱了吧(文字中怎么插图片)
-
图文排版H5手机版秀米有小伙伴留言问添加图片的时候可不可以把图片添加到文字之间比如下面这句话中的小贴纸图片后面可以接着输入文字其实吧这就是咱们的『文字内插入小图片』功能嘛可以用来在文字内加个表情包又...
- Linux环境下C++代码性能分析方法(linux怎么写c++代码)
-
技术背景在开发C++应用程序时,找出代码中运行缓慢的部分是进行性能优化的关键。在Linux系统上,有多种工具和方法可用于对C++代码进行性能分析,每种方法都有其特点和适用场景。实现步骤手动中断调试法在...
- SVG互动图文,让你的文章更有趣!教你4种简单易学的黑科技玩法!
-
如果你是一个公众号创作者,那么你一定想知道如何让你的文章更加吸引人,更加有趣,更加有创意。你可能已经尝试过各种图文排版技巧,但是你是否知道,有一种黑科技可以让你的文章变得更加酷炫,更加互动,更加爆款?...
- Videoscribe怎么实现实心中文汉字的手绘制作
-
很多朋友在制作手绘视频的时候,不知道怎么输入实心的中文汉字,之前我们已经给大家分享了怎么输入汉字的方法,但是有一点遗憾的是输出的汉字是空心的手绘展示,在视觉上并不是非常的美观。经过大家不断的探索,终于...
- 一款用于将文本转化成图表的现代化脚本语言
-
大家好,又见面了,我是GitHub精选君!今天要给大家推荐一个GitHub开源项目terrastruct/d2,该项目在GitHub有超过10.3kStar,用一句话介绍该项目就是:...
- 探秘 Web 水印技术(制作水印网站)
-
作者:fransli,腾讯PCG前端开发工程师Web水印技术在信息安全和版权保护等领域有着广泛的应用,对防止信息泄露或知识产品被侵犯有重要意义。水印根据可见性可分为可见水印和不可见水印(盲水印)...
- 不忍心卸载的五款神仙工具(不忍心卸载的五款神仙工具是什么)
-
001.效率工具uTools-装机必备的生产力工具集uTools是一款非常强大的可以装下几乎所有效率工具的电脑生产力工具集,目前拥有Windows、Mac和Linux三个版本。软件界面...
- 「SVG」飞花令!这份最高检工作报告“超有料”
-
原标题:【SVG】飞花令!这份最高检工作报告“超有料”栏目主编:秦红文字编辑:沈佳灵来源:作者:最高人民检察院...
- svg|2025政府工作报告,有没有你关心的数据?
-
··<setattributeName="visibility"begin="click+0s"dur="1ms"fill="freeze"restart="never"to="hi...
- videoscribe只能输入英文,如何输入中文文本?
-
videoscribe只能输入英文,如何输入中文文本?打开VideoScribe软件,打开要添加中文字体的位置。打开Photoshop并在文件中创建一个新的透明背景图层。注意:必须是透明背景层。...
- 五个流行的SVG在线编辑器(svg编辑工具)
-
随着响应网络的发展,越来越多的高质量的SVG在线编辑器被公众所熟知。SVG矢量图形也越来越受欢迎,以便在任何设备上呈现图像,甚至一些易于使用的SVG在线编辑器,可以替代PS,本文总结了五种流行的SVG...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 简介 (30)
- HTML 响应式设计 (31)
- HTML URL 编码 (32)
- HTML Web 服务器 (31)
- HTML 表单属性 (32)
- HTML 音频 (31)
- HTML5 支持 (33)
- HTML API (36)
- HTML 总结 (32)
- HTML 全局属性 (32)
- HTML 事件 (31)
- HTML 画布 (32)
- HTTP 方法 (30)
- 键盘快捷键 (30)
- CSS 语法 (35)
- CSS 选择器 (30)
- CSS 轮廓 (30)
- CSS 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 中级教程 (30)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)