作用域
作用域是指程序源代码中定义变量的区域。
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。
JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。
执行上下文
每个函数都有自己的执行上下文, 函数执行的上下文规则是:
- 函数开始执行时,它的上下文会被推入一个上下文栈中。
- 函数执行完成后,上下文栈会弹出该函数上下文。
- 将控制权归还给之前的执行上下文
- JS程序的执行流就是通过这个上下文栈来控制的
一个经典的问题
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
三个结果都是3, 原因是给data[i]赋值的时候, 并没有新建一个上下文环境, 创建独立的作用域.如果改成立即执行函数, 就会新建一个该函数的独立作用域, 下次调用该函数时, 就会寻找函数自己的作用域链
可以参考
闭包
实质问题
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用 域之外执行。
看下面这段代码
function foo() {
var a = 2
function bar() {
console.log(a)
}
return bar
}
var baz = foo()
baz() // 2 —— 朋友,这就是闭包的效果。
函数 bar() 的词法作用域能够访问 foo() 的内部作用域。然后我们将 bar() 函数本身当作 一个值类型进行传递。在这个例子中,我们将 bar 所引用的函数对象本身当作返回值。
在 foo() 执行后,其返回值(也就是内部的 bar() 函数)赋值给变量 baz 并调用 baz(),实 际上只是通过不同的标识符引用调用了内部的函数 bar()。
bar() 显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方 执行。
在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃 圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很 自然地会考虑对其进行回收。
而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此 没有被回收。谁在使用这个内部作用域?原来是 bar() 本身在使用。
拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一 直存活,以供 bar() 在之后任何时间进行引用。
bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。
当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到 闭包。
function foo() {
var a = 2
function baz() {
console.log(a) // 2
}
bar(baz)
}
function bar(fn) {
fn() // 妈妈快看呀,这就是闭包!
}
把内部函数 baz 传递给 bar,当调用这个内部函数时(现在叫作 fn),它涵盖的 foo() 内部
作用域的闭包就可以观察到了,因为它能够访问 a。
传递函数当然也可以是间接的。
var fn
function foo() {
var a = 2
function baz() {
console.log(a)
}
fn = baz // 将 baz 分配给全局变量
}
function bar() {
fn() // 妈妈快看呀,这就是闭包!
}
foo()
bar() // 2
无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用 域的引用,无论在何处执行这个函数都会使用闭包。
现在我懂了
日常工作中的闭包
function wait(message) {
setTimeout(function timer() {
console.log(message)
}, 1000)
}
wait('Hello, closure!')
wait(…) 执行 1000 毫秒后,它的内部作用域并不会消失,timer 函数依然保有 wait(…)
作用域的闭包。
在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包!
循环和闭包
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
正常情况下,我们对这段代码行为的预期是分别输出数字 1~5,每秒一次,每次一个。
但实际上,这段代码在运行时会以每秒一次的频率输出五次 6。
首先解释 6 是从哪里来的。这个循环的终止条件是 i 不再 <=5。条件首次成立时 i 的值是 6。因此,输出显示的是循环结束时 i 的最终值。
仔细想一下,这好像又是显而易见的,延迟函数的回调会在循环结束时才执行。事实上, 当定时器运行时即使每个迭代中执行的是setTimeout(…, 0),所有的回调函数依然是在循 环结束后才会被执行,因此会每次输出一个 6 出来。
缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。但是 根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的, 但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。
缺陷是什么?我们需要更多的闭包作用域,特别是在循环的过程中每个迭 代都需要一个闭包作用域。
通过声明并立即执行一个函数来创建作用域
for (var i = 1; i <= 5; i++) {
;(function () {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
})()
}
//上面这样显然是不行的, 因为作用域是空的.没有值传进去
//改成下面这样就可以了
for (var i = 1; i <= 5; i++) {
;(function () {
var j = i
setTimeout(function timer() {
console.log(j)
}, j * 1000)
})()
}
//或者
for (var i = 1; i <= 5; i++) {
;(function (j) {
setTimeout(function timer() {
console.log(j)
}, j * 1000)
})(i)
}
我们使用 IIFE 在每次迭代时都创建一个新的作用 域。换句话说,每次迭代我们都需要一个块作用域。第 3 章介绍了 let 声明,可以用来劫 持块作用域,并且在这个块作用域中声明一个变量。
for (var i = 1; i <= 5; i++) {
let j = i // 是的,闭包的块作用域!
setTimeout(function timer() {
console.log(j)
}, j * 1000)
}
//或者
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
模块
最重要的是要理解模块管理器没有任何特殊的“魔力”。它们符合前面列出的模块模式的两个 特点:为函数定义引入包装函数,并保证它的返回值和模块的 API 保持一致。
function CoolModule() {
var something = 'cool'
var another = [1, 2, 3]
function doSomething() {
console.log(something)
}
function doAnother() {
console.log(another.join(' ! '))
}
return {
doSomething: doSomething,
doAnother: doAnother,
}
}
var foo = CoolModule()
foo.doSomething() // cool
foo.doAnother() // 1 ! 2 ! 3
如果要更简单的描述,模块模式需要具备两个必要条件。
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块 实例)。
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并 且可以访问或者修改私有的状态。
未来的模块机制
// bar.js
function hello(who) {
return "Let me introduce: " + who;
}
export hello;
// foo.js
// 仅从 "bar" 模块导入 hello() import hello from "bar";
var hungry = "hippo";
function awesome() { console.log(
hello( hungry ).toUpperCase() );
}
export awesome;
// baz.js
module foo from "foo";
module bar from "bar";
console.log(
bar.hello( "rhino" )
); // Let me introduce: rhino foo.awesome(); // LET ME INTRODUCE: HIPPO
import 可以将一个模块中的一个或多个 API 导入到当前作用域中,并分别绑定在一个变量 上(在我们的例子里是 hello)。module 会将整个模块的 API 导入并绑定到一个变量上(在 我们的例子里是 foo 和 bar)。export 会将当前模块的一个标识符(变量、函数)导出为公 共 API。这些操作可以在模块定义中根据需要使用任意多次。
模块文件中的内容会被当作好像包含在作用域闭包中一样来处理,就和前面介绍的函数闭 包模块一样。
小结
当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时 就产生了闭包。