深度解析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核心概念的机会,是面试前的必读材料。

相关推荐
橙露2 分钟前
JavaScript 异步编程:Promise、async/await 从原理到实战
开发语言·javascript·ecmascript
我命由我123451 小时前
React Router 6 - 嵌套路由、路由传递参数
前端·javascript·react.js·前端框架·html·ecmascript·js
十六年开源服务商1 小时前
2026年WordPress网站地图完整指南
java·前端·javascript
英俊潇洒美少年2 小时前
MessageChannel 如何实现时间切片
javascript·react.js·ecmascript
技术钱3 小时前
react数据大屏四种适配方案
javascript·react.js·ecmascript
李明卫杭州3 小时前
JavaScript 严格模式下 arguments 的区别
前端·javascript
一次旅行4 小时前
今日心理学知识分享(三)
开发语言·javascript·程序人生·ecmascript
牛十二4 小时前
openclaw安装mcporter搜索小红书
开发语言·javascript·ecmascript
小金鱼Y4 小时前
🔥 前端人必看:浏览器安全核心知识点全解析(XSS/CSRF/DDoS)
前端·javascript·安全
时寒的笔记4 小时前
js逆向05_ob混淆花指令,平坦流,某麦网(突破ob混淆寻找拦截器)
开发语言·前端·javascript