先记住这个答案
区别在于函数什么时候被创建。函数声明在进入作用域时就创建好了,名字和函数体一起提升,所以可以在声明之前调用。函数表达式是把一个函数赋给变量,只有变量声明会按 var、let、const 各自的规则提升,函数值要等执行到赋值那一行才赋上。所以 var 写法提前调用时变量是 undefined,报 TypeError;let 和 const 写法提前访问会报 ReferenceError。
- 函数声明连名字带函数体一起提升
- 函数表达式只提升左边的变量,函数值要执行到才赋上
var提前调用报TypeError,let提前调用报ReferenceError- 具名函数表达式的名字只在函数内部能用
三种写法提前调用的结果
把三种写法都放在调用语句后面,结果一目了然。
var 表达式、let 表达式对比JavaScriptconsole.log(declared()) // 函数声明,可以提前调用
try {
byVar()
} catch (e) {
console.log(e.name) // byVar 此时是 undefined
}
try {
byLet()
} catch (e) {
console.log(e.name) // byLet 还在暂时性死区
}
function declared() { return 'ok' }
var byVar = function () { return 'var' }
let byLet = function () { return 'let' }查看输出与解释
ok
TypeError
ReferenceErrordeclared 在代码开始执行前就已经是一个函数了。byVar 的变量声明被提升,初始值是 undefined,对 undefined 做调用就是 TypeError: byVar is not a function。byLet 的变量也登记了,但在 let 那一行执行前不允许访问,所以是 ReferenceError。
两种错误的含义不同:TypeError 说明变量存在但值不对,ReferenceError 说明变量根本不能访问。排查时看错误类型就能知道是哪种写法出的问题。
具名函数表达式的名字在哪能用
函数表达式也可以带名字,比如 const factorial = function fact(n) {}。这个 fact 和函数声明的名字不一样,它不会出现在外层作用域里,只在函数体内部可以用。
- 递归时用内部名字更可靠
- 在函数体里用
fact(n - 1)递归,即使外面的factorial后来被重新赋值,或者这个函数被传到别处,内部的fact始终指向它自己。在外面写fact(5)会报ReferenceError,因为外层没有这个名字。另外具名表达式在报错堆栈里显示的是fact,排查问题更方便。 - 块里的函数声明别依赖
- 在
if块里写函数声明,严格模式下它只在块内可见;非严格模式下为了兼容老代码,浏览器还会在外层创建一个同名var,具体行为很绕。项目里需要条件定义函数时,直接用const fn = cond ? a : b这种表达式写法,就不会有歧义。
容易答错的地方
- 说函数表达式完全不提升
- 左边的变量是会提升的,
var提升后值是undefined,let和const提升后处于暂时性死区。提升的是变量,没有提升的是函数值。 - 同名时以为后面的
var会覆盖函数声明 var a; function a() {}放在一起,提升阶段函数声明优先,只写了var a而没有赋值时,a还是那个函数。但如果写成var a = 1,执行到这一行时会被赋成 1。
面试官还会怎么问?
箭头函数算声明还是表达式?
箭头函数只能以表达式形式出现,必须赋给变量或作为参数传入,所以它遵循函数表达式的规则,不能提前调用。
export default function () {} 是声明还是表达式?
它是函数声明,可以没有名字,而且会被提升。它的 name 属性是 default。
项目里应该用哪种?
两种都常见。函数声明适合模块里的工具函数,写在文件底部也能在上面调用;const 加箭头函数适合回调和组件内部函数,能防止被重新赋值。团队统一一种风格,并用 ESLint 的 func-style 规则约束即可。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。