2深入理解作用域和闭包

作用域

作用域是指程序源代码中定义变量的区域。

作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。

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]赋值的时候, 并没有新建一个上下文环境, 创建独立的作用域.如果改成立即执行函数, 就会新建一个该函数的独立作用域, 下次调用该函数时, 就会寻找函数自己的作用域链

可以参考

之前写的关于setTimeout的文章

JavaScript深入之闭包

闭包

实质问题

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

看下面这段代码

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

如果要更简单的描述,模块模式需要具备两个必要条件。

  1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块 实例)。
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并 且可以访问或者修改私有的状态。
未来的模块机制
// 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。这些操作可以在模块定义中根据需要使用任意多次。

模块文件中的内容会被当作好像包含在作用域闭包中一样来处理,就和前面介绍的函数闭 包模块一样。

小结

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值