JavaScript 中包含全剧作用域、函数作用域、块级作用域,还包含作用域链、变量提升、闭包和作用域的关系等。
变量和函数的可访问范围即为作用域。
分类
JavaScript 前期仅有全局和函数作用域,ES 6 引入了块级作用域(let 和 const)。
全局作用域(global scope)
- 定义:代码在任意的地方都能访问的作用域
- 特点:
- 全局作用域/函数会挂载到全局对象,可能将造成命名冲突
- 未使用
var/let/const声明的变量会隐式的成为全局变量(严格模式下将会报错)
console.log('使用 var 声明的变量(未声明前):', globalVar); // undefined
// const、let 声明的变量没有作用域提升,报错
// console.log('使用 const 声明的变量(未声明前):', globalConst);
// console.log('使用 let 声明的变量(未声明前):', globalLet);
var globalVar;
let globalLet;
// const 需要在声明的时候赋值,否则报错
// Uncaught SyntaxError:
// Missing initializer in const declaration
const globalConst = 'const';
console.log('使用 var 声明的变量(未赋值前):', globalVar);
console.log('使用 let 声明的变量(未赋值前)', globalLet);
// 其实已经赋值了
console.log('使用 const 声明的变量(未赋值前):', globalConst);
globalVar = 'var';
// const 声明的变量禁止再赋值
// Uncaught TypeError:
// Assignment to constant variable. at <anonymous>: xx:xx
// globalConst = 'const';
globalLet = 'let';
console.log('使用 var 声明的变量(初始赋值后):', globalVar);
console.log('使用 const 声明的变量(初始赋值后):', globalConst);
console.log('使用 let 声明的变量(初始赋值后)', globalLet);
// 变量挂载到全局作用域,打印 'var'
console.log('使用 var 声明的变量(初始赋值后):', globalThis.globalVar);
// 变量未挂载到全局,返回 undefined
console.log('使用 const 声明的变量(初始赋值后):', globalThis.globalConst);
// 变量未挂载到全局,返回 undefined
console.log('使用 let 声明的变量(初始赋值后)', globalThis.globalLet);
函数作用域(function scope)
- 定义:仅在函数内部可访问的作用域,由函数声明或函数表达式创建
- 特点:
- 函数内部声明的变量(
var/let/const) 对外部不可见 - 函数作用域在函数定义时已确定,与调用位置无关
- 函数内部声明的变量(
function fun() {
var funVar = '函数内部的 var';
}
// 无法访问 fun 函数内的变量 funVar
// Uncaught ReferenceError:
// funVar is not defined at <anonymous>:xx:xx
console.log(funVar);
块级作用域(block scope)
- 定义:使用
{}包裹的代码块创建的作用域,由let、const声明的变量仅在此块内有效 (ES 6 新增)。 - 特点:
- 解决了
var的变量提升和全局污染的问题 - 块级作用域在代码执行到块时才会创建(但不属于动态作用域)
- 解决了
if (1 == '1') {
let blockLet = 'let';
const blockConst = 'const';
}
// 下面的代码或终止程序的进行
// Uncaught ReferenceError:
// blockLet is not defined
console.log(blockLet);
console.log(blockConst);
作用域的机制
动词作用域(lexical scope)
- 定义:作用域在代码编写时确定,与函数/变量的调用位置无关
- 核心:函数的作用域由定义时的位置决定,而非调用时位置
function outer() {
const x = 10;
function inner() {
console.log(x); // 访问 outer 作用域的 x
}
return inner;
}
const innerFn = outer();
innerFn(); // 输出 10(inner 定义在 outer 内部,词法作用域链包含 outer)
作用域链(scope chain)
- 定义:函数创建时,会关联一个作用域层级链,用于变量的查找。链的顶端是全局作用域
- 形成过程:
- 函数定义时,记录其所在的直接作用域(父作用域)
- 执行时,作用域链 = 函数自身的作用域链 + 父作用域 + ... + 顶级(全局)作用域;
- 变量查找规则:从链的最底层(自身作用域)想上查找,直到全局作用域(找不到时报错)
const a = 1; // 全局作用域
function fun() {
const a = 2 // 函数作用域
function \_fun() {
const a = 3; // 内部函数的作用域
return a + a + a;
}
\_fun(); // 6
}
fun();
变量提升(hoisting)
- 定义:var 声明的变量和函数声明会被提升到顶级作用域,但初始化的值不会提升
console.log(globalVar); // 不会报错,但是是 undefined
var globalVar = 'var';
console.log(globalVar); // ‘var’
// 不通于变量,函数将整体提升
fun(); // 'hello'
function fun() {
const msg = 'hello';
console.log(msg);
}
而 let/const 会形成暂时性死区(TDZ)
console.log(globalLet); // 将报错阻断程序的支持
let globalLet = 'let';
注意
循环陷进
在上一节中使用闭包解决循环中的问题,而使用 let 亦可解决该问题:
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i); // 每一个执行函数有自己的作用域链
}, 1000);
}
避免全局污染
使用 var 定义的全局变量在某些错误使用的条件下会导致变量值被覆盖的问题
块级作用域与函数声明
ES5 中函数声明只能在全局或函数作用域中,但 ES6 允许在块级作用域中声明函数
if (true) {
const fun = () => console.log('块内函数');
fun(); // 执行
}
// 报错
fun();
作用域与闭包的内存管理
- 问题:使用闭包时会保留外部作用域的引用
下面执行过程中,由于外作用域已使用 let 定义了块级作用域的变量,而在 fun 内部无论是使用 var/let 再次声明同名变量 a 时都将报错 Uncaught SyntaxError: Identifier 'a' has already been declared
let a = 10;
function fun() {
let a = 15;
console.log(a);
}
console.log(a);
fun();
console.log(a);
当外部使用 var 声明变量 a 后,在函数内部尚不存在块级变量,即在内部使用 let 并不影响外部的变量
var a = 10;
function fun() {
let a = 15;
console.log(a); // 15
}
console.log(a); // 10
fun();
console.log(a); // 10
即便是内部使用 var 声明了同名的变量,但该变量将被提升至函数的执行上下文,而不同于函数外部使用 var 定义的变量 a 被提升到全局 globalThis 上。
var a = 10;
function fun() {
var a = 15;
console.log(a); // 15
}
console.log(a); // 10
fun();
console.log(a); // 10