8 个针对高级职位的高级 JavaScript 面试题
myzbx 2025-01-09 14:47 49 浏览
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的前端面试题, 我希望这篇文章对您的面试准备之旅有所帮助。
相关推荐
- 如何设计一个优秀的电子商务产品详情页
- 
        加入人人都是产品经理【起点学院】产品经理实战训练营,BAT产品总监手把手带你学产品电子商务网站的产品详情页面无疑是设计师和开发人员关注的最重要的网页之一。产品详情页面是客户作出“加入购物车”决定的页面... 
- 怎么在JS中使用Ajax进行异步请求?
- 
        大家好,今天我来分享一项JavaScript的实战技巧,即如何在JS中使用Ajax进行异步请求,让你的网页速度瞬间提升。Ajax是一种在不刷新整个网页的情况下与服务器进行数据交互的技术,可以实现异步加... 
- 中小企业如何组建,管理团队_中小企业应当如何开展组织结构设计变革
- 
        前言写了太多关于产品的东西觉得应该换换口味.从码农到架构师,从前端到平面再到UI、UE,最后走向了产品这条不归路,其实以前一直再给你们讲.产品经理跟项目经理区别没有特别大,两个岗位之间有很... 
- 前端监控 SDK 开发分享_前端监控系统 开源
- 
        一、前言随着前端的发展和被重视,慢慢的行业内对于前端监控系统的重视程度也在增加。这里不对为什么需要监控再做解释。那我们先直接说说需求。对于中小型公司来说,可以直接使用三方的监控,比如自己搭建一套免费的... 
- Ajax 会被 fetch 取代吗?Axios 怎么办?
- 
        大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!今天给大家带来的主题是ajax、fetch... 
- 前端面试题《AJAX》_前端面试ajax考点汇总
- 
        1.什么是ajax?ajax作用是什么?AJAX=异步JavaScript和XML。AJAX是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX可以使网页实... 
- Ajax 详细介绍_ajax
- 
        1、ajax是什么?asynchronousjavascriptandxml:异步的javascript和xml。ajax是用来改善用户体验的一种技术,其本质是利用浏览器内置的一个特殊的... 
- 6款可替代dreamweaver的工具_替代powerdesigner的工具
- 
        dreamweaver对一个web前端工作者来说,再熟悉不过了,像我07年接触web前端开发就是用的dreamweaver,一直用到现在,身边的朋友有跟我推荐过各种更好用的可替代dreamweaver... 
- 我敢保证,全网没有再比这更详细的Java知识点总结了,送你啊
- 
        接下来你看到的将是全网最详细的Java知识点总结,全文分为三大部分:Java基础、Java框架、Java+云数据小编将为大家仔细讲解每大部分里面的详细知识点,别眨眼,从小白到大佬、零基础到精通,你绝... 
- 福斯《死侍》发布新剧照 "小贱贱"韦德被改造前造型曝光
- 
        时光网讯福斯出品的科幻片《死侍》今天发布新剧照,其中一张是较为罕见的死侍在被改造之前的剧照,其余两张剧照都是死侍在执行任务中的状态。据外媒推测,片方此时发布剧照,预计是为了给不久之后影片发布首款正式预... 
- 2021年超详细的java学习路线总结—纯干货分享
- 
        本文整理了java开发的学习路线和相关的学习资源,非常适合零基础入门java的同学,希望大家在学习的时候,能够节省时间。纯干货,良心推荐!第一阶段:Java基础重点知识点:数据类型、核心语法、面向对象... 
- 不用海淘,真黑五来到你身边:亚马逊15件热卖爆款推荐!
- 
        Fujifilm富士instaxMini8小黄人拍立得相机(黄色/蓝色)扫二维码进入购物页面黑五是入手一个轻巧可爱的拍立得相机的好时机,此款是mini8的小黄人特别版,除了颜色涂装成小黄人... 
- 2025 年 Python 爬虫四大前沿技术:从异步到 AI
- 
        作为互联网大厂的后端Python爬虫开发,你是否也曾遇到过这些痛点:面对海量目标URL,单线程爬虫爬取一周还没完成任务;动态渲染的SPA页面,requests库返回的全是空白代码;好不容易... 
- 最贱超级英雄《死侍》来了!_死侍超燃
- 
        死侍Deadpool(2016)导演:蒂姆·米勒编剧:略特·里斯/保罗·沃尼克主演:瑞恩·雷诺兹/莫蕾娜·巴卡林/吉娜·卡拉诺/艾德·斯克林/T·J·米勒类型:动作/... 
- 停止javascript的ajax请求,取消axios请求,取消reactfetch请求
- 
        一、Ajax原生里可以通过XMLHttpRequest对象上的abort方法来中断ajax。注意abort方法不能阻止向服务器发送请求,只能停止当前ajax请求。停止javascript的ajax请求... 
- 一周热门
- 最近发表
- 标签列表
- 
- 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 轮廓宽度 (31)
- CSS 谷歌字体 (33)
- CSS 链接 (31)
- CSS 定位 (31)
- CSS 图片库 (32)
- CSS 图像精灵 (31)
- SVG 文本 (32)
- 时钟启动 (33)
- HTML 游戏 (34)
- JS Loop For (32)
 
