面試題
首先像街,讓我們來看一題面試題;
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
items[i].onclick=function() {
console.log(i)
}
}//假設(shè)頁面上有5個li元素,而每次點(diǎn)擊每個li元素輸出的卻都是5肥橙,是否與你預(yù)期不一樣呢栽燕?
作用域
作用域(scope)指的是變量存在的范圍暂氯。Javascript只有兩種作用域:一種是全局作用域潮模,變量在整個程序中一直存在,所有地方都可以讀瘸帐擎厢;另一種是函數(shù)作用域究流,變量只在函數(shù)內(nèi)部存在。
在函數(shù)外部聲明的變量就是全局變量(global variable)动遭,它可以在函數(shù)內(nèi)部讀取芬探。
在函數(shù)內(nèi)部定義的變量,外部無法讀取厘惦,稱為“局部變量”(local variable)偷仿。
var a=1; //全局變量a
function fn1() {
var a=2; //局部變量a
console.log(a)
}
fn1(); //輸出2,全局變量a的值被替代宵蕉;如果沒有var a=2酝静,則獲取全局變量a輸出1
console.log(a) //輸出1,全局變量a保持不變羡玛;
- 就近原則:同時别智,上述代碼也體現(xiàn)了作用域的就近原則,即函數(shù)fn1在console.log(a)的時候會在其自己的作用域里找到a稼稿,如果沒有則往上薄榛,不斷從“父作用域”找,直到找到让歼;
但是請注意敞恋,請不要認(rèn)為就近一定是對的,請看如下代碼:
var a=1;
function fn1() {
var a=2;
fn2();
}
function fn2() {
console.log(a);
}
fn1() //輸出結(jié)果為1;
上面代碼中谋右,函數(shù)fn2最近的a是2硬猫,但函數(shù)fn2是在函數(shù)f的外部聲明的,所以它的作用域綁定外層倚评,內(nèi)部變量a不會到函數(shù)fn1體內(nèi)取值(局部變量外部無法獲取)浦徊,所以輸出1馏予,而不是2天梧。
或者** 我把fn1注釋掉,也許你會一目了然霞丧?**
var a=1;
//function fn1() {
//var a=2;
// fn2();
// }
function fn2() {
console.log(a);
}
很顯然如果fn2被調(diào)用(無論在任何地方調(diào)用呢岗,因?yàn)閒n2是在函數(shù)f的外部聲明),則就是輸出全局變量a為1
立即執(zhí)行函數(shù)(IIFE)
在上述代碼中蛹尝,我們可以看出全局變量很不穩(wěn)點(diǎn)后豫,可被任意獲取改變,容易發(fā)生不必要的麻煩沖突突那,例如:
var a=1;
function fn1()..;
function fn2()..;
function fn3()..;
//成千上萬行代碼
....
function fnx() {
a=2; //全局變量被賦予新值挫酿;
}
fn()
console.log(a) //輸出2
也許你再敲完成千上萬行代碼之后不經(jīng)意把全局變量a給改變了,等你下次再用a愕难,它就是2了(也許你期待的依然是1?);因此應(yīng)該盡量注意小心使用全局變量早龟,或者說盡量避免惫霸,立即執(zhí)行函數(shù)(IIFE)很好地做到了這一點(diǎn);
// 寫法一
var a = 1;
fn1(a);
fn2(a);
// 寫法二
(function (){
var a = 1;
fn1(a);
fn2(a);
}());
通常情況下葱弟,只對匿名函數(shù)使用這種“立即執(zhí)行的函數(shù)表達(dá)式”壹店。它的目的有兩個:
- 一是不必為函數(shù)命名,避免了污染全局變量芝加;
- 二是IIFE內(nèi)部形成了一個單獨(dú)的作用域硅卢,可以封裝一些外部無法讀取的私有變量。
上述例子的寫法二優(yōu)于寫法一藏杖,很好地體現(xiàn)了這兩點(diǎn)将塑;
此外,值得注意:的是立即執(zhí)行函數(shù)顧名思義就是立即調(diào)用蝌麸,由此我們很容易明白函數(shù)后面括號的意義(形如調(diào)用:fn())抬旺;但是你不能在函數(shù)的定義之后加上圓括號,這會產(chǎn)生語法錯誤祥楣。因?yàn)?strong>function這個關(guān)鍵字即可以當(dāng)作語句开财,也可以當(dāng)作表達(dá)式。
function fn(){/* do something */}()
//定義函數(shù)直接加括號误褪,報錯
function fn(){/* do something */}()
//函數(shù)聲明語句
var a=function fn(){/* do something */}()
//表達(dá)式
-
解決辦法:避免讓function出現(xiàn)在行首责鳍,讓引擎將其理解成一個表達(dá)式。最簡單的處理兽间,就是將其放在一個圓括號里面历葛,或者加一些其他符號也可以:
(function(){ /* do something */ }()); // 或者 (function(){ /* do something */ })(); //其他符號 !function(){ /* do something */ }(); ~function(){/* do something */ }(); -function(){ /* do something */ }(); +function(){/* do something */}();
但是!嘀略,我們應(yīng)該格外小心將立即執(zhí)行函數(shù)放入圓括號的使用恤溶,因?yàn)槲覀兌贾缊A括號可以表示函數(shù)定義(也可以用來放置函數(shù)的參數(shù)),調(diào)用帜羊,一不小心很容易出錯,例如你在立即執(zhí)行函數(shù)之前不小心寫了個2:
//寫法一:
2
(function(){
var a=1;
console.log(a)
}())
//報錯咒程,2 is not a function,其實(shí)上述寫法類似與你寫:2(一些參數(shù)),而括號里的內(nèi)容就作為參數(shù)傳遞了讼育;
很顯然你都沒有定義函數(shù)2,調(diào)用它怎會不報錯?
//寫法二:
2
!function(){
var a=1;
console.log(a)
}()
//不報錯帐姻,輸出1
-
立即執(zhí)行函數(shù)也可以接受參數(shù):
var a=1;
!function(a){
//var a;注意,函數(shù)的形參a相當(dāng)于在自己的作用域里聲明了一個局部量a奶段;
console.log(a) //輸出undefined饥瓷,雖然形參a有聲明,但未賦值痹籍;
}()//但是如果立即執(zhí)行函數(shù)沒有形參呢铆,則輸出獲取全局變量a輸出 var a=1; !function(){ //var a; 注意!!!此時沒有形參a,不存在var a這行代碼,因此不可能輸出undefined console.log(a) //從全局作用域獲取全局變量a蹲缠,輸出1 }() //有形參依然輸出全局變量a棺克,那就要把它當(dāng)作實(shí)參傳入進(jìn)去 var a=1; !function(a){ //var a; console.log(a) }(a) //注意鳖宾,這里的a是實(shí)參,傳入的是全局變量中的a,與立即執(zhí)行函數(shù)中的形參a不是同一個a逆航, //因此輸出1鼎文;
變量提升
變量提升很簡單,JavaScript引擎會把變量與函數(shù)名(也當(dāng)作變量)進(jìn)行聲明前置因俐,提升到代碼頭部:
var a=1;
fn1();
function fn1() {
a=2;
}
console.log(a) //輸出結(jié)果為2拇惋,且雖然先調(diào)用fn1比聲明在先,但不會報錯
//實(shí)際執(zhí)行順序抹剩;
var a;
function fn1() {
a=2;
}
a=1
fn1();
console.log(a)
回歸面試題
說了這么多撑帖,終于回到面試題了,看完你就明白我之前為什么巴拉巴拉那么多了澳眷,先看第一題胡嘿,首先先回答一個問題:
var items=document.querySelectorAll('li');
var i;//變量提升,幫助理解為什么i只有一個,就是全局變量i;
for( i=0;i<items.length;i++) {
//i=0,1,2,3,4;
items[i].onclick=function() {
console.log(i)
}
}
//i=5
console.log(i) //輸出5
請問钳踊,哪一個console.log(i)先執(zhí)行衷敌,很顯然由異步我們可以得知,click事件肯定是放到最后的,用戶手速多快都是最后拓瞪,所以肯定是第二個先執(zhí)行缴罗;
也許你看完代碼和解釋很容易理解第二個console.log(i)是輸出5,第一個不理解祭埂,那如果我說面氓,上述代碼中的i,從始至終都只有一個i,就是全局變量i(即便有一個function形成了新的作用域蛆橡,獲取的依然是全局變量i),且第一個console.log(i)又是放到最后執(zhí)行的舌界,這樣你總該理解了吧
好了,問題解釋完了泰演,現(xiàn)在讓我們說說解決辦法吧:
剛才解釋問題的時候找到的原因是異步和從頭到尾是一個i呻拌,異步解決不了,我讓它變成不同的i不就可以了嗎?還記得之前提到的作用域嗎粥血?
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
var newfn=function(j) { //為了生成新的作用域柏锄,形成不同的'i'
console.log(j) //輸出0酿箭,1复亏,2,3缭嫡,4
}
newfn(i)
console.log(j) //報錯缔御,不能獲取局部變量j
// items[i].onclick=function() {
// console.log(i)
// }
}
看到上面的報錯了吧?因此要想click獲取上面新的作用域里不同的'i'妇蛀,我們把代碼改成:
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
var newfn=function(i) { //把形參也改成i好了耕突,注意哦笤成,這不是全局變量i,
//每次循環(huán)都生成一個新的作用域眷茁,新的i;
items[i].onclick=function() {
console.log(i) //此時點(diǎn)擊已經(jīng)可以依次輸出0炕泳,1,2上祈,3培遵,4了
}
}
newfn(i)
}
我聲明完newfn之后又馬上調(diào)用了,我再改一下,去掉函數(shù)名newfn;
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
~function(i) {
items[i].onclick=function() {
console.log(i)
}
}(i)
}
看上面代碼登刺,for循環(huán)里面不就是一個立即執(zhí)行函數(shù)嗎籽腕?是的,不過還有另外的方法纸俭,我還可以再改一下皇耗;觀察上面的代碼不就是onlick后面是一個函數(shù),外加一個新的作用域嗎?
其實(shí)說到這里揍很,不知你有沒有發(fā)現(xiàn)郎楼,那些看似很難復(fù)雜的問題,再你深入了解之后窒悔,你就可以看到它的本質(zhì)箭启,就可以用簡單的方法解決問題了;
//回歸最初的代碼
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
items[i].onclick=function() {
console.log(i)
}
}
看代碼function有了蛉迹,那不就代表新作用域也有了傅寡?那為什么還是沒有發(fā)揮新作用域的作用,即生成不同的i呢北救?
答案很簡單:新作用域里沒有任何局部變量荐操,也沒有用到形參,你要輸出i的每一個不同的值珍策,也沒有將他們以實(shí)參的形式傳入新的作用域里托启;
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
items[i].onclick=function(i) { //多一個形參
//var i,i就是形參,這一點(diǎn)剛才我講過
console.log(i) //輸出上面的形參i,0攘宙,1屯耸,2,3蹭劈,4疗绣;
}(i)//實(shí)參傳入不同的i
}
寫完之后發(fā)現(xiàn)這還是一個立即執(zhí)行函數(shù),沒毛病铺韧,老鐵多矮,它形成了一個單獨(dú)的作用域,這很立即執(zhí)行函數(shù)!K印讯壶!(我們開始不就是這么介紹立即執(zhí)行函數(shù)嗎),所以說湾盗,有些概念可能就是誕生在我們解決問題的時候吧伏蚊。。
不過如果你試著執(zhí)行一下這段代碼就會發(fā)現(xiàn)一個問題(怎么還有問題格粪,是不是好煩啊哈哈哈丙挽,I think so!!!),不用點(diǎn)擊匀借,直接輸出0颜阐,1,2吓肋,3凳怨,4,點(diǎn)擊事件瓦特了!!!
很簡單是鬼,因?yàn)楹瘮?shù)立即執(zhí)行了卻沒有實(shí)際的返回值啊肤舞,此時onclick等于默認(rèn)的返回值undefined,而我們需要的是onclick后面是個函數(shù)啊均蜜,所以return 一個function就可以了
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
items[i].onclick=function(i) {
//var i;不要忘了這個局部變量i哦李剖,下面輸出的i就是從這里獲取的
//再次提醒別搞混全局變量i和局部變量i了
return function() {
console.log(i)
}
}(i)
}
閉包
好了,終于都解決了囤耳,全文完篙顺?快了,還差一個最'神秘'的閉包充择;
首先讓我們簡單認(rèn)識閉包(其實(shí)剛才已經(jīng)用到好幾次了):
var a = 1;
function fn() {
console.log(a);
}
f1() // 1
這就是閉包德玫,怎么樣,是不是很簡單椎麦?那么它到底有什么用呢宰僧?
它的作用之一就是獲取外部作用域的變量,全局作用域的變量大家都能獲取观挎,要閉包來干嘛琴儿,我說的當(dāng)然是閉包用來獲取外部無法獲取的局部變量咯
而通常說閉包是函數(shù)嵌套一個函數(shù),就是因?yàn)楹瘮?shù)內(nèi)的局部變量不能獲取嘁捷,只能嵌套一個函數(shù)(此函數(shù)就是閉包)來獲仍斐伞;
再把嵌套的函數(shù)作為返回值就成功獲取內(nèi)部變量了普气∶瞻蹋看代碼:
function fn1() {
var b=2;
var a= 1; //局部變量外部無法獲取
function fn2() { //閉包,成功獲取a
console.log(a);
}
return fn2; //返回這個閉包到外部
}
var result = fn1(); //result接受现诀,局部變量a成功被外部獲取
result(); // 1
此外夷磕,上述代碼也體現(xiàn)了閉包了作用之二,閉包讓剛才獲取的局部變量始終保持在內(nèi)存中仔沿,記住了它的誕生環(huán)境——fn1(){},這是因?yàn)閞esult始終在內(nèi)存中坐桩,而result(也可以理解為由于返回值,result就是fn2,fn2又用到了fn1的變量)的存在依賴于fn1封锉,從而使得fn1環(huán)境及其內(nèi)部變量一直存在绵跷,并不會在函數(shù)調(diào)用結(jié)束之后被回收;
因此也帶來了副作用:外層函數(shù)每次運(yùn)行成福,都會生成一個新的閉包碾局,而這個閉包又會保留外層函數(shù)的內(nèi)部變量,所以內(nèi)存消耗很大奴艾。因此不能濫用閉包净当,否則會造成網(wǎng)頁的性能問題。
又使內(nèi)部變量一直存在蕴潦,又消耗內(nèi)存
—>謠言像啼?閉包導(dǎo)致內(nèi)存泄漏(用不到(訪問不到)的變量,依然占居著內(nèi)存空間潭苞,不能被再次利用起來忽冻。),并且不會垃圾回收機(jī)制回收此疹?
請仔細(xì)看上面的代碼僧诚,局部變量a是閉包fn2所用到的,因此根本不存在內(nèi)存泄漏的說法蝗碎,至于變量b沒用到振诬,也訪問不到,會不會被回收呢衍菱?還是看大神寫的文章吧赶么,反正我是說不清楚哈哈哈哈
針對不同的javascript引擎,解析閉包對垃圾回收的影響
剛才說的又有點(diǎn)難了,還是說點(diǎn)相對簡單脊串,又基礎(chǔ)辫呻,又重要,又容易弄錯的吧
——>閉包琼锋,立即執(zhí)行函數(shù)傻傻分不清楚放闺,很多人理解錯閉包,覺得它難缕坎,有很大一部分原因是因?yàn)檫@個怖侦,不知道你看到這里還會不會搞混;
還是回到面試題:
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
items[i].onclick=function() {
console.log(i)
}
}
上面的funcition獲取了外部變量i,不就是閉包嘛匾寝?是的搬葬!
這么說來閉包帶來了這道容易做錯的面試題?是的艳悔!
解決之后的代碼:
var items=document.querySelectorAll('li');
for(var i=0;i<items.length;i++) {
items[i].onclick=function(i) {
//var i;不要忘了這個局部變量i哦急凰,下面輸出的i就是從這里獲取的,
//或者也許你把形參改成別的字母你就不會搞混全局變量和局部變量了
return function() {
console.log(i)
}
}(i)
}
上面的function就是一個立即執(zhí)行函數(shù)吧猜年?是的抡锈!
它再獲取外部變量i的值之后,也就是閉包之后乔外?是的床三!
它內(nèi)部形成了一個單獨(dú)的作用域,又將其以局部變量的形式封裝了起來杨幼?是的撇簿!
然后返回輸出封裝的局部變量,解決了問題推汽,所以立即執(zhí)行函數(shù)解決了問題补疑?是的!
綜上歹撒,閉包造成問題莲组,立即執(zhí)行函數(shù)解決問題,不知道你理解了嗎
由一道題目經(jīng)過漫長的過程暖夭,全文終于完锹杈。。
如有錯誤迈着,懇請指正竭望!