摘要:回調(diào)傳遞函數(shù)是將函數(shù)當(dāng)做值并作為參數(shù)傳遞給函數(shù)。這個例子中就是因為事件綁定機制中的傳入了回調(diào)函數(shù),產(chǎn)生了閉包,引用著所在的作用域,所以此處的數(shù)據(jù)無法從內(nèi)存中釋放。
javascript作用域
一門語言需要一套設(shè)計良好的規(guī)則來存儲變量,并且之后可以方便的找到這些變量,這逃規(guī)則被稱為作用域。
這也意味著當(dāng)我們訪問一個變量的時候,決定這個變量能否訪問到的依據(jù)就是這個作用域。
一、詞法作用域作用域共有兩種主要的工作模型,第一種是最為普通的,被大多數(shù)編程語言(包括javascript)采用的詞法作用域,另一種叫做動態(tài)作用域。而我們平時所提及的作用域,就是這里所說的詞法作用域。
要了解詞法作用域,必須要了解javascript引擎以及編譯器的大概工作方式。一般程序中的源碼在執(zhí)行前會進行編譯三步驟。
分詞/語法分析
解析/語法分析
代碼生成
而在分詞/詞法分析這個步驟,就已經(jīng)確定了詞法作用域。也就說作用域在我們書寫代碼的時候就已經(jīng)確定了,引用書中的文字
詞法作用域就是定義在詞法階段的作用域,換句話說,詞法作用域是由你在寫代碼時將變量和塊作用域?qū)懺谀睦飦頉Q定的。
具體結(jié)合編譯器、作用域、引擎來講,編譯器在分詞階段,針對特定的環(huán)境就會生成一個詞法作用域,然后對源代碼中的var a = 3;類似的聲明進行識別,當(dāng)遇到var a,編譯器會詢問作用域中是否有a變量,若無,則在作用域中新增一個a變量。編譯完成之后,引擎執(zhí)行編譯后的代碼,引擎在執(zhí)行的過程中遇到a變量,會去作用域中查找是否有a變量,若有,則將a賦值2。對于var a = 2;一條語句會在兩個過程中操作,正是變量提升現(xiàn)象的原因。(稍后講到)
那什么時候會生成一個詞法作用域呢?
二、函數(shù)作用域這幅圖所展示的三個氣泡,就代表了三個作用域,而編譯器遇到一個函數(shù)定義,就會生成一個作用域。例如當(dāng)編譯器遇到foo函數(shù),會創(chuàng)建一個作用域,再將這個函數(shù)內(nèi)部的標(biāo)識符(a/b/bar)放到詞法作用域中。這個步驟在編譯階段就完成了。當(dāng)js引擎執(zhí)行foo函數(shù)的時候,遇到a變量,就會去詢問早就創(chuàng)建好的作用域是否有a變量存在。
在作用域外,是無法訪問作用域內(nèi)的變量的。
例如
function foo() { var a = 3; } console.log(a); //undefied
正是這個特性,可以被用來實現(xiàn)隱藏內(nèi)部變量
將重要變量聲明放入一個函數(shù)聲明的作用域中,可以防止被作用域外部的語句所引用甚至更改。
根據(jù)函數(shù)作用域,可以引申出如何判斷一個函數(shù)是函數(shù)聲明還是一個函數(shù)表達(dá)式。
最重要的區(qū)別是他們的名稱標(biāo)識符將會綁定在何處。
先聲明一點,任何匿名函數(shù)都是可以添加名稱標(biāo)識符的。例如
setTimeout(function timer() { console.log(1) }, 1000)
對于函數(shù)聲明,名稱標(biāo)識符是綁定在當(dāng)前作用域上的。即可在函數(shù)當(dāng)前作用域調(diào)用這個名稱標(biāo)識符。
而函數(shù)表達(dá)式,名稱標(biāo)識符是綁定在自身的函數(shù)作用域中的。
按照這個區(qū)別,來看以下幾個函數(shù)。
function foo1() {console.log(1)} foo1(); // 1
var bar = function foo2() {console.log(1)} foo2() // undefined
(function foo3() {console.log(1)})() foo3() // undefined
以上的函數(shù)就只有foo1是函數(shù)聲明。
三、塊作用域在js語言中,除了函數(shù),創(chuàng)建作用域的方式還可以通過塊作用域。對于js而言,循環(huán)、ifelse塊并沒有創(chuàng)建塊作用域的功能。
通過ES3規(guī)范的try/catch的catch語句可以創(chuàng)建一個塊作用域,其中聲明的變量僅在catch中有效。
而try-catch也正是let關(guān)鍵字的向前兼容方。
try { undefined(); // 執(zhí)行一個非法操作來強制制造一個異常 } catch(err) { console.log(err); } console.log(err); // err not found
ES6引入了let關(guān)鍵字,提供了除var以外的另一種變量聲明方式,let為其聲明的變量隱式地劫持了所在的塊作用域。
if (true) { { let bar = 3; bar = someting(bar); console.log(bar) } } console.log(bar) // undefined
作于的一個中括號起到劃分塊作用域的作用,顯示的區(qū)別于var等變量。我們可能在之后會修改代碼,看到這個中括號會直白的認(rèn)識到這個是一個塊作用域。
四、變量提升在第一節(jié)我已經(jīng)提到了,對于var a = 3;這樣一條語句,編譯器通過分詞、解析、最后生成機器可以讀的代碼。
而javascript實際上會將其看成兩個聲明:var a、a = 3。第一個聲明在編譯階段進行,第二個賦值聲明會留在原地等待執(zhí)行。
所以在引擎工作去執(zhí)行代碼時,進入到函數(shù)作用域內(nèi)時,首先會執(zhí)行var a操作,而這個過程就好像變量從原先的位置被移動作用域最上面一樣。
console.log(a); // undefined var a = 3;
相當(dāng)于
var a; console.log(a); // undefined a = 3;
另外函數(shù)聲明也會發(fā)生變量提升的現(xiàn)象(連實際函數(shù)值也提升,即可以在函數(shù)聲明前調(diào)用)。而行數(shù)表達(dá)式var a = function foo1() {}發(fā)生提升的是a變量,函數(shù)本身不會發(fā)生提升。
foo(); // 不是ReferenceError 而是 TypeError var foo = function bar() {}
ReferenceError TypeError
這是兩個錯誤標(biāo)記,第一個錯誤標(biāo)記是查詢變量時,若在作用域中查找不到這個變量則發(fā)出,第二個標(biāo)記是能查找到變量(即使是endefined),但是這個變量被錯誤的調(diào)用(比如對null,undefined進行調(diào)用),發(fā)出。
閉包是基于詞法作用域書寫代碼時所產(chǎn)生的自然結(jié)果。
基于詞法作用域產(chǎn)生的結(jié)果,這有點類似于詞法作用域的產(chǎn)生條件。這也意味著閉包在書寫代碼的時候就已經(jīng)形成了。
看一個最經(jīng)典的閉包例子
function foo () { var a = 1; function bar () { console.log(a); //1 } return bar; } var baz = foo(); baz();
基于這個經(jīng)典的例子,結(jié)合書中的話
一個函數(shù)在定義時的詞法作用域以外的地方被調(diào)用,可以記住并訪問原先所在的詞法作用域時,就產(chǎn)生了閉包。也即被返回出去的函數(shù)被調(diào)用時依然持有對該作用域的引用。這個引用就是閉包。
先確定一點,javascript中函數(shù)是可以作為值被傳遞的?;谶@個特性,有多種方法可以行成閉包。只要在一個作用域中,將函數(shù)作為值傳遞到另一個詞法作用域中并調(diào)用,就會形成閉包。
function foo() { var a = 2; function baz() { console.log(a); } bar(baz); } function bar(fn) { fn(); } // 回調(diào)傳遞函數(shù)
var fn; function foo() { var a = 2; function baz() { console.log(a); } fn = baz; } function bar() { fn(); } foo(); bar(); //2 // 間接傳遞函數(shù)
無論通過何種手段將內(nèi)部函數(shù)傳遞到所在的詞法作用域以外,它都會持有對原始定義作用域的引用,無論在何處執(zhí)行這個函數(shù)都會使用閉包。
二、回調(diào) == 閉包再看上一節(jié),回調(diào)中傳遞函數(shù)的例子。
function foo() { var a = 2; function baz() { console.log(a); } bar(baz); } function bar(fn) { fn(); } // 回調(diào)傳遞函數(shù)
是將函數(shù)當(dāng)做值并作為參數(shù)傳遞給函數(shù)。再來看
function wait(message) { setTimeout(function timer () { console.log(message); // hello world }, 1000) } wait("hello world");
setTimeout作為js內(nèi)置的工具函數(shù),將timer 函數(shù)當(dāng)做值傳進去,在setTimeout定義函數(shù)內(nèi)對傳進來的timer進行了調(diào)用。類似于
function setTimeout(fn) { // 延遲多少毫秒 fn(); }
回調(diào)函數(shù)timer在另一個詞法作用域內(nèi)調(diào)用,但是能訪問原先作用域內(nèi)的參數(shù)(message)。
類似jquery中的事件綁定,涉及到傳遞回調(diào)函數(shù),就都有閉包的產(chǎn)生!
三、閉包在循環(huán)中的表現(xiàn)最令人困惑的閉包表現(xiàn)就是在循環(huán)中了。像我們剛剛提及到的setTimeout、事件綁定等回調(diào)函數(shù)都會產(chǎn)生閉包。
for(var i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i); }, i*1000) }
這個循環(huán)的本意是想間隔1秒打印1、2、3、4、5,結(jié)果卻每隔1秒輸出了5次6!
結(jié)合在第二節(jié)中對setTimeout函數(shù)的解析,這個誤區(qū)將很快解開。
首先要明白for循環(huán)沒有塊作用域的概念,即在這個循環(huán)中5次迭代都是在同一個作用域中進行的。
要清楚timer函數(shù)不是在這個作用域中被調(diào)用的,它作為參數(shù)在其他的作用域中調(diào)用。
function timer() { console.log(i); }
這個函數(shù)包括其中的形式參數(shù)i原原本本的被傳遞,在迭代過程中i不會被賦值。
而五次迭代完成后,共用的作用域中的i的值已經(jīng)變成了6 。在其他作用域中的timer函數(shù)調(diào)用過程中需要查詢i,因為產(chǎn)生了閉包,i的值會去原始的作用域中查找,即全是6。
得不到預(yù)期效果的錯其實都在于for循環(huán)中共用一個作用域。想改進也很簡單,即在迭代的過程中,創(chuàng)建對應(yīng)的作用域。另外值得注意的一點是需要把每次迭代的i值傳到作用域內(nèi)。
for(var i = 1; i <= 5; i++) { (function (j) { setTimeout(function timer () { console.log(j) }, j* 1000) })(i) }四、閉包的垃圾回收
本來一個變量被使用完之后就可以利用垃圾回收機制進行垃圾回收,但因為閉包的產(chǎn)生,阻止了這一行為。
function process(data) { // } var someReallyBigData = {}; process( someReallyBigData ); var $btn = $(".j_Btn"); $btn.on("click", function clicker() {});
這個例子中就是因為事件綁定機制中的傳入了clicker回調(diào)函數(shù),產(chǎn)生了閉包,引用著clicker所在的作用域,所以此處的someReallyBigData數(shù)據(jù)無法從內(nèi)存中釋放。
解決辦法也有,聲明一個塊作用域,讓引擎清楚的知道沒有必要保存someReallyBigData餓了。
function process(data) { // } { let someReallyBigData = {}; process( someReallyBigData ); } var $btn = $(".j_Btn"); $btn.on("click", function clicker() {});
閱讀心得,轉(zhuǎn)載請注明出處。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/78518.html
摘要:基本概念首先,函數(shù)不能存儲的值,指向哪里,取決于調(diào)用它的對象。如果沒有這個對象,那默認(rèn)就是調(diào)用非嚴(yán)格模式下。也就是說是在運行的時候定義的,不是在綁定的時候定義的。 基本概念 首先,函數(shù)不能存儲this的值,this指向哪里,取決于調(diào)用它的對象。如果沒有這個對象,那默認(rèn)就是window調(diào)用(非嚴(yán)格模式下)。也就是說this是在運行的時候定義的,不是在綁定的時候定義的。 funct...
摘要:在我們的程序中有很多變量標(biāo)識符,我們現(xiàn)在或者將來將使用它。當(dāng)我們使用時,如果并沒有找到這個變量,在非嚴(yán)格模式下,程序會默認(rèn)幫我們在全局創(chuàng)建一個變量。詞法作用域也就是說,變量的作用域就是他聲明的時候的作用域。 作用域 定義 首先我們來想想作用域是用來干什么的。在我們的程序中有很多變量(標(biāo)識符identifier),我們現(xiàn)在或者將來將使用它。那么多變量,我咋知道我有沒有聲明或者定義過他呢,...
摘要:為什么會存在跨域問題同源策略由于出于安全考慮,瀏覽器規(guī)定不能操作其他域下的頁面,不能接受其他域下的請求不只是,引用非同域下的字體文件,還有引用非同域下的圖片,也被同源策略所約束只要協(xié)議域名端口有一者不同,就被視為非同域。 showImg(https://segmentfault.com/img/remote/1460000017093859?w=1115&h=366); Why 為什么...
摘要:運行規(guī)則根據(jù)的運作原理,我們可以看到,的值和調(diào)用棧通過哪些函數(shù)的調(diào)用運行到調(diào)用當(dāng)前函數(shù)的過程以及如何被調(diào)用有關(guān)。 1. this的誕生 假設(shè)我們有一個speak函數(shù),通過this的運行機制,當(dāng)使用不同的方法調(diào)用它時,我們可以靈活的輸出不同的name。 var me = {name: me}; function speak() { console.log(this.name); }...
摘要:一到底是一門什么樣的計算機編程語言表里不一表面上是動態(tài)解釋執(zhí)行的腳本語言,實際上它是一門編譯語言。與眾不同與傳統(tǒng)語言不同的是,它不是提前編譯的,編譯記過也不能在分布式系統(tǒng)中進行移植。千篇一律引擎進行編譯的步驟和傳統(tǒng)的編譯語言非常相似。 一、JavaScript到底是一門什么樣的計算機編程語言? JavaScript表里不一:表面上是動態(tài)、解釋執(zhí)行的腳本語言,實際上它是一門編譯語言。 ...
閱讀 1210·2021-11-10 11:35
閱讀 2951·2021-09-24 10:35
閱讀 2975·2021-09-22 15:38
閱讀 2815·2019-08-30 15:43
閱讀 1349·2019-08-29 18:39
閱讀 2592·2019-08-29 15:22
閱讀 2802·2019-08-28 18:17
閱讀 619·2019-08-26 13:37