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