Skip to main content

ECMAScript 作用域和闭包

对于宿主环境来说,作用域用来确定在何处以及如何查找变量

对于开发者来说,作用域可以用来隔离变量,不同作用域下的同名变量不会有冲突

作用域链的规则:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。

全局作用域(Global Scope)

指在函数之外声明的变量,在整个代码中都可以被访问。

函数作用域(Function Scope)

指在函数内部声明的变量,这些变量只能在声明它们的函数内部访问。

块级作用域(Block Scope)

指在代码块 {} 内部声明的变量,它们只在声明它们的块内部可见,超出这个块就无法访问。

{
let a = 2;
console.log( a ); // 输出:2
}
console.log( a ); // ReferenceError

对于 ES6 之前没有块级作用域的情况,为了兼容 ES5 代码,代码转换工具会利用 catch 的块作用域。

{
try {
throw undefined;
} catch (a) {
a = 2;
console.log(a); // 输出:2
}
}
console.log(a); // ReferenceError

词法作用域(Lexical Scope)

定义在词法阶段的作用域,换句话说,词法作用域是由你在代码中将变量和函数定义在哪个位置决定的。

在词法作用域中,变量的访问权限由它们在代码中的位置决定,而不是在运行时动态确定的。这意味着函数在定义时会捕获其所在的作用域,而不是调用时的作用域。

词法作用域查找只会查找一级标识符,例如变量名 a、b 和 c。如果代码中引用了 foo.bar.baz,词法作用域查找只会尝试查找 foo 标识符,找到这个变量后,对象属性访问规则会分别接管对 bar 和 baz 属性的访问。

词法作用域的规则是在代码编译阶段确定的,编译过程中的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。在浏览器中,全局变量会被映射到 window 对象上,方便词法分析时找到标识符。

有两个机制可以欺骗词法作用域:eval 和 with。eval 函数可以将字符串代码当作动态生成的代码执行,从而改变词法作用域。with 语句可以创建一个新的词法作用域,并在其中添加一个对象,但是由于其会导致严格模式下的性能问题和可维护性问题,通常不推荐使用。

function foo(str, a) {
eval( str ); // 欺骗!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3

作用域

不存在动态作用域

区别:词法作用域是在定义时确定的,而动态作用域是在运行时确定的。

如果 ECMAScript 有动态作用域,以下代码的结果应该是 3 。

function foo() {
console.log(a); // 输出:2
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();

作用域范围

作用域范围

  1. 全局作用域
  2. foo 的函数作用域
  3. bar 的函数作用域

闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 输出:2

闭包应用

var 没有块级作用域,使用闭包可以在循环中创建一个新的词法作用域,用于保存每次迭代的变量值。

for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})(i);
}

柯理化函数

// 使用柯里化函数实现参数的分步传递
function curryAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
const curriedAdd = curryAdd(1);
const result = curriedAdd(2)(3);
console.log(result); // 输出:6

惰性函数

// 使用惰性函数实现延迟初始化
function lazyInitialize() {
let initialized = false;
return function() {
if (!initialized) {
console.log("Lazy initializing...");
// 进行复杂的初始化操作
initialized = true;
}
};
}
const lazyInitFunction = lazyInitialize();

在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers 或者任何其他的同步异步任务中,只要使用了回调函数,实际上就是在使用闭包。

闭包和内存泄漏

内存泄漏:一块被分配的内存既不能使用,又不能回收,直到浏览器进程结束或系统资源耗尽。

滥用闭包是内存泄漏的一种常见原因。在使用闭包时,如果内部函数持有对外部函数作用域中变量的引用,而这些变量又不再需要使用,就会导致这些变量无法被垃圾回收器回收。