image.jpeg

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

  1. foo()
  2. function foo(){
  3. console.log('foo')
  4. }

不会报错。

  1. foo()
  2. var foo = function (){
  3. console.log('foo')
  4. }

会报错。

主要原因是两种定义函数的方式具有不同的语义,不同的语义触发了不同的行为。

image.jpeg

V8 怎么处理函数声明?

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

  1. var x = 5
  2. function foo(){
  3. console.log('Foo')
  4. }

V8 执行这段代码的流程大致如下图所示:

函数声明与函数表达式 - 图3

在编译阶段,如果解析到函数声明,那么 V8 会将这个函数声明转换为内存中的函数对象,并将其放到作用域中。同样,如果解析到了某个变量声明,也会将其放到作用域中,但是会将其值设置为 undefined,表示该变量还未被使用。然后在 V8 执行阶段,如果使用了某个变量,或者调用了某个函数,那么 V8 便会去作用域查找相关内容。

关于作用域的数据,你也可以使用 D8 来查看,具体操作方式如下:将这段代码保存到 test.js 中;使用“d8 —print-scopes test.js”命令即可查看作用域的状态。

  1. Global scope:
  2. global { // (0x7fb62281ca48) (0, 50)
  3. // will be compiled
  4. // 1 stack slots
  5. // temporary vars:
  6. TEMPORARY .result; // (0x7fb62281cfe8) local[0]
  7. // local vars:
  8. VAR x; // (0x7fb62281cc98)
  9. VAR foo; // (0x7fb62281cf40)
  10. function foo () { // (0x7fb62281cd50) (22, 50)
  11. // lazily parsed
  12. // 2 heap slots
  13. }
  14. }

上面这段就是 V8 生成的作用域,我们可以看到,作用域中包含了变量 x 和 foo,变量 x 的默认值是 undefined,变量 foo 指向了 foo 函数对象,foo 函数对象被 V8 存放在内存中的堆空间了,这些变量都是在编译阶段被装进作用域中的。

因为在执行之前,这些变量都被提升到作用域中了,所以在执行阶段,V8 当然就能获取到所有的定义变量了。我们把这种在编译阶段,将所有的变量提升到作用域的过程称为变量提升

对于变量提升,函数和普通的对象还是存在一些差异的,通过上面的分析我们知道,如果是一个普通变量,变量提升之后的值都是 undefined,如果是声明的函数,那么变量提升之后的值则是函数对象。

执行上面这段代码,我们可以看到,普通变量 x 的值就是 undefined,而函数对象 foo 的值则是完整的对象,那这又是为什么呢?这就是涉及到表达式和语句的区别了。

简单地理解,表达式就是表示值的式子,而语句是操作值的式子

函数声明与函数表达式 - 图4

了解了表达式和语句的区别,接下来我们继续分析上面的问题。我们知道,在 V8 执行var x = 5这段代码时,会认为它是两段代码,一段是定义变量的语句,一段是赋值的表达式,如下所示:

  1. var x = undefined
  2. x = 5

首先,在变量提升阶段,V8 并不会执行赋值的表达式,该阶段只会分析基础的语句,比如变量的定义,函数的声明。

而这两行代码是在不同的阶段完成的,var x 是在编译阶段完成的,也可以说是在变量提升阶段完成的,而x = 5是表达式,所有的表达式都是在执行阶段完成的

总的来说,在 V8 解析 JavaScript 源码的过程中,如果遇到普通的变量声明,那么便会将其提升到作用域中,并给该变量赋值为 undefined,如果遇到的是函数声明,那么 V8 会在内存中为声明生成函数对象,并将该对象提升到作用域中。

函数声明与函数表达式 - 图5


V8 怎么处理函数表达式?

函数表达式与函数声明的最主要区别有以下三点:

  • 函数表达式是在表达式语句中使用 function 的,最典型的表达式是“a=b”这种形式,因为函数也是一个对象,我们把“a = function (){}”这种方式称为函数表达式;
  • 在函数表达式中,可以省略函数名称,从而创建匿名函数(anonymous functions);
  • 一个函数表达式可以被用作一个即时调用的函数表达式——IIFE(Immediately Invoked Function Expression)。
  1. foo()
  2. var foo = function (){
  3. console.log('foo')
  4. }

当执行这段代码的时候,V8 在编译阶段会先查找声明语句,你可以把这段代码拆分为下面两行代码:

  1. var foo = undefined
  2. foo = function (){
  3. console.log('foo')
  4. }

第一行是声明语句,所以 V8 在解析阶段,就会在作用域中创建该对象,并将该对象设置为 undefined,第二行是函数表达式,在编译阶段,V8 并不会处理函数表达式,所以也就不会将该函数表达式提升到作用域中了。

那么在函数表达式之前调用该函数 foo,此时的 foo 只是指向了 undefined,所以就相当于调用一个 undefined,而 undefined 只是一个原生对象,并不是函数,所以当然会报错了。

立即调用的函数表达式 IIFE

JavaScript 中有一个圆括号运算符,圆括号里面可以放一个表达式,比如下面的代码:

  1. (a=3)

括号里面是一个表达式,整个语句也是一个表达式,最终输出 3。

如果在小括号里面放上一段函数的定义,如下所示:

  1. (function () {
  2. //statements
  3. })

因为小括号之间存放的必须是表达式,所以如果在小括号里面定义一个函数,那么 V8 就会把这个函数看成是函数表达式,执行时它会返回一个函数对象

存放在括号里面的函数便是一个函数表达式,它会返回一个函数对象,如果我直接在表达式后面加上调用的括号,这就称立即调用函数表达式(IIFE),比如下面代码:

  1. (function () {
  2. //statements
  3. })()

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