Chapter 2谈过,每个範畴(scope)就像容器一样,里面包含各种的识别子(变数、函式)。
巢状範畴(nested scopes)在撰写程式码时期就已经定义了。
Scope From Functions
一般认为,在JavaScript中,範畴(scope)是以函式为基础。
宣告函式的同时,也建立範畴(scope),但并不是只有函式能建立範畴(scope)。
Example
function foo(a) { var b = 2; function bar() { } var c = 3;}
在foo( )
中,包含了识别字a、b、c、bar,这些都在foo( )
的範畴之中。
全域範畴中只有一个识别字:foo
。
因为a、b、c、bar是属于foo( )
的範畴,所以在foo( )
的外面(全域範畴)是无法存取到这些识别字的:
bar(); //失败console.log( a, b, c ); //失败
以上所有的识别字都能在foo( )
内部存取,如此一来,也能运用到JS变数的「动态」本质,接收不同型别的值。
隐藏在普通範畴中
利用函式範畴的特性,将我们的程式码包在函式里面,建立起如同「隐藏」的效果,让外面无法窥探内部的运作。
这种隐藏的手段,主要是来自于软体设计原理「最小权限原则(Principle of Least Privilege)」。
这个原则指出,我们设计的模组或是API,应该只能透露出最少的细节,并把其他的变数或函式给隐藏起来。
如果我们将所有的变数或函式都宣告于全域範畴之中,那他们都能被其他的内嵌的範畴存取,这会违反最小权限原则,如此一来,我们很有可能会透露应该保有私密性的变数或函式。
function doSomething(a) {b = a + doSomethingElse( a * 2 );console.log( b * 3 );}function doSomethingElse(a) {return a - 1;}var b;doSomething( 2 ); // 15
b
与doSomethingElse( )
本应是doSomething( )
内部的工作细节,但它们宣告于全域範畴中,意味着,在doSomething( )
之外也能存取它们,这容易造成非预期的情况发生。正确的作法是应该将那些细节隐藏在doSomething( )
之中。
function doSomething(a) {function doSomethingElse(a) {return a - 1;}var b;b = a + doSomethingElse( a * 2 );console.log( b * 3 );}doSomething( 2 ); // 15
如此一来,b
与doSomethingElse( )
完全存在于doSomething( )
的範畴之中,外界无法控制它们,功能也没有改变,这种设计方式通常被认为是较佳的。
避免冲突
将特定的识别字隐藏在一个範畴之中的另一个好处是,避免同名但不同用途的识别字发生冲突。
function foo() {function bar(a) {i = 3; //会变更for迴圈的iconsole.log( a + i );}for (var i=0; i<10; i++) {bar( i * 2 ); //无穷迴圈}}foo();
bar( )
内部的i
会意外地影响迴圈的i
,会让i
永远为3,导致无穷迴圈。解决方式是在bar( )
内部改成var i = 3;
,让i
成为bar( )
内的区域变数,而不会影响到for迴圈的另一个i
。
全域命名空间
另一个可能发生冲突的情况是,当你的程式载入其他的程式库,却没有正确地隐藏程式库的私有识别字。
这些程式库通常会在全域範畴宣告变数或函式,有可能是一个物件。这个物件会被当作命成空间使用,所有要对外的功能都会是物件的属性,而非宣告在语意範畴中顶层的识别字。
Example
var MyReallyCoolLibrary = {awesome: "stuff",doSomething: function() {// ...},doAnotherThing: function() {// ...}};
函式作为範畴
刚刚已说明我们可以将识别字宣告于函式中,建立其範畴,不让外面範畴存取。
Example
var a = 2;function foo() {var a = 3;console.log( a ); // 3}foo();console.log( a ); // 2
以上的方式是合理的,但有些问题。首先,我们必须要宣告函式名称foo
,这本身会汙染到全域範畴。再者,我们还得呼叫foo
函式,里面的程式码才会执行。
有个方式可以让该函式不需要名称(或许就不会汙染到包含它的範畴),并且能够自动执行:
var a = 2;(function foo(){var a = 3;console.log( a ); // 3})();console.log( a ); // 2
首先我们看到整个函式被包在( )内部,这表示该函式不会被当成函式宣告,而是一个函式运算式(function-expression)。
要分辨宣告与运算式的方法,是看function
的位置。若function
在叙述句的最开头,那它就是函式宣告;若不是,就是函式运算式。
匿名 VS. 具名
setTimeout( function(){console.log("I waited 1 second!");}, 1000 );
上面的方式被称为匿名函式运算式(anonymous function expression)。函式运算式可以匿名,但函式宣告不行。
但函式运算式有几个缺点:
1.函式运算式在堆叠轨迹没有名字可以显示,会造成除错的困难。
2.若函式进行递迴的话,会得到已被弃用的arguments.callee参考。
3.函式名称可以增加我们的程式可读性。
行内涵式运算式(Inline function expressions),可以解决上述问题,是最佳的实务做法:
setTimeout( function timeoutHandler(){ // <--增加函式名称console.log( "I waited 1 second!" );}, 1000 );
立即调用函式运算式
var a = 2;(function foo(){var a = 3;console.log( a ); // 3})();
将函式包在( )之中,并且在尾部加再上()来执行这个运算式。
第1个()将函式变成运算式,第2个()直接执行该函式。
这种方式很普遍,通称为立即调用函式运算式(IIFE,Immediately Invoked Function Expression)。
IIFE可以使用匿名函式,但若为函式命名,可以减去一些麻烦。
IIFE有另一种表示方式,(function(){ .. }())
,直接把执行的第2个()包在第1个()里面,以上的2种方式功能都一样。
IIFE也可以传入引数:
var a = 2;(function IIFE( global ){var a = 3;console.log( a ); // 3,区域变数aconsole.log( global.a ); // 2,全域变数a})( window );console.log( a ); // 2
在IIFE( )
内外各宣告变数a
,并传入一个window物件,如此一来,我们可以区分全域参考与区域参考的差异。
let
let与varㄧ样都是宣告变数的关键字,不同的是,以let宣告的变数,其範畴是以区块{}
作为基準。
var foo = true;if (foo) {let bar = foo * 2;bar = something( bar );console.log( bar );}console.log( bar ); // 掷出ReferenceError
因为以let宣告的变数bar
,它的範畴限制在if叙述句的大括号{ }中,意味着在此範畴外的环境无法存取bar
。
let 迴圈
for (let i=0; i<10; i++) {console.log( i );}console.log( i ); // ReferenceError
在for迴圈使用let宣告,let不只会将i繫结到for主体,还会重新繫结迴圈每跑一次的值,会将前一个i值再重新指定给i。
const
const也会建立以区块{}为範畴的变数,比较特别的是,宣告时就必须要一併给值,其值是固定不变的,之后任何改变值的行为都会掷出错误。
参考来源:
此为You Don't Know JS系列的笔记。