原文: https://toddmotto.com/everything-you-wanted-to-know-about-javascript-scope/javascript
JavaScript中有許多章節是關於scope
的,可是對於初學者來講(甚至是一些有經驗的JavaScript開發者),這些有關做用域的章節既不直接也不容易理解. 這篇文章的目的就是爲了幫助那些想更深一步學習瞭解JavaScript做用域的開發者,尤爲是當他們聽到一些關於做用域的單詞的時候, 比如:做用域(scope)
,閉包(closure)
,this
,命名空間(namespace)
,函數做用域(function scope)
,全局做用域(global scope)
,詞法做用域(lexical)
,公有變量(public scope)
,私有變量(private scope)
. 但願經過這篇文章你能夠知道下面這些問題的答案:java
this
關鍵字是什麼,做用於又是怎麼影響它的?在JavaScript中,做用域指的是你代碼的當前上下文環境.做用域能夠被全局或者局部地定義.理解JavaScript的做用域是讓你寫出穩健的代碼而且成爲一個更好的開發者的關鍵. 你將會理解那些變量或者函數是能夠訪問的,而且有能力去改變你代碼的做用域進而有能力去寫出運行速度更快,更容易維護,固然調試也很是容易的代碼. 別把做用域想的太複雜,那麼咱們如今是在A做用域
仍是B做用域
?git
當你在開始書寫JavaScript代碼的時候,你所處的做用域就是咱們所說的全局做用域
.若是咱們定義了一個變量,那麼它就是被全局定義的:github
// global scope
var name = 'Todd';
全局做用域是你最好的朋友也是你最壞的噩夢;學會去掌控你的做用域是容易的,若是你那樣作了,你將不會遇到一些關於全局做用域的問題(一般是關於命名空間的衝突). 你也許會常常聽到有人在說全局做用域是很差的,可是你歷來沒有考慮過他們那樣說的真正緣由.全局做用域固然沒有他們說的那樣,相反全局做用域是很好的, 你須要使用它去建立可以在別的做用域訪問的模塊還有接口(APIs),你要在使用它的優勢的同時確保不產生新的問題.編程
不少人之前都使用過jQuery
,當你寫下下面的代碼的時候...設計模式
jQuery('.myClass');
咱們這時就是經過全局做用域來使用jQuery
的,咱們能夠把這種使用叫作命名空間
.有時命名空間就是一個能夠用不一樣單詞來替代的做用域,可是一般指的是最高一級的做用域. 在這個例子中,jQuery
是在全局做用域中,因此也是咱們的命名空間.這個jQuery
的命名空間是定義在全局做用域上的,它做爲這個jQuery
庫的命名空間, 全部在jQuery
庫內的東西都是這個命名空間的派生物.數組
局部做用域指的是那些從全局做用域中定義的許多做用域.JavaScript只有一個全局做用域,每個定義的函數都有本身的局部(嵌套)做用域.那些定義在別的函數中的函數有一個局部的做用域, 而且這個做用域是指向外部的函數.緩存
若是我定義了一個函數,而且在裏面建立了一些變量,這些變量的做用域就是局部的.安全
把下面的當作一個例子:閉包
// Scope A: Global scope out here
var myFunction = function () { // Scope B: Local scope in here };
任何局部的東西在全局是不可見的,除非這些東西被導出;這句話的意思是這樣的,若是我在一個新的做用域裏定義了一些函數或者變量的話,這些變量或者函數在當前的做用域以外是不能夠訪問的. 下面的代碼是關於上面所說的那些的一個小例子:
var myFunction = function () { var name = 'Todd'; console.log(name); // Todd }; // Uncaught ReferenceError: name is not defined console.log(name);
變量name
是局部的變量,它並無暴露在父做用域上,所以它是沒有被定義的.
JavaScript中全部的做用域在建立的時候都只伴隨着函數做用域
,循環語句像for
或者while
,條件語句像if
或者switch
都不可以產生新的做用域. 新的函數 = 新的做用域
這就是規則.下面一個簡單的例子用來解釋做用域的建立:
// Scope A
var myFunction = function () { // Scope B var myOtherFunction = function () { // Scope C }; };
因此說很容易建立新的做用域和局部的變量/函數/對象.
每當你看到一個函數裏面存在着另外一個函數,那麼內部的函數可以訪問外部函數的做用域,這就叫作詞法做用域或者閉包;也被認爲是靜態做用域,下面的代碼是最簡單的方法再一次去解釋咱們所說的內容:
// Scope A
var myFunction = function () { // Scope B var name = 'Todd'; // defined in Scope B var myOtherFunction = function () { // Scope C: `name` is accessible here! }; };
你也許注意到myOtherFunction
沒有在這裏被調用,它只是簡單地被定義.固然它的調用順序也會影響到做用域裏面變量的表現, 在這裏我定義了myOtherFunction
而且在console
語句以後調用了它:
var myFunction = function () { var name = 'Todd'; var myOtherFunction = function () { console.log('My name is ' + name); }; console.log(name); myOtherFunction(); // call function }; // Will then log out: // `Todd` // `My name is Todd`
很容易理解和使用詞法做用域,任何被定義在它的父做用域上的變量/對象/函數,在做用域鏈上都是能夠訪問到的.例如:
var name = 'Todd'; var scope1 = function () { // name is available here var scope2 = function () { // name is available here too var scope3 = function () { // name is also available here! }; }; };
須要記住的一個重要地方是,詞法做用域是不可逆的,咱們能夠從下面的例子中看到結果:
// name = undefined
var scope1 = function () { // name = undefined var scope2 = function () { // name = undefined var scope3 = function () { var name = 'Todd'; // locally scoped }; }; };
固然咱們能夠返回一個指向name
的引用,可是永遠不會是name
變量自己.
做用域鏈爲一個給定的函數創建了做用域.就像咱們知道的那樣,每個被定義的函數都有它本身嵌套的做用域,而且任何定義在別的函數中的函數都有一個 鏈接外部函數的局部做用域,這個鏈接就是咱們所說的做用域鏈中的鏈.它經常是在代碼中那些可以定義做用域的位置,當咱們訪問一個變量的時候, JavaScript
從最裏面的做用域沿着做用域鏈向外部開始查找,直到找到咱們想要的那個變量/對象/函數.
閉包和詞法做用域是緊密聯繫在一塊兒的,關於閉包是如何工做的一個好例子就是當咱們返回一個函數的引用的時候,這是一個更實際的用法. 在咱們的做用域裏,咱們能夠返回一些東西以便這些東西可以在父做用域裏被訪問和使用:
var sayHello = function (name) { var text = 'Hello, ' + name; return function () { console.log(text); }; };
咱們這裏使用的閉包
概念使咱們在sayHello
的做用域不可以被外部(公共的)做用域訪問.單獨運行這個函數不會有什麼結果由於它只是返回了一個函數:
sayHello('Todd'); // nothing happens, no errors, just silence...
這個函數返回了一個函數,那就意味着咱們須要對它進行賦值,而後對它進行調用:
var helloTodd = sayHello('Todd'); helloTodd(); // will call the closure and log 'Hello, Todd'
好吧,我撒謊了,你也能夠直接調用它,你也許以前已經見到過像這樣的函數,這種方式也是能夠運行你的閉包:
sayHello('Bob')(); // calls the returned function without assignment
AngularJS的$compile
方法使用了上面的技術,你能夠將當前做用的引用域傳遞給這個閉包:
$compile(template)(scope);
咱們能夠猜想他們關於這個方法的(簡化)代碼大概是下面這個樣子:
var $compile = function (template) { // some magic stuff here // scope is out of scope, though... return function (scope) { // access to `template` and `scope` to do magic with too }; };
固然一個函數沒必要有返回值也可以被稱爲一個閉包.只要可以訪問外部變量的一個即時的詞法做用域就建立了一個閉包.
this
每個做用域都綁定了一個不一樣值的this
,這取決於這個函數是如何調用的.咱們都使用過this
關鍵詞,可是並非全部的人都理解它,還有當它被調用的時候是如何的不一樣. 默認狀況下,this
指向的是最外層的全局對象window
.咱們能夠很容易的展現關於不一樣的調用方式咱們綁定的this
的值也是不一樣的:
var myFunction = function () { console.log(this); // this = global, [object Window] }; myFunction(); var myObject = {}; myObject.myMethod = function () { console.log(this); // this = Object { myObject } }; var nav = document.querySelector('.nav'); // <nav class="nav"> var toggleNav = function () { console.log(this); // this = <nav> element }; nav.addEventListener('click', toggleNav, false);
當咱們處理this
的值的時候咱們又遇到了一些問題,舉個例子若是我添加一些代碼在上面的例子中.就算是在同一個函數內部,做用域和this
都是會發生改變的:
var nav = document.querySelector('.nav'); // <nav class="nav"> var toggleNav = function () { console.log(this); // <nav> element setTimeout(function () { console.log(this); // [object Window] }, 1000); }; nav.addEventListener('click', toggleNav, false);
因此這裏發生了什麼?咱們建立了一個新的做用域,這個做用域沒有被咱們的事件處理程序調用,因此默認狀況下,這裏的this
指向的是window
對象. 固然咱們能夠作一些事情不讓這個新的做用域影響咱們,以便咱們可以訪問到這個正確的this
值.你也許已經見到過咱們這樣作的方法了,咱們可使用that
變量緩存當前的this
值, 而後在新的做用域中使用它.
var nav = document.querySelector('.nav'); // <nav class="nav"> var toggleNav = function () { var that = this; console.log(that); // <nav> element setTimeout(function () { console.log(that); // <nav> element }, 1000); }; nav.addEventListener('click', toggleNav, false);
這是一個小技巧,讓咱們可以使用到正確的this
值,而且在新的做用域解決一些問題.
.call()
,.apply()
或者.bind()
改變做用域有時,你須要根據你所處理的狀況來處理JavaScript的做用域.一個簡單的例子展現如何在循環的時候改變做用域:
var links = document.querySelectorAll('nav li'); for (var i = 0; i < links.length; i++) { console.log(this); // [object Window] }
這裏的this
沒有指向咱們須要的元素,咱們不可以在這裏使用this
調用咱們須要的元素,或者改變循環裏面的做用域. 讓咱們來思考一下如何可以改變咱們的做用域(好吧,看起來好像是咱們改變了做用域,可是實際上咱們真正作的事情是去改變咱們那個函數的運行上下文).
.call()和.apply() .call()
和.apply()
函數是很是實用的,它們容許你傳遞一個做用域到一個函數裏面,這個做用與綁定了正確的this
值. 讓咱們來處理上面的那些代碼吧,讓循環裏面的this
指向正確的元素值:
var links = document.querySelectorAll('nav li'); for (var i = 0; i < links.length; i++) { (function () { console.log(this); }).call(links[i]); }
你能夠看到我是如何作的,首先咱們建立了一個當即執行的函數(新的函數就代表建立了新的做用域), 而後咱們調用了.call()
方法,將數組裏面的循環元素link[i]
當作參數傳遞給了.call()
方法, 而後咱們就改變了哪一個當即執行的函數的做用域.咱們可使用.call()
或者.apply()
方法,可是它們的不一樣之處是參數的傳遞形式, .call()
方法的參數的傳遞形式是這樣的.call(scope, arg1, arg2, arg3)
,.apply()
的參數的傳遞形式是這樣的.apply(scope, [arg1, arg2])
.
因此當你須要改變你的函數的做用域的時候,不要使用下面的方法:
myFunction(); // invoke myFunction
而應該是這樣,使用.call()
去調用咱們的方法
myFunction.call(scope); // invoke myFunction using .call()
.bind() 不像上面的方法,使用.bind()
方法不會調用一個函數,它僅僅在函數調用以前,綁定咱們須要的值.就像咱們知道的那樣, 咱們不可以給函數的引用傳遞參數.就像下面這樣:
// works
nav.addEventListener('click', toggleNav, false); // will invoke the function immediately nav.addEventListener('click', toggleNav(arg1, arg2), false);
咱們能夠解決這個問題,經過在它裏面建立一個新的函數:
nav.addEventListener('click', function () { toggleNav(arg1, arg2); }, false);
可是這樣就改變了做用域,咱們又一次建立了一個不須要的函數,這樣作須要花費不少,當咱們在一個循環中綁定事件監聽的時候. 這時候就須要.bind()
閃亮登場了,由於咱們可使用他來進行綁定做用域,傳遞參數,而且函數還不會當即執行:
nav.addEventListener('click', toggleNav.bind(scope, arg1, arg2), false);
上面的函數沒有被當即調用,而且做用域在須要的狀況下也會改變,並且函數的參數也是能夠經過這個方法傳入的.
在許多編程語言中,你應該聽到過私有做用域或者共有做用域,在JavaScript中,是沒有這些概念的.固然咱們也能夠經過一些手段好比閉包來模擬公共做用域或者是私有做用域.
經過使用JavaScript的設計模式,好比模塊
模式,咱們能夠創造公共做用域和私有做用域.一個簡單的方法建立私有做用域就是使用一個函數去包裹咱們本身定義的函數. 就像上面所說的那樣,函數建立了一個與全局做用域隔離的一個做用域:
(function () {
// private scope inside here })();
咱們可能須要爲咱們的應用添加一些函數:
(function () {
var myFunction = function () { // do some stuff here }; })();
可是當咱們去調用位於函數內部的函數的時候,這些函數在外部的做用域是不可獲得的:
(function () {
var myFunction = function () { // do some stuff here }; })(); myFunction(); // Uncaught ReferenceError: myFunction is not defined
成功了,咱們建立了私有的做用域.可是問題又來了,我如何在公共做用域內使用咱們以前定義好的函數?不要擔憂,咱們的模塊設計模式或者說是提示模塊模式, 容許咱們將咱們的函數在公共做用域內發揮做用,它們使用了公共做用域和私有做用域以及對象.在下面我定義了個人全局命名空間,叫作Module
, 這個命名空間裏包含了與那個模塊相關的全部代碼:
// define module
var Module = (function () { return { myMethod: function () { console.log('myMethod has been called.'); } }; })(); // call module + methods Module.myMethod();
上面的return
聲明代表了咱們返回了咱們的public
方法,這些方法是能夠在全局做用域裏使用的,不過須要經過命名空間來調用. 這就代表了咱們的那個模塊只是存在於哪一個命名空間中,它能夠包含咱們想要的任意多的方法或者變量.咱們也能夠按照咱們的意願來擴展這個模塊:
// define module
var Module = (function () { return { myMethod: function () { }, someOtherMethod: function () { } }; })(); // call module + methods Module.myMethod(); Module.someOtherMethod();
那麼咱們的私有方法該如何使用以及定義呢?老是有許多的開發者隨意的堆砌他們的方法在那個模塊裏面,這樣的作法污染了全局的命名空間. 那些幫助咱們的代碼運行而且是沒必要要出如今全局做用域的方法,就不要導出在全局做用域中,咱們只導出那些須要在全局做用域內被調用的函數. 咱們能夠定義私有的方法,只要不返回它們就行:
var Module = (function () { var privateMethod = function () { }; return { publicMethod: function () { } }; })();
上面的代碼意味着,publicMethod
是能夠在全局的命名空間裏調用的,可是privateMethod
是不能夠的,由於它是在私有的做用域中被定義的. 這些私有的函數方法通常都是一些幫助性的函數,好比addClass
,removeClass
,Ajax/XHR calls
,Arrays
,Objects
等等.
這裏有一些概念須要咱們知道,就是同一個做用域中的函數變量能夠訪問在同一個做用域中的函數或者變量,甚至是這些函數已經被做爲結果返回. 這意味着,咱們的公共函數能夠訪問咱們的私有函數,因此這些私有的函數是仍然能夠運行的,只不過他們不能夠在公共的做用域裏被訪問而已.
var Module = (function () { var privateMethod = function () { }; return { publicMethod: function () { // has access to `privateMethod`, we can call it: // privateMethod(); } }; })();
這容許一個很是強大級別的交互,以及代碼的安全;JavaScript很是重要的一個部分就是確保安全.這就是爲何咱們不可以把全部的函數都放在公共的做用域內, 由於一旦那樣作了就會暴漏咱們系統的漏洞,讓一些心懷惡意的人可以對這些漏洞進行攻擊.
下面的例子就是返回了一個對象,而後在這個對象上面調用一些公有的方法的例子:
var Module = (function () { var myModule = {}; var privateMethod = function () { }; myModule.publicMethod = function () { }; myModule.anotherPublicMethod = function () { }; return myModule; // returns the Object with public methods })(); // usage Module.publicMethod();
一個比較規範的命名私有方法的約定是,在私有方法的名字前面加上一個下劃線,這能夠快速的幫助你區分公有方法或者私有方法:
var Module = (function () { var _privateMethod = function () { }; var publicMethod = function () { }; })();
這個約定幫助咱們能夠簡單地給咱們的函數索引賦值,當咱們返回一個匿名對象的時候:
var Module = (function () { var _privateMethod = function () { }; var publicMethod = function () { }; return { publicMethod: publicMethod, anotherPublicMethod: anotherPublicMethod } })();