Javacript 中有一系列範圍的概念。對於新的JS的開發人員無法理解這些概念,甚至一些經驗豐富的開發人員也未必能。這篇文章主要目的協助理解JavaScript中的一些概念如:scope,closure, this, namespace, function scope, global scope, lexical scope and public/private scope. 希望從這篇文章中能回答如下的問題:
- 什麼是範圍(scope)?
- 什麼是全域(Global)和局部(Local)範圍?
- 什麼是命名空間和範圍的區別?
- 什麼是this關鍵字且範圍對其的影響?
- 什麼是函數範圍、詞彙範圍?
- 什麼是閉包?
- 什麼是公有和私人範圍?
- 如何理解和建立上述內容?
1、什麼是範圍( Scope)?
在JavaScript中,範圍通常是指代碼的上下文(context)。能夠定義全域或者局部範圍。理解JavaScript的範圍是編寫強健的代碼和成為一個好的開發人員的前提。你需要掌握在那裡擷取變數和函數,在那裡能夠能夠改變你的代碼內容相關的範圍以及如何能夠編寫快速和可讀性強以及便於調試的代碼。
想象範圍非常簡單,我們在範圍A還是範圍B?
2、什麼是全域範圍( Global Scope)?
在寫第一行JavaScript代碼之前,我們處在全域範圍中。此時我們定義一個變數,通常都是全域變數。
// global scopevar name = 'Todd';
全域範圍即是你的好友又是你的噩夢。學習控製作用域很簡單,學會後使用全域變數就不會遇到問題(通常為命名空間衝突)。經常會聽到大夥說 “全域範圍不好”,但是從沒有認真想過為什麼。不是全域範圍不好,而是使用問題。在建立跨範圍Modules/APIs的時候,我們必須在不引起問題的情況下使用它們。
...我們正在全域範圍中擷取jQuery,我們可以把這種引用稱為命名空間。命名空間通常是指範圍中可以交換word,但是其通常引用更進階別的範圍。在上面的例子中,jQuery 在全域範圍中,也稱為命名空間。jQuery 作為命名空間定義在全域範圍中,其作為jQuery庫的命令空間,庫中的所有內容成為命名空間的子項(descendent )。
2、什麼是局部範圍( Local Scope)?
局部範圍通常位於全域範圍後。一般來說,存在一個全域範圍,每個函數定義了自己的局部範圍。任何定義於其他函數內部的函數都有一個局部範圍,該局部範圍連結到外部函數。
如果定義了一個函數並在裡面建立變數,那麼這些變數就是局部變數。例如:
// Scope A: Global scope out herevar myFunction = function () {// Scope B: Local scope in here};
任何的局部作用變數對全域變數來說是不可見的。除非對外暴露。如在新的範圍內定義了函數和變數,他們為當前新範圍內的變數,不能夠在當前範圍外被訪問到。下面為一個簡單的說明樣本:
var myFunction = function () {var name = 'Todd';console.log(name); // Todd};// Uncaught ReferenceError: name is not definedconsole.log(name);
變數name為局部變數,沒有暴露給父範圍,因此出現not defined。
3、函數範圍
JavaScript 中函數域為最小域範圍。for與while迴圈或者if和switch都不能構建範圍。規則就是,新函數新域。一個建立域的簡單樣本如下:
// Scope Avar myFunction = function () {// Scope Bvar myOtherFunction = function () {// Scope C};};
非常方便的建立新的域和本地變數、函數和對象。
4、詞彙範圍( Lexical Scope)
當遇到一個函數嵌套到另一函數中,內建函式能夠訪問外部函數的範圍,那麼這種方式叫做詞彙範圍(Lexical Socpe)或者閉包,也稱為成為靜態範圍。最能說明該問題的樣本如下:
// Scope Avar myFunction = function () {// Scope Bvar name = 'Todd'; // defined in Scope Bvar myOtherFunction = function () {// Scope C: `name` is accessible here!};};
這裡只是簡單的定義了myOtherFunction,並沒有調用。這種調用順序也會影響變數的輸出。這裡我在另一控制台中再定義和調用一個函數。
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 herevar scope2 = function () {// name is available here toovar scope3 = function () {// name is also available here!};};};
唯一需要注意的事情是詞彙域不後項起作用,下面的方式詞彙域是不起作用的:
// name = undefinedvar scope1 = function () {// name = undefinedvar scope2 = function () {// name = undefinedvar scope3 = function () {var name = 'Todd'; // locally scoped};};};
能返回對name的引用,但是永遠也無法返回變數本身。
5、範圍鏈
函數的範圍由範圍鏈構成。我們知道,每個函數可以定義嵌套的範圍,任何內嵌函數都有一個局部範圍串連外部函數。這種嵌套關係我們可以稱為鏈。域一般由代碼中的位置決定。當解釋(resolving)一個變數,通常從範圍鏈的最裡層開始,向外搜尋,直到發現要尋找的變數、對象或者函數。
6、閉包(Closures)
閉包和詞法域( Lexical Scope)很像。返回函數引用,這種實際應用,是一個可以用來解釋閉包工作原理的好例子。在我們的域內部,我們可以返回對象,能夠被父域使用。
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'
好吧,欺騙大家感情了。在實際情況中可能會遇到如下調用閉包的函數,這樣也是行的通的。
sayHello2('Bob')(); // calls the returned function without assignment
Angular js 在$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};};
閉包並不一定需要返回函數。單純在中間詞彙域量的範圍外簡單訪問變數就創造了一個閉包。
7、範圍和this關鍵字
根據函數被觸發的方式不一樣,每個範圍可以綁定一個不同的this值。我們經常使用this,但是我們並不是都瞭解其具體指代什麼。 this預設是執行最外層的全域對象,windows對象。我們能夠很容易的列舉出不同觸發函數綁定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> elementsetTimeout(function () {console.log(this); // [object Window]}, 1000);};nav.addEventListener('click', toggleNav, false);
發生了什嗎?我們建立了一個新的範圍且沒有在event handler中觸發,所以其得到預期的windows對象。如果想this值不受新建立的範圍的影響,我們能夠採取一些做法。以前可能也你也見過,我們使用that建立一個對this的緩衝引用並詞彙綁定:
var nav = document.querySelector('.nav'); // <nav class="nav">var toggleNav = function () {var that = this;console.log(that); // <nav> elementsetTimeout(function () {console.log(that); // <nav> element}, 1000);};nav.addEventListener('click', toggleNav, false);
這是使用this的一個小技巧,能夠解決新建立的範圍問題。
8、使用.call(), .apply() 和.bind()改變範圍
有時候,需要根據實際的需求來變化代碼的範圍。一個簡單的例子,如在迴圈中如何改變範圍:
var links = document.querySelectorAll('nav li');for (var i = 0; i < links.length; i++) {console.log(this); // [object Window]}
這裡的this並沒有指向我們的元素,因為我們沒有觸發或者改變範圍。我們來看看如何改變範圍(看起來我們是改變範圍,其實我們是改變調用函數執行的上下文)。
9、.call() and .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]);}
能夠看到剛將數組迴圈的當前元素通過links[i]傳遞進去,這改變了函數的範圍,因此this的值變為當前迴圈的元素。這個時候,如果需要我們可以使用this。我們既可以使用.call()又可以使用.apply()來改變域。但是這兩者使用還是有區別的,其中.call(scope, arg1, arg2, arg3)輸入單個參數,而.apply(scope, [arg1, arg2])輸入數組作為參數。
非常重要,需要注意的事情是.call() or .apply()實際已經已經取代了如下調用函數的方式調用了函數。
myFunction(); // invoke myFunction
可以使用.call()來鏈式調用:
myFunction.call(scope); // invoke myFunction using .call()
10、.bind()
和上面不一樣的是,.bind()並不觸發函數,它僅僅是在函數觸發前綁定值。非常遺憾的是其只在 ECMASCript 5中才引入。我們都知道,不能像下面一樣傳遞參數給函數引用:
// worksnav.addEventListener('click', toggleNav, false);// will invoke the function immediatelynav.addEventListener('click', toggleNav(arg1, arg2), false);
通過在內部建立一個新的函數,我們能夠修複這個問題(譯註:函數被立即執行):
nav.addEventListener('click', function () {toggleNav(arg1, arg2);}, false);
但是這樣的話,我們再次建立了一個沒用的函數,如果這是在迴圈中綁定事件監聽,會影響代碼效能。這個時候.bind()就派上用場了,在不需要調用的時候就可以傳遞參數。
nav.addEventListener('click', toggleNav.bind(scope, arg1, arg2), false);
函數並沒被觸發,scope可以被改變,且參數在等著傳遞。
11、私人和公開範圍
在許多的程式設計語言中,存在public和private的範圍,但是在javascript中並不存在。但是在JavaScript中通過閉包來類比public和private的範圍。
使用JavaScript的設計模式,如Module模式為例。一個建立private的簡單方式將函數內嵌到另一個函數中。如我們上面掌握的,函數決定scope,通過scope排除全域的scope:
(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
成功的建立了一個私人範圍。那麼怎麼讓函公有呢?有一個非常好的模式(模組模式)允許通過私人和公用範圍以及一個object對象來正確的設定函數範圍。暫且將全域命名空間稱為Module,裡麵包含了所有與模組相關的代碼:
// define modulevar Module = (function () {return {myMethod: function () {console.log('myMethod has been called.');}};})();// call module + methodsModule.myMethod();
這兒的return 語句返回了公用的方法,只有通過命名空間才能夠被訪問到。這就意味著,我們使用Module 作為我們的命名空間,其能夠包含我們需要的所有方法。我們可以根據實際的需求來擴充我們的模組。
// define modulevar Module = (function () {return {myMethod: function () {},someOtherMethod: function () {}};})();// call module + methodsModule.myMethod();Module.someOtherMethod();
那私人方法怎麼辦呢?許多的開發人員採取錯誤的方式,其將所有的函數都至於全域範圍中,這導致了對全域命名空間汙染。 通過函數我們能避免在全域域中編寫代碼,通過API調用,保證可以全域擷取。下面的樣本中,通過建立不返回函數的形式建立私人域。
var Module = (function () {var privateMethod = function () {};return {publicMethod: function () {}};})();
這就意味著publicMethod 能夠被調用,而privateMethod 由於私人範圍不能被調用。這些私人範圍函數類似於: helpers, addClass, removeClass, Ajax/XHR calls, Arrays, Objects等。
下面是一個有趣事,相同範圍中的對象只能訪問相同的範圍,即使有函數被返回之後。這就意味我們的public方法能夠訪問我們的private方法,這些私人方法依然可以起作用,但是不能夠在全域左右域中訪問。
var Module = (function () {var privateMethod = function () {};return {publicMethod: function () {// has access to `privateMethod`, we can call it:// privateMethod();}};})();
這提供了非常強大互動性和安全性機制。Javascript 的一個非常重要的部分是安全性,這也是為什麼我們不能將所有的函數放在全域變數中,這樣做易於被攻擊。這裡有個通過public和private返回Object對象的例子:
var Module = (function () {var myModule = {};var privateMethod = function () {};myModule.publicMethod = function () {};myModule.anotherPublicMethod = function () {};return myModule; // returns the Object with public methods})();// usageModule.publicMethod();
通常私人方法的命名開頭使用底線,從視覺上將其與公有方法區別開。
var Module = (function () {var _privateMethod = function () {};var publicMethod = function () {};})();
當返回匿名對象的時候,通過簡單的函數引用賦值,Module可以按照對象的方式來用。
var Module = (function () {var _privateMethod = function () {};var publicMethod = function () {};return {publicMethod: publicMethod,anotherPublicMethod: anotherPublicMethod}})();
以上就是關於JavaScript範圍的全部內容,希望對大家的學習有所協助。