深度解析JavaScript面试热点:事件循环、上下文、箭头函数、变量作用域与ES6模块

JavaScript面试中经常涉及到事件循环、上下文、箭头函数、变量作用域以及ES6模块等核心概念。通过清晰的代码示例,我们深入讨论这些主题,揭示其中的关键细节。

事件循环(Event Loop)

JavaScript开发者每天都与事件循环打交道,本文通过实际代码展示了 setTimeoutPromise 和同步代码之间的交互。通过分析回应,我们纠正了一些开发者对于Promise构造函数中执行器函数同步调用的错误观念,同时详细讨论了微任务队列和宏任务队列的执行顺序。

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

console.log(2);

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

console.log(5);
// 输出结果:2 3 5 4 1

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

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

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

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

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

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

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

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

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

上下文(Context)

在面试中,对于 this 关键字的理解至关重要。我们通过具体的例子讨论了普通函数和箭头函数中 this 的不同行为,并解释了在不同上下文中函数调用的结果。

普通函数
javascript 复制代码
'use strict';

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

function callFoo(fn) {
  fn();
}

let obj = { foo };

callFoo(obj.foo);
// 输出结果:undefined
箭头函数

深入研究了箭头函数内部的 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());
// 输出结果:10

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

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

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

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

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

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

const num = new Num();

console.log(num.getNum());

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

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

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

变量作用域

通过临时死区、变量声明提升等概念,详细探讨了变量作用域。通过具体的代码案例,让读者更好地理解在不同作用域中变量的行为。

javascript 复制代码
'use strict';

console.log(foo());

let bar = 'bar';

function foo() {
  return bar;
}

bar = 'baz';
// 输出结果:ReferenceError: bar is not defined

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

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

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

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

回到我们的例子。

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

代码抛出一个错误:

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

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

console.log(typeof foo);

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

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

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

foo(5);

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

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

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

ES6模块

对ES6模块的导入机制进行了解析,强调了导入会被提升的特性。通过示例,展示了模块间依赖的加载顺序。

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

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

console.log(sum(1, 2));
// 输出结果:helper.js index.js 3

导入会被提升。

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

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

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

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

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

javascript 复制代码
var num = 8; function num() {}

将被转换为:

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

结果,num = 8。

提高难度

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

console.log(typeof foo);

-------------------------
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

最后,通过一个Promise链的例子,深入讨论了Promise的执行过程,特别是在抛出错误和使用 catch 处理程序时的执行流程。

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}`));
// 输出结果:error 1 then 2

这篇文章通过清晰的代码示例和深度解析,为读者提供了深入理解JavaScript核心概念的机会,是面试前的必读材料。

相关推荐
GDAL10 分钟前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
小马哥编程2 小时前
Function.prototype和Object.prototype 的区别
javascript
王小王和他的小伙伴3 小时前
解决 vue3 中 echarts图表在el-dialog中显示问题
javascript·vue.js·echarts
学前端的小朱3 小时前
处理字体图标、js、html及其他资源
开发语言·javascript·webpack·html·打包工具
outstanding木槿3 小时前
react+antd的Table组件编辑单元格
前端·javascript·react.js·前端框架
小k_不小3 小时前
C++面试八股文:指针与引用的区别
c++·面试
好名字08213 小时前
前端取Content-Disposition中的filename字段与解码(vue)
前端·javascript·vue.js·前端框架
摇光933 小时前
js高阶-async与事件循环
开发语言·javascript·事件循环·宏任务·微任务
胡西风_foxww4 小时前
【ES6复习笔记】Class类(15)
javascript·笔记·es6·继承··class·静态成员
布兰妮甜4 小时前
使用 WebRTC 进行实时通信
javascript·webrtc·实时通信