最失败的 JavaScript 面试问题

2023-09-19 10:25:05 浏览数 (1)

文列举了一些常见但容易出错的JavaScript面试问题,并提供了相应的解释和示例代码。这篇文章的目标是帮助读者更好地理解这些问题,以便在JavaScript面试中更好地回答它们。

事件循环 Event loop

很难想象有哪个JavaScript面试不会提到事件循环这个主题。这并非没有道理,这个主题确实是非常基础的,并且每天都被React、Vue、你用的任何框架的开发者所使用。

小测验1:只有18%的正确答案

作为示例,我们选择了一个看似涵盖了这个主题所有方面的小测验。

尝试自己做一下,并阅读解释:

代码语言:javascript复制
setTimeout(() => console.log(1), 0);

console.log(2);

new Promise(res => {
  console.log(3)
  res();
}).then(() => console.log(4));

console.log(5);

解释:

在这个例子中,我们看到了 setTimeoutPromise 以及一些同步代码。

一个正确回答了这个测验的开发者的内部对话可能是这样的:

给定 0 延迟,我们传递给 setTimeout 的函数会同步调用还是异步调用?

尽管 setTimeout 函数有零延迟,回调函数是异步调用的。引擎会将回调函数放在回调队列(宏任务队列)中,并在调用栈为空时将其移至调用栈。因此,数字 1 将被跳过,数字 2 将首先在控制台中显示。

我们作为参数传递给 Promise 构造函数的函数会同步调用还是异步调用?

Promise 构造函数接受的函数参数是同步执行的。因此,在控制台中接下来要显示的数字是 3。 给定零延迟,我们传递给 promise 的 then 处理程序的函数会同步调用还是异步调用?

then方法中的回调是异步执行的,即使 promise 没有延迟就解决了。与 setTimeout 不同的是,引擎会将 promise 回调放在另一个队列中 —— 工作队列(微任务队列),在那里它将等待执行。因此,接下来进入控制台的数字是 5

哪个优先级更高 —— 微任务队列还是宏任务队列,换句话说 —— Promise 还是 setTimeout

微任务(Promise)比宏任务(setTimeout)有更高的优先级,所以下一个在控制台中的数字将是4,最后一个是1

通过分析回应,我们可以得出结论,大多数受访者在假设传递给 Promise 构造函数作为参数的执行器函数是异步调用的方面是错误的(44%的人选择了这个选项)。

上下文(Context)

关于上下文的问题甚至可能会难倒经验丰富的开发者。例如,只有29%的开发者解决了这个复杂但本质上很简单的任务。

小测验1:只有29%的正确答案

尝试自己做一下,并阅读解释:

代码语言:javascript复制
'use strict';

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

function callFoo(fn) {
  fn();
}

let obj = { foo };

callFoo(obj.foo);

解释:

this 的值是在函数被调用时设定的。

在示例中,obj.foo 函数作为一个参数传递给另一个 callFoo 函数,后者在没有上下文的情况下调用它。

在普通模式下,当没有执行上下文并且代码在浏览器环境中运行时,this 指向 window 对象,在严格模式下,它是 undefined

正确答案是 undefined

小测验2:只有28%的正确答案

另一个常见的面试问题是箭头函数内部 this 的值。

代码语言:javascript复制
'use strict';
var x = 5;
var y = 5;

function Operations(op1 = x, op2 = y) {
  this.x = op1;
  this.y = op2;
};

Operations.prototype.sum = () => this.x   this.y;

const op = new Operations(10, 20);

console.log(op.sum());

尝试自己做一下,并阅读解释。

解释

箭头函数没有自己的 this。相反,箭头函数体内的 this 指向该箭头函数定义所在作用域的this 值。

我们的函数是在全局作用域中定义的。

全局作用域中的 this 指向全局对象(即使在严格模式下也是如此)。因此,答案是 10

小测验2:只有39%的正确答案

另一个关于箭头函数的问题可能是这样的。

代码语言:javascript复制
const Num = () => {
  this.getNum = () => 10;
}

Num.prototype.getNum = () => 20;

const num = new Num();

console.log(num.getNum());

尝试自己做一下,并阅读解释。

解释:

箭头函数不能用作构造函数,当使用 new 调用时会抛出错误。它们也没有原型属性:

TypeError:无法设置undefined的属性(设置'getNum')

这样的问题比较少见,但你应该为它们做好准备。你可以在 MDN 上查看更多关于箭头函数的信息。

变量作用域

这个主题值得探讨,不仅因为它在面试中很受欢迎,而且还有实际应用的原因。如果你能很好地理解变量作用域,那么你将节省大量的调试代码的时间。

让我们看一些常见的例子。

小测验1:只有38%的正确答案

代码语言:javascript复制
'use strict';

console.log(foo());

let bar = 'bar';

function foo() {
  return bar;
}

bar = 'baz';

尝试自己做一下,并阅读解释。

解释:

let / const 变量定义之前的作用域中的位置被称为临时死区。

如果我们在 let / const 变量定义之前尝试访问它们,将会抛出引用错误。

要轻松记住一种编程语言是如何工作的,了解为什么它是这样工作的会很有帮助(简单吧!)。

这种行为是因为 const 变量而被选中的。访问未定义的 var 变量时,我们得到的是undefined。对于 const 变量来说,这是不可接受的,因为它将不再是一个常量。

let 变量的行为以类似的方式完成,以便您可以轻松地在这两种类型的变量之间切换。

回到我们的例子。

由于函数调用在 bar 变量的定义之上,该变量处于临时死区。

代码抛出一个错误:

ReferenceError:初始化前不能访问'bar'

小测验2:只有33%的正确答案

代码语言:javascript复制
let func = function foo() {
  return 'hello';
}

console.log(typeof foo);

尝试自己做一下,并阅读解释。

解释:

在命名函数表达式中,名称只在函数体内部是局部的,外部无法访问。因此,全局作用域中不存在foo

typeof运算符对未定义的变量返回undefined。

小测验3:只有36%的正确答案

以下示例不推荐在实际生活中使用,但你应该知道这段代码至少会如何工作,以满足面试官的兴趣。

代码语言:javascript复制
function foo(bar, getBar = () => bar) {
  var bar = 10;
  console.log(getBar());
}

foo(5);

尝试自己做一下,并阅读解释。

解释:

对于具有复杂参数(解构、默认值)的函数,参数列表被封闭在其自己的作用域内。

因此,在函数体中创建 bar 变量不会影响参数列表中同名的变量,getBar() 函数通过闭包从其参数中获取 bar

一般来说,我们注意到尽管ES6已经发布了7年多,但开发人员对其特性的理解仍然很差。当然,每个人都知道这个版本中特性的语法,但只有少数人能更深入地理解它。

ES6模块。

如果你是面试官,并且由于某种原因你不喜欢候选人,那么模块绝对可以帮你让任何人都失败。

为了这篇文章的目的,我们选择了关于这个主题最简单的任务之一。但相信我们,ES6模块要复杂得多。

小测验1:只有41%的正确答案

代码语言:javascript复制
console.log('index.js');

import { sum } from './helper.js';

console.log(sum(1, 2));

尝试自己做一下,并阅读解释。

解释

导入会被提升。

提升是JS中的一种机制,其中变量和函数声明在代码执行之前被移动到它们的作用域的顶部。

所有依赖项将在代码运行之前加载。

所以,答案是:helper.js index.js 3

提升

另一个热门的面试题目是提升。

小测验1:只有40%的正确答案

尽管选定的小测验与现实脱节,但它完美地解释了提升的机制。如果你明白这段代码是如何工作的,你几乎不应该在其他所有有关提升的问题上遇到任何问题。

代码语言:javascript复制
'use strict';

var num = 8;

function num() {
  return 10;
}

console.log(num);

尝试自己做一下,并阅读解释。

解释

函数和变量声明被放在其作用域的顶部,变量的初始化发生在脚本执行时。

具有相同名称的变量的重复声明将被跳过。

函数总是首先被提升。无论函数和具有相同名称的变量的声明在代码中以何种顺序出现,函数都优先,因为它上升得更高。

示例1
代码语言:javascript复制
function num() {}
var num;
console.log(typeof num); // function
示例2
代码语言:javascript复制
var num;
function num() {}
console.log(typeof num); // function

变量总是在最后被初始化。

`var num = 8; function num() {}`

将被转换为:

代码语言:javascript复制
function num() {}
var num; // repeated declaration is ignored
num = 8;

结果,num = 8。

小测验2:只有12%的正确答案

还记得我们说模块很难吗?模块加上提升可以让任何程序员的脑袋都要爆炸。

代码语言:javascript复制
import foo from './module.mjs';

console.log(typeof foo);
代码语言:javascript复制
foo = 25;

export default function foo() {}

尝试自己做一下,并阅读解释。

解释

export default function foo() {}

等于

代码语言:javascript复制
function foo() {}
export { foo as default }

现在是时候记住函数是被提升的,变量初始化总是在函数/变量声明之后发生。

在引擎处理完模块代码后,你可以将其想象成以下形式:

代码语言:javascript复制
function foo() {}
foo = 25;
export { foo as default }

所以正确答案是数字。

Promises

程序员对promises的主题了解得比他们自己认为的要好。这个主题上的面试问题通常是最基础的,大多数人都能应对。但我们仍然不能绕过它,因为面试官也是如此。

小测验1:46%的正确答案

尝试自己做一下,并阅读解释。

代码语言:javascript复制
Promise.resolve(1)
  .then(x => { throw x })
  .then(x => console.log(`then ${x}`))
  .catch(err => console.log(`error ${err}`))
  .then(() => Promise.resolve(2))
  .catch(err => console.log(`error ${err}`))
  .then(x => console.log(`then ${x}`));
解释

我们来看看这段代码将如何逐步执行。

  1. 第一个 then 处理程序抛出一个错误(意味着 — 返回一个被拒绝的promise)。
  2. 下一个 then 处理程序由于错误被抛出而没有触发,取而代之的是执行转移到下一个 catch
  3. catch 处理程序打印一个错误并返回一个空的 promise。像 then 处理程序一样,catch 处理程序总是返回一个 promise
  4. 因为 catch 处理程序返回了一个 promise,所以下一个 then 处理程序被调用,并返回一个值为 2promise

最后一个 then 处理程序被调用,并打印2。

0 人点赞