理解 JavaScript 範圍

來源:互聯網
上載者:User

標籤: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 範圍(轉)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.