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作用域的全部内容,希望对大家的学习有所帮助。