JavaScript的做用域、閉包、(apply, call, bind)

介紹

JavaScript 有一個特徵————做用域。理解做用域scope可使你的代碼脫穎而出,減小錯誤,幫助你用它構造強大的設計模式。html

什麼是做用域

做用域就是在代碼執行期間變量,函數和對象能被獲取到的特定的代碼範圍。換句話說,做用域決定了變量和其餘資源在你的代碼區域中的可見性。ajax

爲何會有做用域?———最小存取原則

那麼,限制變量的可見性不讓其在代碼中到處可見的意義是什麼?優點之一 是做用域使你的代碼具有必定的安全性。一個通用的計算機安全性原則就是讓用戶每次只訪問他們須要的東西。編程

想一想計算機管理員:他們須要控制不少公司系統的東西,給他們徹底的用戶權限彷佛是能夠理解的。假設一個公司有三個系統管理員,他們都有系統的全部權限,一切進展順利。可是忽然厄運降臨,其中一人的系統被可惡的病毒感染了,而如今不知道是誰哪裏出錯了。如今意識到應該給他們基本權限的用戶帳戶只在他們須要的時候授予他們徹底的權限。這會幫助你追蹤變更並一直知曉哪一個帳戶作了什麼。這就叫作最小存取原則。好像很直觀吧,這個原則也用於程序語言設計,在包括JS在內的編程語言中它叫作做用域設計模式

當你享受編程之旅時,你會意識到你的代碼的做用域部分幫助你提高效率,追蹤bug並減小bug。做用域同時解決了你在編程時不一樣做用域內的同名變量的問題。不要把環境/上下文做用域搞混,他們是不一樣的。數組

JavaScript的做用域

JavaScript有兩種類型的做用域:瀏覽器

  • 全局做用域
  • 局部做用域

定義在函數內部的變量在本地範圍內,而定義在函數外部的變量的做用域是全局。每一個函數的觸發調用都會建立一個新的做用域。安全

全局做用域

當你開始寫JS的時候,你就已經處在全局範圍內了,一個變量若不在函數內,即是全局變量。閉包

// the scope is by default global
var name = 'Hammad';

全局範圍內的變量能夠在其餘範圍內獲取或修改。app

var name = 'Hammad';

console.log(name); // logs 'Hammad'

function logName() {
    console.log(name); // 'name' is accessible here and everywhere else
}

logName(); // logs 'Hammad'

局部做用域

定義在函數內的變量就在局部做用域。
每次調用那個函數他們都有不一樣的做用域,也就是說同名變量能夠在不一樣的函數內使用。由於這些變量與他們各自的函數綁定,各自有不一樣的做用域,沒法在其餘函數內獲取。編程語言

// Global Scope
function someFunction() {
    // Local Scope #1
    function someOtherFunction() {
        // Local Scope #2
    }
}

// Global Scope
function anotherFunction() {
    // Local Scope #3
}
// Global Scope

塊語句

ifswitch這種條件語句或forwhile這種循環語句————非函數的塊語句,不會創造新的做用域。定義在塊語句內的變量將保持他們當前的做用域。

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引入了 letconst關鍵字,能夠用於替換 var。相比 var,後者支持 塊做用域的聲明。
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
只要你的應用激活了,全局做用域也就激活了。局部做用域則隨着你的函數的調用和執行而激活。

Context————上下文/環境

許多開發者常常把做用域和上下文弄混淆,好像它們是相同的概念。非也。做用域就是咱們以上討論的,而上下文是指你得代碼特定區域內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關鍵字調用函數,上下文的值會改變爲所調用的函數的實例。例如:
function logFunction() {
    console.log(this);
}

new logFunction(); // logs logFunction {}

嚴格模式下上下文默認爲undefined

  • 將"use strict"放在腳本文件的第一行,則整個腳本都將以"嚴格模式"運行。若是這行語句不在第一行,則無效,整個腳本以"正常模式"運行。
  • 若是不一樣模式的代碼文件合併成一個文件,這一點須要特別注意。(嚴格地說,只要前面不是產生實際運行結果的語句,"use strict"能夠不在第一行,好比直接跟在一個空的分號後面。)將"use strict"放在函數體的第一行,則整個函數以"嚴格模式"運行。
  • 對於腳本,最好將整個腳本文件放在一個當即執行的匿名函數之中。

執行上下文

爲了完全弄清楚以上困惑,在執行上下文中的上下文指的是做用域而不是上下文。這是個奇怪的命名慣例可是由於JavaScript已經明確了它,咱們只需記住便可。
JavaScript是一個單線程語言因此他一次只能執行一個任務。剩下的任務在執行上下文中以隊列形式存在。正如我以前所說,當JavaScript編譯器開始執行代碼時,上下文(做用域)就被默認設置爲全局的了。這個全局的上下文會添加在執行上下文中,它其實是啓動執行上下文的第一個上下文。
隨後,
每一個函數請求會添加它的上下文到執行上下文。當函數內的另外一個函數或其餘地方的函數調用時也同樣。

每一個函數都會建立本身的執行上下文。
一旦瀏覽器執行完上下文的代碼,上下文會從執行上下文中彈出, 在執行上下文中的當前上下文的狀態會被傳遞給父級上下文。瀏覽器總會執行在任務棧最頂端的執行上下文(也就是你代碼中最內部的做用域)。
只能有一個全局上下文但函數上下文能夠有多個。

執行上下文有兩個階段:建立 和 執行。

建立階段

第一個階段是建立階段,是指函數被調用尚未被執行的時期,在建立階段會作三件事情:

  1. 建立變量對象
  2. 建立做用域鏈
  3. 設置上下文的值(this

代碼執行階段

第二個階段是代碼執行階段,這個階段將爲變量賦值,最終執行代碼。

詞法域

詞法域是指在一組函數中,內部函數能夠獲取到他的父級做用域內的變量和其餘資源。這意味這子函數在詞法上綁定了父級的執行上下文。詞法域有時也指靜態域。

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不能被父級訪問獲取。也就是說,同名變量內部函數的優先權高於外層函數。

閉包

閉包的概念與詞法域關係緊密。當一個內部函數試圖訪問外部函數的做用域鏈即其詞法域外的變量值時,閉包就會被建立了。閉包包含他們本身的的做用域鏈,他們父級做用域鏈以及全局的做用域。閉包就是可以讀取其餘函數內部變量的函數,因爲在Javascript語言中,只有函數內部的子函數才能讀取局部變量,所以能夠把閉包簡單理解成"定義在一個函數內部的函數"。

閉包不只能夠獲取函數內部的變量,也能夠獲取其外部函數的參數資源。
 var name = "The Window";

  var object = {
    name : "My Object",

    getNameFunc : function(){
      return function(){
        return this.name;
      };

    }

  };

  alert(object.getNameFunc()());   // =>The Window
 var name = "The Window";

  var object = {
    name : "My Object",

    getNameFunc : function(){
      var that = this;
      return function(){
        return that.name;
      };

    }

  };

  alert(object.getNameFunc()());  // My Object

閉包甚至在函數已經返回後也能夠獲取其外部函數的變量。這容許返回函數一直能夠獲取其外部函數的全部資源。

當一個函數返回一個內部函數時,即便你調用外部函數時返回函數並不會被請求執行。你必須用一個獨立的變量保存外部函數的調用請求,而後以函數形式調用該變量:

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'

一樣能夠用()()替換變量分配執行的過程。

function greet() {
    name = 'Hammad';
    return function () {
        console.log('Hi ' + name);
    }
}

greet()(); // logs 'Hi Hammad'

閉包最大用處有兩個,一個是前面提到的能夠讀取函數內部的變量,另外一個就是讓這些變量的值始終保持在內存中

公共域和私有域

在許多其餘編程語言中,你能夠用 public, private and protected設置屬性和類的方法的可見性。JavaScript中沒有相似的公共域和私有域的機制。可是咱們能夠用閉包模擬這種機制,爲了將全部資源與全局域獨立開來,應該這樣封裝函數:

(function () {
  // private scope
})();

()在函數最後是告訴編譯器直接在讀到該函數時不用等到函數調用就執行它,咱們能夠在裏面添加函數和變量而不用擔憂他們被外部獲取到。可是若是咱們想讓外部獲取它們即想暴露部分變量或函數供外部修改獲取怎麼辦?模塊模式————閉包的一種,支持咱們在一個對象內利用公共域和私有域訪問審視咱們的函數。

模塊模式

模塊模式:

var Module = (function() {
    function privateMethod() {
        // do something
    }

    return {
        publicMethod: function() {
            // can call privateMethod();
        }
    };
})();

模塊的返回語句包含了咱們的公共函數。那些沒有返回的即是私有函數。沒有返回函數使得它們在模塊命名空間外沒法被存取。可是公共函數能夠存取方便咱們的輔助函數,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);

一種常見的閉包致使的bug由當即調用函數表達式解決的例子:

// This example is explained in detail below (just after this code box).​
​function celebrityIDCreator (theCelebrities) {
    var i;
    var uniqueID = 100;
    for (i = 0; i < theCelebrities.length; i++) {
      theCelebrities[i]["id"] = function ()  {
        return uniqueID + i;
      }
    }
    
    return theCelebrities;
}
​
​var actionCelebs = [{name:"Stallone", id:0}, {name:"Cruise", id:0}, {name:"Willis", id:0}];
​
​var createIdForActionCelebs = celebrityIDCreator (actionCelebs);
​
​var stalloneID = createIdForActionCelebs [0];

console.log(stalloneID.id()); // 103

事實上結果的全部id都是103,而不是按順序得出的101,102,103...。
由於for循環中的匿名函數獲得是外部函數變量的引用而非變量實際值,而i的值最終結果爲3,故全部id103,這樣修改能夠獲得預想效果:

function celebrityIDCreator (theCelebrities) {
    var i;
    var uniqueID = 100;
    for (i = 0; i < theCelebrities.length; i++) {
        theCelebrities[i]["id"] = function (j)  { // the j parametric variable is the i passed in on invocation of this IIFE​
            return function () {
                return uniqueID + j; // each iteration of the for loop passes the current value of i into this IIFE and it saves the correct value to the array​
            } () // BY adding () at the end of this function, we are executing it immediately and returning just the value of uniqueID + j, instead of returning a function.​
        } (i); // immediately invoke the function passing the i variable as a parameter​
    }
​
    return theCelebrities;
}
​
​var actionCelebs = [{name:"Stallone", id:0}, {name:"Cruise", id:0}, {name:"Willis", id:0}];
​
​var createIdForActionCelebs = celebrityIDCreator (actionCelebs);
​
​var stalloneID = createIdForActionCelebs [0];

console.log(stalloneID.id); // 100​
​
​var cruiseID = createIdForActionCelebs [1];
console.log(cruiseID.id); // 101

利用.call(), .apply().bind()改變上下文

CallApply 函數 在調用函數時能夠用來改變上下文。這賦予了你難以置信的編程能力。爲了使用兩個函數,你須要在函數上調用它而非用()觸發,並將上下文做爲第一個參數傳遞。函數自己的參數可在上下文後傳遞。

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()將剩餘參數以,隔開,而.appley()會將這些參數包含在一個數組裏傳遞。

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>

這裏我想起來之前看到過的.caller().callee():

  • .caller()是指調用函數的函數體,返回函數體,相似於toString()
  • .callee()Arguments的一個成員,表示對函數對象自己的引用,經常使用屬性是lengtharguments.length是指實參長度,callee.length形參長度。

具體可參考這裏

對象能夠有方法,一樣函數對象也能夠有方法。事實上,一個JavaScript函數生來就有四種內置函數

  • Function.prototype.apply()
  • Function.prototype.bind() (Introduced in ECMAScript 5 (ES5))
  • Function.prototype.call()
  • Function.prototype.toString() 將函數字符串化
.prototype => .__proto__

不一樣於CallApplyBind自己不調用函數,只用來在調用函數前綁定上下文的值和其餘參數,例如:

(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傳遞一個數組,它返回的是一個新函數

var person1 = {firstName: 'Jon', lastName: 'Kuperman'};
var person2 = {firstName: 'Kelly', lastName: 'King'};

function say() {
    console.log('Hello ' + this.firstName + ' ' + this.lastName);
}

var sayHelloJon = say.bind(person1);
var sayHelloKelly = say.bind(person2);

sayHelloJon(); // Hello Jon Kuperman
sayHelloKelly(); // Hello Kelly King

Happy Coding!

相關文章
相關標籤/搜索