Skip to content

函数声明与函数表达式:差异不在写法

函数声明和函数表达式看起来都能"定义一个函数",但它们进入运行时的路径完全不同。区别不在于 function foo() {} vs const foo = function() {} 的语法,而在于引擎何时创建绑定、何时生成函数对象、名称在什么范围内可见。


差异的根源:创建时机

函数声明在当前作用域的 DeclarationInstantiation 阶段完成绑定。在代码真正逐行执行之前,环境记录里已经存在这个函数名,直接指向对应的函数对象。

js
foo(); // ✅ 可以调用

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

函数表达式不同。var bar = function() {} 中,var bar 的声明确实在 DeclarationInstantiation 阶段进入环境记录,但初始值为 undefined。函数对象在赋值语句实际执行时才被创建并赋给 bar

js
bar(); // TypeError: bar is not a function

var bar = function () {
  console.log('ok');
};

三种声明方式在调用前的状态:

js
console.log(typeof foo); // 'function' —— 函数声明,绑定已包含函数对象
console.log(typeof bar); // 'undefined' —— var 已提升但值为 undefined
console.log(baz);         // ReferenceError —— const 在 TDZ 中

function foo() {}
var bar = function () {};
const baz = function () {};

函数声明的覆盖规则

同一作用域内的同名函数声明,后声明的覆盖先声明的——而且这个覆盖在 DeclarationInstantiation 阶段就已确定:

js
console.log(foo()); // 2

function foo() { return 1; }
function foo() { return 2; }

这不是执行到了第二行才覆盖。在 DeclarationInstantiation 阶段,引擎按源码顺序处理每个函数声明——第二个 foo 声明直接覆盖第一个的绑定。到执行阶段,foo 已经指向第二个函数。

函数表达式则遵循普通的赋值规则:按执行顺序逐个赋值。

这个差异在条件分支中产生复杂的行为:

js
if (flag) {
  function foo() {}
}

不同引擎对块内函数声明的处理不同——现代规范(ES6+)允许在块内声明函数,但提升行为是"部分提升"(绑定提升到外层,但函数体只在进入该块时赋值)。问题的根源是 Annex B 的向后兼容——旧浏览器(IE)中块内函数声明会被当作全局函数声明来处理。

工程建议:不要在块内使用函数声明。如果需要条件性创建函数,用函数表达式:

js
let foo;
if (flag) {
  foo = function () {};
}

命名函数表达式的名称绑定

js
const fn = function inner() {
  return inner(); // ✅ inner 在函数体内可见
};

inner(); // ReferenceError —— inner 在外部不可见

inner 的名称绑定只存在于函数体内部的作用域。规范中这是通过创建一个新的函数作用域(FunctionExpressionScope)实现的,inner 被作为 const 绑定添加到该作用域中——不可覆盖。

这对递归特别有价值:

js
const factorial = function calc(n) {
  if (n <= 1) return 1;
  return n * calc(n - 1);
};

const copy = factorial;
let factorial = null;
copy(5); // 120 —— calc 内部绑定不受外部 factorial 重赋值影响

IIFE:表达式语境的本质

js
// ❌ 语法错误
function () {}();

// ✅ 括号把它从声明语境切换到表达式语境
(function () {})();

原因在于解析器:当它看到 function token 时,默认按函数声明规则解析——要求必须有标识符。function () {} 缺少标识符,解析失败。括号改变了上下文——在 Grouping Operator () 内部,function 被解析为函数表达式,此时标识符是可选的,且表达式结果允许直接跟调用运算符 ()

其他创建表达式语境的方式:

js
void function () {}();
!function () {}();
+function () {}();
0, function () {}();

但括号是最清晰的方式。在现代代码中,IIFE 的使用频率已大幅下降——模块系统(ESM)和块级作用域(let/const)解决了 IIFE 原本要解决的问题(作用域隔离)。


调试栈与命名

匿名函数表达式在错误栈中显示为 anonymous,增加排查成本。现代引擎能通过变量名推断函数名(name inference),但这是引擎的附加能力,不是语言层面的稳定保证。

js
const handler = function clickHandler() {
  throw new Error('fail');
};
// 错误栈中显示 'clickHandler',比 'handler' 或 '<anonymous>' 更有用

在生产代码中,作为回调传递的命名函数表达式能显著降低 error tracking 中的识别成本——特别是被多层传递后。