作用域
作用域是一個變數和函數的作用範圍,javascript中函數內宣告的所有變數在函數體內始終是可見的,在javascript中有全域作用域和局部作用域,但是沒有區塊級作用域,局部變數的優先權高於全域變量,透過幾個範例來了解下javascript中作用域的那些「潛規則」(這些也是在前端面試中經常問到的問題)。
1. 變數宣告提前
範例1:
var scope="global"; function scopeTest(){ console.log(scope); var scope="local" } scopeTest(); //undefined
此處的輸出是undefined,並沒有報錯,這是因為在前面我們提到的函數內的聲明在函數體內始終可見,上面的函數等效於:
var scope="global"; function scopeTest(){ var scope; console.log(scope); scope="local" } scopeTest(); //local
注意,如果忘記var,那麼變數就被宣告為全域變數了。
2. 沒有區塊級作用域
和其他我們常用的語言不同,在Javascript中沒有區塊級作用域:
function scopeTest() { var scope = {}; if (scope instanceof Object) { var j = 1; for (var i = 0; i < 10; i++) { //console.log(i); } console.log(i); //输出10 } console.log(j);//输出1 }
在javascript中變數的作用範圍是函數級的,即在函數中所有的變數在整個函數中都有定義,這也帶來了一些我們稍不注意就會碰到的「潛規則」:
var scope = "hello"; function scopeTest() { console.log(scope);//① var scope = "no"; console.log(scope);//② }
在①處輸出的值竟然是undefined,簡直喪心病狂啊,我們已經定義了全域變數的值啊,這地方不應該是hello嗎?其實,上面的程式碼等效於:
var scope = "hello"; function scopeTest() { var scope; console.log(scope);//① scope = "no"; console.log(scope);//② }
宣告提前、全域變數優先權低於局部變量,根據這兩條規則就不難理解為什麼輸出undefined了。
作用域鏈
在javascript中,每個函數都有自己的執行上下文環境,當程式碼在這個環境中執行時,會創建變數物件的作用域鏈,作用域鍊是一個物件列表或物件鏈,它保證了變數物件的有序存取。
作用域鏈的前端是當前代碼執行環境的變數對象,常被稱之為“活躍對象”,變數的查找會從第一個鏈的對象開始,如果對象包含變數屬性,那麼就停止查找,如果沒有就會繼續向上級作用域鏈查找,直到找到全域物件中:
作用域鏈的逐級查找,也會影響程式的效能,變數作用域鏈越長對效能影響越大,這也是我們盡量避免使用全域變數的一個主要原因。
閉包
基礎概念
作用域是理解閉包的一個前提,閉包是指在目前作用域內總是能存取外部作用域中的變數。
function createClosure(){ var name = "jack"; return { setStr:function(){ name = "rose"; }, getStr:function(){ return name + ":hello"; } } } var builder = new createClosure(); builder.setStr(); console.log(builder.getStr()); //rose:hello
上面的範例在函數中傳回了兩個閉包,這兩個閉包都維持著對外部作用域的引用,因此不管在哪裡呼叫總是能夠存取外部函數中的變數。在一個函數內部定義的函數,會將外部函數的活躍物件加入自己的作用域鏈中,因此上面實例中透過內部函數能夠存取外部函數的屬性,這也是javascript模擬私有變數的一種方式。
注意:由於閉包會額外的附帶函數的作用域(內部匿名函數攜帶外部函數的作用域),因此,閉包會比其它函數多佔用些記憶體空間,過度的使用可能會導致記憶體佔用的增加。
閉包中的變數
使用閉包時,由於作用域鏈機制的影響,閉包只能取得內部函數的最後一個值,這引起的一個副作用就是如果內部函數在一個循環中,那麼變數的值始終為最後一個值。
//该实例不太合理,有一定延迟因素,此处主要为了说明闭包循环中存在的问题 function timeManage() { for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i); },1000) }; }
上面的程式並沒有按照我們預期的輸入1-5的數字,而是5次全部輸出了5。再來看一個範例:
function createClosure(){ var result = []; for (var i = 0; i < 5; i++) { result[i] = function(){ return i; } } return result; }
呼叫createClosure()[0]()回傳的是5,createClosure()[4]()回傳值仍然是5。透過以上兩個例子可以看出閉包在帶有循環的內部函數使用時存在的問題:因為每個函數的作用域鏈中都保存著對外部函數(timeManage、createClosure)的活躍對象,因此,他們都引用著同一變數i,當外部函數回傳時,此時的i值為5,所以內部的每個函數i的值也是5。
那麼如何解決這個問題呢?我們可以透過匿名包裹器(匿名自執行函數表達式)來強制傳回預期的結果:
function timeManage() { for (var i = 0; i < 5; i++) { (function(num) { setTimeout(function() { console.log(num); }, 1000); })(i); } }
或在閉包匿名函數中再傳回一個匿名函數賦值:
function timeManage() { for (var i = 0; i < 10; i++) { setTimeout((function(e) { return function() { console.log(e); } })(i), 1000) } } //timeManager();输出1,2,3,4,5 function createClosure() { var result = []; for (var i = 0; i < 5; i++) { result[i] = function(num) { return function() { console.log(num); } }(i); } return result; } //createClosure()[1]()输出1;createClosure()[2]()输出2
无论是匿名包裹器还是通过嵌套匿名函数的方式,原理上都是由于函数是按值传递,因此会将变量i的值复制给实参num,在匿名函数的内部又创建了一个用于返回num的匿名函数,这样每个函数都有了一个num的副本,互不影响了。
闭包中的this
在闭包中使用this时要特别注意,稍微不慎可能会引起问题。通常我们理解this对象是运行时基于函数绑定的,全局函数中this对象就是window对象,而当函数作为对象中的一个方法调用时,this等于这个对象(TODO 关于this做一次整理)。由于匿名函数的作用域是全局性的,因此闭包的this通常指向全局对象window:
var scope = "global"; var object = { scope:"local", getScope:function(){ return function(){ return this.scope; } } }
调用object.getScope()()返回值为global而不是我们预期的local,前面我们说过闭包中内部匿名函数会携带外部函数的作用域,那为什么没有取得外部函数的this呢?每个函数在被调用时,都会自动创建this和arguments,内部匿名函数在查找时,搜索到活跃对象中存在我们想要的变量,因此停止向外部函数中的查找,也就永远不可能直接访问外部函数中的变量了。总之,在闭包中函数作为某个对象的方法调用时,要特别注意,该方法内部匿名函数的this指向的是全局变量。
幸运的是我们可以很简单的解决这个问题,只需要把外部函数作用域的this存放到一个闭包能访问的变量里面即可:
var scope = "global"; var object = { scope:"local", getScope:function(){ var that = this; return function(){ return that.scope; } } } object.getScope()()返回值为local。
内存与性能
由于闭包中包含与函数运行期上下文相同的作用域链引用,因此,会产生一定的负面作用,当函数中活跃对象和运行期上下文销毁时,由于必要仍存在对活跃对象的引用,导致活跃对象无法销毁,这意味着闭包比普通函数占用更多的内存空间,在IE浏览器下还可能会导致内存泄漏的问题,如下:
function bindEvent(){ var target = document.getElementById("elem"); target.onclick = function(){ console.log(target.name); } }
上面例子中匿名函数对外部对象target产生一个引用,只要是匿名函数存在,这个引用就不会消失,外部函数的target对象也不会被销毁,这就产生了一个循环引用。解决方案是通过创建target.name副本减少对外部变量的循环引用以及手动重置对象:
function bindEvent(){ var target = document.getElementById("elem"); var name = target.name; target.onclick = function(){ console.log(name); } target = null; }
闭包中如果存在对外部变量的访问,无疑增加了标识符的查找路径,在一定的情况下,这也会造成性能方面的损失。解决此类问题的办法我们前面也曾提到过:尽量将外部变量存入到局部变量中,减少作用域链的查找长度。
总结:闭包不是javascript独有的特性,但是在javascript中有其独特的表现形式,使用闭包我们可以在javascript中定义一些私有变量,甚至模仿出块级作用域,但闭包在使用过程中,存在的问题我们也需要了解,这样才能避免不必要问题的出现。