原文的免费链接:https://github.com/getify/You-Dont-Know-JS/tree/master/scope%20%26%20closures
Chapter 2: Lexical Scope
Some highlights:
- eval和with都有窜改文法作用域(lexical scope)的能力,eval会影响它被执行的上下文的作用域,with会直接插入一个新的作用域,它的范畴就是with的参数指定的对象;
- 两者都会被严谨模式影响,eval将不能够声明新的变量,而with会完全被禁止运行;
- 虽然说,with把新的对象当做一个新作用域插入进来,它的代码块里用var声明的变量还是会被提升到外围的函数作用域,因为没有块作用域;
- 最好根本避免使用eval和with,因为JavaScript引擎遇到它们会放弃任何优化;
- setTimeout()和setInterval()也有类似的影响,最好避免使用;
- 函数构造器new Function(..)也会有类似影响,虽然它比eval安全一些,但是作者也不建议使用,他没有给出具体原因。
疑问:哪些方法可以替代setTimeout()和setInterval()?
Chapter 3: Functions Versus Block Scope
这一章前面的笔墨是在解释作用域这样的设计究竟有什么好处,可以解决哪些编程上的问题,比如说命名冲突,隐藏细节,还有一些JS库利用这个特性来实现命名空间,和依赖管理的库对模块模式的应用等等。
函数表达式(Function Expression)
作者是以一种欲扬先抑的方式介绍函数表达式的。如果说函数的最大用处是创建一个新的作用域来容纳一段代码,那么当你想把一段逻辑隔离起来的时候,你就可以做下面的操作:
var a = 2;
function foo() { // <-- insert this
var a = 3;
console.log(a); // 3
} // <-- and this
foo(); // <-- and this
console.log(a); // 2
就是用函数把它包起来,然后再呼叫这个函数,就像示例里的foo,可是这个做法有缺点!缺点就是,foo被定义在了包含它的作用域里,它“污染”了上层的作用域。那怎么办?答案就是使用函数表达式:
var a = 2;
(function foo(){ // <-- insert this
var a = 3;
console.log( a ); // 3
})(); // <-- and this
console.log( a ); // 2
这个做法被称为IIFE(immediately-invoked function expression)。这样做的好处是,foo只在它自身的作用域内可见,出了foo函数的作用域后,它就不存在了,这样就解决了上面提出的缺点。所以它是将自己限制在自身作用域的一种方法。
但是这里作者也遗留了一个问题没有讲清楚,究竟怎样区分函数声明与函数表达式,作者依然把问题留在了什么是statement和什么是expression的层面上,作者说如果function关键字是第一个出现在statement里的,那就是声明。这个问题还是留待考察了statement和expression的区别再说吧。
另外,作者不建议使用匿名函数,任何时候都给函数一个命名,有三个好处:
- 调试容易;
- 迭代函数可以呼叫自己;
- 增加可读性;
所以作者建议即使是IIFE,也给一个名字好些:
var a = 2;
(function IIFE(){
var a = 3;
console.log(a);
}());
console.log( a );
有些人好奇括号放哪里的问题,其实两种写法是等价的:
(function(){ .. })()
和:
(function(){ .. }())
下面就是IIFE的一些变种:
var a = 2;
(function IIFE(global){
var a = 3;
console.log(a);
console.log(global.a);
})(window);
console.log(a);
var a = 2;
(function IIFE(def){
def(window);
})(function def(global){
var a = 3;
console.log(a);
console.log(global.a);
});
基本上来说,就作用域而言,通用的规则一直是它是以函数为单元的,可是也有特例,就是在有些情况下能创建块作用域。
块作用域
块作用域在JS里会在少数情况下出现,就是with和try-catch,可是with不建议使用,另外有两本书也数落了with的坏处:《Effective JavaScript》和《High Performance JavaScript》。
作者介绍了块作用域的诸多好处:更清晰的变量规划,以及及时被垃圾回收器回收过期的变量,提高效率等等。但是JS并不真正支持块作用域,作者提到了ES6新加入的相应的操作符。
let
2014年所做的实验证明let还没有被当时的IE和Chrome支持,我其实不确定我当时做的实验方法是有效的,可能我当时的结论不准备。
现在再来重新测试一次,只需要试验下述代码片段:
{
let bar = 2;
console.log( bar );
}
console.log( bar );
在Chrome Version 57.0.2987.133 (64-bit) 和Firefox Version 52.0.2 (32-bit)中看到的结果都是:
>>>2 >>>ReferenceError: bar is not defined |
也就是说let现在已经被支持了!!!!!!
let还有一个非常大的意义,就是当用于for循环的时候,它能确保每次循环里面的j的值都会不同:
{
let j;
for (j=0; j<10; j++) {
let i = j; // re-bound for each iteration!
console.log( i );
}
}
疑问:这个问题是个很经典的与闭包有关的面试问题,我需要再找出原本的代码来测试下,在循环体里面需要有个新定义的函数,被定义在数组里,或者用setInterval()来调用。
let的问题在附录里还有讨论。
const
var foo = true;
if (foo) {
var a = 2;
const b = 3; // block-scoped to the containing `if`
a = 3; // just fine!
b = 4; // error!
}
console.log( a ); // 3
console.log( b ); // ReferenceError!
这段代码在2014年IE里的测试结果是:
这段代码在Chrome Version 57.0.2987.133 (64-bit) 和Firefox Version 52.0.2 (32-bit)中都和预期一样。不仅如此,现在在Komodo里甚至直接就有语法错误的提醒:
Chapter 4: Hoisting
先判断下下面的代码输出什么:
a = 2;
var a;
console.log(a); // 2
console.log(b); // undefined
var b = 2;
JavaScript的执行过程不是很多人理解的那样,其实在执行以前是预先编译一次的,这种过程不像C#和Java那种编译生成静态文件,可是它确实是有一个预处理的,而这个处理过程中会对变量和函数的声明做一次处理。所以像:
... // code 1 var a = 2; ... // code 2 |
这样的语句会被处理两次,第一次就是var a的部分,在预编译时处理,第二部分就是a=2,会在执行的过程里真正得到执行。因此,前面那句等价于下面的写法:
var a; ... // code 1 a = 2; ... // code 2 |
并且var a的声明会被放在当前作用域的最顶端。
这个现象被称作变量提升(variable hoisting)。变量提升的范畴是针作用域而言的,提升的操作不会超过所属作用域。
总结变量提升的规则如下:
- 对变量而言,被提升的部分只是声明,对函数声明而言,整个函数都会被提升;
foo(); function foo() { var c = 2; console.log(c); }
- 函数表达式却并不会被提升,即使它有命名;
/* name identifier in function expression is not hoisted */ foo(); // not ReferenceError, but TypeError! bar(); // ReferenceError var foo = function bar() { // ... };
- 在提升中,如果遇到相同命名出现,假如它同时被声明为变量和函数,那么函数的优先级高过变量,该函数将被提升;
/*Function First*/ foo(); // 1 var foo; // should be hoisted, but it is variable, so it is omitted function foo() { // should be hoisted, and it is function, so it is hoisted firstly console.log(1); } foo = function() { // will override the previous definition console.log(2); };
- 如果同一个命名被多个函数声明使用,那么后来者居上;
foo(); // 3 function foo() { console.log( 1 ); } var foo = function() { console.log( 2 ); }; function foo() { console.log( 3 ); }
- 因为不存在块作用域,逻辑代码块里的声明也都会被提升,切记;
这段代码现在运行都会出错,因为现今的多数引擎的实现在这个问题上都不遵照规范,参见DS.Lab:函数。foo_V_5(); // "b" var a_V_5 = true; if (a_V_5) { function foo_V_5() { console.log("a"); } } else { function foo_V_5() { console.log("b"); } } //Throw ReferenceError: foo_V_5 is not defined in Firefox
Some highlights:
- If the statement is like: var boo = function(){}, then it will be interpreted as two separate parts: declaration & assignment, and only the declaration will be hoisted.
/* only declaration is hoisted, but function expression */ foo_V_1(); // not ReferenceError, but TypeError! var foo_V_1 = function bar() { // ... };
/* name identifier in function expression is not hoisted */ foo_V_2(); // TypeError bar_V_2(); // ReferenceError var foo_V_2 = function bar_V_2() { // ... };
- Otherwise, if the statement is a function expression like: function boo(){}, it will be hoisted as a whole.
foo_V_0(); function foo_V_0() { var c_V = 2; console.log( c_V ); }
- If the same name identifier is assigned to with a variable and appears in a function expression as well, then the one for function will be hoisted, and the statements assigning other values to the identifier as variable will be ignored.
/*Function First*/ foo_V_3(); // 1 var foo_V_3; // should be hoisted, but it is variable, so it is omitted function foo_V_3() { // should be hoisted, and it is function, so it is hoisted firstly console.log( 1 ); } foo_V_3 = function() { // will override the previous definition console.log( 2 ); };
- However, if there is other statements assigning function expressions to the identifier follows, then function definition will be overridden.
foo(); // 3 function foo() { console.log( 1 ); } var foo = function() { console.log( 2 ); }; function foo() { console.log( 3 ); }
- Function declaration will be hoisted to the enclosing scope, that may across the block, but the sample code fails to run in Firefox.
foo_V_5(); // "b" var a_V_5 = true; if (a_V_5) { function foo_V_5() { console.log("a"); } } else { function foo_V_5() { console.log("b"); } } //Throw ReferenceError: foo_V_5 is not defined in Firefox
Chapter 5: Scope Closure
可能作者Kyle是属于比较文艺的人,第一节Enlightenment完全是抒情。抒情之后的很长一段篇幅也是和Dmitry的博客一样,讨论了学术上究竟怎么定义和界定闭包,而且作者比较纠结,因为他比较执着于从文字上全面而详尽地说清楚这个问题。下面是他引用来的定义:
Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.
作者想尝试强调的一点是,闭包是由于一些机制导致的一个结果,或者一个现象,在很多代码里,这个机制都一定会被用到,可是它没有引起能被观察到的闭包现象,所以不能算作闭包,比如:
function foo() {
var a = 2;
function bar() {
console.log( a ); // 2
}
bar();
}
foo();
bar在foo里被定义也在foo里被调用,这没有体现出foo函数终结了bar也能在其外面访问a的闭包特性,所以作者认为这个不算闭包。
除了直接在所创建作用域以外被调用外,像下面这样,也跟Dmitry所讲的一样,作为返回值被创建的函数对象也是闭包:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 -- Whoa, closure was just observed, man.
或者,作为传递出去给其他函数的参数:
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 -- Whoa, closure was just observed, man.
或者是间接传递:
var fn;
function foo() {
var a = 2;
function baz() {
console.log( a );
}
fn = baz; // assign `baz` to global variable
}
function bar() {
fn(); // look ma, I saw closure!
}
foo();
bar(); // 2
在接下来,作者也总结了闭包的用法:Ajax,setTimeout,回调函数,事件句柄函数等等。
然后就又说道了一个经典的面试问题,这个问题在Dmitry的ES3系列第六章(并且提出了另一个解决办法,就是将新变量绑定到函数对象自身上)也有讲到:
for (var i=1; i<=5; i++) {
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 );
}
或者用let关键字解决:
for (var i=1; i<=5; i++) {
let j = i; // yay, block-scope for closure!
setTimeout( function timer(){
console.log( j );
}, j*1000 );
}
再优化一下:
for (let i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}
模块
这里讲的就是设计模式了,模块模式利用了闭包这个特性。
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
构成模块模式的两个标准:
- 要有一个外面的承载的函数,并且它至少要被调用一次来创建这个模块;
- 这个承载函数至少得返回一个内函数,于是该函数有能力访问私有部分,通过它的闭包。
模块模式可以和单例模式结合:
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
参数化:
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"
参数化+单例模式:
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
时下流行的模块模式
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
};
})();
使用如下:
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
modules[name] = impl.apply(impl,deps)这句没有完全看懂。impl同时也可以作为this的值传给apply。
ES6的模块模式
还没有被实现,我先略过。
Some highlights:
- The function is being invoked well outside of its author-time lexical scope. Closure lets the function continue to access the lexical scope it was defined in at author time.
function foo() { var a = 2; function baz() { console.log( a ); // 2 } bar( baz ); } function bar(fn) { fn(); }
var fn; function foo() { var a = 2; function baz() { console.log( a ); } fn = baz; // assign baz to global variable } function bar() { fn(); } foo(); bar(); // 2
- Module pattern
To state it more simply, there are two requirements for the module pattern to be exercised: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
- There must be an outer enclosing function, and it must be invoked at least once (each time creates a new module instance).
- The enclosing function must return back at least one inner function, so that this inner function has closure over the private scope, and can access and/or modify that private state.
- Module as singleton.
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
- Passing parameter when creating module.
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"
- Singlton design with parameters.
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
APPENDIX
APPENDIX-A
这里回到了作用域规则的话题,静态vs.动态。
简单讲,动态的变量查找是完全由函数调用的位置和方式决定的,而静态是完全由函数声明的位置决定的。JS的作用域是静态的,但是函数内this值的绑定则是动态的。
APPENDIX-B
在ES3里,并不支持块作用域,除非try-catch和with的用法,在ES5里通过let支持了块作用域。但是在ES5以前的版本里,如果想要做到块作用域,可以利用catch:
try{throw 2}catch(a){
console.log( a ); // 2
}
console.log( a ); // ReferenceError
这段代码在2014年测试时,在Chrome和Firefox里并不向预期的那样。但是现在在Chrome 58.0.3029.110 (64-bit)和Firefox Developer Edition 54.0a2 (2017-04-18) (32-bit)中运行都符合书中所说的结果。
另外let也有另一种语法:
let (a = 2) {
console.log( a ); // 2
}
console.log( a ); // ReferenceError
作者称这种做法为显示的块作用域,而前面的做法就是隐式的。它的官方名称是let block或者let statement。有些像C#里的using()块。
最后作者提到了时下流行的transpiler,转译器,集翻译和编译于一体的一种东西,它们主持一种新的JS语法,这个版本支持所有JS新标准的功能,然后在将用这种语法写出的程序转换成某个旧版本标准的JS代码。也即是说你可以用它来基于ES6的新功能构建你的程序,假如你的目标运行环境是ES3标准,转译器会生成ES3的JS代码,同时实现了你的功能。Babel就是个这样的产品,这个名称的由来就是圣经里的巴别塔。另外微软的TypeScript也是走的这个方向。
APPENDIX-C
ES6新增了箭头函数的语法:
var foo = a => {
console.log( a );
};
foo( 2 ); // 2
它简化了定义函数的语法。但除此以外,它对于this的值做了一种静态的绑定。
它可以用来解决前面提到的this绑定丢失的问题,按照作者的解释,箭头函数的this绑定原则很简单,它们只寻找最接近的上级文法作用域:
The short explanation is that arrow-functions do not behave at all like normal functions when it comes to their this binding. They discard all the normal rules for this binding, and instead take on the this value of their immediate lexical enclosing scope, whatever it is.
根据他的讨论和示例代码,我做了如下实验:
var obj = {
id: "awesome",
cool: () => {
console.log( this.id );
}
};
var id = "not awesome";
obj.cool(); // awesome
setTimeout( obj.cool, 100 ); // not awesome
但是这段代码在Chrome 48里执行的结果没有像我预期的,它输出了两次not awesome。也就是说全局对象是最接近的上一级作用域,那就是说this的绑定还是按照调用的情况来决定的。
其余的细节暂时略过。需要对箭头函数做一些研究先。
Some highlights:
- Javascript does NOT support dynamic scope, which means to care about where the functions are called from rather than how and where they are declared
function foo() { console.log( a ); // 2 } function bar() { var a = 3; foo(); } var a = 2; bar();
You may be confused and mix closure up with dynamic scope, try the following snippets may help to clear it up:function foo() { console.log( a ); // ReferenceError: a is not defined } function bar() { var a = 3; foo(); } bar();
function bar() { var a = 3; function foo() { console.log( a ); // 3 } foo(); } var a = 2; bar();
function bar() { var a = 3; function foo() { console.log( a ); // 3 } foo(); } bar();
- The trick to use block scope in pre-ES6 environments
try {throw 2} catch(a){ console.log( a ); // 2 } console.log( a ); // ReferenceError, but in Chrome & Firefox I got 2,