函数表达式:涉及大量概念,函数表达式到底该怎么学?

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

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

在这段代码中,我声明了一个foo函数,然后在foo函数之前调用了foo函数,执行这段代码,我们看到foo函数被正确执行了。

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

在这段代码中,我定义了一个变量foo,然后将一个函数赋值给了变量foo,同样在源码中,
我们也是在foo函数的前面调用foo,执行这段代码,我们发现报错了,提示的错误信息如下
所示:

VM130:1 Uncaught TypeError: foo is not a function
at <anonymous>:1:1

为什么会报错?
其主要原因是这两种定义函数的方式具有不同语义,不同的语义触发了不同的行为。

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

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

函数声明

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

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

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

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

在编译阶段,如果解析到函数声明,那么V8会将这个函数声明转换为内存中的函数对象,并将其放到作用域中。同样,如果解析到了某个变量声明,也会将其放到作用域中,但是会将其值设置为undefined,表示该变量还未被使用。
将这段代码保存到test.js中;
使用“d8 --print-scopes test.js”命令即可查看作用域的状态。
执行后结果:

Global scope:
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解析JavaScript源码的过程中,如果遇到普通的变量声明,那么便会将其提升到作用域中,并给该变量赋值为undefined,如果遇到的是函数声明,那么V8会在内存中为声明生成函数对象,并将该对象提升到作用域中。

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

我们在一个表达式中使用function来定义一个函数,那么就把该函数称为函数表达式。
比如
foo = 1
它是一个表达式,这时候我们把右边的数字1替换成函数定义,那么这就变成了函数表达式,
如下所示

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

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

  • 函数表达式是在表达式语句中使用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中的立即函数调用表达式正是使用了这个特性来实现了非常广泛的应用,下面我们就来一起看看立即函数调用表达式。
JavaScript中有一个圆括号运算符,圆括号里面可以放一个表达式,比如下面的代码:

(a=3)

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

(function () {
    //statements
})

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

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

因为函数立即表达式也是一个表达式,所以V8在编译阶段,并不会为该表达式创建函数对象。这样的一个好处就是不会污染环境,函数和函数内部的变量都不会被其他部分的代码访问到。
在ES6之前,JavaScript中没有私有作用域的概念,如果在多人开发的项目中,你模块中的变量可能覆盖掉别人的变量,所以使用函数立即表达式就可以将我们内部变量封装起来,避免了相互之间的变量污染。
另外,因为函数立即表达式是立即执行的,所以将一个函数立即表达式赋给一个变量时,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果。如下所示:

var a = (function () {
    return 1
})()
此文章为5月Day11学习笔记,内容来源于极客时间《图解 Google V8》,日拱一卒,每天进步一点点💪💪

豪猪
4 声望4 粉丝

undefined