前瞻
在前面的章节中,了解了 V8 引擎中的对象和函数,以及”函数是一等公民”的概念。
细分函数,又有函数声明和函数表达式,可先看之前的文章《关于 JavaScript 函数引用的理解》
那么,V8 引擎是如何处理函数声明和函数表达式的?
函数声明与函数表达式的差异
可以猜想一下上方的代码的运行结果。
显然,第一段代码可以正常运行,而第二段代码会报错,同样是在定义的函数之前调用函数,为什么会出现这种差异呢?
主要因为这两种定义函数的方式在编译器看来具有不同语义,进而触发了不同的行为。
因为语义的不同,所以第一种称为函数声明,第二种称为函数表达式。
V8 引擎是怎么处理函数声明的?
V8 引擎在执行 JavaScript 的过程中,会先对其进行编译,然后再执行,如下面这段代码:
大致执行过程
其实执行细节,前面的章节都有提到过,这里再回顾一遍。
细节
-
在编译阶段:
如果解析到函数声明,那么V8 引擎会将这个函数声明转换为内存中的函数对象,并将其放到作用域中。同样,如果解析到某个变量声明,也会将其放到作用域中,但是会将其值初始化会 undefined,表示该变量还未被使用。
-
在执行阶段:
如果使用了某个变量,或者调用了某个函数,那么 V8 引擎便会去作用域查找相关内容。
-
上方代码的作用域:
d8 调试工具测试
可以看到,作用域中包含了变量 x 和 foo,且变量 x 的默认值是 undefined,变量 foo 指向了 foo 函数对象,foo 函数对象被 V8 引擎存放在内存中的堆空间了。因此,可以得出,这些变量都是在编译阶段被装进作用域的。
因为在执行之前,这些变量都被提升到了作用域中了,因此在执行阶段,V8 引擎理所当然地就能获取到所有的变量了,这种在编译阶段,将所有的变量提升到了作用域的过程称为变量提升。
回到最开始的问题,为什么会出现差异?这是因为函数声明在编译阶段就被提升到了作用域中,在执行阶段,只要是在作用域中存在的变量或者对象,都是可以被使用的。通过上面的分析,知道了如果是一个普通变量,变量提升之后的值都是 undefined,如果是声明的函数,那么变量提升之后的值则是函数对象。
为什么函数声明经过变量提升后,其值会是函数对象?
是因为表达式和语句的区别。
那么什么是表达式,什么又是语句呢?
以上都是表达式,因为它们都会返回一个值。
以上就是语句了,执行该语句时,V8 引擎并不会返回任何值给你。
同样,函数声明也是一个语句。
当执行这段代码时,V8 引擎并不会返回任何的值,它只会在编译阶段解析 foo 函数,并将函数对象存储到内存中。
语句亦可以操作表达式
知道了表达式和语句的区别,接着探索。
V8 引擎在执行上方 var x = 3 这段代码时,会认为它是两段代码
- 一段是定义变量的语句:var x = undefined
- 一段是赋值的表达式:x = 3
首先,在变量提升阶段,V8 引擎并不会执行赋值的表达式,该阶段只会解析基础的语句,如变量的定义、函数的声明。因此,这两行代码是在不同的阶段完成的,var x 是在编译阶段完成的,也可以说是变量提升阶段,而 x = 3 是表达式,所有的表达式是在执行阶段完成的。在变量提升阶段(即编译阶段),V8 引擎将这些变量存放在作用域时,还会将其值初始化为 undefined。综上所述,表达式是不会在编译阶段执行的。函数是一个对象,所以在编译阶段,V8 引擎就会将整个函数对象提升到作用域中,并不是给该函数名称赋一个 undefined。
小结
- 如果遇到普通的变量声明,那么便会将其提升到作用域中,并将其值初始化为 undefined
- 如果遇到的是函数声明,那么 V8 引擎会在内存中为函数声明生成一个函数对象,并将该对象提升到作用域中
V8 引擎是怎么处理函数表达式的?
函数表达式与函数声明的最主要区别
- 函数表达式是在表达式语句中使用 function 的
- 在函数表达式中,可以省略函数名称,从而创建匿名函数
- 一个函数表达式可以被用作一个即时调用的函数表达式——IIFE(Immediately Invoked Function Expression)
执行上方代码时,V8 引擎会先查找声明语句。
第一行是声明语句,V8 引擎在解析阶段,就会在作用域中创建该对象,并将该对象设置为 undefined
第二行是函数表达式,在编译阶段,V8 引擎并不会处理函数表达式,所以也就不会将该函数表达式提升到作用域中了
因此,在函数表达式之前调用该函数 foo,此时 foo 只是指向了 undefined,所以就相当于调用一个 undefined,而 undefined 并不是函数,于是就报错了。
立即调用的函数表达式(IIFE)
上面我们清楚了,在编译阶段,V8 引擎是不会处理函数表达式的。
JavaScript 中有一个圆括号运算符,圆括号里可以放一个表达式
因为小括号之间存放的必须是表达式,所以如果在小括号里定义一个函数,那么 V8 引擎就会把这个函数看成是函数表达式,执行时它会返回一个函数对象。倘若,在表达式后面加上调用的括号,就称为立即调用函数表达式(IIFE)
因为函数表达式也是表达式,所以 V8 在编译阶段,并不会为该表达式创建函数对象。这样的好处是不会污染环境,函数和函数内部的变量都不会被其他部分的代码访问到(或称为隔离)
另外,因为立即调用的函数表达式是立即执行的,所以将一个立即调用的函数表达式赋给一个变量时,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果,如下:
总结
- 函数声明的本质是语句,而函数表达式的本质是表达式
- 如果提升一个变量,那么 V8 引擎在将变量提升到作用域时,会将其值初始化为 undefined,如果是函数声明,那么 V8 引擎会在内存中创建该函数对象,并提升到整个函数对象到作用域中
- 在编译阶段,V8 引擎并不会将函数表达式中的函数对象提升到全局作用域中,因此无法在函数表达式之前使用该函数
- IIFE 是一种特别的表达式,可以起到变量隔离和代码隐藏的作用