首先我們回顧下之前一篇關于介紹數組遍歷的文章:
請先看上一篇中提到的for循環代碼:
var array = [];array.length = 10000000;//(一千萬)for(var i=0,length=array.length;i<length;i++){ array[i] = 'hi';}var t1 = +new Date();for(var i=0,length=array.length;i<length;i++){}var t2 = +new Date();console.log(t2-t1);//以下是連續5次的運行時間//168+158+170+159+165 = 820(ms)
我們再看下面一段代碼, 測試環境為 chrome 52.0.2743.116 (64-bit):
var t1 = +new Date();(function(){//閉包 for(var i=0,length=array.length;i<length;i++){ //array.push(i); }})();var t2 = +new Date();console.log(t2-t1);//以下是連續5次的運行時間://8+6+8+7+6 = 35(ms)
計算一下: 820/35 = 23 效率提升大致20倍. 實際上, 在 Firefox 及 Safari 對 for有做底層優化的情況下, 仍然有4~6倍的性能提升. 這是為什么呢?
我們注意到兩段代碼最大的區別就是, 第二段代碼使用了匿名函數包裹for循環. 我們將在后面講到, 請耐心閱讀.
作用域
所謂作用域, 指的是, 變量在聲明它們的函數體以及這個函數體嵌套的任意函數體內都是有定義的.
js中只有函數作用域
眾所周知, JS中并沒有塊作用域, 只有函數作用域. 如下:
for(var i=0;i<10;i++){ ;}console.log(i);//10function f(){ var a = 123;}f();console.log(a);//a is not defined
因此 js 中只有一種局部作用域, 即函數作用域.
使用 var 聲明變量
通常我們知道, js 作為一種弱類型語言, 聲明一個變量只需要var保留字, 如果在函數中不使用 var 聲明變量, 該變量將提升為全局變量, 進而脫離函數作用域, 如下:
function f(){ b = 123;}f();console.log(b);//123
此時相對于前面使用var聲明的 a 變量, b 變量被提升為全局變量, 在函數作用域外依然可以訪問.
既然在函數作用域內不使用 var 聲明變量, 會將變量提升為全局變量, 那么在全局下, 不使用var, 會怎么樣呢?
//全局下不使用var聲明,該變量依然是全局變量c = "hello scope";console.log(c);//hello scopeconsole.log(window.c);//hello scope//查看c變量的屬性console.log(Object.getOwnPropertyDescriptor(window, 'c'));//Object {value: "hello scope", writable: true, enumerable: true, configurable: true} ,此時c變量可賦值,可列舉,可配置//試著刪除c變量delete c;//true 表示c變量被成功刪除console.log(c);//c is not definedconsole.log(window.c);//undefined//使用var聲明后再刪除d變量var d = 1;console.log(Object.getOwnPropertyDescriptor(window, 'd'));//Object {value: 1, writable: true, enumerable: true, configurable: false} ,此時d變量可賦值,可列舉,但不可配置delete d;//false 表示d變量刪除失敗console.log(d);//1 console.log(window.d);//1
綜上, 有如下規律:
JS中的作用域鏈
函數對象和其它對象一樣,擁有可以通過代碼訪問的屬性和一系列僅供JavaScript引擎訪問的內部屬性。其中一個內部屬性是[[Scope]],由ECMA-262標準第三版定義,該內部屬性包含了函數被創建的作用域中對象的集合,這個集合被稱為函數的作用域鏈,它決定了哪些數據能被函數訪問。
我們先看一個栗子:
var e = "hello";function f(){ e = "scope chain"; var g = = "good";}
以上作用域鏈的圖如下所示:
函數執行時, 在函數 f 內部會生成一個 active object 和 scope chain. JavaScript引擎內部對象會放入 active object中, 外部的 e 變量處于scope chain的第二層, index=1, 而內部的g變量處于scope chain的頂層, index=0, 因此訪問g變量總比訪問e變量來的快些.
閉包
聊到作用域, 就不得不說閉包, 那么, 什么是閉包?
“官方”的解釋是:閉包是一個擁有許多變量和綁定了這些變量的環境的表達式(通常是一個函數),因而這些變量也是該表達式的一部分。
這是什么意思呢, 簡單來說就是:
ES6之前, 通常我們實現的模塊就是利用了閉包. 閉包依賴的結構有個鮮明的特點, 即: 一個函數在詞法作用域之外執行. 如下, f2是閉包的關鍵, 它的詞法作用域便是函數f的內部私有作用域, 且它在f的作用域外部執行.
var h = 1;function f(){ var i = 2; return function f2(){ var j = 3 + i + h; console.log(j); }}var ff = f();ff();//6
由于定義時 f2 處于 f 的內部, 因此 f2 內可以訪問到 f 的內部私有作用域, 這樣通過返回 f2 就能保證在 f 函數外部也能訪問到 i 變量.
當f2執行時, 變量 j 處于scope chain的 index0的位置上, 變量 i 和變量 h 分別處于 scope chain 的 index1 index2 的位置上. 因此 j 的賦值過程其實就是沿著 scope chain 第二層 第三層 依次找到 i 和 h 的值, 然后將它們和3一起求和, 最終賦值給 j .
瀏覽器沿著 scope chain 尋找變量總是需要耗費CPU時間, 越是 scope chain 的 外層(或者離f2越遠的變量), 瀏覽器查找起來越是需要時間, 因為 scope chain 需要歷經更多次遍歷. 因此全局變量(window)總是需要最多的訪問時間.
閉包內的微觀世界
如果要更加深入的了解閉包以及函數 f 和嵌套函數 f2 的關系,我們需要引入另外幾個概念:函數的執行環境(excution context)、活動對象(call object)、作用域(scope)、作用域鏈(scope chain)。以函數a從定義到執行的過程為例闡述這幾個概念。
到此, 整個函數 f 從定義到執行的步驟就完成了. 此時 f 返回函數 f2 的引用給 ff, 又函數 f2 的作用域鏈包含了對函數 f 的活動對象的引用, 也就是說 f2 可以訪問到 f 中定義的所有變量和函數. 函數 f2 被 ff 引用, 函數 f2又依賴函數 f , 因此函數 f 在返回后不會被GC回收.
當函數 f2 執行的時候亦會像以上步驟一樣. 因此, 執行時 f2 的作用域鏈包含了3個對象: f2 的活動對象、f 的活動對象和window對象, 如下圖所示:
如圖所示, 當在函數 f2 中訪問一個變量的時候, 搜索順序是:
小結, 本段中提到了兩個重要的詞語: 函數的定義與執行. 文中提到函數的作用域是在定義函數時候就已經確定, 而不是在執行的時候確定(參看步驟1和3).用一段代碼來說明這個問題:
function f(x) { var g = function () { return x; } return g;}var h = f(1);alert(h());
這段代碼中變量h指向了f中的那個匿名函數(由g返回).
alert(h())
確定的, 那么此時h的作用域鏈是: h的活動對象->alert的活動對象->window對象.如果第一種假設成立, 那輸出值就是undefined; 如果第二種假設成立, 輸出值則為1。
運行結果證明了第2個假設是正確的,說明函數的作用域確實是在定義這個函數的時候就已經確定了.
閉包有可能導致IE瀏覽器內存泄漏
先看一個栗子:
function f(){ var div = document.createElement("div"); div.onclick = function(){ return false; }}
上述div的click事件就是一個閉包, 由于該閉包的存在使得 f 函數內部的 div 變量對DOM元素的引用將一直存在.
而早期IE瀏覽器( IE9之前 ) js 對象和 DOM 對象使用不同的垃圾收集方法, DOM對象使用計數垃圾回收機制, 只要匿名函數( 比如說onclick事件 )存在, DOM對象的引用便至少為1,因此它所占用的內存就永遠不會被銷毀.
有趣的是,不同的IE版本將導致不同的現象:
總結一下, 閉包的優點: 共享函數作用域, 便于開放一些接口或變量供外部使用;
注意事項: 由于閉包可能會使得函數中變量被長期保存在內存中, 從而大量消耗內存, 影響頁面性能, 因此不能濫用, 并且在IE瀏覽中可能導致內存泄露. 解決方法是,在退出函數之前,將不使用的局部變量全部刪除.
for循環問題分析
我們再來看看開篇的for循環問題, 增加匿名函數后, for循環內部的變量便處于匿名函數的局部作用域下, 此時訪問 length 屬性, 或者訪問 i 屬性, 都只需要在匿名函數作用域內查找即可, 因此查詢效率大大提升(測試數據發現提升有兩百多倍).
使用匿名函數后, 不止是作用域查詢更快, 作用域內的變量還與外部隔離, 避免了像 i , length 這樣的變量對后續代碼產生影響. 可謂一舉兩得.
踩個作用域的坑
下面我們來踩一個作用域經典的坑.
var div = document.getElementsByTagName("div");for(var i=0,len=div.length;i<len;i++){ div[i].onclick = function(){ console.log(i); }}
上述代碼的本意是每次點擊div, 打印div的索引, 實際上打印的卻是 len 的值. 我們來分析下原因.
點擊div時, 將會執行 console.log(i)
語句, 顯然 i 變量不在 click 事件的局部作用域內, 瀏覽器將沿著 scope chain 尋找 i 變量, 在 index1
的地方, 即 for循環開始的地方, 此處定義了一個 i 變量, 又 js 沒有塊作用域, 故 i 變量并不會在 for循環塊執行完成后被銷毀,又 i的最后一次自加使得 i = len
, 于是瀏覽器在scope chain index=1
索引的地方停下來了, 返回了i的值, 即len的值.
為了解決這個問題, 我們將根據癥結, 對癥下藥, 從作用域入手, 改變click事件的局部作用域, 如下:
var div = document.getElementsByTagName("div");for(var i=0,len=div.length;i<len;i++){ (function(n){ div[n].onclick = function(){ console.log(n); } })(i);}
由于 click 事件被閉包包裹, 并且閉包自執行, 因此閉包內 n 變量的值每次都不一樣, 點擊div時, 瀏覽器將沿著 scope chain 尋找 n 變量, 最終會找到閉包內的 n 變量, 并且打印出div 的索引.
this作用域
前面我們學習了作用域鏈, 閉包等基礎知識, 下面我們來聊聊神秘莫測的this作用域.
熟悉OOP的開發人員都知道, this是對象實例的引用, 始終指向對象實例. 然而 js 的世界里, this隨著它的執行環境改變而改變, 并且它總是指向它所在方法的對象. 如下,
function f(){ alert(this);}var o = {};o.func = f;f();//[object Window]o.func();//[object Object]console.log(f===window.f);//true
當f單獨執行時, 其內部this指向window對象, 但是當f成為o對象的屬性func時, this指向的是o對象, 又f === window.f
, 故它們實際上指向的都是this所在方法的對象.
下面我們來應用下
Array.prototype.slice.call([1,2,3],1);//[2,3],正確用法Array.prototype.slice([1,2,3],1);//[], 錯誤用法,此時slice內部this仍然指向Array.prototypevar slice = Array.prototype.slice;slice([1,2,3],1);//Uncaught TypeError: Array.prototype.slice called on null or undefined//此時slice內部this指向的是window對象,離開了原來的Array.prototype對象作用域,故報錯~~
總結下, this的使用只需要注意一點:
this 總是指向它所在方法的對象.
with語句
聊到作用域鏈就不得不說with語句了, with語句可以用來臨時改變作用域, 將語句中的對象添加到作用域的頂部.
語法: with (expression){statement}
例如:
var k = {name:"daicy"};with(k){ console.log(name);//daicy}console.log(name);//undefined
with 語句用于對象 k, 作用域第一層為 k 對象內部作用域, 故能直接打印出 name 的值, 在with之外的語句不受此影響.
再看一個栗子:
var l = [1,2,3];with(l) { console.log(map(function(i){ return i*i; }));//[1,4,9]}
在這個例子中,with 語句用于數組,所以在調用 map()
方法時,解釋程序將檢查該方法是否是本地函數。如果不是,它將檢查偽對象 l,看它是否為該對象的方法, 又map是Array對象的方法, 數組l繼承了該方法, 故能正確執行.
注意: with語句容易引起歧義, 由于需要強制改變作用域鏈, 它將帶來更多的cpu消耗, 建議慎用 with 語句.
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對武林網的支持。
新聞熱點
疑難解答