函数与变量的优先级:搞懂这3个核心场景,再也不踩坑
在 JavaScript 开发中,我们经常会遇到这样的困惑:当函数和变量同名时,到底谁会被优先访问?为什么有时候打印的是函数,有时候却是变量值?其实这背后藏着 JS 引擎的执行机制——变量提升与函数提升的优先级规则。
今天这篇文章,就从「提升机制的本质」出发,通过 3 个核心场景的代码案例,帮你彻底搞懂函数与变量的优先级逻辑,从此避开这类基础却致命的 bugs。
一、先理清:变量提升 vs 函数提升
在聊优先级之前,我们必须先明确两个基础概念:变量提升(Hoisting)和函数提升。这是 JS 引擎在执行代码前的「预编译」阶段会做的核心操作。
简单来说:
- 变量提升:var 声明的变量会被提升到当前作用域顶部,但仅提升声明,不提升赋值(值为 undefined);let/const 声明的变量也会提升,但存在「暂时性死区」,不能在声明前访问。
- 函数提升:函数声明(function 关键字声明)会被完整提升到当前作用域顶部,包括函数体;而函数表达式(如 var fn = function(){})不会提升函数体,仅提升变量声明(同 var 变量)。
而函数与变量的优先级,核心就体现在「提升阶段的竞争」——谁在提升后更“靠前”,谁就会被优先访问。
二、3 个核心场景,吃透优先级规则
场景 1:函数声明 vs var 变量(同名)
先看一段经典代码:
console.log(a); // 输出:function a() {} var a = 10; function a() {} console.log(a); // 输出:10为什么第一次打印的是函数,而不是 undefined 或 10?这就是优先级规则在起作用:函数声明的提升优先级高于 var 变量声明。
拆解 JS 引擎的预编译和执行过程:
- 预编译阶段:先提升函数声明 function a() {},再提升 var a(但由于 a 已经被函数声明占用,var a 的提升会被忽略,不会重复声明)。此时作用域顶部的 a 是函数 a() {}。
- 执行阶段:第一行 console.log(a),访问的是提升后的函数 a,所以输出函数体。
- 执行 var a = 10; 时,是对已存在的 a 进行赋值,将函数覆盖为 10。
- 第二行 console.log(a),此时 a 已经是 10,所以输出 10。
注意:这里是「变量声明」被忽略,不是「赋值」被忽略。如果变量声明后有赋值操作,依然会覆盖函数。
场景 2:函数声明 vs let/const 变量(同名)
如果把 var 换成 let/const,情况会完全不同:
console.log(a); // 报错:Cannot access 'a' before initialization let a = 10; function a() {}为什么会报错?因为 let/const 存在「暂时性死区(TDZ)」:
虽然 let/const 变量也会提升,但提升后会被置于暂时性死区中,在声明语句(let a = 10)执行前,任何访问都会报错。此时即使有同名的函数声明,也无法突破暂时性死区——let/const 的暂时性死区优先级高于函数提升。
补充:如果函数声明在 let 声明之后,同样会报错,因为同名的 let 变量提升后占据了标识符,函数声明无法重复定义:
let a = 10; function a() {} // 报错:Identifier 'a' has already been declared场景 3:函数表达式 vs 变量(同名)
函数表达式(如 var fn = function(){})本质上是「变量声明 + 函数赋值」,所以它的提升规则和 var 变量完全一致,优先级自然也和 var 变量相同(不存在函数声明的高优先级)。
看代码案例:
console.log(b); // 输出:undefined var b = function() { console.log('函数表达式'); }; var b = 20; console.log(b); // 输出:20拆解过程:
- 预编译阶段:提升两个 var b 声明,由于重复声明,仅保留一个,此时 b 的值为 undefined。
- 执行阶段:第一行 console.log(b),输出 undefined。
- 执行 var b = function(){},将 b 赋值为函数。
- 执行 var b = 20,将 b 赋值为 20,覆盖函数。
- 第二行 console.log(b),输出 20。
这里要注意:函数表达式的函数体不会被提升,所以第一次打印的是 undefined,而不是函数——这是和函数声明的核心区别。
三、总结:优先级核心规则(表格梳理)
为了方便记忆,用表格汇总不同场景下的优先级顺序(从高到低):
| 场景 | 优先级顺序 | 核心结论 |
|---|---|---|
| 函数声明 vs var 变量 | 函数声明 > var 变量声明 | 同名时,预编译后先存在函数,var 声明被忽略,赋值后覆盖函数 |
| 函数声明 vs let/const 变量 | let/const 暂时性死区 > 函数声明 | 同名时,声明前访问报错;声明后函数无法重复定义 |
| 函数表达式 vs 变量(任意声明) | 同变量优先级(无函数提升优势) | 函数表达式仅提升变量声明,值为 undefined,赋值后覆盖 |
四、实战避坑建议
理解了优先级规则后,在实际开发中可以通过以下几点避免踩坑:
- 避免函数和变量同名:这是最根本的解决办法,尽量让标识符具有唯一性,减少优先级竞争。
- 优先使用 let/const 替代 var:let/const 的暂时性死区可以避免“声明前访问”的bug,同时不允许重复声明,让代码更严谨。
- 区分函数声明和函数表达式:如果需要函数在声明前被访问,用函数声明;如果需要控制函数的可用时机,用函数表达式(配合 var/let 声明)。
- 养成“先声明后使用”的习惯:无论变量还是函数,尽量在作用域顶部声明,避免依赖提升机制,让代码可读性更强。
五、最后案例
console.log(c); var c = 30; function c() { console.log('c1'); } var c = function() { console.log('c2'); }; console.log(c);核心提示:结合场景 1 和场景 3 的规则,拆解预编译和执行过程。
总结一下:函数与变量的优先级,本质是「提升机制的竞争」。记住“函数声明优于 var 变量声明,let/const 暂时性死区优于函数声明”这两个核心点,再结合具体场景拆解,就能轻松应对所有相关问题。