javascript
你想知道的关于JavaScript作用域的一切(译)
原文鏈接:?Everything you wanted to know about JavaScript scope
原文作者:?Todd Motto
JavaScript中有許多章節(jié)是關(guān)于scope的,但是對(duì)于初學(xué)者來(lái)說(shuō)(甚至是一些有經(jīng)驗(yàn)的JavaScript開(kāi)發(fā)者),這些有關(guān)作用域的章節(jié)既不直接也不容易理解. 這篇文章的目的就是為了幫助那些想更深一步學(xué)習(xí)了解JavaScript作用域的開(kāi)發(fā)者,尤其是當(dāng)他們聽(tīng)到一些關(guān)于作用域的單詞的時(shí)候, 好比:作用域(scope),閉包(closure),this,命名空間(namespace),函數(shù)作用域(function scope),全局作用域(global scope),詞法作用域(lexical),公有變量(public scope),私有變量(private scope). 希望通過(guò)這篇文章你可以知道下面這些問(wèn)題的答案:
- 什么是作用域?
- 什么是全局(局部)作用域?
- 什么是命名空間,它和作用域有什么不同?
- this關(guān)鍵字是什么,作用于又是怎么影響它的?
- 什么是函數(shù)/詞法作用域?
- 什么是閉包?
- 什么是共有/私有作用域?
- 我怎么樣才能夠理解/創(chuàng)建/實(shí)踐上面所有的情況
什么是作用域?
在JavaScript中,作用域指的是你代碼的當(dāng)前上下文環(huán)境.作用域可以被全局或者局部地定義.理解JavaScript的作用域是讓你寫(xiě)出穩(wěn)健的代碼并且成為一個(gè)更好的開(kāi)發(fā)者的關(guān)鍵. 你將會(huì)理解那些變量或者函數(shù)是可以訪問(wèn)的,并且有能力去改變你代碼的作用域進(jìn)而有能力去寫(xiě)出運(yùn)行速度更快,更容易維護(hù),當(dāng)然調(diào)試也非常容易的代碼. 別把作用域想的太復(fù)雜,那么我們現(xiàn)在是在A作用域還是B作用域?
什么是全局作用域
當(dāng)你在開(kāi)始書(shū)寫(xiě)JavaScript代碼的時(shí)候,你所處的作用域就是我們所說(shuō)的全局作用域.如果我們定義了一個(gè)變量,那么它就是被全局定義的:
// global scope var name = 'Todd';全局作用域是你最好的朋友也是你最壞的噩夢(mèng);學(xué)會(huì)去掌控你的作用域是容易的,如果你那樣做了,你將不會(huì)遇到一些關(guān)于全局作用域的問(wèn)題(通常是關(guān)于命名空間的沖突). 你也許會(huì)經(jīng)常聽(tīng)到有人在說(shuō)全局作用域是不好的,但是你從來(lái)沒(méi)有考慮過(guò)他們那樣說(shuō)的真正原因.全局作用域當(dāng)然沒(méi)有他們說(shuō)的那樣,相反全局作用域是很好的, 你需要使用它去創(chuàng)建能夠在別的作用域訪問(wèn)的模塊還有接口(APIs),你要在使用它的優(yōu)點(diǎn)的同時(shí)確保不產(chǎn)生新的問(wèn)題.
很多人以前都使用過(guò)jQuery,當(dāng)你寫(xiě)下下面的代碼的時(shí)候...
jQuery('.myClass');我們這時(shí)就是通過(guò)全局作用域來(lái)使用jQuery的,我們可以把這種使用叫做命名空間.有時(shí)命名空間就是一個(gè)可以用不同單詞來(lái)替代的作用域,但是通常指的是最高一級(jí)的作用域. 在這個(gè)例子中,jQuery是在全局作用域中,所以也是我們的命名空間.這個(gè)jQuery的命名空間是定義在全局作用域上的,它作為這個(gè)jQuery庫(kù)的命名空間, 所有在jQuery庫(kù)內(nèi)的東西都是這個(gè)命名空間的派生物.
什么是局部作用域
局部作用域指的是那些從全局作用域中定義的許多作用域.JavaScript只有一個(gè)全局作用域,每一個(gè)定義的函數(shù)都有自己的局部(嵌套)作用域.那些定義在別的函數(shù)中的函數(shù)有一個(gè)局部的作用域, 并且這個(gè)作用域是指向外部的函數(shù).
如果我定義了一個(gè)函數(shù),并且在里面創(chuàng)建了一些變量,這些變量的作用域就是局部的.
把下面的當(dāng)做一個(gè)例子:
// Scope A: Global scope out here var myFunction = function () { // Scope B: Local scope in here };任何局部的東西在全局是不可見(jiàn)的,除非這些東西被導(dǎo)出;這句話的意思是這樣的,如果我在一個(gè)新的作用域里定義了一些函數(shù)或者變量的話,這些變量或者函數(shù)在當(dāng)前的作用域之外是不可以訪問(wèn)的. 下面的代碼是關(guān)于上面所說(shuō)的那些的一個(gè)小例子:
var myFunction = function () {var name = 'Todd'; console.log(name); // Todd }; // Uncaught ReferenceError: name is not defined console.log(name);變量name是局部的變量,它并沒(méi)有暴露在父作用域上,因此它是沒(méi)有被定義的.
函數(shù)作用域
JavaScript中所有的作用域在創(chuàng)建的時(shí)候都只伴隨著函數(shù)作用域,循環(huán)語(yǔ)句像for或者while,條件語(yǔ)句像if或者switch都不能夠產(chǎn)生新的作用域.?新的函數(shù) = 新的作用域這就是規(guī)則.下面一個(gè)簡(jiǎn)單的例子用來(lái)解釋作用域的創(chuàng)建:
// Scope A var myFunction = function () { // Scope B var myOtherFunction = function () { // Scope C }; };所以說(shuō)很容易創(chuàng)建新的作用域和局部的變量/函數(shù)/對(duì)象.
詞法作用域
每當(dāng)你看到一個(gè)函數(shù)里面存在著另一個(gè)函數(shù),那么內(nèi)部的函數(shù)能夠訪問(wèn)外部函數(shù)的作用域,這就叫做詞法作用域或者閉包;也被認(rèn)為是靜態(tài)作用域,下面的代碼是最簡(jiǎn)單的方法再一次去解釋我們所說(shuō)的內(nèi)容:
// Scope A var myFunction = function () { // Scope B var name = 'Todd'; // defined in Scope B var myOtherFunction = function () { // Scope C: `name` is accessible here! }; };你也許注意到myOtherFunction沒(méi)有在這里被調(diào)用,它只是簡(jiǎn)單地被定義.當(dāng)然它的調(diào)用順序也會(huì)影響到作用域里面變量的表現(xiàn), 在這里我定義了myOtherFunction并且在console語(yǔ)句之后調(diào)用了它:
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`很容易理解和使用詞法作用域,任何被定義在它的父作用域上的變量/對(duì)象/函數(shù),在作用域鏈上都是可以訪問(wèn)到的.例如:
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! }; }; };需要記住的一個(gè)重要地方是,詞法作用域是不可逆的,我們可以從下面的例子中看到結(jié)果:
// name = undefined var scope1 = function () { // name = undefined var scope2 = function () { // name = undefined var scope3 = function () { var name = 'Todd'; // locally scoped }; }; };當(dāng)然我們可以返回一個(gè)指向name的引用,但是永遠(yuǎn)不會(huì)是name變量本身.
作用域鏈
作用域鏈為一個(gè)給定的函數(shù)建立了作用域.就像我們知道的那樣,每一個(gè)被定義的函數(shù)都有它自己嵌套的作用域,并且任何定義在別的函數(shù)中的函數(shù)都有一個(gè) 連接外部函數(shù)的局部作用域,這個(gè)連接就是我們所說(shuō)的作用域鏈中的鏈.它常常是在代碼中那些能夠定義作用域的位置,當(dāng)我們?cè)L問(wèn)一個(gè)變量的時(shí)候,?JavaScript從最里面的作用域沿著作用域鏈向外部開(kāi)始查找,直到找到我們想要的那個(gè)變量/對(duì)象/函數(shù).
閉包
閉包和詞法作用域是緊密聯(lián)系在一起的,關(guān)于閉包是如何工作的一個(gè)好例子就是當(dāng)我們返回一個(gè)函數(shù)的引用的時(shí)候,這是一個(gè)更實(shí)際的用法. 在我們的作用域里,我們可以返回一些東西以便這些東西能夠在父作用域里被訪問(wèn)和使用:
var sayHello = function (name) {var text = 'Hello, ' + name; return function () { console.log(text); }; };我們這里使用的閉包概念使我們?cè)趕ayHello的作用域不能夠被外部(公共的)作用域訪問(wèn).單獨(dú)運(yùn)行這個(gè)函數(shù)不會(huì)有什么結(jié)果因?yàn)樗皇欠祷亓艘粋€(gè)函數(shù):
sayHello('Todd'); // nothing happens, no errors, just silence...這個(gè)函數(shù)返回了一個(gè)函數(shù),那就意味著我們需要對(duì)它進(jìn)行賦值,然后對(duì)它進(jìn)行調(diào)用:
var helloTodd = sayHello('Todd'); helloTodd(); // will call the closure and log 'Hello, Todd'好吧,我撒謊了,你也可以直接調(diào)用它,你也許之前已經(jīng)見(jiàn)到過(guò)像這樣的函數(shù),這種方式也是可以運(yùn)行你的閉包:
sayHello('Bob')(); // calls the returned function without assignmentAngularJS的$compile方法使用了上面的技術(shù),你可以將當(dāng)前作用的引用域傳遞給這個(gè)閉包:
$compile(template)(scope);我們可以猜測(cè)他們關(guān)于這個(gè)方法的(簡(jiǎn)化)代碼大概是下面這個(gè)樣子:
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 }; };當(dāng)然一個(gè)函數(shù)不必有返回值也能夠被稱(chēng)為一個(gè)閉包.只要能夠訪問(wèn)外部變量的一個(gè)即時(shí)的詞法作用域就創(chuàng)建了一個(gè)閉包.
作用域和this
每一個(gè)作用域都綁定了一個(gè)不同值的this,這取決于這個(gè)函數(shù)是如何調(diào)用的.我們都使用過(guò)this關(guān)鍵詞,但是并不是所有的人都理解它,還有當(dāng)它被調(diào)用的時(shí)候是如何的不同. 默認(rèn)情況下,this指向的是最外層的全局對(duì)象window.我們可以很容易的展示關(guān)于不同的調(diào)用方式我們綁定的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);當(dāng)我們處理this的值的時(shí)候我們又遇到了一些問(wèn)題,舉個(gè)例子如果我添加一些代碼在上面的例子中.就算是在同一個(gè)函數(shù)內(nèi)部,作用域和this都是會(huì)發(fā)生改變的:
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);所以這里發(fā)生了什么?我們創(chuàng)建了一個(gè)新的作用域,這個(gè)作用域沒(méi)有被我們的事件處理程序調(diào)用,所以默認(rèn)情況下,這里的this指向的是window對(duì)象. 當(dāng)然我們可以做一些事情不讓這個(gè)新的作用域影響我們,以便我們能夠訪問(wèn)到這個(gè)正確的this值.你也許已經(jīng)見(jiàn)到過(guò)我們這樣做的方法了,我們可以使用that變量緩存當(dāng)前的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);這是一個(gè)小技巧,讓我們能夠使用到正確的this值,并且在新的作用域解決一些問(wèn)題.
使用.call(),.apply()或者.bind()改變作用域
有時(shí),你需要根據(jù)你所處理的情況來(lái)處理JavaScript的作用域.一個(gè)簡(jiǎn)單的例子展示如何在循環(huán)的時(shí)候改變作用域:
var links = document.querySelectorAll('nav li'); for (var i = 0; i < links.length; i++) { console.log(this); // [object Window] }這里的this沒(méi)有指向我們需要的元素,我們不能夠在這里使用this調(diào)用我們需要的元素,或者改變循環(huán)里面的作用域. 讓我們來(lái)思考一下如何能夠改變我們的作用域(好吧,看起來(lái)好像是我們改變了作用域,但是實(shí)際上我們真正做的事情是去改變我們那個(gè)函數(shù)的運(yùn)行上下文).
-
.call()和.apply()?.call()和.apply()函數(shù)是非常實(shí)用的,它們?cè)试S你傳遞一個(gè)作用域到一個(gè)函數(shù)里面,這個(gè)作用與綁定了正確的this值. 讓我們來(lái)處理上面的那些代碼吧,讓循環(huán)里面的this指向正確的元素值:
var links = document.querySelectorAll('nav li'); for (var i = 0; i < links.length; i++) { (function () { console.log(this); }).call(links[i]); }你可以看到我是如何做的,首先我們創(chuàng)建了一個(gè)立即執(zhí)行的函數(shù)(新的函數(shù)就表明創(chuàng)建了新的作用域), 然后我們調(diào)用了.call()方法,將數(shù)組里面的循環(huán)元素link[i]當(dāng)做參數(shù)傳遞給了.call()方法, 然后我們就改變了哪個(gè)立即執(zhí)行的函數(shù)的作用域.我們可以使用.call()或者.apply()方法,但是它們的不同之處是參數(shù)的傳遞形式,?.call()方法的參數(shù)的傳遞形式是這樣的.call(scope, arg1, arg2, arg3),.apply()的參數(shù)的傳遞形式是這樣的.apply(scope, [arg1, arg2]).
所以當(dāng)你需要改變你的函數(shù)的作用域的時(shí)候,不要使用下面的方法:
myFunction(); // invoke myFunction而應(yīng)該是這樣,使用.call()去調(diào)用我們的方法
myFunction.call(scope); // invoke myFunction using .call() -
.bind() 不像上面的方法,使用.bind()方法不會(huì)調(diào)用一個(gè)函數(shù),它僅僅在函數(shù)調(diào)用之前,綁定我們需要的值.就像我們知道的那樣, 我們不能夠給函數(shù)的引用傳遞參數(shù).就像下面這樣:
// works nav.addEventListener('click', toggleNav, false); // will invoke the function immediately nav.addEventListener('click', toggleNav(arg1, arg2), false);我們可以解決這個(gè)問(wèn)題,通過(guò)在它里面創(chuàng)建一個(gè)新的函數(shù):
nav.addEventListener('click', function () { toggleNav(arg1, arg2); }, false);但是這樣就改變了作用域,我們又一次創(chuàng)建了一個(gè)不需要的函數(shù),這樣做需要花費(fèi)很多,當(dāng)我們?cè)谝粋€(gè)循環(huán)中綁定事件監(jiān)聽(tīng)的時(shí)候. 這時(shí)候就需要.bind()閃亮登場(chǎng)了,因?yàn)槲覀兛梢允褂盟麃?lái)進(jìn)行綁定作用域,傳遞參數(shù),并且函數(shù)還不會(huì)立即執(zhí)行:
nav.addEventListener('click', toggleNav.bind(scope, arg1, arg2), false);上面的函數(shù)沒(méi)有被立即調(diào)用,并且作用域在需要的情況下也會(huì)改變,而且函數(shù)的參數(shù)也是可以通過(guò)這個(gè)方法傳入的.
私有/共有的作用域
在許多編程語(yǔ)言中,你應(yīng)該聽(tīng)到過(guò)私有作用域或者共有作用域,在JavaScript中,是沒(méi)有這些概念的.當(dāng)然我們也可以通過(guò)一些手段比如閉包來(lái)模擬公共作用域或者是私有作用域.
通過(guò)使用JavaScript的設(shè)計(jì)模式,比如模塊模式,我們可以創(chuàng)造公共作用域和私有作用域.一個(gè)簡(jiǎn)單的方法創(chuàng)建私有作用域就是使用一個(gè)函數(shù)去包裹我們自己定義的函數(shù). 就像上面所說(shuō)的那樣,函數(shù)創(chuàng)建了一個(gè)與全局作用域隔離的一個(gè)作用域:
(function () {// private scope inside here })();我們可能需要為我們的應(yīng)用添加一些函數(shù):
(function () {var myFunction = function () {// do some stuff here }; })();但是當(dāng)我們?nèi)フ{(diào)用位于函數(shù)內(nèi)部的函數(shù)的時(shí)候,這些函數(shù)在外部的作用域是不可得到的:
(function () {var myFunction = function () {// do some stuff here }; })(); myFunction(); // Uncaught ReferenceError: myFunction is not defined成功了,我們創(chuàng)建了私有的作用域.但是問(wèn)題又來(lái)了,我如何在公共作用域內(nèi)使用我們之前定義好的函數(shù)?不要擔(dān)心,我們的模塊設(shè)計(jì)模式或者說(shuō)是提示模塊模式, 允許我們將我們的函數(shù)在公共作用域內(nèi)發(fā)揮作用,它們使用了公共作用域和私有作用域以及對(duì)象.在下面我定義了我的全局命名空間,叫做Module, 這個(gè)命名空間里包含了與那個(gè)模塊相關(guān)的所有代碼:
// define module var Module = (function () {return { myMethod: function () { console.log('myMethod has been called.'); } }; })(); // call module + methods Module.myMethod();上面的return聲明表明了我們返回了我們的public方法,這些方法是可以在全局作用域里使用的,不過(guò)需要通過(guò)命名空間來(lái)調(diào)用. 這就表明了我們的那個(gè)模塊只是存在于哪個(gè)命名空間中,它可以包含我們想要的任意多的方法或者變量.我們也可以按照我們的意愿來(lái)擴(kuò)展這個(gè)模塊:
// define module var Module = (function () {return { myMethod: function () { }, someOtherMethod: function () { } }; })(); // call module + methods Module.myMethod(); Module.someOtherMethod();那么我們的私有方法該如何使用以及定義呢?總是有許多的開(kāi)發(fā)者隨意的堆砌他們的方法在那個(gè)模塊里面,這樣的做法污染了全局的命名空間. 那些幫助我們的代碼運(yùn)行并且是不必要出現(xiàn)在全局作用域的方法,就不要導(dǎo)出在全局作用域中,我們只導(dǎo)出那些需要在全局作用域內(nèi)被調(diào)用的函數(shù). 我們可以定義私有的方法,只要不返回它們就行:
var Module = (function () {var privateMethod = function () { }; return { publicMethod: function () { } }; })();上面的代碼意味著,publicMethod是可以在全局的命名空間里調(diào)用的,但是privateMethod是不可以的,因?yàn)樗窃谒接械淖饔糜蛑斜欢x的. 這些私有的函數(shù)方法一般都是一些幫助性的函數(shù),比如addClass,removeClass,Ajax/XHR calls,Arrays,Objects等等.
這里有一些概念需要我們知道,就是同一個(gè)作用域中的函數(shù)變量可以訪問(wèn)在同一個(gè)作用域中的函數(shù)或者變量,甚至是這些函數(shù)已經(jīng)被作為結(jié)果返回. 這意味著,我們的公共函數(shù)可以訪問(wèn)我們的私有函數(shù),所以這些私有的函數(shù)是仍然可以運(yùn)行的,只不過(guò)他們不可以在公共的作用域里被訪問(wèn)而已.
var Module = (function () {var privateMethod = function () { }; return { publicMethod: function () { // has access to `privateMethod`, we can call it: // privateMethod(); } }; })();這允許一個(gè)非常強(qiáng)大級(jí)別的交互,以及代碼的安全;JavaScript非常重要的一個(gè)部分就是確保安全.這就是為什么我們不能夠把所有的函數(shù)都放在公共的作用域內(nèi), 因?yàn)橐坏┠菢幼隽司蜁?huì)暴漏我們系統(tǒng)的漏洞,讓一些心懷惡意的人能夠?qū)@些漏洞進(jìn)行攻擊.
下面的例子就是返回了一個(gè)對(duì)象,然后在這個(gè)對(duì)象上面調(diào)用一些公有的方法的例子:
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();一個(gè)比較規(guī)范的命名私有方法的約定是,在私有方法的名字前面加上一個(gè)下劃線,這可以快速的幫助你區(qū)分公有方法或者私有方法:
var Module = (function () {var _privateMethod = function () { }; var publicMethod = function () { }; })();這個(gè)約定幫助我們可以簡(jiǎn)單地給我們的函數(shù)索引賦值,當(dāng)我們返回一個(gè)匿名對(duì)象的時(shí)候:
var Module = (function () {var _privateMethod = function () { }; var publicMethod = function () { }; return { publicMethod: publicMethod, anotherPublicMethod: anotherPublicMethod } })();總結(jié)
以上是生活随笔為你收集整理的你想知道的关于JavaScript作用域的一切(译)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 2D 游戏引擎 AlloyGameEng
- 下一篇: POJ 1201 amp; HDU138