欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

深入理解Javascript中的作用域鏈和閉包

 更新時間:2017年04月25日 10:15:45   作者:路易斯  
之前我們講到數(shù)組遍歷, 本文我們更進(jìn)一步, 講講如何提高遍歷的效率. 下面這篇文章主要深入的介紹了Javascript中作用域鏈和閉包的相關(guān)資料,需要的朋友可以參考下

首先我們回顧下之前一篇關(guān)于介紹數(shù)組遍歷的文章:

請先看上一篇中提到的for循環(huán)代碼:

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);
//以下是連續(xù)5次的運(yùn)行時間
//168+158+170+159+165 = 820(ms)

我們再看下面一段代碼, 測試環(huán)境為 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);
//以下是連續(xù)5次的運(yùn)行時間:
//8+6+8+7+6 = 35(ms)

計(jì)算一下: 820/35 = 23 效率提升大致20倍. 實(shí)際上, 在 Firefox 及 Safari 對 for有做底層優(yōu)化的情況下, 仍然有4~6倍的性能提升. 這是為什么呢?

我們注意到兩段代碼最大的區(qū)別就是, 第二段代碼使用了匿名函數(shù)包裹for循環(huán). 我們將在后面講到, 請耐心閱讀.

作用域

所謂作用域, 指的是, 變量在聲明它們的函數(shù)體以及這個函數(shù)體嵌套的任意函數(shù)體內(nèi)都是有定義的.

js中只有函數(shù)作用域

眾所周知, JS中并沒有塊作用域, 只有函數(shù)作用域. 如下:

for(var i=0;i<10;i++){
 ;
}
console.log(i);//10
function f(){
 var a = 123;
}
f();
console.log(a);//a is not defined

因此 js 中只有一種局部作用域, 即函數(shù)作用域.

使用 var 聲明變量

通常我們知道, js 作為一種弱類型語言, 聲明一個變量只需要var保留字, 如果在函數(shù)中不使用 var 聲明變量, 該變量將提升為全局變量, 進(jìn)而脫離函數(shù)作用域, 如下:

function f(){
 b = 123;
}
f();
console.log(b);//123

此時相對于前面使用var聲明的 a 變量, b 變量被提升為全局變量, 在函數(shù)作用域外依然可以訪問.

既然在函數(shù)作用域內(nèi)不使用 var 聲明變量, 會將變量提升為全局變量, 那么在全局下, 不使用var, 會怎么樣呢?

//全局下不使用var聲明,該變量依然是全局變量
c = "hello scope";
console.log(c);//hello scope
console.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 defined
console.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

綜上, 有如下規(guī)律:

  • 不使用var保留字聲明變量, 變量提升為全局變量, 而不論變量處于哪種作用域;
  • 如果不使用var聲明, 該變量便可配置, 即可被 delete 保留字刪除, 刪除后該變量便不可訪問; 如果使用var聲明, 該變量便不可配置, 即不能被 delete 保留字刪除;
  • 只要是全局變量都可以直接訪問, 也可使用 “window.變量名” 來訪問, 不管該變量是不是通過var來聲明的;

JS中的作用域鏈

函數(shù)對象和其它對象一樣,擁有可以通過代碼訪問的屬性和一系列僅供JavaScript引擎訪問的內(nèi)部屬性。其中一個內(nèi)部屬性是[[Scope]],由ECMA-262標(biāo)準(zhǔn)第三版定義,該內(nèi)部屬性包含了函數(shù)被創(chuàng)建的作用域中對象的集合,這個集合被稱為函數(shù)的作用域鏈,它決定了哪些數(shù)據(jù)能被函數(shù)訪問。

我們先看一個栗子:

var e = "hello";
function f(){
 e = "scope chain";
 var g = = "good";
}

以上作用域鏈的圖如下所示:

函數(shù)執(zhí)行時, 在函數(shù) f 內(nèi)部會生成一個 active object 和 scope chain. JavaScript引擎內(nèi)部對象會放入 active object中, 外部的 e 變量處于scope chain的第二層, index=1, 而內(nèi)部的g變量處于scope chain的頂層, index=0, 因此訪問g變量總比訪問e變量來的快些.

閉包

聊到作用域, 就不得不說閉包, 那么, 什么是閉包?

“官方”的解釋是:閉包是一個擁有許多變量和綁定了這些變量的環(huán)境的表達(dá)式(通常是一個函數(shù)),因而這些變量也是該表達(dá)式的一部分。

這是什么意思呢, 簡單來說就是:

  • 函數(shù)執(zhí)行時返回內(nèi)部私有函數(shù), 或者通過其他方式將內(nèi)部私有函數(shù)保留在外(比如說通過將其內(nèi)部私有函數(shù)的引用賦值外部變量), 從而阻止該函數(shù)內(nèi)部作用域等被執(zhí)行引擎回收.
  • 在函數(shù)外部通過訪問暴露在外的函數(shù)內(nèi)部私有函數(shù), 從而具有訪問函數(shù)內(nèi)部私有作用域的效果, 就是閉包.

ES6之前, 通常我們實(shí)現(xiàn)的模塊就是利用了閉包. 閉包依賴的結(jié)構(gòu)有個鮮明的特點(diǎn), 即: 一個函數(shù)在詞法作用域之外執(zhí)行. 如下, f2是閉包的關(guān)鍵, 它的詞法作用域便是函數(shù)f的內(nèi)部私有作用域, 且它在f的作用域外部執(zhí)行.

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 的內(nèi)部, 因此 f2 內(nèi)可以訪問到 f 的內(nèi)部私有作用域, 這樣通過返回 f2 就能保證在 f 函數(shù)外部也能訪問到 i 變量.

當(dāng)f2執(zhí)行時, 變量 j 處于scope chain的 index0的位置上, 變量 i 和變量 h 分別處于 scope chain 的 index1 index2 的位置上. 因此 j 的賦值過程其實(shí)就是沿著 scope chain 第二層 第三層 依次找到 i 和 h 的值, 然后將它們和3一起求和, 最終賦值給 j .

瀏覽器沿著 scope chain 尋找變量總是需要耗費(fèi)CPU時間, 越是 scope chain 的 外層(或者離f2越遠(yuǎn)的變量), 瀏覽器查找起來越是需要時間, 因?yàn)?scope chain 需要?dú)v經(jīng)更多次遍歷. 因此全局變量(window)總是需要最多的訪問時間.

閉包內(nèi)的微觀世界

  如果要更加深入的了解閉包以及函數(shù) f 和嵌套函數(shù) f2 的關(guān)系,我們需要引入另外幾個概念:函數(shù)的執(zhí)行環(huán)境(excution context)、活動對象(call object)、作用域(scope)、作用域鏈(scope chain)。以函數(shù)a從定義到執(zhí)行的過程為例闡述這幾個概念。

  • 當(dāng)定義函數(shù) f 的時候, js解釋器會將函數(shù)a的作用域鏈(scope chain)設(shè)置為定義 f 時 a 所在的”環(huán)境”, 如果 f 是一個全局函數(shù),則scope chain中只有window對象。
  • 當(dāng)執(zhí)行函數(shù) f 的時候, f 會進(jìn)入相應(yīng)的執(zhí)行環(huán)境(excution context).
  • 在創(chuàng)建執(zhí)行環(huán)境的過程中, 首先會為 f 添加一個scope屬性, 即a的作用域, 其值就為第1步中的scope chain. 即a.scope=f 的作用域鏈.
  • 然后執(zhí)行環(huán)境會創(chuàng)建一個活動對象(call object). 活動對象也是一個擁有屬性的對象, 但它不具有原型而且不能通過JavaScript代碼直接訪問. 創(chuàng)建完活動對象后, 把活動對象添加到 f 的作用域鏈的最頂端. 此時a的作用域鏈包含了兩個對象: f 的活動對象和window對象.
  • 下一步是在活動對象上添加一個arguments屬性, 它保存著調(diào)用函數(shù) f 時所傳遞的參數(shù).
  • 最后把所有函數(shù) f 的形參和內(nèi)部的函數(shù) f2 的引用也添加到 f 的活動對象上. 在這一步中, 完成了函數(shù) f2 的定義, 因此如同第3步, 函數(shù) f2 的作用域鏈被設(shè)置為 f2 所被定義的環(huán)境, 即 f 的作用域.

到此, 整個函數(shù) f 從定義到執(zhí)行的步驟就完成了. 此時 f 返回函數(shù) f2 的引用給 ff, 又函數(shù) f2 的作用域鏈包含了對函數(shù) f 的活動對象的引用, 也就是說 f2 可以訪問到 f 中定義的所有變量和函數(shù). 函數(shù) f2 被 ff 引用, 函數(shù) f2又依賴函數(shù) f , 因此函數(shù) f 在返回后不會被GC回收.

當(dāng)函數(shù) f2 執(zhí)行的時候亦會像以上步驟一樣. 因此, 執(zhí)行時 f2 的作用域鏈包含了3個對象: f2 的活動對象、f 的活動對象和window對象, 如下圖所示:

如圖所示, 當(dāng)在函數(shù) f2 中訪問一個變量的時候, 搜索順序是:

  • 先搜索自身的活動對象, 如果存在則返回, 如果不存在將繼續(xù)搜索函數(shù) f 的活動對象, 依次查找, 直到找到為止.
  • 如果函數(shù) f2 存在prototype原型對象, 則在查找完自身的活動對象后先查找自身的原型對象, 再繼續(xù)查找. 這就是Javascript中的變量查找機(jī)制.
  • 如果整個作用域鏈上都無法找到, 則返回undefined.

小結(jié), 本段中提到了兩個重要的詞語: 函數(shù)的定義與執(zhí)行. 文中提到函數(shù)的作用域是在定義函數(shù)時候就已經(jīng)確定, 而不是在執(zhí)行的時候確定(參看步驟1和3).用一段代碼來說明這個問題:

function f(x) { 
 var g = function () { return x; }
 return g;
}
var h = f(1);
alert(h());

這段代碼中變量h指向了f中的那個匿名函數(shù)(由g返回).

  • 假設(shè)函數(shù)h的作用域是在執(zhí)行alert(h())確定的, 那么此時h的作用域鏈?zhǔn)? h的活動對象->alert的活動對象->window對象.
  • 假設(shè)函數(shù)h的作用域是在定義時確定的, 就是說h指向的那個匿名函數(shù)在定義的時候就已經(jīng)確定了作用域. 那么在執(zhí)行的時候, h的作用域鏈為: h的活動對象->f的活動對象->window對象.

如果第一種假設(shè)成立, 那輸出值就是undefined; 如果第二種假設(shè)成立, 輸出值則為1。

運(yùn)行結(jié)果證明了第2個假設(shè)是正確的,說明函數(shù)的作用域確實(shí)是在定義這個函數(shù)的時候就已經(jīng)確定了.

閉包有可能導(dǎo)致IE瀏覽器內(nèi)存泄漏

先看一個栗子:

function f(){
 var div = document.createElement("div"); 
 div.onclick = function(){
 return false;
 }
}

上述div的click事件就是一個閉包, 由于該閉包的存在使得 f 函數(shù)內(nèi)部的 div 變量對DOM元素的引用將一直存在.

而早期IE瀏覽器( IE9之前 ) js 對象和 DOM 對象使用不同的垃圾收集方法, DOM對象使用計(jì)數(shù)垃圾回收機(jī)制, 只要匿名函數(shù)( 比如說onclick事件 )存在, DOM對象的引用便至少為1,因此它所占用的內(nèi)存就永遠(yuǎn)不會被銷毀.

有趣的是,不同的IE版本將導(dǎo)致不同的現(xiàn)象:

  • 如果是IE 6, 內(nèi)存泄漏,直到關(guān)閉IE進(jìn)程為止;
  • 如果是IE 7,內(nèi)存泄漏, 直到離開當(dāng)前頁面為止;
  • 如果是IE 8, GC回收器回收他們的內(nèi)存,無論當(dāng)前是不是compatibility模式.

總結(jié)一下, 閉包的優(yōu)點(diǎn): 共享函數(shù)作用域, 便于開放一些接口或變量供外部使用;

注意事項(xiàng): 由于閉包可能會使得函數(shù)中變量被長期保存在內(nèi)存中, 從而大量消耗內(nèi)存, 影響頁面性能, 因此不能濫用, 并且在IE瀏覽中可能導(dǎo)致內(nèi)存泄露. 解決方法是,在退出函數(shù)之前,將不使用的局部變量全部刪除.

for循環(huán)問題分析

我們再來看看開篇的for循環(huán)問題, 增加匿名函數(shù)后, for循環(huán)內(nèi)部的變量便處于匿名函數(shù)的局部作用域下, 此時訪問 length 屬性, 或者訪問 i 屬性, 都只需要在匿名函數(shù)作用域內(nèi)查找即可, 因此查詢效率大大提升(測試數(shù)據(jù)發(fā)現(xiàn)提升有兩百多倍).

使用匿名函數(shù)后, 不止是作用域查詢更快, 作用域內(nèi)的變量還與外部隔離, 避免了像 i , length 這樣的變量對后續(xù)代碼產(chǎn)生影響. 可謂一舉兩得.

踩個作用域的坑

下面我們來踩一個作用域經(jīng)典的坑.

var div = document.getElementsByTagName("div");
for(var i=0,len=div.length;i<len;i++){
 div[i].onclick = function(){
 console.log(i);
 }
}

上述代碼的本意是每次點(diǎn)擊div, 打印div的索引, 實(shí)際上打印的卻是 len 的值. 我們來分析下原因.

點(diǎn)擊div時, 將會執(zhí)行 console.log(i) 語句, 顯然 i 變量不在 click 事件的局部作用域內(nèi), 瀏覽器將沿著 scope chain 尋找 i 變量, 在 index1 的地方, 即 for循環(huán)開始的地方, 此處定義了一個 i 變量, 又 js 沒有塊作用域, 故 i 變量并不會在 for循環(huán)塊執(zhí)行完成后被銷毀,又 i的最后一次自加使得 i = len, 于是瀏覽器在scope chain index=1索引的地方停下來了, 返回了i的值, 即len的值.

為了解決這個問題, 我們將根據(jù)癥結(jié), 對癥下藥, 從作用域入手, 改變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 事件被閉包包裹, 并且閉包自執(zhí)行, 因此閉包內(nèi) n 變量的值每次都不一樣, 點(diǎn)擊div時, 瀏覽器將沿著 scope chain 尋找 n 變量, 最終會找到閉包內(nèi)的 n 變量, 并且打印出div 的索引.

this作用域

前面我們學(xué)習(xí)了作用域鏈, 閉包等基礎(chǔ)知識, 下面我們來聊聊神秘莫測的this作用域.

熟悉OOP的開發(fā)人員都知道, this是對象實(shí)例的引用, 始終指向?qū)ο髮?shí)例. 然而 js 的世界里, this隨著它的執(zhí)行環(huán)境改變而改變, 并且它總是指向它所在方法的對象. 如下,

function f(){
 alert(this);
}
var o = {};
o.func = f;
f();//[object Window]
o.func();//[object Object]
console.log(f===window.f);//true

當(dāng)f單獨(dú)執(zhí)行時, 其內(nèi)部this指向window對象, 但是當(dāng)f成為o對象的屬性func時, this指向的是o對象, 又f === window.f, 故它們實(shí)際上指向的都是this所在方法的對象.

下面我們來應(yīng)用下

Array.prototype.slice.call([1,2,3],1);//[2,3],正確用法
Array.prototype.slice([1,2,3],1);//[], 錯誤用法,此時slice內(nèi)部this仍然指向Array.prototype
var slice = Array.prototype.slice;
slice([1,2,3],1);//Uncaught TypeError: Array.prototype.slice called on null or undefined
//此時slice內(nèi)部this指向的是window對象,離開了原來的Array.prototype對象作用域,故報錯~~

總結(jié)下, this的使用只需要注意一點(diǎn):

this 總是指向它所在方法的對象.

with語句

聊到作用域鏈就不得不說with語句了, with語句可以用來臨時改變作用域, 將語句中的對象添加到作用域的頂部.

語法: with (expression){statement}

例如:

var k = {name:"daicy"};
with(k){
 console.log(name);//daicy
}
console.log(name);//undefined

with 語句用于對象 k, 作用域第一層為 k 對象內(nèi)部作用域, 故能直接打印出 name 的值, 在with之外的語句不受此影響.
再看一個栗子:

var l = [1,2,3];
with(l) {
 console.log(map(function(i){
 return i*i;
 }));//[1,4,9]
}

在這個例子中,with 語句用于數(shù)組,所以在調(diào)用 map() 方法時,解釋程序?qū)z查該方法是否是本地函數(shù)。如果不是,它將檢查偽對象 l,看它是否為該對象的方法, 又map是Array對象的方法, 數(shù)組l繼承了該方法, 故能正確執(zhí)行.

注意: with語句容易引起歧義, 由于需要強(qiáng)制改變作用域鏈, 它將帶來更多的cpu消耗, 建議慎用 with 語句.

總結(jié)

以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學(xué)習(xí)或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。

相關(guān)文章

最新評論