Published on

V8相关(二)

Authors
  • avatar
    Name
    Et cetera
    Twitter

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

首先,函数表达式和函数声明有着本质上的差异

// 声明了一个 foo 函数,然后在 foo 函数之前调用了 foo 函数,执行这段代码,foo 函数被正确执行了
foo()
function foo() {
  console.log('foo')
}
// 同样执行foo()函数
foo()
var foo = function () {
  console.log('foo')
}
// 执行这段代码报错如下:
// VM130:1 Uncaught TypeError: foo is not a function
// at <anonymous>:1:1

// 这是告诉我们,变量 foo 并不是一个函数,所以无法被调用

同样两段代码,结果不同,具体原因如图所示:

因为语义不同,所以我们给这两种定义函数的方式使用了不同的名称,第一种称之为函数声明,第二种称之为函数表达式

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

函数声明:

function name([param,[, param,[..., param]]]) {
  [statements]
}

V8 处理函数声明的过程:先对其进行编译,然后再执行

var x = 5
function foo() {
  console.log('foo')
}

编译阶段,如果解析到函数声明,那么 V8 会将这个函数声明转换为内存中的函数对象,并将其放到作用域中。同样,如果解析到了某个变量声明,也会将其放到作用域中,但是会将其值设置为 undefined,表示该变量还未被使用

然后在 V8 执行阶段,如果使用了某个变量,或者调用了某个函数,那么 V8 便会去作用域查找相关内容

// D8查看作用域
Global global { // (0x7fb62281ca48) (0, 50)
  // will be compiled
  // 1 stack slots
  // temporary vars:
  TEMPORARY .result; // (0x7fb62281cfe8) local[0]
  // local vars:
  VAR x; // (0x7fb62281cc98)
  VAR foo; // (0x7fb62281cf40)

  function foo () { // (0x7fb62281cd50) (22, 50)
  // lazily parsed
  // 2 heap slots
  }
}

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

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

所以为什么可以在函数声明之前调用该函数了:这是因为声明的函数在编译阶段就被提升到作用域中,在执行阶段,只要是在作用域中存在的变量或者对象,都是可以使用的

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

console.log(x) // undefined
console.log(foo) // [Function: foo]
var x = 10969
function foo() {}

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

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

// V8执行这段代码时
var x = 5

// 等同于执行
var x = undefined // 定义变量的语句
x = 5 // 赋值的表达式

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

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

变量提升阶段,V8 将这些变量存放在作用域时,还会给它们赋一个默认的 undefined值,所以在定义一个普通的变量之前,使用该变量,那么该变量的值就是 undefined

表达式是不会在编译阶段执行的

然后看执行函数:

function foo() {
  console.log('foo')
}

执行上面这段代码,它并没有输出任何内容,所以可以肯定,

函数声明并不是一个表达式

,而是一个语句.V8 在变量提升阶段,如果遇到函数声明,那么 V8 同样会对该函数声明执行变量提升操作

函数也是一个对象,所以在编译阶段,V8 就会将整个函数对象提升到作用域中,并不是给该函数名称赋一个 undefined,理解这一点尤为重要

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

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

我们在一个表达式中使用 function关键字 来定义一个函数,那么就把该函数称为函数表达式

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

  • 函数表达式是在表达式语句中使用 function 的,最典型的表达式是a = b这种形式,因为函数也是一个对象,我们把a = function (){}这种方式称为函数表达式
  • 在函数表达式中,可以省略函数名称,从而创建匿名函数(anonymous functions)
  • 一个函数表达式可以被用作一个即时调用的函数表达式——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 中的立即函数调用表达式正是使用了这个特性来实现了非常广泛的应用,下面我们就来一起看看立即函数调用表达式

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

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

;(function () {
  // statements
})()

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

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