文章

V8引擎(五)

V8

V8 引擎学习(五)

V8引擎(五)

前瞻

在前面的章节中,了解了 V8 引擎中的对象和函数,以及”函数是一等公民”的概念。

细分函数,又有函数声明和函数表达式,可先看之前的文章《关于 JavaScript 函数引用的理解》

那么,V8 引擎是如何处理函数声明和函数表达式的?

函数声明与函数表达式的差异

// 函数声明
foo();
function foo() {
  console.log("函数声明 - foo");
}
 
// 函数表达式
foo();
var foo = function () {
  console.log("函数表达式 - foo");
};

可以猜想一下上方的代码的运行结果。

显然,第一段代码可以正常运行,而第二段代码会报错,同样是在定义的函数之前调用函数,为什么会出现这种差异呢?

主要因为这两种定义函数的方式在编译器看来具有不同语义,进而触发了不同的行为。

image-20221030215111344

因为语义的不同,所以第一种称为函数声明,第二种称为函数表达式

V8 引擎是怎么处理函数声明的?

V8 引擎在执行 JavaScript 的过程中,会先对其进行编译,然后再执行,如下面这段代码:

var x = 3;
function foo() {
  console.log("foo");
}

大致执行过程

image-20221030221347205

其实执行细节,前面的章节都有提到过,这里再回顾一遍。

细节

  1. 编译阶段

    ​ 如果解析到函数声明,那么V8 引擎会将这个函数声明转换为内存中的函数对象并将其放到作用域中。同样,如果解析到某个变量声明,也会将其放到作用域中,但是会将其值初始化会 undefined,表示该变量还未被使用

  2. 执行阶段

    如果使用了某个变量,或者调用了某个函数,那么 V8 引擎便会去作用域查找相关内容

  3. 上方代码的作用域

d8 调试工具测试

Inner function scope:
function foo () { // (000002101BDC2470) (361, 390)
  // 2 heap slots
}
Global scope:
global { // (000002101BDC2168) (0, 390)
  // will be compiled
  // 1 stack slots
  // temporary vars:
  TEMPORARY .result;  // (000002101BDC2718) local[0]
  // local vars:
  VAR x;  // (000002101BDC23B8)
  VAR foo;  // (000002101BDC2670)
 
  function foo () { // (000002101BDC2470) (361, 390)
    // lazily parsed
    // 2 heap slots
  }
}

​ 可以看到,作用域中包含了变量 x 和 foo,且变量 x 的默认值是 undefined,变量 foo 指向了 foo 函数对象,foo 函数对象被 V8 引擎存放在内存中的堆空间了。因此,可以得出,这些变量都是在编译阶段被装进作用域的

​ 因为在执行之前,这些变量都被提升到了作用域中了,因此在执行阶段,V8 引擎理所当然地就能获取到所有的变量了,这种在编译阶段,将所有的变量提升到了作用域的过程称为变量提升

​ 回到最开始的问题,为什么会出现差异?这是因为函数声明在编译阶段就被提升到了作用域中,在执行阶段,只要是在作用域中存在的变量或者对象,都是可以被使用的。通过上面的分析,知道了如果是一个普通变量,变量提升之后的值都是 undefined,如果是声明的函数,那么变量提升之后的值则是函数对象

为什么函数声明经过变量提升后,其值会是函数对象?

是因为表达式语句的区别。

那么什么是表达式,什么又是语句呢?

x = 3;
3 === 4;

以上都是表达式,因为它们都会返回一个值

var x;

以上就是语句了,执行该语句时,V8 引擎并不会返回任何值给你

同样,函数声明也是一个语句

function foo() {
  return 1;
}

当执行这段代码时,V8 引擎并不会返回任何的值,它只会在编译阶段解析 foo 函数,并将函数对象存储到内存中

语句亦可以操作表达式

// 语句操作表达式
if (1) {
  console.log(1); // 表达式
}

知道了表达式和语句的区别,接着探索。

V8 引擎在执行上方 var x = 3 这段代码时,会认为它是两段代码

  • 一段是定义变量的语句:var x = undefined
  • 一段是赋值的表达式:x = 3

​ 首先,在变量提升阶段,V8 引擎并不会执行赋值的表达式,该阶段只会解析基础的语句,如变量的定义、函数的声明。因此,这两行代码是在不同的阶段完成的,var x 是在编译阶段完成的,也可以说是变量提升阶段,而 x = 3 是表达式,所有的表达式是在执行阶段完成的在变量提升阶段(即编译阶段),V8 引擎将这些变量存放在作用域时,还会将其值初始化为 undefined。综上所述,表达式是不会在编译阶段执行的函数是一个对象,所以在编译阶段,V8 引擎就会将整个函数对象提升到作用域中,并不是给该函数名称赋一个 undefined

小结

  • 如果遇到普通的变量声明,那么便会将其提升到作用域中,并将其值初始化为 undefined
  • 如果遇到的是函数声明,那么 V8 引擎会在内存中为函数声明生成一个函数对象,并将该对象提升到作用域中

image-20221030232741304

V8 引擎是怎么处理函数表达式的?

函数表达式与函数声明的最主要区别

  • 函数表达式是在表达式语句中使用 function 的
  • 在函数表达式中,可以省略函数名称,从而创建匿名函数
  • 一个函数表达式可以被用作一个即时调用的函数表达式——IIFE(Immediately Invoked Function Expression)
foo();
var foo = function () {
  console.log("foo");
};

执行上方代码时,V8 引擎会先查找声明语句

// 拆分成以下代码
var foo = undefined;
foo = function () {
  console.log("foo");
};

第一行是声明语句,V8 引擎在解析阶段,就会在作用域中创建该对象,并将该对象设置为 undefined

第二行是函数表达式,在编译阶段,V8 引擎并不会处理函数表达式,所以也就不会将该函数表达式提升到作用域中了

因此,在函数表达式之前调用该函数 foo,此时 foo 只是指向了 undefined,所以就相当于调用一个 undefined,而 undefined 并不是函数,于是就报错了。

立即调用的函数表达式(IIFE)

上面我们清楚了,在编译阶段,V8 引擎是不会处理函数表达式的。

JavaScript 中有一个圆括号运算符圆括号里可以放一个表达式

// 在小括号里放入一段函数的定义
(funciton() {
	// some statements
})
 
// 立即调用函数表达式(IIFE)
(funciton() {
	// some statements
})()

因为小括号之间存放的必须是表达式,所以如果在小括号里定义一个函数,那么 V8 引擎就会把这个函数看成是函数表达式,执行时它会返回一个函数对象。倘若,在表达式后面加上调用的括号,就称为立即调用函数表达式(IIFE)

因为函数表达式也是表达式,所以 V8 在编译阶段,并不会为该表达式创建函数对象。这样的好处是不会污染环境,函数和函数内部的变量都不会被其他部分的代码访问到(或称为隔离)

另外,因为立即调用的函数表达式是立即执行的所以将一个立即调用的函数表达式赋给一个变量时,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果,如下:

var l = (function () {
  return 1;
})();

总结

  • 函数声明的本质是语句,而函数表达式的本质是表达式
  • 如果提升一个变量,那么 V8 引擎在将变量提升到作用域时,会将其值初始化为 undefined,如果是函数声明,那么 V8 引擎会在内存中创建该函数对象,并提升到整个函数对象到作用域中
  • 在编译阶段,V8 引擎并不会将函数表达式中的函数对象提升到全局作用域中,因此无法在函数表达式之前使用该函数
  • IIFE 是一种特别的表达式,可以起到变量隔离和代码隐藏的作用