you don't know js -- Scope and Closures学习笔记——第五章(闭包) 下篇
以下内容为自己看原版尝试做的翻译,仅当一个自己的看书记录,书中内容绝大部分都翻译了,但由于个人能力有限,建议各位看客不要迷信翻译的质量,推荐购买其英文原版学习观看。
Block Scoping Revisited
仔细看看我们之前解决方案的分析。我们在每次循环中使用了一个IIFE来创建一个新的作用域。换句话说,我们实际上每次循环都需要一个块级作用域。第三章展示了let
的定义,它可以实现在块中声明一个变量。
它本质上将一个块转变成了一个可以闭包的作用域。所以,下面的酷炫代码也可以达到效果。
for(var i=1; i<=5; i++){
let j = i; // yay, block-scope for closure!
setTimeout(function timer(){
console.log(j);
}, j*1000);
}
但,这不是全部。还有可以将let
声明放在for
循环的头部。这种方式使得变量不会仅在循环中定义一次,而是每次循环都会定义。而且,也会在每次前一个循环的最后为接下来的循环赋值。
for(let i=1; i<=5; i++){
setTimeout(function timer(){
console.log(i);
}, i*1000);
}
这有多酷炫?块级作用域和闭包手拉手的解决了世界性的难题。
Modules
还有一种不同于回调的代码模式也利用了闭包的力量。我们来看看他们中最强大的部分:模块
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 = CooliModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2! 3!
在JavaScript中我们将这种模式成为模块。最普通的实现模块模式的方法常被称为revealing module,我们这里展示的是其变种。
我们来看看这个代码。
首先,CoolModule()
仅仅是一个函数,但是它是作为一个模块实例被创建和调用。如果没有执行这个外部函数,内部作用域和闭包的创建就不会发生。
其次,CoolModule()
函数返回了一个基于对象语法{key: value, ...}的对象。我们返回的对象有一个指向内部函数的引用,但是没有内部的数据的引用。我们仍保持了这些数据隐藏和私有。可以适当的认为这个对象返回的值本质上是一个我们模块的公有API。
这个对象的返回值最终赋给了外部的foo
变量,然后我们可以在这个API上访问那些属性方法,比如foo.doSomething()
。
It is not required that we return an actual object (literal) from
our module. We could just return back an inner function directly.
jQuery is actually a good example of this. The jQuery
and $ identifiers are the public API for the jQuery module, but
they are, themselves, just functions (which can themselves have
properties, since all functions are objects).
doSomething()
和doAnother()
函数有一个在模块实例内部作用域的闭包。当我们通过在返回对象上的属性引用将那些函数弄到词法作用域的外面的时候,我们就建立了一个运用闭包的条件。
简单来说,模块模式的运用需要两个条件:
-
必须有一个外部函数,并且至少被调用一次(每次都创建一个新的模块实例)。
-
这个函数必须返回至少一个内部函数,这样这个内部函数就有一个在私有作用域上的闭包,并且可以访问和/或修改私有属性。
一个仅有一个函数属性的对象不是一个真正的模块。一个通过函数调用返回的仅包含数据属性没有闭包函数的对象也不是一个真正的模块。
上面的代码段展示了一个独立的名为CoolModule()
的模块创造器,可以被调用任意次,每次都会创建一个新的模块实例。基于这个模式的一个小变种是当你只关心一个实例时,单例:
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(){
// modifying the public 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对象的引用,你可以在内部修改这个模块实例,包括增加或者一处方法和属性,并且改变他们的值。
Modern Modules
不同的模块依赖加载器/管理器本质上是将这种模块定义的模式包裹成一个友好的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)
。This is invoking the definition wrapper function for a module
(passing in any dependencies), and storing the return value, the module’s
API, into an internal list of modules tracked by name.
下面是我们用其来定义一些模块的例子:
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
的实例作为一个依赖参数,并且相应的使用它。
花一些时间来感受下通过闭包来实现我们的目的的强大之处。关键点是的模块管理器并没有任何特别的“魔法”。
Future Modules
ES6增加了对于模块的支持。当通过模块系统加载时,ES6把一个文件作为一个单独的模块。每一个模块既可以导入其他模块或者特定的API成员,同时也可以导出它们自己的公有API成员。
Function-based modules aren’t a statically recognized pattern
(something the compiler knows about), so their API semantics
aren’t considered until runtime. That is, you can actually
modify a module’s API during the runtime (see earlier publi
cAPI discussion).
By contrast, ES6 module APIs are static (the APIs don’t change
at runtime). Since the compiler knows that, it can (and does!)
check during (file loading and) compilation that a reference to
a member of an imported module’s API actually exists. If the
API reference doesn’t exist, the compiler throws an “early”
error at compile time, rather than waiting for traditional dynamic
runtime resolution (and errors, if any).
ES6模块并没有“内联”格式,它们(每一个模块)必须在独立的文件中定义。浏览器/引擎有一个默认的“模块加载器”(它可被重写,但这里不讨论),当模块导入的时候,它同步的加载。
例如:
bar.js
function hello(who){
return "Let me introduce: " + who;
}
export hello;
foo.js
// import only 'hello()' from the "bar" module
import hello from "bar";
var hungry = "hippo";
function awesome(){
console.log(hello(hungry).toUpperCase());
}
export awesome;
baz.js
// import the entire "foo" and "bar" modules
module foo from "foo";
module bar from "bar";
console.log(bar.hello("rhino")); // Let me introduce: rhino
foo.awesome(); // LET ME INTRODUCE: HIPPO
Function-based modules aren’t a statically recognized pattern
(something the compiler knows about), so their API semantics
aren’t considered until runtime. That is, you can actually
modify a module’s API during the runtime (see earlier publi
cAPI discussion).
By contrast, ES6 module APIs are static (the APIs don’t change
at runtime). Since the compiler knows that, it can (and does!)
check during (file loading and) compilation that a reference to
a member of an imported module’s API actually exists. If the
API reference doesn’t exist, the compiler throws an “early”
error at compile time, rather than waiting for traditional dynamic
runtime resolution (and errors, if any).
import
从一个模块的API中导入一个或多个成员到当前的作用域,并各自绑定到一个变量上(我们例子中的hello
)。module
将整个模块API导入并绑定到一个变量上(我们例子中的foo
,bar
)。export
为当前的模块导出一个标识符(变量,函数)到公有API。这些操作符在一个模块定义中可以按需使用多次。
在模块文件中的内容就像在一个闭包作用域中,和之前的函数闭包模块类似。
复习
Closure seems to the unenlightened like a mystical world set apart
inside of JavaScript that only the few bravest souls can reach. But it’s
actually just a standard and almost obvious fact of how we write code
in a lexically scoped environment, where functions are values and can
be passed around at will.
Closure is when a function can remember and access its lexical scope
even when it’s invoked outside its lexical scope.
Closures can trip us up, for instance with loops, if we’re not careful to
recognize them and how they work. But they are also an immensely
powerful tool, enabling patterns like modules in their various forms.
Modules require two key characteristics: 1) an outer wrapping function
being invoked, to create the enclosing scope 2) the return value
of the wrapping function must include reference to at least one inner
function that then has closure over the private inner scope of the
wrapper.
Now we can see closures all around our existing code, and we have the
ability to recognize and leverage them to our own benefit!