《你不知道的JavaScript》-- 精读(五)

知识点

1.实质问题

当函数能够记住并访问所在的词法做用域时,就产生了闭包,即便函数是在当前词法做用域以外执行。闭包

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

根据前面的定义,严格来讲上述代码并非闭包,最准确地用来解释bar()对a的引用的方法是词法做用域的查找规则,而这些规则只是闭包的一部分。app

function foo(){
    var a = 2;
    function bar(){
        console.log(a);
    }
    return bar;
}
var baz = foo();
baz(); // 2 ---- 这就是闭包的效果
复制代码

上述代码中,在foo()执行后,其返回值(也就是内部的bar()函数)赋值给变量baz并调用baz(),实际上只是经过不一样的标识符引用调用了内部的函数bar()。异步

bar()显然能够被正常执行。可是在这个例子中,它在本身定义的词法做用域之外的地方执行。ide

在foo()执行后,一般会期待foo()的整个内部做用域都被销毁,由于咱们知道引擎有垃圾回收器来释放再也不使用的内存空间。而闭包的“神奇”之处正是能够阻止这件事情的发生。事实上内部做用域依然存在,所以没有被回收,由于bar()自己在使用。函数

由于bar()所声明的位置,它拥有涵盖foo()内部做用域的闭包。使得该做用域一直存活,以供bar()在以后任什么时候间进行引用。工具

bar()依然持有对该做用域的引用,而这个引用就叫做闭包。ui

闭包使得函数能够继续访问定义时的词法做用域。固然,不管使用何种方式对函数类型的值进行传递,当函数在别处被调用时均可以观察到闭包。spa

function foo(){
    var a = 2;
    function baz(){
        console.log(a);
    }
    bar(baz);
}
function bar(fn){
    fn(); // 这就是闭包
}
复制代码

把内部函数baz传递给bar,当调用这个内部函数时(如今叫做fn),它涵盖的foo()内部做用域的闭包就能够观察到了,由于它可以访问a。code

传递函数固然也能够是间接的。对象

var fn ;
function foo(){
    var a = 2;
    function baz(){
        console.log(a);
    }
    fn = baz; // 将baz分配给全局变量
}
function bar(){
    fn(); // 这就是闭包
}
复制代码

不管经过何种手段将内部函数传递到所在的词法做用域之外,它都会持有对原始定义做用域的引用,不管在何处执行这个函数都会使用闭包。

2.如今我懂了

function wait(message){
    setTimeout(function timer(){
        console.log(message);
    },1000)
}
wait("Hello,closure!")
复制代码

将一个内部函数(名为timer)传递给setTimeout(..)。timer具备涵盖wait(..)做用域的闭包,所以还保有对变量message的引用。

wait(..)执行1000毫秒后,它的内部做用域并不会消失,timer函数依然保有wait(..)做用域的闭包。

本质上,不管什么时候何地,若是将(访问它们各自词法做用域的)函数看成第一级的值类型并处处传递,你就会看到闭包在这些函数中的做用。在定时器、事件监听器、Ajax请求、跨窗口通讯、Web Workers或者任何其余的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包。

3.循环和闭包

for(var i = 1;i <= 5; i++){
    setTimeout(function timer(){
        console.log(i); // 每秒一次的频率输出5次6
    },i*1000)
}
复制代码

根据做用域的工做原理,实际状况是尽管循环中的五个函数是在各个迭代中分别定义的,可是它们都被封闭在一个共享的全局做用域中,所以实际上只有一个i。

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

在迭代内使用IIFE会为每一个迭代都生成一个新的做用域,使得延迟函数的回调能够将新的做用域封闭在每一个迭代内部,每一个迭代中都会含有一个具备正确值的变量供咱们访问。

4.重返块做用域

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

5.模块

function foo(){
    var something = "cool";
    var another = [1,2,3];
    function doSomething(){
        console.log(something);
    }
    function doAnother(){
        console.log(another.join(" ! ")
    }
}
复制代码

私有数据变量something和another,以及doSomething()和doAnother()两个内部函数,它们的词法做用域(而这就是闭包)也就是foo()的内部做用域。

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.封闭函数必须返回至少一个内部函数,这样内部函数才能在私有做用域中造成闭包,而且能够访问或者修改私有的状态。

一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并非真正的模块。

var foo = (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
    }
})()
foo.doSomething(); // cool
foo.doAnother(); // 1!2!3
复制代码

将模块函数转换成了IIFE,当即调用这个函数并将返回值直接赋值给单例的模块实例标识符foo。

模块也是普通的函数,所以能够接收参数:

function CoolModule(id){
    function identify(){
        console.log(id);
    }
    return {
        identify: identify
    }
}
var foo1 = CoolModule("foo 1");
var foo2 = CoolModule("foo 2");

foo1.identify(); // "foo 1"
foo2.identify(); // "foo 2"
复制代码

模块模式的一个简单但强大的用法是命名将要做为公共API返回的对象:

var foo = (function CoolModule(id){
    function change(){
        // 修改公共API
        publicAPI.identify = identify2;
    }
    function identify1(){
        console.log(id);
    }
    function identify2(){
        console.log(id.toUpperCase());
    }
    var publicAPI = {
        change: change,
        identify: identify1
    }
    return publicAPI;
}("foo mocule");

foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE
复制代码

经过在模块实例的内部保留对公共API对象的内部引用,能够从内部对模块实例进行修改,包括添加或删除方法和属性,以及修改它们的值。

6.现代的模块机制

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

var MyModules = (function Manager(){
    var modules = {};
    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
    }
})()
复制代码

这段代码的核心是modules[name]=impl.apply(impl,deps)。为了模块的定义引入了包装函数(能够传入任何依赖),而且将返回值,也就是模块的API,储存在一个根据名字来管理的模块列表中。

下面展现了如何使用上面的代码来定义模块:

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.awefome(); // LET ME INTRODUCE: HIPPO
复制代码

"foo"和"bar"模块都是经过一个返回公共API的函数来定义的。"foo"甚至接受"bar"的实例做为依赖参数,并能相应地使用它。

模块就是模块,即便在它们外层加上一个友好的包装工具也不会发生任何变化。

7.将来的模块机制

ES6中为模块增长了一级语法支持。在经过模块系统进行加载时,ES6会将文件当作独立的模块来处理。每一个模块均可以导入其余模块或特定的API成员,一样也能够导出本身的API成员。

基于函数的模块并非一个能被静态识别的模式,所以能够在运行时修改一个模块的API。 相比之下,ES6模块API是静态的,所以在编译期就会检查导入模块的API成员的引用是否存在,若是不存在,编译器会在编译时就报错,而不会等到运行期动态解析(而且报错)。

ES6的模块没有“行内”格式,必须被定义在独立的文件中。

// 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
// 导入完整的"foo"和"bar"模块
module foo from "foo";
module bar from "bar";

console.log(bar.hello("rhino"));
foo.awesome();
复制代码

import能够将一个模块中的一个或多个API导入到当前做用域中,并分别绑定在一个变量上。module会将整个模块的API导入并绑定到一个变量上。export会将当前模块的一个标识符(变量、函数)导出为公共API。这些操做能够在模块定义中根据须要使用任意屡次。

总结

咱们在词法做用域的环境下写代码,而其中的函数也是值,能够随意传来传去。

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

闭包是一个很是强大的工具,能够用多种形式来实现模块等模式。

模块有两个主要特征:

  • 1.为建立内部做用域而调用了一个包装函数
  • 2.包装函数的返回值必须至少包括一个对内部函数的引用,这样就会建立涵盖整个包装函数内部做用域的闭包。

巴拉巴拉

关于脑子一热

个人经历告诉我,脑子一热作的事情,多半会后悔,并且会很是后悔。可是怎么去避免呢,方法我还没找到,每次我遇到这样的情绪,都会找各类理由去逃避,这是目前个人低级应对措施,至关低级。若是能从根源消除是最好不过的了,但是我尚未那么大的控制力,因此只能慢慢去培养,尽可能减小这种上头的次数了。

相关文章
相关标签/搜索