標籤:nes 地址 gre als 函數 自己的 tools ops turned
簡介
JavaScript 有個特性稱為範圍。儘管對於很多開發新手來說,範圍的概念不容易理解,我會儘可能地從最簡單的角度向你解釋它們。理解範圍能讓你編寫更優雅、錯誤更少的代碼,並能協助你實現強大的設計模式。
什麼是範圍?
範圍是你的代碼在運行時,各個變數、函數和對象的可訪問性。換句話說,範圍決定了你的代碼裡的變數和其他資源在各個地區中的可見度。
為什麼需要範圍?最小訪問原則
那麼,限制變數的可見度,不允許你代碼中所有的東西在任意地方都可用的好處是什嗎?其中一個優勢,是範圍為你的代碼提供了一個安全層級。電腦安全中,有個常規的原則是:使用者只能訪問他們當前需要的東西。
想想電腦系統管理員吧。他們在公司各個系統上擁有很多控制權,看起來甚至可以給予他們擁有全部許可權的帳號。假設你有一家公司,擁有三個管理員,他們都有系統的全部存取權限,並且一切運轉正常。但是突然發生了一點意外,你的一個系統遭到惡意病毒攻擊。現在你不知道這誰出的問題了吧?你這才意識到你應該只給他們基本使用者的帳號,並且只在需要時賦予他們完全的訪問權。這能協助你跟蹤變化並記錄每個人的操作。這叫做最小訪問原則。眼熟嗎?這個原則也應用於程式設計語言設計,在大多數程式設計語言(包括 JavaScript)中稱為範圍,接下來我們就要學習它。
在你的編程旅途中,你會意識到範圍在你的代碼中可以提升效能,跟蹤 bug 並減少 bug。範圍還解決不同範圍的同名變數命名問題。記住不要弄混範圍和上下文。它們是不同的特性。
JavaScript中的範圍
在 JavaScript 中有兩種範圍
當變數定義在一個函數中時,變數就在局部範圍中,而定義在函數之外的變數則從屬於全域範圍。每個函數在調用的時候會建立一個新的範圍。
全域範圍
當你在文檔中(document)編寫 JavaScript 時,你就已經在全域範圍中了。JavaScript 文檔中(document)只有一個全域範圍。定義在函數之外的變數會被儲存在全域範圍中。
// the scope is by default globalvar 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‘
局部範圍
定義在函數中的變數就在局部範圍中。並且函數在每次調用時都有一個不同的範圍。這意味著同名變數可以用在不同的函數中。因為這些變數綁定在不同的函數中,擁有不同範圍,彼此之間不能訪問。
// Global Scopefunction someFunction() { // Local Scope ##1 function someOtherFunction() { // Local Scope ##2 }} // Global Scopefunction 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 definedconsole.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 objectlogFunction();
如果範圍定義在一個對象的方法中,上下文就是這個方法所在的那個對象。
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 模式(strict mode)中調用函數時,上下文預設是 undefined。
執行環境
為瞭解決掉我們從上面學習中會出現的各種困惑,“執行環境(context)”這個詞中的“環境(context)”指的是範圍而並非上下文。這是一個怪異的命名規範,但由於 JavaScript 的文檔如此,我們只好也這樣約定。
JavaScript 是一種單線程語言,所以它同一時間只能執行單個任務。其他任務排列在執行環境中。當 JavaScript 解析器開始執行你的代碼,環境(範圍)預設設為全域。全域環境添加到你的執行環境中,事實上這是執行環境裡的第一個環境。
之後,每個函數調用都會添加它的環境到執行環境中。無論是函數內部還是其他地方調用函數,都會是相同的過程。
每個函數都會建立它自己的執行環境。
當瀏覽器執行完環境中的代碼,這個環境會從執行環境中彈出,執行環境中當前環境的狀態會轉移到父級環境。瀏覽器總是先執行在執行棧頂的執行環境(事實上就是你代碼最裡層的範圍)。
全域環境只能有一個,函數環境可以有任意多個。
執行環境有兩個階段:建立和執行。
建立階段
第一階段是建立階段,是函數剛被調用但代碼並未執行的時候。建立階段主要發生了 3 件事。
- 建立變數對象
- 建立範圍鏈
- 設定上下文(this)的值
變數對象
變數對象(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 greetLettergreetLetter = greet(); // calling greetLetter calls the returned function from the greet() functiongreetLetter(); // 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 Scopepublic $property;public function method() { // ...} // Private Sccpeprivate $property;private function method() { // ...} // Protected Scopeprotected $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(); // worksModule.privateMethod(); // Uncaught ReferenceError: privateMethod is not defined
一種習慣是以底線作為開始命名私人函數,並返回包含共有函數的匿名對象。這使它們在很長的對象中很容易被管理。向下面這樣:
var Module = (function () { function _privateMethod() { // do something } function publicMethod() { // do something } return { publicMethod: publicMethod, }})();立即執行函數運算式(IIFE)
另一種形式的閉包是立即執行函數運算式(Immediately-Invoked Function Expression,IIFE)。這是一種在 window 上下文中自調用的匿名函數,也就是說this的值是window。它暴露了一個單一全域介面用來互動。如下所示:
(function(window) { // do anything})(this);使用 .call(), .apply() 和 .bind() 改變上下文
Call 和 Apply 函數來改變函數調用時的上下文。這帶給你神奇的編程能力(和終極統治世界的能力)。你只需要使用 call 和 apply 函數並把上下文當做第一個參數傳入,而不是使用括弧來調用函數。函數自己的參數可以在上下文後面傳入。
function hello() { // do something...} hello(); // the way you usually call ithello.call(context); // here you can pass the context(value of this) as the first argumenthello.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 itintroduce.call(window, ‘Batman‘, ‘to save Gotham‘); // pass the arguments one by one after the contexttintroduce.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.apply()Function.prototype.bind() (Introduced in ECMAScript 5 (ES5))Function.prototype.call()Function.prototype.toString()
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 範圍及相關概念有了更好地理解。如果有東西不清楚,可以在評論區提問。
範圍常伴你的代碼左右,享受編碼!
前端技術書箱列表: https://github.com/jobbole/awesome-web-dev-books
原文地址:http://web.jobbole.com/91134/
理解 JavaScript 範圍(轉)