函数表达式与函数声明

一道面试题引发的思考

从上到下依次说出执行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var foo = function () {
console.log("foo1");
};
foo();

var foo = function () {
console.log("foo2");
};
foo();

function foo() {
console.log("foo1");
}
foo();

function foo() {
console.log("foo2");
}
foo();

输出结果:foo1 foo2 foo2 foo2

分析

拆分函数表达式

函数表达式是将函数作为一个值赋给一个变量或属性

1
2
3
4
5
6
7
8
9
10
11
var foo = function () {
console.log("foo1");
};
foo();

var foo = function () {
console.log("foo2");
};
foo();

// foo1 foo2
  • foo 首先会变量提升,然后进行赋值为 function
  • 当执行第一个 foo 的时候,此时 foo 就是我们赋值的这个函数。
  • 接着执行第二个 foo 的赋值操作,由于函数作用域的特性,后面定义的函数将覆盖前面定义的函数。
  • 由于在调用函数之前就进行了函数的重新定义,所以在调用函数时,实际执行的是最后定义的那个函数
  • 打印:foo1、foo2

这种定义函数的方式,我们称为函数表达式

拓展:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
foo();
var foo = function () {
console.log("foo1");
};

var foo = function () {
console.log("foo1");
};
foo();

var foo = function () {
console.log("foo2");
};
foo();

上面代码直接报错

  • 用 var 定义的变量会变量提升
  • 声明会被拿到函数或全局作用域的顶部,并且输出 undefined
  • 当执行 foo() 的时候,foo 还是 undefined,所以会报错
  • 由于 js 从按照顺序从上往下执行,所以当执行 foo = function(){} 的时候,才对 foo 进行赋值为一个函数。

拆分函数声明

1
2
3
4
5
6
7
8
9
10
11
function foo() {
console.log("foo1");
}
foo();

function foo() {
console.log("foo2");
}
foo();

// foo2 foo2
  • 函数声明会在任何代码执行之前先被读取并添加到执行上下文,也就是函数声明提升。
  • 这里使用了函数声明定义了两个 foo 函数
  • 由于函数声明提升,第二个 foo 会覆盖第一个 foo
  • 当调用第一个 foo 的时候,其实已经被第二个 foo 覆盖了,所以这两个打印的都是 foo2。

当两段代码结合

  • 当开始解析的时候,函数声明就已经提升了
  • 第四个 foo 会覆盖第三个 foo
  • 然后 js 开始从上往下执行
  • 第一个赋值操作之后执行 foo()后,打印了 foo1
  • 第二个赋值之后执行 foo(),打印了”foo2”
  • 下面两个 foo 的执行其实是第二个赋值了的 foo,因为函数声明开始从刚开始就被提升了,而下面的赋值会覆盖 foo

总结

整体分析代码的执行过程

  1. 通过函数表达式定义变量 foo 并赋值为一个匿名函数,该函数在被调用时打印foo1
  2. 接着,通过函数表达式重新定义变量 foo,赋值为另一个匿名函数,该函数在被调用时打印foo2
  3. 使用函数声明定义了两个名为 foo 的函数。函数声明会在作用域中进行提升。后面的会覆盖前面的,由于声明从一开始就提升了,而又执行了两个赋值操作,所以此时 foo 是第二个赋值的函数。
  4. 然后调用 foo(),输出foo2
  5. 再调用 foo(),也输出foo2

函数表达式与函数声明的区别

  • 函数声明在代码解析阶段就会被提升(函数声明提升)
  • 函数表达式则需要在赋值语句执行到达时才会创建函数对象