Javacript 中有一系列作用域的概念。對於新的JS的開發人員無法理解這些概念,甚至一些經驗豐富的開發者也未必能。這篇文章主要目的幫助理解JavaScript中的一些概念如:scope,closure, this, namespace, function scope, global scope, lexical scope and public/private scope. 希望從這篇文章中能回答如下的問題:
1、什麼是作用域( Scope)?
在JavaScript中,作用域通常是指代碼的上下文(context)。能夠定義全局或者局部作用域。理解JavaScript的作用域是編寫強健的代碼和成為一個好的開發者的前提。你需要掌握在那裡獲取變量和函數,在那裡能夠能夠改變你的代碼上下文的作用域以及如何能夠編寫快速和可讀性強以及便於調試的代碼。
想象作用域非常簡單,我們在作用域A還是作用域B?
2、什麼是全局作用域( Global Scope)?
在寫第一行JavaScript代碼之前,我們處在全局作用域中。此時我們定義一個變量,通常都是全局變量。
// global scopevar name = 'Todd';
全局作用域即是你的好友又是你的噩夢。學習控制作用域很簡單,學會後使用全局變量就不會遇到問題(通常為命名空間沖突)。經常會聽到大伙說 “全局作用域不好”,但是從沒有認真想過為什麼。不是全局作用域不好,而是使用問題。在創建跨作用域Modules/APIs的時候,我們必須在不引起問題的情況下使用它們。
jQuery('.myClass');
...我們正在全局作用域中獲取jQuery,我們可以把這種引用稱為命名空間。命名空間通常是指作用域中可以交換word,但是其通常引用更高級別的作用域。在上面的例子中,jQuery 在全局作用域中,也稱為命名空間。jQuery 作為命名空間定義在全局作用域中,其作為jQuery庫的命令空間,庫中的所有內容成為命名空間的子項(descendent )。
2、什麼是局部作用域( Local Scope)?
局部作用域通常位於全局作用域後。一般來說,存在一個全局作用域,每個函數定義了自己的局部作用域。任何定義於其他函數內部的函數都有一個局部作用域,該局部作用域鏈接到外部函數。
如果定義了一個函數並在裡面創建變量,那麼這些變量就是局部變量。例如:
// 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為局部變量,沒有暴露給父作用域,因此出現not defined。
3、函數作用域
JavaScript 中函數域為最小域范圍。for與while循環或者if和switch都不能構建作用域。規則就是,新函數新域。一個創建域的簡單示例如下:
// Scope A var myFunction = function () { // Scope B var myOtherFunction = function () {// Scope C};};
非常方便的創建新的域和本地變量、函數和對象。
4、詞匯作用域( Lexical Scope)
當遇到一個函數嵌套到另一函數中,內部函數能夠訪問外部函數的作用域,那麼這種方式叫做詞匯作用域(Lexical Socpe)或者閉包,也稱為成為靜態作用域。最能說明該問題的示例如下:
// Scope A var myFunction = function () { // Scope B var name = 'Todd'; // defined in Scope B var 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 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的引用,但是永遠也無法返回變量本身。
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> element setTimeout(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> element setTimeout(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中才引入。我們都知道,不能像下面一樣傳遞參數給函數引用:
// 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);
函數並沒被觸發,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 module var Module = (function () { return {myMethod: function () { console.log('myMethod has been called.');}}; })(); // call module + methods Module.myMethod();
這兒的return 語句返回了公共的方法,只有通過命名空間才能夠被訪問到。這就意味著,我們使用Module 作為我們的命名空間,其能夠包含我們需要的所有方法。我們可以根據實際的需求來擴展我們的模塊。
// define module var Module = (function () { return {myMethod: function () {}, someOtherMethod: function () {}};})(); // call module + methods Module.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})(); // usage Module.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作用域的全部內容,希望對大家的學習有所幫助。