深入学习js之浅谈作用域(作用域闭包)
2017-08-12 00:46
453 查看
初学js总会对闭包产生疑惑,其实闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它们而有意识地创建闭包。
1.闭包的概念
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
下面我们看一段代码,清晰地展示闭包
解析:
函数bar()的词法作用域能够访问foo()的内部作用域。然后我们将bar()函数本身当作一个值类型进行传递。在这个例子中,我们将bar所引用的函数对象本身当作返回值。
在foo()执行后,其返回值(也就是内部的bar()函数)赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用调用了内部的函数bar()。
bar()显然可以被正常地执行。但是在这个例子中,它在定义的语法作用域以外的地方执行。
在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收机制用来释放不再使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。
而闭包的神奇之处在于它可以阻止这件事的发生。事实上内部作用域依然存在,使得该作用域能够一直存活,以供bar()在之后任何时间进行引用。
bar()依然持有对该作用域的引用,而这个引用就叫做闭包。
因此,在几微妙后baz被实际调用(调用内部函数bar()),它可以访问定义时的词法作用域,因此它可以访问变量a
当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。
间接的传递函数。
将一个内部函数(名为timer)传递给setTimeout(..)。timer具有涵盖wait(..)作用域的闭包,因此还保有对变量message的引用。
wait(..)执行1秒后,它的内部作用域不会消失,timer函数依然保有wait(..)作用域的闭包。
在引擎内部,内置的工具函数setTimeout(..)持有对一个参数的引用,这个参数也许叫作fn或者fnc,或其他名字。引起会调用这个函数,在例子中就是内部的timer函数,而词法作用域在这个过程中保持完整。
这就是闭包
在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是使用闭包。
2.循环和闭包
预期这段代码分别输出1-5,每秒一次,每次一个
事实上这段代码的输出是每秒一次输出五次6
为什么?
首先解释6是从哪来的。这个循环的终止条件是i不在<=5。条件首次成立时i的值是6。因此,输出显示的是循环结束时,i的最终值。
那么代码中到底有什么缺陷导致它与期不一致?
缺陷是我们试图假设循环中每个迭代在运行时都会给自己捕获一个i的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,
c0ca
但是他们都被封闭在一个共享的全局作用域中,因此实际上只有一个i,所以所有函数共享一个i引用。
因此我们需要更多的闭包作用域,特别是在循环过程中每个迭代都需要一个闭包作用域。
前面的文章说过IIFE会通过声明并立即执行一个函数来创建作用域
以上代码还不行,因为此时IIFE只是个空作用域
上述代码可行,但还可以改进
在迭代内使用IIFE会为每个迭代生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值得变量供我们访问。
3.块作用域
前面介绍了let声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。
这里注意一点for循环头部的let声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
4.模块模式
首先CoolModule()只是一个函数,必须要通过调用它来创建一个模块实例。如果不执行外部函数,内部作用域和闭包都无法被创建。
其次,CoolModule()返回一个对象字面量语法{key:value,...}来表示的对象。这个返回的对象中含有对内部函数而不是内部数据变量的引用。我们保持内部数据变量是隐藏且私有的状态。可以将这个对象类型的返回值看作本质上是模块的公共API。
从模块中返回一个实际的对象并不是必须的,也可以直接返回一个内部函数,jQuery就是一个例子。jQuery和$标识符就是jQuery模块的公共API,但他们本身都是函数(由于函数也是对象,它们本身也可以拥有属性)
简而言之,模块模式需要具备两个条件
1.必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
2.封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有状态。
当只需要一个实例时,可以对这个模式进行简单的改进来实现单例模式
模块模式另一个简单但强大的用法是命名将要作为公共API返回的对象
现代的模块机制
大多数模块依赖加载器/管理器本质上都是将这种模块定义封装进一个友好的API。这里并不会研究某个具体的库,为了宏观了解这里介绍一些核心概念
下面展示了如何用它来定义模块:
模块管理器符合模块模式的两个特点:调用包装了函数定义的包装函数,并且将返回值作为该模块的API
换而言之,模块就是模块,即使在它们外层加上一个友好的包装工具也不会发生任何变化。
ES6中的模块机智
基于函数的模块并不是一个能被静态识别的模式(编译器无法识别),它们的API语法只有在运行时才会被考虑进来。因此可以在运行时修改一个模块的API(参考之前的public API)
而ES6的模块API是静态的(API不会再运行时改变)。由于编译器知道这一点,因此可以在编译器检查对导入模块的API成员的引用是否真实存在。如果API引用不存在,编译器会在编译时就抛出“早期”错误,而不会等到运行期再动态解析(并且报错)
ES6的模块没有“行内”格式,必须定义在独立的文件中(一个文件一个模块)。浏览器或引擎有一个默认的“模块加载器”(可以被重载),可以在导入模块时同步加载模块文件
import可以将一个模块中的一个或多个API导入到当前作用域中,并分别绑在一个变量上(例子中的hello),module会将整个模块的API导入并绑定到一个变量上(例子中的foo和bar)
模块文件中的内容会被当作好像包含在作用域闭包中一样来处理,就和前面介绍的函数闭包模块一样。
总结:1.当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
2.闭包是个强大的工具,可以用多种形式来实现模块等模式。
3.模块有两个主要特征:3.1为创建内部作用域而调用了一个包装函数;3.2包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。
如若有误欢迎指出!
1.闭包的概念
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
下面我们看一段代码,清晰地展示闭包
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()依然持有对该作用域的引用,而这个引用就叫做闭包。
因此,在几微妙后baz被实际调用(调用内部函数bar()),它可以访问定义时的词法作用域,因此它可以访问变量a
当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。
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");
将一个内部函数(名为timer)传递给setTimeout(..)。timer具有涵盖wait(..)作用域的闭包,因此还保有对变量message的引用。
wait(..)执行1秒后,它的内部作用域不会消失,timer函数依然保有wait(..)作用域的闭包。
在引擎内部,内置的工具函数setTimeout(..)持有对一个参数的引用,这个参数也许叫作fn或者fnc,或其他名字。引起会调用这个函数,在例子中就是内部的timer函数,而词法作用域在这个过程中保持完整。
这就是闭包
在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是使用闭包。
2.循环和闭包
for (var i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i); }, i * 1000 ); }
预期这段代码分别输出1-5,每秒一次,每次一个
事实上这段代码的输出是每秒一次输出五次6
为什么?
首先解释6是从哪来的。这个循环的终止条件是i不在<=5。条件首次成立时i的值是6。因此,输出显示的是循环结束时,i的最终值。
那么代码中到底有什么缺陷导致它与期不一致?
缺陷是我们试图假设循环中每个迭代在运行时都会给自己捕获一个i的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,
c0ca
但是他们都被封闭在一个共享的全局作用域中,因此实际上只有一个i,所以所有函数共享一个i引用。
因此我们需要更多的闭包作用域,特别是在循环过程中每个迭代都需要一个闭包作用域。
前面的文章说过IIFE会通过声明并立即执行一个函数来创建作用域
for (var i = 1; i <= 5; i++) { (function () { setTimeout(function timer() { console.log(i); }, i * 1000 ); })(); }
以上代码还不行,因为此时IIFE只是个空作用域
for (var i = 1; i <= 5; i++) { (function () { var j = i; setTimeout(function timer() { console.log(i); }, j * 1000 ); })(); }
上述代码可行,但还可以改进
for (var i = 1; i <= 5; i++) { (function (j) { setTimeout(function timer() { console.log(i); }, j * 1000 ); })(i); }
在迭代内使用IIFE会为每个迭代生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值得变量供我们访问。
3.块作用域
前面介绍了let声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。
这里注意一点for循环头部的let声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
for (let i = 1; i < 5; i++) { setTimeout(function timer() { console.log(i); }, i * 1000 ); }
4.模块模式
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这个模式在js中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露,这里展示的是其变体。
首先CoolModule()只是一个函数,必须要通过调用它来创建一个模块实例。如果不执行外部函数,内部作用域和闭包都无法被创建。
其次,CoolModule()返回一个对象字面量语法{key:value,...}来表示的对象。这个返回的对象中含有对内部函数而不是内部数据变量的引用。我们保持内部数据变量是隐藏且私有的状态。可以将这个对象类型的返回值看作本质上是模块的公共API。
从模块中返回一个实际的对象并不是必须的,也可以直接返回一个内部函数,jQuery就是一个例子。jQuery和$标识符就是jQuery模块的公共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。
模块模式另一个简单但强大的用法是命名将要作为公共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 module" ); foo.identify();// foo module foo.change(); foo.identify();// FOO MODULE
现代的模块机制
大多数模块依赖加载器/管理器本质上都是将这种模块定义封装进一个友好的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.awesome(); //LET ME INTRODUCE: HIPPO"foo"和“bar”模块都是通过返回一个公共API的函数来定义的。“foo”甚至接收“bar”的实例作为依赖函数,并能相应使用它。
模块管理器符合模块模式的两个特点:调用包装了函数定义的包装函数,并且将返回值作为该模块的API
换而言之,模块就是模块,即使在它们外层加上一个友好的包装工具也不会发生任何变化。
ES6中的模块机智
基于函数的模块并不是一个能被静态识别的模式(编译器无法识别),它们的API语法只有在运行时才会被考虑进来。因此可以在运行时修改一个模块的API(参考之前的public API)
而ES6的模块API是静态的(API不会再运行时改变)。由于编译器知道这一点,因此可以在编译器检查对导入模块的API成员的引用是否真实存在。如果API引用不存在,编译器会在编译时就抛出“早期”错误,而不会等到运行期再动态解析(并且报错)
ES6的模块没有“行内”格式,必须定义在独立的文件中(一个文件一个模块)。浏览器或引擎有一个默认的“模块加载器”(可以被重载),可以在导入模块时同步加载模块文件
bar.js function hello(who) { return "hello " + who; } export hello; foo.js //仅从"bar"模块导入hello() import hello from "bar"; function awesome() { console.log( hello("world").toUpperCase() ); } export awesome; baz.js module foo from "foo"; module bar from "bar" console.log( bar.hello("world") // hello world ); foo.awesome();//HELLO WORLD
import可以将一个模块中的一个或多个API导入到当前作用域中,并分别绑在一个变量上(例子中的hello),module会将整个模块的API导入并绑定到一个变量上(例子中的foo和bar)
模块文件中的内容会被当作好像包含在作用域闭包中一样来处理,就和前面介绍的函数闭包模块一样。
总结:1.当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
2.闭包是个强大的工具,可以用多种形式来实现模块等模式。
3.模块有两个主要特征:3.1为创建内部作用域而调用了一个包装函数;3.2包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。
如若有误欢迎指出!
相关文章推荐
- 深入学习js之浅谈作用域(隐藏作用域和块作用域)
- 深入学习js之浅谈作用域(RHS和LHS)
- js:深入闭包(作用域:下)
- 深入理解js --作用域与闭包
- js重点浅谈(跨域,作用域和作用域链,闭包,原型和原型链继承)
- 深入学习js之浅谈设计模式(混入)
- JS学习之闭包、this关键字、预解释、作用域综合
- 深入学习js之浅谈对象(对象的常见特性)
- 浅谈js作用域和闭包
- 深入学习JS执行--创建执行上下文(变量对象,作用域链,this)
- JS 面试知识学习历程(第三天) -- 作用域和闭包
- js学习之----深入理解闭包
- 深入学习js之浅谈原型
- js:深入闭包(作用域:上)
- 【JS深入学习】—— 一句话解释闭包
- 深入学习js之浅谈设计模式(行为委托)
- Js学习笔记之this和闭包_优就业
- 深入理解javascript原型和闭包(13)-【作用域】和【上下文环境】
- javascript学习笔记(十三) js闭包(转)
- 深入理解js 闭包