【You Don't Know JS: Scope & Closures】Chapter 3 笔记

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

bdoSomethingElse( )本应是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

如此一来,bdoSomethingElse( )完全存在于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也会建立以区块{}为範畴的变数,比较特别的是,宣告时就必须要一併给值,其值是固定不变的,之后任何改变值的行为都会掷出错误。

参考来源:
http://img2.58codes.com/2024/20112573OWtzPwjWh4.jpg

此为You Don't Know JS系列的笔记。


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章