JavaScript 作用域
写了这么多代码,有没有想过一个问题。为什么有的变量在哪里都能用,有的变量只能在函数内部用,还有的变量出了花括号就消失了。这些现象都和变量声明的位置有关。作用域就是变量可访问的范围,这篇把作用域彻底讲清楚,它是理解下一篇闭包的基础。
全局作用域
在函数和花括号块之外声明的变量属于全局作用域,任何地方都能访问,包括函数内部。
const siteName = '我的博客';
function showSite() {
console.log(siteName); // 函数内也能访问
}
showSite(); // 输出 我的博客
console.log(siteName); // 输出 我的博客
siteName 声明在最外层,函数内函数外都能用。全局变量虽然方便,但建议少用。多个脚本都往全局放变量,名字很容易冲突,一个地方改错所有地方受影响,代码一多就难以维护。
函数作用域
函数内部声明的变量属于局部作用域,只在本函数内可访问,函数外面访问不到。
function greet() {
const message = '你好';
console.log(message); // 函数内可以
}
greet();
console.log(message); // 报错,message 未定义
函数执行完,内部的局部变量就被回收了。两个函数声明同名变量互不干扰,因为各自的变量只在自己的函数里。
function fn1() {
const name = '小明';
console.log(name);
}
function fn2() {
const name = '小红';
console.log(name);
}
fn1(); // 输出 小明
fn2(); // 输出 小红
块级作用域
let 和 const 声明的变量还有一个特性,只在最近的 {} 花括号内有效,这叫块级作用域。if 和 for 的花括号都是块。
if (true) {
const num = 10;
console.log(num); // 输出 10
}
console.log(num); // 报错,num 在块外不存在
for 的循环变量就是典型例子,循环结束 i 就消失了。
for (let i = 0; i < 3; i++) {
console.log(i);
}
console.log(i); // 报错
这个特性让代码更安全,块内临时用的变量不会泄漏到外面。for 循环那篇提到的”每次迭代独立绑定”也是靠这个特性。变量声明到底用 let 还是 const,可以参考JavaScript 变量声明 let 与 const那篇。
变量查找从内到外
内层作用域使用变量时,先找自己这一层,找不到就往外层找,再找不到继续往外,直到全局作用域。找到就停止,不会回头找里层的同名变量。
const name = '全局的小明';
function outer() {
const name = '函数的小明';
function inner() {
console.log(name); // 输出 函数的小明
}
inner();
}
outer();
inner 内部没有声明 name,往外找到 outer 里的 name,用它,不再继续往外找。如果 inner 自己声明了 name,就用自己那个,外层的更看不到。
作用域链的直观理解
把作用域想象成一层套一层的盒子。最外层是全局盒子,函数每嵌套一层就多一个盒子。变量查找就像从最内层盒子开始,一层层往外翻,找到就停,翻到全局还没有就报未定义的错误。
const a = 1; // 全局盒子
function fn1() { // 盒子 1,包住下面的内容
const b = 2;
function fn2() { // 盒子 2,fn1 的子盒子
const c = 3;
console.log(a + b + c); // 内层可以访问外层的 a 和 b
}
}
fn2 里能同时用 a、b、c,因为沿着盒子链从内到外都找得到。反过来外层盒子永远看不到内层盒子的变量。这条从内到外的链就叫作用域链,下一篇闭包的本质就和它有关。
声明前访问会报错
let 和 const 还有一个特点,声明之前不可访问。在声明语句之前使用变量会报错,这段时间称为暂时性死区。
console.log(num); // 报错,num 在声明前被使用
const num = 5;
把 const 换成 let 效果一样,都报错。但函数声明不一样,函数声明有提升机制,在定义之前调用也能正常工作。
greet(); // 输出 你好,定义之前调用也可以
function greet() {
console.log('你好');
}
JavaScript 在运行前会把函数声明整体提升到作用域顶部,这个机制称为提升。变量的情况不一样,let 和 const 受暂时性死区限制,声明前使用就报错。所以实战中统一先声明再使用,养成好习惯,不必刻意依赖提升。
小结
- 作用域是变量可访问的范围,全局作用域处处可访问,函数作用域只在函数内有效。
- let 和 const 拥有块级作用域,只在最近的 {} 内有效,全局变量尽量少用。
- 变量查找从内层到外层,找到即停,这条从内到外的链就是作用域链,可以想象成一层套一层的盒子。
- let 和 const 声明前访问会报错,属于暂时性死区。函数声明有提升,定义之前调用也能工作。