《你不知道的JavaScript-上卷》-笔记-5-作用域闭包

5.2 实质问题

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

js 复制代码
function foo() {
    var a = 2;
    
    function bar() {
        console.log( a );
    }
    
    return bar;
}

var baz = foo();

baz(); // 2 ------ 朋友,这就是闭包的效果
  • 在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,而闭包的"神奇"之处正是可以阻止这件事情的发生。
  • 是 bar() 本身在使用上内部作用域,从而导致foo() 的整个内部作用域无法被回收
  • 该内部作用域能够一直存活,以供 bar() 在之后任何时间进行引用
  • bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。

无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。

  • 把内部函数 baz 传递给 bar,当调用这个内部函数时(现在叫作 fn),它涵盖的 foo() 内部作用域的闭包就可以观察到了,因为它能够访问 a。
js 复制代码
function foo() {
    var a = 2;
    function baz() {
        console.log(a); // 2
    }
    bar(baz);
}

function bar(fn) {
    fn(); // 妈妈快看呀,这就是闭包!这里fn是形参,实参是baz函数,也就是在外部函数中调用了baz,而baz引用到foo中的作用域,使得a是可以被访问的
}

foo();//2

bar(foo);//2

间接传递函数也是可以闭包的

  • 无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
js 复制代码
var fn;
function foo() {
    var a = 2;
    function baz() {
        console.log(a);
    }
    fn = baz; // 将 baz 分配给全局变量
}
function bar() {
    fn(); // 妈妈快看呀,这就是闭包!
}
foo();
bar(); // 2

5.3 现在我懂了

js 复制代码
function wait(message) {
    setTimeout(function timer() {
        console.log(message);
    }, 1000);
}
wait("Hello, closure!");
  • 将一个内部函数(名为 timer)传递给 setTimeout(..)。timer 具有涵盖 wait(..) 作用域的闭包,因此还保有对变量 message 的引用。
  • 深入到引擎的内部原理中,内置的工具函数 setTimeout(..) 持有对一个参数的引用,这个参数也许叫作 fn 或者 func,或者其他类似的名字。引擎会调用这个函数,在例子中就是内部的 timer 函数,而词法作用域在这个过程中保持完整. 这就是闭包。

本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。

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

5.4 循环和闭包

js 复制代码
        for (var i = 1; i <= 5; i++) {
            setTimeout(function timer() {
                console.log(i);//6
            }, i * 1000);
        }
  • 我们对这段代码行为的预期是分别输出数字 1~5,每秒一次,每次一个
  • 但实际上,这段代码在运行时会以每秒一次的频率输出五次 6

代码中到底有什么缺陷导致它的行为同语义所暗示的不一致

  • 缺陷是我们试图假设循环中的每个迭代在运行时都会给自己"捕获"一个 i 的副本。
  • 但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。

解决方式1,我们需要更多的闭包作用域,特别是在循环的过程中每个迭代都需要一个闭包作用域。

js 复制代码
        for (var i = 1; i <= 5; i++) {
            (function () {
                setTimeout(function timer() {
                    console.log(i);
                }, i * 1000);
            })();
        }

上面的写法并不能解决问题,因为虽然创建了多个闭包作用域,但是闭包作用域是空的!

正确的做法

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

在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问.


重返块作用域

对前面的解决方案的分析,每次迭代我们都需要一个块作用域,let 声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。

本质上这是将一个块转换成一个可以被关闭的作用域。

js 复制代码
for (var i=1; i<=5; i++) {

    let j = i; // 是的,闭包的块作用域!

    setTimeout( function timer() {
        console.log( j );
    }, j*1000 );

}

for 循环头部的 let 声明还会有一个特殊的行为.

  • 这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。(啊?是这样理解的吗?!!!我很疑惑!)
js 复制代码
for (let i=1; i<=5; i++) {

    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );

}

5.5 模块

js 复制代码
 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

这个模式在 JavaScript 中被称为模块

  • CoolModule() 只是一个函数,必须要通过调用它来创建一个模块实例。
  • CoolModule() 返回一个用对象字面量语法 { key: value, ... } 来表示的对象。
    • 这个返回的对象中含有对内部函数而不是内部数据变量的引用。
    • 我们保持内部数据变量是隐藏且私有的状态。
    • 可以将这个对象类型的返回值看作本质上是模块的公共 API。

模块模式

需要具备两个必要条件

  1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

5.5.1 现代的模块机制

大多数模块依赖加载器 / 管理器本质上都是将这种模块定义封装进一个友好的 API。

js 复制代码
      //依赖加载器 / 管理器
       var MyModules = (function Manager() {
            var modules = {};//  {moduleName:module}
            
            //定义模块 name=模块名字 deps=依赖的模块名字 impl=模块的实现
            function define(name, deps, impl) {
                for (var i = 0; i < deps.length; i++) {
                    deps[i] = modules[deps[i]];
                }
                modules[name] = impl.apply(impl, deps);
            }
            function get(name) {
                return modules[name];
            }
            return {
                define: define,
                get: get
            };
        })();
        
        // 定义模块
        MyModules.define("bar", [], function () {
            function hello(who) {
                return "Let me introduce: " + who;
            }
            return {
                hello: hello
            };
        });
        
        //定义模块
        MyModules.define("foo", ["bar"], function (bar) {
            var hungry = "hippo";
            function awesome() {
                console.log(bar.hello(hungry).toUpperCase());
            }
            return {
                awesome: awesome
            };
        });
        
        var bar = MyModules.get("bar");
        var foo = MyModules.get("foo");

        console.log(bar.hello("hippo")); // Let me introduce: hippo
        foo.awesome(); // LET ME INTRODUCE: HIP

5.5.2 未来的模块机制

ES6 中为模块增加了一级语法支持。但通过模块系统进行加载时,ES6 会将文件当作独立的模块来处理。每个模块都可以导入其他模块或特定的 API 成员,同样也可以导出自己的API 成员。 - import 可以将一个模块中的一个或多个 API 导入到当前作用域中,并分别绑定在一个变量上(在我们的例子里是 hello)。

  • module 会将整个模块的 API 导入并绑定到一个变量上(在我们的例子里是 foo 和 bar)。export 会将当前模块的一个标识符(变量、函数)导出为公共 API。
  • 这些操作可以在模块定义中根据需要使用任意多次。

baz.js

js 复制代码
// 导入完整的 "foo" 和 "bar" 模块
module foo from "foo";

module bar from "bar";

console.log(bar.hello( "rhino" )); // Let me introduce: rhino

foo.awesome(); // LET ME INTRODUCE: HIPPO
相关推荐
中微子1 分钟前
JavaScript事件循环机制:面试官最爱问的10个问题详解
前端
Eighteen Z10 分钟前
CSS揭秘:10.平行四边形
前端·css·css3
拾光拾趣录18 分钟前
虚拟DOM
前端·vue.js·dom
爱学习的茄子18 分钟前
JavaScript事件循环深度解析:理解异步执行的本质
前端·javascript·面试
1024小神19 分钟前
cocos游戏开发中多角色碰撞,物理反弹后改变方向的实现逻辑
前端·javascript
摆烂为不摆烂23 分钟前
😁深入JS(五): 一文让你完全理解 hash 与 history 路由,手写前端路由
前端
1024小神23 分钟前
cocos游戏开发中,如何制作一个空气墙
前端·javascript
爱编程的喵23 分钟前
深入理解JavaScript事件循环机制:从同步到异步的完整解析
前端·javascript
202624 分钟前
11. vite打包优化
前端·javascript·vite
拾光拾趣录29 分钟前
组件封装的⼀些基本准则
前端·前端工程化