關鍵字:域(scope),閉包(closure),關鍵字this,命名空間(namespace),函數域(function scope),全局域(global scope),詞法作用域(lexical scope)以及公共域和私有域(public/private scope)
什么是域?
在JavaScript里,域指的是代碼當前的上下文語境。域可以是公共定義的,也可以是本地定義的。理解JavaScript中的域,是你寫出無可挑剔的代碼以及成為更好的程序員的關鍵。
什么是全局域?
在你開始寫一行JavaScript代碼的時候,你正處在我們所說的全局域中。此時我們定義一個變量,那它就被定義在全局域中:
1 // global scope
2 var name = 'Todd';
全局域是你最好的朋友,同時也是你最心悸的夢魘。學會控制各種域並不難,當你這么做之后,你就不會再遇到有關全局域的問題(多發生在與命名空間沖突時)。你或許經常聽到有人說“全局域太糟糕了”,但卻未聽他們評判過個中緣由。其實,全局域並沒有那么糟糕,因為你要在全局域當中創造可以被其他域所訪問的模塊和APIs,所以你必須學會揚長避短地使用它。
似乎大家都喜歡如此寫jQuery代碼,是不是你也這么干呢:
1 jQuery('.myClass');
這樣我們正在公共域中訪問jQuery,我們可以把這種訪問稱之為命名空間。命名名空間在某些條件下可以理解為域,但通常它指的是最上層的域。在上面的例子里,jQuery作為命名空間存在公共域中。jQuery 命名空間在全局域中被定義,全局域就是jQuery庫的命名空間,因為所有在命名空間中的東西都成為這個命名空間的派生。
什么是本地域?
本地域是指那些在全局域中定義的域。一般只能有一個全局域,定義其中的每一個函數都有自己的本地域。任何定義在其它函數里的函數都有一個連接那個外部函數的本地域。
假設我定義了一個函數,並在其中創建了幾個變量,那這些變量就屬於本地域。看下面的例子:
1 // Scope A: Global scope out here
2 var myFunction = function () { 3 // Scope B: Local scope in here
4 };
任何屬於本地域的物件對全局域都是不可見的-除非他們被暴露出來,也就是說,如果我在一個新的域中定義了一些函數和變量,它們是無法從當前那個域的外部被訪問的。來看一個簡單的例子:
1 var myFunction = function () { 2 var name = 'Todd'; 3 console.log(name); // Todd
4 }; 5 // Uncaught ReferenceError: name is not defined
6 console.log(name);
變量name是屬於本地域的,它沒有暴露給它的父域,因此它是未定義的。
函數域
在JavaScript中所有的域都是並且只能是被函數域(function scope)所創建,它們不能被for/while循環或者if/switch表達式創建。New function = new scope - 僅此而已。一個簡單的例子來說明域的創建:
1 // Scope A
2 var myFunction = function () { 3 // Scope B
4 var myOtherFunction = function () { 5 // Scope C
6 }; 7 };
創建新的域以及創建本地變量、函數、對象都是如此簡單。
詞法定義域
每當你看到一個函數在另一個函數里的時候,內部的那個函數可以訪問外部的函數,這被稱作詞法定義域或是閉包 - 有時也被稱作靜態域。又來了,看下面這個例子:
1 // Scope A
2 var myFunction = function () { 3 // Scope B
4 var name = 'Todd'; // defined in Scope B
5 var myOtherFunction = function () { 6 // Scope C: `name` is accessible here!
7 }; 8 };
你會注意到 myOtherFunction 只是被簡單的定義一下並沒有被調用。調用順序也會對域中變量該如何反應起到作用,這里我已經定義了一個函數然后在另一個Console下面調用了它:
1 var myFunction = function () { 2 var name = 'Todd'; 3 var myOtherFunction = function () { 4 console.log('My name is ' + name); 5 }; 6 console.log(name); 7 myOtherFunction(); // call function
8 }; 9 // Will then log out:
10 // `Todd`
11 // `My name is Todd`
詞法作用域很好用,任何定義在父域中的變量、對象、函數,都可以被子域鏈訪問到,舉個例子:
1 var name = 'Todd'; 2 var scope1 = function () { 3 // name is available here
4 var scope2 = function () { 5 // name is available here too
6 var scope3 = function () { 7 // name is also available here!
8 }; 9 }; 10 };
唯一需要記住的是詞法作用域不能反過來用。這里我們看看詞法作用域是如何不工作的:
1 // name = undefined
2 var scope1 = function () { 3 // name = undefined
4 var scope2 = function () { 5 // name = undefined
6 var scope3 = function () { 7 var name = 'Todd'; // locally scoped
8 }; 9 }; 10 };
我總是可以返回一個引用給最上層的name,但卻從來不是變量('Todd')本身。
域鏈
域鏈給一個已知的函數建立了作用域。正如我們所知的那樣,每一個被定義的函數都有自己的嵌套作用域,同時,任何被定義在其他函數中的函數都有一個本地域連接着外部的函數 - 這種連接被稱作鏈。這就是在代碼中定義作用域的地方。當我們在處理一個變量的時候,JavaScript就會開始從最里層的域向外查找直到找到要找的那個變量、對象或函數。
閉包
閉包和詞法作用域非常相近。一個關於閉包如何工作的更好或者更實際的例子就是返回一個函數的引用。我們可以返回域中的東西,使得它們可以被其父域所用。
1 var sayHello = function (name) { 2 var text = 'Hello, ' + name; 3 return function () { 4 console.log(text); 5 }; 6 };
我們此處所用的閉包使得sayHello里的域無法被公共域訪問到。單是調用這個函數不會發生什么,因為它只是返回了一個函數而已:
1 sayHello('Todd'); // nothing happens, no errors, just silence..
這個函數返回了一個函數,就是說它需要分配然后才是調用:
1 var helloTodd = sayHello('Todd'); 2 helloTodd(); // will call the closure and log 'Hello, Todd'
好吧,我撒謊了,你可以調用它,或許你已經看到了像這樣的函數,但是這會調用你的閉包:
1 sayHello2('Bob')(); // calls the returned function without assignment
AngularJS就為其 $compile 方法用了上面的技術,當前作用域作為引用傳遞給閉包:
1 $compile(template)(scope);
我們可以猜測代碼或許應該像下面這樣:
1 var $compile = function (template) { 2 // some magic stuff here
3 // scope is out of scope, though...
4 return function (scope) { 5 // access to `template` and `scope` to do magic with too
6 }; 7 };
一個函數不是只有返回什么東西的時候才會稱作閉包。簡單地使詞法作用域的外層可以訪問其中的變量,這便創建了一個閉包。
作用域和關鍵字‘this’
每一個作用域都會根據函數的調用方式來綁定不同的 this 的值。我們都用過 this 關鍵字,但不是我們所有人都理解以及區別 this 在調用當中的變化。默認情況下 this 值得是做外層的公共對象 - window( node.js 里是 exports)。大概其看一下以不同方式調用函數時 this 值的不同:
1 var myFunction = function () { 2 console.log(this); // this = global, [object Window]
3 }; 4 myFunction(); 5
6 var myObject = {}; 7 myObject.myMethod = function () { 8 console.log(this); // this = Object { myObject }
9 }; 10
11 var nav = document.querySelector('.nav'); // <nav>
12 var toggleNav = function () { 13 console.log(this); // this = <nav> element
14 }; 15 nav.addEventListener('click', toggleNav, false);
這里還有個問題,就算在同一個函數中,作用域也是會變,this 的值也是會變:
1 var nav = document.querySelector('.nav'); // <nav>
2 var toggleNav = function () { 3 console.log(this); // <nav> element
4 setTimeout(function () { 5 console.log(this); // [object Window]
6 }, 1000); 7 }; 8 nav.addEventListener('click', toggleNav, false);
那這里究竟發生了什么?我們新創建了一個不會從事件控制器調用的作用域,所以它也如我們所預期的那樣,默認是指向 window 對象的。 如果我們想要訪問這個 this 值,有幾件事我們可以讓我們達到目的。可能以前你就知道了,我們可以用一個像 that 這樣的變量來緩存對 this 的引用:
1 var nav = document.querySelector('.nav'); // <nav>
2 var toggleNav = function () { 3 var that = this; 4 console.log(that); // <nav> element
5 setTimeout(function () { 6 console.log(that); // <nav> element
7 }, 1000); 8 }; 9 nav.addEventListener('click', toggleNav, false);
用 call,apply 和 bind 改變作用域
有時你會根據需要更改作用域。一個簡單的證明如何在循環中更改作用域:
1 var links = document.querySelectorAll('nav li'); 2 for (var i = 0; i < links.length; i++) { 3 console.log(this); // [object Window]
4 }
在這里 this 值 不是指我們的元素,我們沒有調用任何東西或者改變作用域。讓我們來看一下如何改變作用域(看上去我們改變的是作用域,但是我們真正在做的卻是更改函數被調用的上下文語境)。
.call() and .apply()
.call() 和 .apply() 這兩個方法的確很美好,他們允許你傳遞一個函數給作用域,並綁定正確的 this 值。讓我們看一下如何將 this 綁定給上面例子中的每個元素:
1 var links = document.querySelectorAll('nav li'); 2 for (var i = 0; i < links.length; i++) { 3 (function () { 4 console.log(this); 5 }).call(links[i]); 6 }
你可以看到我傳遞了當前的元素數組迭代( links[i] ),它蓋面了函數的作用域以至於 this 值變成了每個元素。 我們可以用 this 綁定任何我們想要的。我們可以用 call 或者 apply 任一方法改變作用域,他們的區別是: .call(scope, arg1, arg2, arg3) 接收的是用逗號隔開的獨立參數,而 .apply(scope, [arg1, arg2]) 接收的是一個參數數組。
記得用 call() or .apply() 而不是像下面這樣調用你的函數非常重要:
1 myFunction(); // invoke myFunction
You'll let .call() handle it and chain the method:
1 myFunction.call(scope); // invoke myFunction using .call()
.bind()
不同於上述方法,使用 .bind() 不會調用一個函數, 它只是在函數運行前綁定了一個值。ECMASCript5 當中才引入這個方法實在是太晚太可惜了,因為它是如此的美妙。如你所知,我們不能出傳遞參數給函數,就像這樣:
1 // works
2 nav.addEventListener('click', toggleNav, false); 3
4 // will invoke the function immediately
5 nav.addEventListener('click', toggleNav(arg1, arg2), false);
我們可以通過在其中創建一個新的函數來搞定它:
1 nav.addEventListener('click', function () { 2 toggleNav(arg1, arg2); 3 }, false);
還是那個問題,這個改變了作用域的同時我們也創建了一個不需要的函數,這對性能是一種浪費如果我們在循環內部綁定事件監聽器。 盡管這使得我們可以傳遞參數進去,似乎應該算是 .bind() 的用武之地,但是這個函數不會被執行:
1 nav.addEventListener('click', toggleNav.bind(scope, arg1, arg2), false);
這個函數不會執行,並且作用域可以根據需要更改,但是參數還是在等待被傳入。
私有域和公共域
在許多編程語言中,你將聽到關於公共域和私有域,在 JavaScript 里沒有這樣的東西。但是我們可以通過像閉包一樣的東西來模擬公共域和私有域。
我們可以通過使用 JavaScript 設計模式比如模塊模式,來創建公共域和私有域。一個簡單的創建私有域的途徑就是把我們的函數包裝進一個函數中。如我們之前學到的,函數創建作用域來使其中的東西不可被全局域訪問:
1 (function () { 2 // private scope inside here
3 })();
我們可能會緊接着創建一個新的函數在我們的應用中使用:
1 (function () { 2 var myFunction = function () { 3 // do some stuff here
4 }; 5 })();
當我們准備調用函數的時候,它不應在全局域里:
1 (function () { 2 var myFunction = function () { 3 // do some stuff here
4 }; 5 })(); 6
7 myFunction(); // Uncaught ReferenceError: myFunction is not defined
成功!我們就此創建了一個私有域。但是如果我像讓這個函數變成公共的,要怎么做呢?有一個很好的模式(被稱作模塊模式)允許我們正確地處理函數作用域。這里我在全局命名空間里建立了一個包含我所有相關代碼的模塊:
1 // define module
2 var Module = (function () { 3 return { 4 myMethod: function () { 5 console.log('myMethod has been called.'); 6 } 7 }; 8 })(); 9
10 // call module + methods
11 Module.myMethod();
在這里,return 的東西就是 public 方法返回的東西,它可以被全局域訪問。我們的模塊來關心我們的命名空間,它可以包含我們想要任意多的方法在里面:
1 // define module
2 var Module = (function () { 3 return { 4 myMethod: function () { 5
6 }, 7 someOtherMethod: function () { 8
9 } 10 }; 11 })(); 12
13 // call module + methods
14 Module.myMethod(); 15 Module.someOtherMethod();
那私有方法呢?這里是很多開發者做錯的地方,他們把所有的函數都堆砌在全局域里以至於污染了整個全局命名空間。可工作的函數代碼不一定非在全局域里才行,除非像 APIs 這種要在全局域里可以被訪問的函數。這里我們來寫一個沒有被返回出來的函數:
1 var Module = (function () { 2 var privateMethod = function () { 3
4 }; 5 return { 6 publicMethod: function () { 7
8 } 9 }; 10 })();
這就意味着 publicMethod 可以被調用,但是 privateMethod 則不行,因為它被域私有了!這些私有的函數可以是任何你能想到的對象或方法。
但是這里還有個有點擰巴的地兒,那就是任何在同一個域中的東西都可以訪問同一域中的其他東西,就算在這兒函數被返回出去以后。也就是說,我們的公共函數可以訪問私有函數,所以私有函數依然可以和全局域互動,但是不能被全局域訪問。
1 var Module = (function () { 2 var privateMethod = function () { 3
4 }; 5 return { 6 publicMethod: function () { 7 // has access to `privateMethod`, we can call it:
8 // privateMethod();
9 } 10 }; 11 })();
這種互動是充滿力量同時又保證了代碼安全。JavaScript中很重要的一塊就是保證代碼的安全,這就解釋了為什么我們不能接受把所有的函數都放在公共域中,因為這樣的話,他們都被暴露出來很容易受到攻擊。
下面有個例子,返回了一個對象,用到了 public 和 private 方法:
1 var Module = (function () { 2 var myModule = {}; 3 var privateMethod = function () { 4
5 }; 6 myModule.publicMethod = function () { 7
8 }; 9 myModule.anotherPublicMethod = function () { 10
11 }; 12 return myModule; // returns the Object with public methods
13 })(); 14
15 // usage
16 Module.publicMethod();
比較精巧的命名方式就是在私有方法名字前加下划線,這可以幫我們在視覺上區分公共的和私有的方法:
1 var Module = (function () { 2 var _privateMethod = function () { 3
4 }; 5 var publicMethod = function () { 6
7 }; 8 })();
這里我們可以借助面向對象的方式來添加對函數的引用:
1 var Module = (function () { 2 var _privateMethod = function () { 3
4 }; 5 var publicMethod = function () { 6
7 }; 8 return { 9 publicMethod: publicMethod, 10 anotherPublicMethod: anotherPublicMethod 11 } 12 })();
