1. 原因
  2. 内部过程
  3. 参考

函数声明与函数表达式

偶然看到了一道题, 问如下代码中的console输出什么:

1
2
3
4
5
var a = 1;
(function a(){
a = 2;
console.log(a)}
)()

结果console输出的不是数值2,而是函数本身:

1
2
3
4
ƒ a(){
a = 2;
console.log(a)
}

原因

有两种声明函数的方式:

  • 函数声明
1
2
3
function foo() {
console.log('I am foo')
}
  • 函数表达式
1
2
3
4
5
6
7
8
9
10
11
var bar = function foo() {
console.log('I am foo')
}
// 这是个函数表达式,而非函数声明
(function foo() {
console.log('I am foo')
})()
// 匿名函数表达式
(function() {
console.log('I am foo')
})()

函数表达式内部有一个特殊的变量.该变量和函数名相同,指向函数本身,无法被修改.

变量foo只能在函数内部使用:

1
2
3
4
var bar = function foo() {
console.log(foo) // ƒ foo() { ...
}
bar()

变量foo不能在函数外使用:

1
2
3
4
var bar = function foo() {
console.log(foo)
}
foo() // ReferenceError: foo is not defined

变量foo不能被修改:

1
2
3
4
5
var bar = function foo() {
foo = 1
console.log(foo) // ƒ foo() { ...
}
bar()

但如果在内部用var声明一个同名变量,则foo会被覆盖:

1
2
3
4
5
var bar = function foo() {
var foo = 1
console.log(foo) // 1
}
bar()

不同于函数表达式, 函数声明内部没有这个特殊变量.
虽然用"函数声明"创建的函数也能在内部用调用foo,但其实调用的是函数外部作用域的foo.

1
2
3
4
5
6
7
function foo() {
foo = 1
console.log(foo) // 1
}
foo()
// foo 变成了1
console.log(foo) // 1

作为对比,函数表达式的表现:

1
2
3
4
5
6
7
var bar = function foo() {
foo = 1
console.log(foo) // ƒ foo() { ...
}
bar()
// 无论内外,都没有变
console.log(bar) // ƒ foo() { ...

最后,再回到最上面的这道题:

1
2
3
4
5
var a = 1;
(function a(){
a = 2;
console.log(a)}
)()

函数a是一个函数表达式,而非函数声明(因为被群组运算符包裹).
其内部变量a指向其本身,且无法被修改.因此"a = 2" 这句话是无效的.
最终console.log输出函数本身.

内部过程

函数声明的语句结构为:

1
2
FunctionExpression :
function Identifieropt ( FormalParameterList opt ) { FunctionBody }

函数声明的内部解析过程如下:

FunctionExpression : function Identifier ( FormalParameterList opt ) { FunctionBody } is evaluated as follows:
1 Let funcEnv be the result of calling NewDeclarativeEnvironment passing the running execution context’s Lexical Environment as the argument
2 Let envRec be funcEnv’s environment record.
3 Call the CreateImmutableBinding concrete method of envRec passing the String value of Identifier as the argument.
4 Let closure be the result of creating a new Function object as specified in 13.2 with parameters specified by FormalParameterListopt and body specified by FunctionBody. Pass in funcEnv as the Scope. Pass in true as the Strict flag if the FunctionExpression is contained in strict code or if its FunctionBody is strict code.
5 Call the InitializeImmutableBinding concrete method of envRec passing the String value of Identifier and closure as the arguments.
6 Return closure.

1 令 funcEnv 为调用 NewDeclarativeEnvironment 的结果(用当前执行环境的 Lexical Environment 作为参数).
2 令 envRec为 funcEnv 的环境记录项.
3 调用envRec的CreateImmutableBinding方法(用Identifier为参数).
4 令closure 为依照13.2创建的函数对象(用FormalParameterList,FunctionBody, funcEnv(作为scope) strict作为参数.如果函数表达式处于严格模式/内部包含'use strict',则strict参数为true,否则为false).
5 调用envRec的InitializeImmutableBinding方法(用Identifier字符串和closure作为参数).
6 返回closure.

其中步骤3 ~ 5 的作用为:
3 在函数内部创建了一个和函数同名的"不可变"变量
4 创建函数对象
5 初始化步骤3创建的不可变变量,将其指向步骤4创建的函数对象

作为对比,函数声明的内部解析过程如下:

The production FunctionDeclaration : function Identifier ( FormalParameterListopt ) { FunctionBody } is instantiated as follows during Declaration Binding instantiation (10.5):
1 Return the result of creating a new Function object as specified in 13.2 with parameters specified by FormalParameterListopt, and body specified by FunctionBody. Pass in the VariableEnvironment of the running execution context as the Scope. Pass in true as the Strict flag if the FunctionDeclaration is contained in strict code or if its FunctionBody is strict code.

和函数表达式不同,函数声明仅创建了一个函数对象,其内部作用域没有特殊的同名变量.

参考

ECMAScript Language Specification - ECMA-262 Edition 5.1
ES5/函数定义 - HTML5 Chinese Interest Group Wiki