JavaScript 有個特性稱爲做用域。儘管對於不少開發新手來講,做用域的概念不容易理解,我會盡量地從最簡單的角度向你解釋它們。理解做用域能讓你編寫更優雅、錯誤更少的代碼,並能幫助你實現強大的設計模式。javascript
做用域是你的代碼在運行時,各個變量、函數和對象的可訪問性。換句話說,做用域決定了你的代碼裏的變量和其餘資源在各個區域中的可見性。html
那麼,限制變量的可見性,不容許你代碼中全部的東西在任意地方均可用的好處是什麼?其中一個優點,是做用域爲你的代碼提供了一個安全層級。計算機安全中,有個常規的原則是:用戶只能訪問他們當前須要的東西。java
想一想計算機管理員吧。他們在公司各個系統上擁有不少控制權,看起來甚至能夠給予他們擁有所有權限的帳號。假設你有一家公司,擁有三個管理員,他們都有系統的所有訪問權限,而且一切運轉正常。可是忽然發生了一點意外,你的一個系統遭到惡意病毒攻擊。如今你不知道這誰出的問題了吧?你這才意識到你應該只給他們基本用戶的帳號,而且只在須要時賦予他們徹底的訪問權。這能幫助你跟蹤變化並記錄每一個人的操做。這叫作最小訪問原則。眼熟嗎?這個原則也應用於編程語言設計,在大多數編程語言(包括 JavaScript)中稱爲做用域,接下來咱們就要學習它。編程
在你的編程旅途中,你會意識到做用域在你的代碼中能夠提高性能,跟蹤 bug 並減小 bug。做用域還解決不一樣範圍的同名變量命名問題。記住不要弄混做用域和上下文。它們是不一樣的特性。設計模式
在 JavaScript 中有兩種做用域數組
當變量定義在一個函數中時,變量就在局部做用域中,而定義在函數以外的變量則從屬於全局做用域。每一個函數在調用的時候會建立一個新的做用域。瀏覽器
當你在文檔中(document)編寫 JavaScript 時,你就已經在全局做用域中了。JavaScript 文檔中(document)只有一個全局做用域。定義在函數以外的變量會被保存在全局做用域中。安全
// the scope is by default global var name = 'Hammad';
全局做用域裏的變量可以在其餘做用域中被訪問和修改。閉包
var name = 'Hammad'; console.log(name); // logs 'Hammad' function logName() { console.log(name); // 'name' is accessible here and everywhere else } logName(); // logs 'Hammad'
定義在函數中的變量就在局部做用域中。而且函數在每次調用時都有一個不一樣的做用域。這意味着同名變量能夠用在不一樣的函數中。由於這些變量綁定在不一樣的函數中,擁有不一樣做用域,彼此之間不能訪問。app
// Global Scope function someFunction() { // Local Scope ##1 function someOtherFunction() { // Local Scope ##2 } } // Global Scope function anotherFunction() { // Local Scope ##3 } // Global Scope
塊級聲明包括if和switch,以及for和while循環,和函數不一樣,它們不會建立新的做用域。在塊級聲明中定義的變量從屬於該塊所在的做用域。
if (true) { // this 'if' conditional block doesn't create a new scope var name = 'Hammad'; // name is still in the global scope } console.log(name); // logs 'Hammad'
ECMAScript 6 引入了let和const關鍵字。這些關鍵字能夠代替var。
var name = 'Hammad'; let likes = 'Coding'; const skills = 'Javascript and PHP';
和var關鍵字不一樣,let和const關鍵字支持在塊級聲明中建立使用局部做用域。
if (true) { // this 'if' conditional block doesn't create a scope // name is in the global scope because of the 'var' keyword var name = 'Hammad'; // likes is in the local scope because of the 'let' keyword let likes = 'Coding'; // skills is in the local scope because of the 'const' keyword const skills = 'JavaScript and PHP'; } console.log(name); // logs 'Hammad' console.log(likes); // Uncaught ReferenceError: likes is not defined console.log(skills); // Uncaught ReferenceError: skills is not defined
一個應用中全局做用域的生存週期與該應用相同。局部做用域只在該函數調用執行期間存在。
不少開發者常常弄混做用域和上下文,彷佛二者是一個概念。但並不是如此。做用域是咱們上面講到的那些,而上下文一般涉及到你代碼某些特殊部分中的this值。做用域指的是變量的可見性,而上下文指的是在相同的做用域中的this的值。咱們固然也可使用函數方法改變上下文,這個以後咱們再討論。在全局做用域中,上下文老是 Window 對象。
// logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…} console.log(this); function logFunction() { console.log(this); } // logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…} // because logFunction() is not a property of an object logFunction();
若是做用域定義在一個對象的方法中,上下文就是這個方法所在的那個對象。
class User { logName() { console.log(this); } } (new User).logName(); // logs User {}
(new User).logName()
是建立對象關聯到變量並調用logName方法的一種簡便形式。經過這種方式你並不須要建立一個新的變量。
你可能注意到一點,就是若是你使用new關鍵字調用函數時上下文的值會有差別。上下文會設置爲被調用的函數的實例。考慮一下上面的這個例子,用new關鍵字調用的函數。
function logFunction() { console.log(this); } new logFunction(); // logs logFunction {}
當在嚴格模式(strict mode)中調用函數時,上下文默認是 undefined。
爲了解決掉咱們從上面學習中會出現的各類困惑,「執行環境(context)」這個詞中的「環境(context)」指的是做用域而並不是上下文。這是一個怪異的命名約定,但因爲 JavaScript 的文檔如此,咱們只好也這樣約定。
JavaScript 是一種單線程語言,因此它同一時間只能執行單個任務。其餘任務排列在執行環境中。當 JavaScript 解析器開始執行你的代碼,環境(做用域)默認設爲全局。全局環境添加到你的執行環境中,事實上這是執行環境裏的第一個環境。
以後,每一個函數調用都會添加它的環境到執行環境中。不管是函數內部仍是其餘地方調用函數,都會是相同的過程。
每一個函數都會建立它本身的執行環境。
當瀏覽器執行完環境中的代碼,這個環境會從執行環境中彈出,執行環境中當前環境的狀態會轉移到父級環境。瀏覽器老是先執行在執行棧頂的執行環境(事實上就是你代碼最裏層的做用域)。
全局環境只能有一個,函數環境能夠有任意多個。
執行環境有兩個階段:建立和執行。
第一階段是建立階段,是函數剛被調用但代碼並未執行的時候。建立階段主要發生了 3 件事。
變量對象(Variable Object)也稱爲活動對象(activation object),包含全部變量、函數和其餘在執行環境中定義的聲明。當函數調用時,解析器掃描全部資源,包括函數參數、變量和其餘聲明。當全部東西裝填進一個對象,這個對象就是變量對象。
'variableObject': { // contains function arguments, inner variable and function declarations }
在執行環境建立階段,做用域鏈在變量對象以後建立。做用域鏈包含變量對象。做用域鏈用於解析變量。當解析一個變量時,JavaScript 開始從最內層沿着父級尋找所需的變量或其餘資源。做用域鏈包含本身執行環境以及全部父級環境中包含的變量對象。
'scopeChain': { // contains its own variable object and other variable objects of the parent execution contexts }
執行環境能夠用下面抽象對象表示:
executionContextObject = { 'scopeChain': {}, // contains its own variableObject and other variableObject of the parent execution contexts 'variableObject': {}, // contains function arguments, inner variable and function declarations 'this': valueOfThis }
執行環境的第二個階段就是代碼執行階段,進行其餘賦值操做而且代碼最終被執行。
詞法做用域的意思是在函數嵌套中,內層函數能夠訪問父級做用域的變量等資源。這意味着子函數詞法綁定到了父級執行環境。詞法做用域有時和靜態做用域有關。
function grandfather() { var name = 'Hammad'; // likes is not accessible here function parent() { // name is accessible here // likes is not accessible here function child() { // Innermost level of the scope chain // name is also accessible here var likes = 'Coding'; } } }
你可能注意到了詞法做用域是向前的,意思是子執行環境能夠訪問name。但不是由父級向後的,意味着父級不能訪問likes。這也告訴了咱們,在不一樣執行環境中同名變量優先級在執行棧由上到下增長。一個變量和另外一個變量同名,內層函數(執行棧頂的環境)有更高的優先級。
閉包的概念和咱們剛學習的詞法做用域緊密相關。當內部函數試着訪問外部函數的做用域鏈(詞法做用域以外的變量)時產生閉包。閉包包括它們本身的做用域鏈、父級做用域鏈和全局做用域。
閉包不只能訪問外部函數的變量,也能訪問外部函數的參數。
即便函數已經return,閉包仍然能訪問外部函數的變量。這意味着return的函數容許持續訪問外部函數的全部資源。
當你的外部函數return一個內部函數,調用外部函數時return的函數並不會被調用。你必須先用一個單獨的變量保存外部函數的調用,而後將這個變量當作函數來調用。看下面這個例子:
function greet() { name = 'Hammad'; return function () { console.log('Hi ' + name); } } greet(); // nothing happens, no errors // the returned function from greet() gets saved in greetLetter greetLetter = greet(); // calling greetLetter calls the returned function from the greet() function greetLetter(); // logs 'Hi Hammad'
值得注意的是,即便在greet函數return後,greetLetter函數仍能夠訪問greet函數的name變量。若是不使用變量賦值來調用greet函數return的函數,一種方法是使用()兩次()(),以下所示:
function greet() { name = 'Hammad'; return function () { console.log('Hi ' + name); } } greet()(); // logs 'Hi Hammad'
在許多其餘編程語言中,你能夠經過 public、private 和 protected 做用域來設置類中變量和方法的可見性。看下面這個 PHP 的例子
// Public Scope public $property; public function method() { // ... } // Private Sccpe private $property; private function method() { // ... } // Protected Scope protected $property; protected function method() { // ... }
將函數從公有(全局)做用域中封裝,使它們免受攻擊。但在 JavaScript 中,沒有 共有做用域和私有做用域。然而咱們能夠用閉包實現這一特性。爲了使每一個函數從全局中分離出去,咱們要將它們封裝進以下所示的函數中:
(function () { // private scope })();
函數結尾的括號告訴解析器當即執行此函數。咱們能夠在其中加入變量和函數,外部沒法訪問。但若是咱們想在外部訪問它們,也就是說咱們但願它們一部分是公開的,一部分是私有的。咱們可使用閉包的一種形式,稱爲模塊模式(Module Pattern),它容許咱們用一個對象中的公有做用域和私有做用域來劃分函數。
模塊模式以下所示:
var Module = (function() { function privateMethod() { // do something } return { publicMethod: function() { // can call privateMethod(); } }; })();
Module 的return語句包含了咱們的公共函數。私有函數並無被return。函數沒有被return確保了它們在 Module 命名空間沒法訪問。但咱們的共有函數能夠訪問咱們的私有函數,方便它們使用有用的函數、AJAX 調用或其餘東西。
Module.publicMethod(); // works Module.privateMethod(); // Uncaught ReferenceError: privateMethod is not defined
一種習慣是如下劃線做爲開始命名私有函數,並返回包含共有函數的匿名對象。這使它們在很長的對象中很容易被管理。向下面這樣:
var Module = (function () { function _privateMethod() { // do something } function publicMethod() { // do something } return { publicMethod: publicMethod, } })();
另外一種形式的閉包是當即執行函數表達式(Immediately-Invoked Function Expression,IIFE)。這是一種在 window 上下文中自調用的匿名函數,也就是說this的值是window。它暴露了一個單一全局接口用來交互。以下所示:
(function(window) { // do anything })(this);
Call 和 Apply 函數來改變函數調用時的上下文。這帶給你神奇的編程能力(和終極統治世界的能力)。你只須要使用 call 和 apply 函數並把上下文當作第一個參數傳入,而不是使用括號來調用函數。函數本身的參數能夠在上下文後面傳入。
function hello() { // do something... } hello(); // the way you usually call it hello.call(context); // here you can pass the context(value of this) as the first argument hello.apply(context); // here you can pass the context(value of this) as the first argument
.call()和.apply()的區別是 Call 中其餘參數用逗號分隔傳入,而 Apply 容許你傳入一個參數數組。
function introduce(name, interest) { console.log('Hi! I'm '+ name +' and I like '+ interest +'.'); console.log('The value of this is '+ this +'.') } introduce('Hammad', 'Coding'); // the way you usually call it introduce.call(window, 'Batman', 'to save Gotham'); // pass the arguments one by one after the contextt introduce.apply('Hi', ['Bruce Wayne', 'businesses']); // pass the arguments in an array after the context // Output: // Hi! I'm Hammad and I like Coding. // The value of this is [object Window]. // Hi! I'm Batman and I like to save Gotham. // The value of this is [object Window]. // Hi! I'm Bruce Wayne and I like businesses. // The value of this is Hi.
Call 比 Apply 的效率高一點。
下面這個例子列舉文檔中全部項目,而後依次在控制檯打印出來。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Things to learn</title> </head> <body> <h1>Things to Learn to Rule the World</h1> <ul> <li>Learn PHP</li> <li>Learn Laravel</li> <li>Learn JavaScript</li> <li>Learn VueJS</li> <li>Learn CLI</li> <li>Learn Git</li> <li>Learn Astral Projection</li> </ul> <script> // Saves a NodeList of all list items on the page in listItems var listItems = document.querySelectorAll('ul li'); // Loops through each of the Node in the listItems NodeList and logs its content for (var i = 0; i < listItems.length; i++) { (function () { console.log(this.innerHTML); }).call(listItems[i]); } // Output logs: // Learn PHP // Learn Laravel // Learn JavaScript // Learn VueJS // Learn CLI // Learn Git // Learn Astral Projection </script> </body> </html>
HTML文檔中僅包含一個無序列表。JavaScript 從 DOM 中選取它們。列表項會被從頭至尾循環一遍。在循環時,咱們把列表項的內容輸出到控制檯。
輸出語句包含在由括號包裹的函數中,而後調用call函數。相應的列表項傳入 call 函數,確保控制檯輸出正確對象的 innerHTML。
對象能夠有方法,一樣函數對象也能夠有方法。事實上,JavaScript 函數有 4 個內置方法:
Function.prototype.toString()
返回函數代碼的字符串表示。
到如今爲止,咱們討論了.call()、.apply()和toString()。與 Call 和 Apply 不一樣,Bind 並非本身調用函數,它只是在函數調用以前綁定上下文和其餘參數。在上面提到的例子中使用 Bind:
(function introduce(name, interest) { console.log('Hi! I'm '+ name +' and I like '+ interest +'.'); console.log('The value of this is '+ this +'.') }).bind(window, 'Hammad', 'Cosmology')(); // logs: // Hi! I'm Hammad and I like Cosmology. // The value of this is [object Window].
Bind 像call函數同樣用逗號分隔其餘傳入參數,不像apply那樣用數組傳入參數。
這些概念是 JavaScript 的基礎,若是你想鑽研更深的話,理解這些很重要。我但願你對 JavaScript 做用域及相關概念有了更好地理解。若是有東西不清楚,能夠在評論區提問。
做用域常伴你的代碼左右,享受編碼!