You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

330 lines
14 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 04 | 函数表达式:涉及大量概念,函数表达式到底该怎么学?
你好,我是李兵。
前面几节我们聊了V8中的对象和函数并介绍了函数为什么会被称为是一等公民了解这些之后我们就可以来学习函数表达式了。
函数表达式在JavaScript中非常基础也非常重要使用函数表达式可以用来实现代码隐藏还可以实现变量隔离所以函数表达式被广泛地应用在各个项目中了解函数表达式的底层工作机制可以帮助我们更加深刻地理解项目。
但是学好函数表达式并不容易。因为它涉及到了很多底层概念比如表达式、语句、函数即对象在JavaScript中而且函数表达式和函数声明看起来类似都是定义一个函数然后再调用该函数很容易把二者搞混淆了。
![](https://static001.geekbang.org/resource/image/51/31/51ae06e8a9dc4a589958065429bec231.jpg "你知道两者的区别吗?")
实际上,函数表达式和函数声明有着本质上的差异。理解了这种差异,你对函数表达式的理解也就加深了。
## 函数声明与函数表达式的差异
那么它们具体有什么差异呢?我们先来看一段代码:
```
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
```
这是告诉我们变量foo并不是一个函数所以无法被调用。
同样是在定义的函数之前调用函数,第一段代码就可以正确执行,而第二段代码却报错,这是为什么呢?
其主要原因是这两种定义函数的方式具有不同语义,不同的语义触发了不同的行为。
![](https://static001.geekbang.org/resource/image/a7/10/a74668eb5bf183538ce9b47a20eb0610.jpg "不同的语义,触发不同的行为")
因为语义不同,所以我们给这两种定义函数的方式使用了不同的名称,第一种称之为**函数声明**,第二种称之为**函数表达式。**
下面我们就来分别分析下函数声明和函数表达式的语义以及V8是怎么处理函数声明和函数表达式的。
## V8是怎么处理函数声明的
我们先来看函数声明,**函数声明**定义了一个具有指定参数的函数,其声明语法如下所示:
```
function name([param,[, param,[..., param]]]) {
[statements]
}
```
接下来我们来看看V8是怎么处理函数声明的。
我们知道V8在执行JavaScript的过程中会先对其进行编译然后再执行比如下面这段代码
```
var x = 5
function foo(){
console.log('Foo')
}
```
V8执行这段代码的流程大致如下图所示
![](https://static001.geekbang.org/resource/image/49/32/49eb14dd3c00438988595896c348c732.jpg)
在编译阶段如果解析到函数声明那么V8会将这个函数声明转换为内存中的函数对象并将其放到作用域中。同样如果解析到了某个变量声明也会将其放到作用域中但是会将其值设置为undefined表示该变量还未被使用。
然后在V8执行阶段如果使用了某个变量或者调用了某个函数那么V8便会去作用域查找相关内容。
关于作用域的数据你也可以使用D8来查看具体操作方式如下
* 将这段代码保存到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生成的作用域我们可以看到作用域中包含了变量x和foo变量x的默认值是undefined变量foo指向了foo函数对象foo函数对象被V8存放在内存中的堆空间了这些变量都是在编译阶段被装进作用域中的。
因为在执行之前这些变量都被提升到作用域中了所以在执行阶段V8当然就能获取到所有的定义变量了。我们把这种在编译阶段将所有的变量提升到作用域的过程称为**变量提升**。
了解了变量提升,我们就能解释,为什么可以在函数声明之前调用该函数了,这是因为声明的函数在编译阶段就被提升到作用域中,在执行阶段,只要是在作用域中存在的变量或者对象,都是可以使用的。
对于变量提升函数和普通的对象还是存在一些差异的通过上面的分析我们知道如果是一个普通变量变量提升之后的值都是undefined如果是声明的函数那么变量提升之后的值则是函数对象我们可以通过下面的代码来实践下
```
console.log(x)
console.log(foo)
var x = 5
function foo(){
}
```
执行上面这段代码我们可以看到普通变量x的值就是undefined而函数对象foo的值则是完整的对象那这又是为什么呢这就是涉及到表达式和语句的区别了。
简单地理解,表达式就是表示值的式子,而语句是操作值的式子。
比如:
```
x = 5
```
就是表达式,因为执行这段代码,它会返回一个值。同样,`6 === 5` 也是一个表达式因为它会返回False。
而语句则不同了,比如你定义了一个变量:
```
var x
```
这就是一个语句执行该语句时V8并不会返回任何值给你。
同样,当我声明了一个函数时,这个函数声明也是一个语句,比如下面这段函数声明:
```
function foo(){
return 1
}
```
当执行到这段代码时V8并没有返回任何的值它只是解析foo函数并将函数对象存储到内存中。
![](https://static001.geekbang.org/resource/image/24/43/244971073e6e41d10cefbb1de13bb343.jpg "表达式和语句")
了解了表达式和语句的区别接下来我们继续分析上面的问题。我们知道在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会在内存中为声明生成函数对象并将该对象提升到作用域中。
![](https://static001.geekbang.org/resource/image/ec/e6/ec7dc43a09baf57985b1cefda1caf4e6.jpg)
## V8是怎么处理函数表达式的
了解了函数声明,我们再来看看函数表达式。**我们在一个表达式中使用function来定义一个函数那么就把该函数称为函数表达式。**
比如:
```
foo = 1
```
它是一个表达式这时候我们把右边的数字1替换成函数定义那么这就变成了函数表达式如下所示
```
foo = function (){
console.log('foo')
}
```
函数表达式与函数声明的最主要区别有以下三点:
* 函数表达式是在表达式语句中使用function的最典型的表达式是“a=b”这种形式因为函数也是一个对象我们把“a = function (){}”这种方式称为函数表达式;
* 在函数表达式中可以省略函数名称从而创建匿名函数anonymous functions
* 一个函数表达式可以被用作一个即时调用的函数表达式——IIFEImmediately 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)
```
括号里面是一个表达式整个语句也是一个表达式最终输出3。
如果在小括号里面放上一段函数的定义,如下所示:
```
(function () {
//statements
})
```
因为小括号之间存放的必须是表达式所以如果在小阔号里面定义一个函数那么V8就会把这个函数看成是函数表达式执行时它会返回一个函数对象。
存放在括号里面的函数便是一个函数表达式,它会返回一个函数对象,如果我直接在表达式后面加上调用的括号,这就称为**立即调用函数表达式**IIFE比如下面代码
```
(function () {
//statements
})()
```
因为函数立即表达式也是一个表达式所以V8在编译阶段并不会为该表达式创建函数对象。**这样的一个好处就是不会污染环境,函数和函数内部的变量都不会被其他部分的代码访问到。**
在ES6之前JavaScript中没有私有作用域的概念如果在多人开发的项目中你模块中的变量可能覆盖掉别人的变量所以使用函数立即表达式就可以将我们内部变量封装起来避免了相互之间的变量污染。
另外,因为函数立即表达式是立即执行的,所以将一个函数立即表达式赋给一个变量时,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果。如下所示:
```
var a = (function () {
return 1
})()
```
## 总结
今天我们主要学习V8是如何处理函数表达式的。函数表达式在实际的项目应用中非常广不过由于函数声明和函数表达式之间非常类似非常容易引起人们的误解所以我们先从通过两段容易让人误解的代码分析了函数声明和函数表达式之间的区别。函数声明的本质是语句而函数表达式的本质则是表达式。
函数声明和变量声明类似V8在编译阶段都会对其执行变量提升的操作将它们提升到作用域中在执行阶段如果使用了某个变量就可以直接去作用域中去查找。
不过V8对于提升函数和提升变量的策略是不同的如果提升了一个变量那么V8在将变量提升到作用域中时还会为其设置默认值undefined如果是函数声明那么V8会在内存中创建该函数对象并提升整个函数对象。
函数表达式也是表达式的一种在编译阶段V8并不会将表达式中的函数对象提升到全局作用域中所以无法在函数表达式之前使用该函数。函数立即表达式是一种特别的表达式主要用来封装一些变量、函数可以起到变量隔离和代码隐藏的作用因此在一些大的开源项目中有广泛的应用。
## 思考题
留给你一道经典面试题,看看下面这两段代码打印出来的结果是什么?欢迎你在留言区与我分享讨论。
```
var n = 1;
(function foo(){
n = 100;
console.log(n);
}())
console.log(n);
```
```
var n = 1;
function foo(){
n = 100;
console.log(n);
}
console.log(n);
foo()
```
感谢你的阅读,如果你觉得这一讲的内容对你有所启发,也欢迎把它分享给你的朋友。