寫這篇文章的原因很簡(jiǎn)單,臨近畢業(yè),不得不考慮找工作的問(wèn)題,面臨著就業(yè)的壓力,開始在前輩的教育下,開始刷題之旅,從leetcode到??停瑥木幊痰娇陀^題。無(wú)奈自己實(shí)在是真的渣,牛客上的Javascript客觀題的準(zhǔn)確率一度掉到了53%,很多一些經(jīng)典的知識(shí),如閉包,繼承等都完全不知所云。于是從頭開始系統(tǒng)學(xué)習(xí)一些不了解的知識(shí),并在總結(jié)別人博客的基礎(chǔ)上談一下自己的理解,如有錯(cuò)誤,還請(qǐng)?jiān)谠u(píng)論區(qū)留言糾正我。
這里感謝一下阮一峰大神,本文很大程度都借鑒了阮老師的學(xué)習(xí)Javascript閉包(Closure),查看原文請(qǐng)點(diǎn)擊這里。
1 變量的作用域
阮老師解釋到,變量的作用域分為兩種,全局變量和局部變量。這兩個(gè)概念理解起來(lái)很容易,Javascript函數(shù)內(nèi)部可以直接讀取到全局變量,如下例所示。
var name = "John";//name定義在函數(shù)外部 此時(shí)的name是一個(gè)全局變量
function print(){
console.log(name);//此時(shí)控制臺(tái)輸出John
}
Javascript函數(shù)外部不能讀取函數(shù)內(nèi)部的局部變量,如下例所示。
function print(){
var name = "Jenny";//name定義在函數(shù)內(nèi)部 此時(shí)的name是一個(gè)局部變量
}
console.log(name);//此時(shí)控制臺(tái)輸出undefined 因?yàn)闊o(wú)法在全局中找到name變量
2 閉包簡(jiǎn)介
一般的,不能通過(guò)外部函數(shù)讀取局部變量。但是,在實(shí)際開發(fā)中,我們有時(shí)候需要得到函數(shù)內(nèi)的局部變量。解決這個(gè)問(wèn)題,可以兩種方法,方法一,在函數(shù)a的內(nèi)部再定義一個(gè)函數(shù)b,來(lái)獲取函數(shù)a的局部變量。
function print(){//此時(shí)print相當(dāng)于函數(shù)a
var name = "Jay";
function print2(){//此時(shí)print2相當(dāng)于函數(shù)b
console.log(name);
}
}
在上面的代碼中,函數(shù)print2就被包括在函數(shù)print內(nèi)部,這是print內(nèi)部的所有局部變量,對(duì)print2都是可見的,但是反過(guò)來(lái)就不行,print2內(nèi)部的局部變量,對(duì)print就是不可見的。這就是Javascript語(yǔ)言特有的“鏈?zhǔn)阶饔糜颉苯Y(jié)構(gòu)(chain scope)。子對(duì)象會(huì)一級(jí)一級(jí)地向上尋找所有父對(duì)象的變量。所以,父對(duì)象的所有變量,對(duì)子對(duì)象都是可見的,反之則不成立。
——阮一峰
方法二,既然print2可以讀取print中的局部變量,我們只需返回print2的值,就可以在print外部讀取它的內(nèi)部變量。
function print(){
var name = "Jay";
function print2(){
console.log(name);
}
return print2;
}
var result = print();
result();//此時(shí)控制臺(tái)輸出Jay
這里的print2函數(shù)就是一個(gè)閉包。阮老師對(duì)閉包的理解是,閉包就是能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。閉包的本質(zhì),就是將函數(shù)內(nèi)部和函數(shù)外部連接起來(lái)供我們使用。
3 閉包的用途
閉包的最大用處有兩個(gè),一個(gè)是前面提到的可以讀取函數(shù) ,另一個(gè)就是讓這些變量的值始終保持在內(nèi)存中。
function print(){
var num = 4399;
add = function(){//這里add要寫成全局函數(shù),否則在調(diào)用的時(shí)候會(huì)說(shuō)沒有定義
num += 1;
}
function print2(){
console.log(num);
}
return print2;
}
var result = print();//result實(shí)際是閉包print2函數(shù)
result();//控制臺(tái)輸出4399
add();
result();//控制臺(tái)輸出4400,說(shuō)明num一直保存在內(nèi)存中,調(diào)用add函數(shù)后執(zhí)行加1運(yùn)算
注意:
1、閉包會(huì)是的函數(shù)中的變量都被保存在內(nèi)存中,內(nèi)存消耗很大,所以不能濫用閉包,否則會(huì)造成網(wǎng)頁(yè)的性能問(wèn)題,在IE中可能導(dǎo)致內(nèi)存泄露。解決方法是,在退出函數(shù)之前,將不使用的局部變量全部刪除。
2、閉包會(huì)在父函數(shù)外部,改變父函數(shù)內(nèi)部變量的值。所以,如果你把父函數(shù)當(dāng)作對(duì)象使用,把閉包當(dāng)作它的公用方法,把內(nèi)部變量當(dāng)作它的私有屬性,這時(shí)一定要小心,不要隨便改變父函數(shù)內(nèi)部變量的值。
4 幾個(gè)例子
首先我們看下阮老師文章里面留下的兩個(gè)思考題。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
//console.log(this);//控制臺(tái)輸出Window {...}
return this.name;
}
}
}
console.log(object.getNameFunc()());//控制臺(tái)輸出The Window
個(gè)人理解,object.getNameFunc()()指的是return function(){return this.name;}這個(gè)函數(shù),可以把object.getNameFunc()整體看成一個(gè)函數(shù)A,即A()返回this.name,函數(shù)里面的this找不到就往上一層找,因此,this指的是Window對(duì)象。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
//console.log(this);//控制臺(tái)輸出Object{...}
//console.log(that);//控制臺(tái)輸出Object{...}
return function(){
//console.log(this);//控制臺(tái)輸出Window{...}
//console.log(that);//控制臺(tái)輸出Object{...}
//return this.name;//控制臺(tái)輸出The Window
return that.name;
}
}
}
console.log(object.getNameFunc()());//控制臺(tái)輸出My Object
這里一開始this賦值給了一個(gè)對(duì)象中的局部變量,此時(shí)這里的this和that指的都是Object對(duì)象。后面return函數(shù)里面this沒有對(duì)其賦值,找不到this,所以只有在方法調(diào)用的時(shí)候,才能確定其指向(換言之,繼續(xù)往上一層找),而that指的是Object對(duì)象,所以that.name輸出"My Object"。
接下來(lái)看一個(gè)更讓你頭大的。下面這個(gè)例子是Zing大神給我耐心解答的時(shí)候舉出的,覺得理解起來(lái)更有幫助。這里也要感謝下Zing大神。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
//console.log(this);//控制臺(tái)輸出Object{name : "I have a headache"}
return this.name;
}
}
}
var test = {"name" : "I have a headache", b2 : object.getNameFunc()}
console.log(test.b2());//輸出I have a headache
這里調(diào)用的時(shí)候test調(diào)用自己的b2方法,b2又調(diào)用了object的getNameFunc()方法。getNameFunc返回的this.name在這個(gè)函數(shù)中找不到this,因此需要在上一層找,于是就找到了test對(duì)象,這里this.name即test.name。
function f1(){
var arr = new Array();
for (var i = 0;i < 3;i++){
arr[i] = function(){
return i;
}
}
return arr;
}
var test = f1();
console.log(test[0]());//控制臺(tái)輸出3
console.log(test[1]());//控制臺(tái)輸出3
console.log(test[2]());//控制臺(tái)輸出3
這道題就是我遇到的一個(gè)很經(jīng)典的錯(cuò)題,當(dāng)時(shí)我認(rèn)為應(yīng)該返回0,1,2,萬(wàn)萬(wàn)沒想到,/(ㄒoㄒ)/~~,一個(gè)都沒猜對(duì)。這里要注意,var test = f1();只是定義函數(shù),真正執(zhí)行方法的是test[0](),test[1](),test[2](),當(dāng)這些方法執(zhí)行的時(shí)候,i已經(jīng)變成了3,因此控制臺(tái)都會(huì)輸出3。那么怎么修改一下才能讓它輸出0,1,2呢?這里給出三種方法。
方法一
function f1(){
var arr = new Array();
for (var i = 0;i < 3;i++){
arr[i] = function(num){
return function(){
return num;
}
}(i);
}
return arr;
}
var test = f1();
console.log(test[0]());//控制臺(tái)輸出0
console.log(test[1]());//控制臺(tái)輸出1
console.log(test[2]());//控制臺(tái)輸出2
方法一給出的這種解法是我個(gè)人認(rèn)為最難理解的一種,這里沒有直接返回i,而是通過(guò)一個(gè)匿名函數(shù),返回一個(gè)num值。在調(diào)用匿名函數(shù)的時(shí)候,傳入一個(gè)變量i。由于函數(shù)是按值傳遞的,所以就會(huì)將變量i的值復(fù)制給參數(shù)num,而在匿名函數(shù)內(nèi)部,又返回一個(gè)訪問(wèn)num的閉包。這樣一來(lái),就可以返回各自不同的數(shù)值。
下面看方法二,方法二是方法一的改進(jìn)版,不過(guò)方法二比方法一理解起來(lái)要好一點(diǎn)。
方法二
function f1(){
var arr = new Array();
function test(num){
return function(){
return num;
}
}
for (var i = 0;i < 3;i++){
arr[i] = test(i);
}
return arr;
}
var test = f1();
console.log(test[0]());//控制臺(tái)輸出0
console.log(test[1]());//控制臺(tái)輸出1
console.log(test[2]());//控制臺(tái)輸出2
這里將test函數(shù)單獨(dú)抽出來(lái),個(gè)人認(rèn)為比方法一看上去好理解一些,這里不再詳細(xì)解釋,請(qǐng)讀者自行理解。
下面看方法三,方法三是前些日子看ES6時(shí)無(wú)意間看到的。
ES6新增了let命令,用來(lái)聲明變量。它的用法類似于var,但是所聲明的變量,只在let命令所在的代碼塊內(nèi)有效。
——阮一峰《ECMAScript 6 入門》
方法三
function f1(){
var arr = new Array();
for (let i = 0;i < 3;i++){
arr[i] =function(){
return i;
}
}
return arr;
}
var test = f1();
console.log(test[0]());//控制臺(tái)輸出0
console.log(test[1]());//控制臺(tái)輸出1
console.log(test[2]());//控制臺(tái)輸出2
let聲明的變量只在本輪循環(huán)有效,所以每次循環(huán)的i都是一個(gè)新的變量。你可能會(huì)問(wèn),如果每一輪循環(huán)的變量i都是重新聲明的,那它怎么知道上一輪循環(huán)的值,從而計(jì)算出本輪循環(huán)的值?這是因?yàn)镴avascript引擎內(nèi)部會(huì)記住上一輪循環(huán)的值,初始化本輪的變量i時(shí),就在上一輪循環(huán)的基礎(chǔ)上進(jìn)行計(jì)算。
5 總結(jié)
以上是我個(gè)人對(duì)Javascript閉包的一些理解,希望對(duì)各位有所幫助。寫的不對(duì)的地方還請(qǐng)各位大神提出意見,我會(huì)加以改正。碼字不易,請(qǐng)尊重作者版權(quán),轉(zhuǎn)載注明出處。
By BeLLESS 2017.4.7 9:26