8 个针对高级职位的高级 JavaScript 面试题
zhezhongyun 2024-12-15 17:54 29 浏览
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
最后总结
以上就是我今天这篇文章想与您分享的8个关于JS的前端面试题, 我希望这篇文章对您的面试准备之旅有所帮助。
相关推荐
- JPA实体类注解,看这篇就全会了
-
基本注解@Entity标注于实体类声明语句之前,指出该Java类为实体类,将映射到指定的数据库表。name(可选):实体名称。缺省为实体类的非限定名称。该名称用于引用查询中的实体。不与@Tab...
- Dify教程02 - Dify+Deepseek零代码赋能,普通人也能开发AI应用
-
开始今天的教程之前,先解决昨天遇到的一个问题,docker安装Dify的时候有个报错,进入Dify面板的时候会出现“InternalServerError”的提示,log日志报错:S3_USE_A...
- 用离散标记重塑人体姿态:VQ-VAE实现关键点组合关系编码
-
在人体姿态估计领域,传统方法通常将关键点作为基本处理单元,这些关键点在人体骨架结构上代表关节位置(如肘部、膝盖和头部)的空间坐标。现有模型对这些关键点的预测主要采用两种范式:直接通过坐标回归或间接通过...
- B 客户端流RPC (clientstream Client Stream)
-
客户端编写一系列消息并将其发送到服务器,同样使用提供的流。一旦客户端写完消息,它就等待服务器读取消息并返回响应gRPC再次保证了单个RPC调用中的消息排序在客户端流RPC模式中,客户端会发送多个请...
- 我的模型我做主02——训练自己的大模型:简易入门指南
-
模型训练往往需要较高的配置,为了满足友友们的好奇心,这里我们不要内存,不要gpu,用最简单的方式,让大家感受一下什么是模型训练。基于你的硬件配置,我们可以设计一个完全在CPU上运行的简易模型训练方案。...
- 开源项目MessageNest打造个性化消息推送平台多种通知方式
-
今天介绍一个开源项目,MessageNest-可以打造个性化消息推送平台,整合邮件、钉钉、企业微信等多种通知方式。定制你的消息,让通知方式更灵活多样。开源地址:https://github.c...
- 使用投机规则API加快页面加载速度
-
当今的网络用户要求快速导航,从一个页面移动到另一个页面时应尽量减少延迟。投机规则应用程序接口(SpeculationRulesAPI)的出现改变了网络应用程序接口(WebAPI)领域的游戏规则。...
- JSONP安全攻防技术
-
关于JSONPJSONP全称是JSONwithPadding,是基于JSON格式的为解决跨域请求资源而产生的解决方案。它的基本原理是利用HTML的元素标签,远程调用JSON文件来实现数据传递。如果...
- 大数据Doris(六):编译 Doris遇到的问题
-
编译Doris遇到的问题一、js_generator.cc:(.text+0xfc3c):undefinedreferenceto`well_known_types_js’查找Doris...
- 网页内嵌PDF获取的办法
-
最近女王大人为了通过某认证考试,交了2000RMB,官方居然没有给线下教材资料,直接给的是在线教材,教材是PDF的但是是内嵌在网页内,可惜却没有给具体的PDF地址,无法下载,看到女王大人一点点的截图保...
- 印度女孩被邻居家客人性骚扰,父亲上门警告,反被围殴致死
-
微信的规则进行了调整希望大家看完故事多点“在看”,喜欢的话也点个分享和赞这样事儿君的推送才能继续出现在你的订阅列表里才能继续跟大家分享每个开怀大笑或拍案惊奇的好故事啦~话说只要稍微关注新闻的人,应该...
- 下周重要财经数据日程一览 (1229-0103)
-
下周焦点全球制造业PMI美国消费者信心指数美国首申失业救济人数值得注意的是,下周一希腊还将举行第三轮总统选举需要谷歌日历同步及部分智能手机(安卓,iPhone)同步日历功能的朋友请点击此链接,数据公布...
- PyTorch 深度学习实战(38):注意力机制全面解析
-
在上一篇文章中,我们探讨了分布式训练实战。本文将深入解析注意力机制的完整发展历程,从最初的Seq2Seq模型到革命性的Transformer架构。我们将使用PyTorch实现2个关键阶段的注意力机制变...
- 聊聊Spring AI的EmbeddingModel
-
序本文主要研究一下SpringAI的EmbeddingModelEmbeddingModelspring-ai-core/src/main/java/org/springframework/ai/e...
- 前端分享-少年了解过iframe么
-
iframe就像是HTML的「内嵌画布」,允许在页面中加载独立网页,如同在画布上叠加另一幅动态画卷。核心特性包括:独立上下文:每个iframe都拥有独立的DOM/CSS/JS环境(类似浏...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 教程 (33)
- HTML 简介 (35)
- HTML 实例/测验 (32)
- HTML 测验 (32)
- HTML 参考手册 (28)
- JavaScript 和 HTML DOM 参考手册 (32)
- HTML 拓展阅读 (30)
- HTML中如何键入空格 (27)
- HTML常用标签 (29)
- HTML文本框样式 (31)
- HTML滚动条样式 (34)
- HTML5 浏览器支持 (33)
- HTML5 新元素 (33)
- HTML5 WebSocket (30)
- HTML5 代码规范 (32)
- HTML5 标签 (717)
- HTML5 标签 (已废弃) (75)
- HTML5电子书 (32)
- HTML5开发工具 (34)
- HTML5小游戏源码 (34)
- HTML5模板下载 (30)
- HTTP 状态消息 (33)
- HTTP 方法:GET 对比 POST (33)
- 键盘快捷键 (35)
- 标签 (226)