引入
想象一下,你是Javascript的初学者,当你写下以下代码
console.log(myName);
myName='帅哥'
当你满怀期待的运行代码,你满怀信心地按下运行键,结果输出了 undefined。你挠挠头:“我明明还没赋值,为什么不是报错?”
这时,JavaScript 的编译器默默在背后说:“兄弟,我在偷偷帮你搬家呢。”
这就是我们今天要聊的话题——变量提升(Hoisting) ,以及它背后的三位主角:var、let 和 const。
我们先介绍一下JS执行的机制
- 有一段代码 硬盘读入内存
- V8引擎 chrome 心脏,负责解析和执行代码
- 编译阶段
- 执行阶段
var和函数变量提升
var变量提升
var 是JS早期声明变量的方式,它的特点
只提升声明,不提升赋值
函数作用域
允许重复声明
简单看完以下代码
console.log(age); // undefined
var age = 25;
在JS中其实被翻译成了这样:变量被提升至了当前作用域顶部,只提升变量是因为:声明变量是编译阶段的,赋值是在js代码执行才发生的,其实就是以下代码
var age;//声明变量被提升到顶部
console.log(age);//undefined
age=25
函数变量变量提升
函数变量提升,是整个函数定义的提升,简单来说,就是函数内部的代码也被提升了
sayHello(); // 输出 "Hello!"
function sayHello() {
console.log("Hello!");
}
const和let的崛起
let和const变量提升
有一天,JavaScript 决定整顿秩序,引入了两个新成员:let 和 const。
它们带来了新的规则:TDZ(暂时性死区)。var和函数变量提升是在环境变量和let、const是不同的
let和const变量提升就在TDZ(暂时性死区),与var和函数变量分割开来
什么意思?就是说:你在变量声明之前访问它,就会直接报错!
console.log(a);//报错:ReferenceError!
let a=2;
let vs const
let:可以重新赋值,但不能重复声明const:一旦声明就不能再赋值(注意是不能重新赋值,如果是对象或数组,里面的属性还是可以改的
let num1=20
let num1=30//报错,不能重复声明
nu1=30//可以重新赋值
const num2=20//常量,不能重新赋值
num2=30//报错,不能重新赋值
const obj={
name:'帅哥'
}//对象可以修改
obj.name='美女'
console.log(obj.name)//输出美女
作用域战争
我们先了解一下作用域
在 JavaScript 中,作用域 (Scope) 定义了变量在代码中哪些部分是可见和可访问的。 它就像一个容器,规定了变量的生命周期和作用范围。 理解作用域对于编写正确、可维护的 JavaScript 代码至关重要,因为它直接影响着变量的查找和访问方式
全局作用域
全局作用域,见名知意,就是全局能够访问的变量
let num = 100;
function fn() {
console.log(num);
}
fn()//输出100,
可以看到外面再全局声明的变量是可以再函数内部访问的,因为在全局作用域中声明了num,所以在函数作用域中可以访问到num,但是在函数作用域中没有声明num,所以会向上查找,找到全局作用域中的num,所以输出100
函数作用域
在函数内部声明的变量在函数外部是无法访问的
function fn() {
let num = 10;
}
fn()
console.log(num);//num is not defined
当我们在函数作用域里面先打印再声明变量,和全局作用域类似,也是提升到当前作用域的TDZ(暂时性死区),var也和在全局作用域的时候类似
function fn() {
console.log(a);
let a = 1;
}
fn()//ReferenceError: Cannot access 'a' before initialization
块级作用域
这是 let 和 const 的专属领地。任何 {} 都可以成为它们的作用域边界。
if (true) {
let blockVar = "块级变量";
}
console.log(blockVar); // 报错:blockVar is not defined
如果是var的话就会输出内容
if (true) {
var blockVar = "块级变量";
}
console.log(blockVar); // 输出 块级变量
作用域链与查找机制
变量查找
变量查找的本质就是一级一级往上面查找所需的变量当你在一个函数中访问一个变量时,JavaScript 会按照以下顺序查找:当前作用域 -> 父级作用域 ...-> 全局作用域 没找到的话就会报错
以下代码inner()这个函数在当前作用域是能找到name1这个变量的,而outer(),在其内部是没有name2这个变量的,所以得往上一级查找,就在全局作用域找到了这个变量,所以打印了张三
let name2 = "张三";
function outer() {
function inner() {
let name1 = "王五";
console.log(name1); // 王五
}
inner();
console.log(name2); // 张三
}
outer();//张三
闭包
闭包是作用域链的一个高级玩法,简单来说就是函数记住了它创建时的环境。每次调用,count都会+1
function counter() {
let count = 0;
return function () {
count++;
console.log(count);
};
}
const increment = counter();
increment(); // 1
increment(); // 2
var let const 区别
| 特性 | var | let | const |
|---|---|---|---|
| 是否有 TDZ | ❌ | ✅ | ✅ |
| 是否允许重复声明 | ✅ | ❌ | ❌ |
| 是否可重新赋值 | ✅ | ✅ | ❌ |
| 作用域类型 | 函数作用域 | 块级作用域 | 块级作用域 |
## 结语:编程语言也是人写的,也有脾气
JavaScript 一开始是个“临时工”,为了打商战快速开发浏览器脚本而诞生。它容忍了很多“坏习惯”,比如变量提升、重复声明等等。
但随着前端工程化的深入,这些“坏习惯”成了隐患。于是,es6 应运而生,带来了 let 和 const,也带来了更清晰的变量管理机制。
🧠 小测验(答案见文末)
相信看完这篇文字你对变量提升和作用域有了一定的了解来做一个小测验
以下代码会输出什么内容
console.log(a);
var a=1
以下代码会报错吗
console.log(a);
let a=1
以下代码会输出什么内容
function foo() {
var x = 1;
if (true) {
var x = 2;
console.log(x);
}
console.log(x);
}
foo();
以下代码输出了什么
const obj = { name: "Tom" };
obj.name = "Jerry";
console.log(obj.name);
📚 参考资料
以下是MDN参考资料
✅ 答案揭晓
undefined- 报错:
ReferenceError 2和2"Jerry"(对象内容可变,只是引用不可变)
希望这篇文章对大家有所帮助,能够帮大家更好理解变量提升和作用域