乐趣区

关于前端:被难倒了-针对高级前端的8个级JavaScript面试问题

首发于公众号 大迁世界,欢送关注。📝 每周 7 篇实用的前端文章 🛠️ 分享值得关注的开发工具 😜分享集体守业过程中的趣事

JavaScript 是一种功能强大的语言,也是构建古代 Web 的根底之一。这种弱小的语言也有一些本人的怪癖。例如,你晓得 0 === -0 会计算为 true,或者 Number("") 会返回 0 吗?

有时候,这些怪癖会让你百思不得其解,甚至让你狐疑 Brendan 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。然而,在 duplicate 函数外部呈现了一个重大的问题。

duplicate 函数应用循环来遍历给定数组中的每个我的项目。但在循环外部,它应用 push() 办法在数组开端增加新元素。这导致数组每次都会变长,从而产生一个问题:循环永远不会进行。因为数组长度一直减少,循环条件(i < array.length)始终为真。这使得循环有限进行上来,导致程序陷入僵局。

为了解决因为数组长度增长而导致的有限循环问题,能够在进入循环之前将数组的初始长度存储在一个变量中。而后,能够应用这个初始长度作为循环迭代的限度。这样,循环只会针对数组中的原始元素进行,并不会受到因为增加反复项而导致数组增长的影响。以下是批改后的代码:

function duplicate(array) {
  var initialLength = array.length; // 存储初始长度
  for (var i = 0; i < initialLength; i++) {array.push(array[i]); // 推入每个元素的正本
  }
  return array;
}

const arr = [1, 2, 3];
const newArr = duplicate(arr);
console.log(newArr);

输入将显示数组开端的反复元素,并且循环不会导致有限循环:

[1, 2, 3, 1, 2, 3]

3- prototype 和 proto 的区别

prototype 属性是与 JavaScript 中的构造函数相关联的属性。构造函数用于在 JavaScript 中创建对象。当您定义一个构造函数时,还能够将属性和办法附加到其 prototype 属性上。这些属性和办法而后变得能够被该构造函数创立的所有对象实例拜访。因而,prototype 属性充当共享方法和属性的通用存储库。

思考以下代码片段:

// 构造函数
function Person(name) {this.name = name;}

// 增加一个办法到 prototype
Person.prototype.sayHello = function() {console.log(`Hello, my name is ${this.name}.`);
};

// 创立实例
const person1 = new Person("Haider Wain");
const person2 = new Person("Omer Asif");

// 调用共享的办法
person1.sayHello();  // 输入:Hello, my name is Haider Wain.
person2.sayHello();  // 输入:Hello, my name is Omer Asif.

另一方面,__proto__ 属性,通常读作 “dunder proto”,存在于每一个 JavaScript 对象中。在 JavaScript 中,除了原始类型外,所有都能够被视为对象。每个这样的对象都有一个原型,该原型作为对另一个对象的援用。__proto__ 属性简略地是对这个原型对象的援用。

当你试图拜访对象上的一个属性或办法时,JavaScript 会进行查找过程来找到它。这个过程次要波及两个步骤:

对象的自有属性:JavaScript 首先查看对象本身是否间接领有所需的属性或办法。如果在对象内找到了该属性,则间接拜访和应用。
原型链查找:如果在对象本身没有找到该属性,JavaScript 将查看对象的原型(由 __proto__ 属性援用)并在那里搜寻该属性。这个过程会递归地沿着原型链进行,直到找到该属性或直到查找达到 Object.prototype
如果在 Object.prototype 中甚至没有找到该属性,JavaScript 将返回 undefined,示意该属性不存在。

4- 作用域

当编写 JavaScript 代码时,了解作用域的概念十分重要。作用域指的是变量在代码的不同局部的可拜访性或可见性。上面咱们通过一个代码片段来更认真地理解这个概念:

function foo() {console.log(a);
}

function bar() {
  var a = 3;
  foo();}

var a = 5;
bar();

代码定义了两个函数 foo()bar(),以及一个值为 5 的变量 a。所有这些申明都产生在全局作用域中。在 bar() 函数外部,申明了一个变量 a 并赋值为 3。那么当 bar() 函数被调用时,你认为会输入哪个值的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 函数的作用域。果然,那里存在一个值为 3a。因而,控制台语句将输入3

5- 对象强制类型转换

const obj = {valueOf: () => 42,
  toString: () => 27};
console.log(obj + '');

一个引人入胜的方面是探索 JavaScript 如何解决对象转换为根本值,例如字符串、数字或布尔值。这是一个乏味的问题,测试你是否理解对象的强制类型转换。

在像字符串连贯或算术运算这样的场景中与对象一起工作时,这种转换至关重要。为了实现这一点,JavaScript 依赖两个非凡的办法:valueOftoString

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 办法,只留下了返回数字 27toString办法。在这种状况下,JavaScript 将依赖 toString 办法进行对象转换。

6- 了解对象键(Object Keys)

当在 JavaScript 中应用对象时,了解键是如何在其余对象的上下文中被解决和调配的十分重要。思考以下代码片段,并花点工夫猜想输入:

let a = {};
let b = {key: 'test'};
let c = {key: 'test'};

a[b] = '123';
a = '456';

console.log(a);

乍一看,这段代码仿佛应该生成一个具备两个不同键值对的对象a。然而,因为 JavaScript 对对象键的解决形式,后果齐全不同。

JavaScript 应用默认的 toString() 办法将对象键转换为字符串。为什么呢?在 JavaScript 中,对象键总是字符串(或 symbols),或者通过隐式强制转换主动转换为字符串。当你在对象中应用除字符串之外的任何值(例如,数字、对象或符号)作为键时,JavaScript 将在应用它作为键之前外部将该值转换为其字符串示意模式。

因而,当咱们在对象 a 中应用对象 bc作为键时,两者都转换为雷同的字符串示意模式:[object Object]。因为这种行为,第二个赋值a = '456'; 会笼罩第一个赋值a[b] = '123';

最终,当咱们记录对象 a 时,咱们察看到以下输入:

{'[object Object]': '456' }

7- 双等号运算符

console.log([] == ![]);

这个有点简单。那么,你认为输入会是什么呢?

这个问题相当简单。那么,你认为输入后果会是什么呢?让咱们一步一步地来评估。首先,让咱们看一下两个操作数的类型:

typeof([]) // "object"
typeof(![]) // "boolean"

对于 [],它是一个对象,这是能够了解的,因为在 JavaScript 中,包含数组和函数在内的一切都是对象。但操作数 ![] 是如何具备布尔类型的呢?让咱们尝试了解一下。当你应用 ! 与一个原始值(primitive value)一起时,会产生以下转换:

  • Falsy Values(假值):如果原始值是一个假值(例如 false0nullundefinedNaN 或一个空字符串 ''),利用 ! 将把它转换为 true。
  • Truthy Values(真值):如果原始值是一个真值(即任何不是假值的值),利用 ! 将把它转换为 false。

在咱们的案例中,[] 是一个空数组,这在 JavaScript 中是一个真值。因为 [] 是真值,![] 变成了 false。因而,咱们的表达式变为:

[] == ![]
[] == false

当初,让咱们持续理解 == 运算符。当应用 == 运算符比拟两个值时,JavaScript 会执行“形象相等性比拟算法(Abstract Equality Comparison Algorithm)”。这个算法会思考比拟值的类型并进行必要的转换。

在咱们的状况中,让咱们把 x 记作 []y 记作 ![]。咱们查看了 xy 的类型,并发现 x 是对象,y 是布尔值。因为 y 是布尔值,x 是对象,算法的第 7 个条件被利用:

如果 Type(y) 是 Boolean,则返回 x == ToNumber(y) 的比拟后果。

这意味着如果其中一个类型是布尔值,咱们须要在比拟之前将其转换为数字。ToNumber(y) 的值是多少呢?如咱们所见,[] 是一个真值,取反使其变为 false。因而,Number(false)0

[] == false
[] == Number(false)
[] == 0

当初咱们有了 [] == 0 的比拟,这次算法的第 8 个条件起作用:

如果 Type(x) 是 String 或 Number,而 Type(y) 是 Object,则返回 x == ToPrimitive(y) 的比拟后果。

基于这个条件,如果其中一个操作数是对象,咱们必须将其转换为一个原始值。这就是“ToPrimitive 算法”呈现的中央。咱们须要将 x(即 [])转换为一个原始值。数组在 JavaScript 中是对象。当将对象转换为原始值时,valueOftoString 办法会起作用。在这种状况下,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

至此,咱们曾经利用了强制转换(coercion)来解决了咱们探讨的最初几个问题,这是把握 JavaScript 和解决面试中这类常见问题的重要概念。我强烈建议你查看我的对于强制转换的具体博客文章。它以清晰和彻底的形式解释了这个概念。这里是链接。

交换

首发于公众号 大迁世界,欢送关注。📝 每周一篇实用的前端文章 🛠️ 分享值得关注的开发工具 ❓ 有疑难?我来答复

本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试残缺考点、材料以及我的系列文章。

退出移动版