函數(shù)船响、作用域鏈與閉包

閉包是js中一個(gè)極為NB的武器,但也不折不扣的成了初學(xué)者的難點(diǎn)躲履。因?yàn)閷W(xué)好閉包就要學(xué)好作用域见间,正確理解作用域鏈,然而想做到這一點(diǎn)就要深入的理解函數(shù)工猜,所以我們從函數(shù)說起米诉。

函數(shù)的聲明和調(diào)用

首先說明一下,本文基于原生js環(huán)境篷帅,不涉及DOM部分
最基本的就是函數(shù)的定義和調(diào)用史侣,注意區(qū)分以下形式:

//以2下個(gè)是函數(shù)的定義
function func(){  //函數(shù)聲明
  /*code*/
}
var func = function(){   //函數(shù)表達(dá)式
  /*code*/
};

//以下2個(gè)是函數(shù)的調(diào)用(執(zhí)行)
func(); //無法得到函數(shù)的返回值
var returnValue = func();  //執(zhí)行函數(shù)并將返回值賦給returnValue, 如果函數(shù)沒有指定返回值,返回undefined

//以下2各定義了立即執(zhí)行函數(shù)
(function(){
  /*code*/
})();
(function(){
  /*code*/
}());

立即執(zhí)行函數(shù)直接聲明一個(gè)匿名函數(shù)魏身,立即使用惊橱,省得定義一個(gè)用一次就不用的函數(shù),而且免了命名沖突的問題叠骑。如果寫為如下形式可獲得立即執(zhí)行函數(shù)的返回值李皇。

var returnValue = (function(){return 1;}());
var returnValue = (function(){return 1;})();

除此之外削茁,函數(shù)還有一種非常常見的調(diào)用方式——回調(diào)函數(shù)宙枷。將一個(gè)函數(shù)作為參數(shù)傳入另一個(gè)函數(shù)掉房,并在這個(gè)函數(shù)內(nèi)執(zhí)行。比如下面這個(gè)形式

document.addEventListener("click", console.log, false);

理解了上面的部分慰丛,我們看一個(gè)典型的例子卓囚,好好理解一下函數(shù)的定義和調(diào)用的關(guān)系,這個(gè)一定要分清诅病。下面這段代碼很具有代表性:

var arr = [];
for(var i = 0; i < 10; i++){
    arr[i] = function(){
    return i;
  };
}
for(var j = 0; j < arr.length; j++){
  console.log(arr[j]() + " ");
}  //得到輸出:10 10 10 10 10 10 10 10 10 10

我們需要理解這里面第一個(gè)for循環(huán)其實(shí)相當(dāng)于如下形式哪亿,它只是定義了10個(gè)函數(shù),并把函數(shù)放在數(shù)組中,并沒有執(zhí)行函數(shù)贤笆。由于js遵循詞法作用域(lexical scoping), i是一個(gè)全局變量蝇棉,所以第二個(gè)for循環(huán)調(diào)用函數(shù)的時(shí)候,i等于10

var i = 0;
arr[0] = function(){ return i; }; i++;
arr[1] = function(){ return i; }; i++;
arr[2] = function(){ return i; }; i++;
//......省略
arr[9] = function(){ return i; }; i++;
//此時(shí)i == 10 循環(huán)結(jié)束

再講完了閉包我們?cè)倩貋斫鉀Q這個(gè)問題芥永。

關(guān)于函數(shù)的參數(shù)傳遞這里就不多說了篡殷,值得強(qiáng)調(diào)的是,上述2種定義函數(shù)的方式是有區(qū)別的埋涧,想理解這個(gè)區(qū)別板辽,先要理解聲明提前。

變量聲明提前

這個(gè)地方簡單理解一下js的預(yù)處理過程棘催。js代碼會(huì)在執(zhí)行前進(jìn)行預(yù)處理劲弦,預(yù)處理的時(shí)候會(huì)進(jìn)行變量聲明提前,每個(gè)作用域的變量(用var聲明的變量醇坝,沒有用var聲明的變量不會(huì)提前)和函數(shù)定義會(huì)提前到這個(gè)作用域內(nèi)的開頭邑跪。
函數(shù)中的變量聲明會(huì)提前到函數(shù)的開始,但初始化不會(huì)呼猪。比如下面這個(gè)代碼呀袱。因此我們應(yīng)該避免在函數(shù)中間聲明變量,以增加代嗎的可讀性郑叠。

function(){
  console.log(a);  //undefined
  f();  //f called
  /*...*/
  function f(){
    console.log("f called");
  }
  var a = 3;
  console.log(a);  //3
}

這段代碼等于(并且瀏覽器也是這么做的):

function(){
  function f(){
    console.log("f called");
  }
  var a;
  console.log(a);  //undefined
  f();  //f called
  /*...*/
  a = 3;
  console.log(a);  //3
}

不同函數(shù)定義方式的區(qū)別

第一個(gè)區(qū)別:

function big(){
  func();//函數(shù)正常執(zhí)行
  func1();//TypeError: func1 is not a function
  function func(){  //這個(gè)函數(shù)聲明會(huì)被提前
    console.log("func is called");
  }
  var func1 = function(){  //這個(gè)函數(shù)聲明會(huì)被提前夜赵,但不是個(gè)函數(shù),而是變量
    console.log("func1 is called");
  };
}
big();

第二個(gè)區(qū)別乡革,比較下面2段代碼

function f() {
  var b=function(){return 1;};
  function b(){return 0;};
  console.log(b());

  console.log(a());
  function a(){return 0;};
  var a=function(){return 1;};
}
f();

不難發(fā)現(xiàn)寇僧,用表達(dá)式定義的函數(shù)可以覆蓋函數(shù)聲明直接定義的函數(shù);但是函數(shù)聲明定義的函數(shù)卻不能覆蓋表達(dá)式定義的函數(shù)沸版。
實(shí)際中我們發(fā)現(xiàn)嘁傀,定義在調(diào)用之前var f = function(){};會(huì)覆蓋function f(){},而定義在調(diào)用之后function f(){}會(huì)覆蓋var f = function(){};(你可以以不同順序組合交換上面代碼中的行,驗(yàn)證這個(gè)結(jié)論)

第三個(gè)區(qū)別视粮,其實(shí)這個(gè)算不上區(qū)別

var fun = function fun1(){
  //內(nèi)部可見:fun和fun1
  console.log(fun1 === fun);
};
//外部僅fun可見
fun();  //true 說明這是同一個(gè)對(duì)象的2各不同引用
fun1(); //ReferenceError: fun1 is not defined

此外還有一個(gè)定義方法如下:

var func = new Function("alert('hello')");

這個(gè)方式不常用细办,也不建議使用。因?yàn)樗x的函數(shù)都是在window中的,更嚴(yán)重的是笑撞,這里的代碼實(shí)在eval()中解析的岛啸,這使得這個(gè)方式很糟糕,會(huì)帶來性能下降和安全風(fēng)險(xiǎn)茴肥。具體就不贅述了坚踩。

詞法作用域

C++和Java等語言使用的都是塊級(jí)作用域,js與它們不同瓤狐,遵循詞法作用域(lexical scoping)瞬铸。講的通俗一些,就是函數(shù)定義決定變量的作用域础锐,函數(shù)內(nèi)是一部分嗓节,函數(shù)外是另一部分,內(nèi)部可以訪問外部的變量皆警,但外部無法直接訪問內(nèi)部的變量赦政。首先我們看下面這個(gè)代碼

//這里是全局作用域
var a = 3;
var b = 2;
var c = 20;
function f(){ //這里是一個(gè)局部作用域
  var a = 12; //這是一個(gè)局部變量
  b = 10; //覆蓋了全局變量
  var d = e = 15;   //只有第一參數(shù)d是局部變量,后面的都是全局變量
  f = 13; //新的全局變量
  console.log(a + " " + b + " " + d);
}
f(); //12 10 15
console.log(a);  //3
console.log(b);  //10
console.log(c);  //20
console.log(d);  //undefined
console.log(e);  //15
console.log(f);  //13

<small>注:原生js在沒有定使用義的變量時(shí)會(huì)得到undefined耀怜,并在使用過程中遵循隱式類型轉(zhuǎn)換恢着,但現(xiàn)在的瀏覽器不會(huì)這樣,它們會(huì)直接報(bào)錯(cuò)财破。不過在函數(shù)中使用滯后定義的變量依然是undefined掰派,不會(huì)報(bào)錯(cuò),這里遵循聲明提前的原則左痢。</small>

這是一個(gè)最基本的作用域模型靡羡。我們上文提到過,函數(shù)里面可以訪問外面的變量俊性,函數(shù)外部不能直接訪問內(nèi)部的變量.
我們?cè)倏匆粋€(gè)復(fù)雜一點(diǎn)的:

var g = "g";
function f1(a){
  var b = "f1";
  function f2(){
    var c = "f2";
    console.log(a + b + c + g);
  }
  f2();
}
f1("g"); //gf1f2g

在js中略步,函數(shù)里面定義函數(shù)十分普遍,這就需要我們十分了解作用域鏈定页。
如下這個(gè)代碼定義了下圖中的作用域鏈:

var g = 10;
function f1(){
  var f_1 = "f1";
  function f2(){
    var f_2 = "f2";
    function f3(){
      var f_3 = "f3";
      /*function f...*/
    }
  }
}
作用域嵌套

這里內(nèi)層的函數(shù)可以由內(nèi)向外查找外層的變量(或函數(shù))趟薄,當(dāng)找到相應(yīng)的變量(或函數(shù))立即停止向外查找,并使用改變量(或函數(shù))典徊。而外層的函數(shù)不能訪問內(nèi)層的變量(或函數(shù))杭煎,這樣的層層嵌套就形成了作用域鏈。

值得一提的是卒落,函數(shù)的參數(shù)在作用于上相當(dāng)于在函數(shù)內(nèi)部第一行就聲明了的變量羡铲,注意這里指的僅僅是聲明,但不一定完成初始化儡毕,也就說明參數(shù)在沒有傳入值的時(shí)候值為undefined也切。

回調(diào)函數(shù)

那么問題來了,在一個(gè)函數(shù)外部永遠(yuǎn)不能訪問函數(shù)內(nèi)部的變量嗎?答案是否定的雷恃,我們可以用回調(diào)函數(shù)實(shí)現(xiàn)這個(gè)過程:

function A(arg){
  console.log(arg);
}
function B(fun){
  var a = "i am in function B";
  var i = 10;
  fun(a);
}

B(A); //i am in function B

上面這個(gè)過程對(duì)于B而言疆股,只把自己內(nèi)部的變量a給了fun,而外部的A無論如何也訪問不到B中的i變量褂萧,也就是說傳入的fun函數(shù)只能訪問B想讓它訪問的變量,因此回調(diào)函數(shù)這樣的設(shè)計(jì)可以在代碼的隔離和開放中間取得一個(gè)極好的平衡葵萎。
說句題外話:javascript特別適用于事件驅(qū)動(dòng)編程导犹,因?yàn)榛卣{(diào)模式支持程序以異步方式運(yùn)行。

好了羡忘,如果上面的你都看懂了谎痢,那么可以開始看閉包了。

閉包

閉包是指有權(quán)訪問另一個(gè)函數(shù)作用域中的變量的函數(shù)卷雕,創(chuàng)建閉包的最常見的方式就是在一個(gè)函數(shù)內(nèi)創(chuàng)建另一個(gè)函數(shù)节猿,通過另一個(gè)函數(shù)訪問這個(gè)函數(shù)的局部變量。閉包主要是為了區(qū)分私有和公有的方法和變量漫雕,類似于c++和java中對(duì)象的public成員和protected成員滨嘱。

一言以蔽之:作用域的嵌套構(gòu)成閉包!

構(gòu)成閉包以下幾個(gè)必要條件

  1. 函數(shù)(作用域)嵌套函數(shù)
  2. 函數(shù)(作用域)內(nèi)部可以引用外部的參數(shù)和變量
  3. 參數(shù)和變量不會(huì)被垃圾回收機(jī)制回收浸间√辏可以查看: 內(nèi)存管理與垃圾回收

閉包的優(yōu)缺點(diǎn)

  • 優(yōu)點(diǎn)
  1. 希望一個(gè)變量長期駐扎在內(nèi)存中(如同c++中static局部變量)
  2. 避免全局變量的污染
  3. 私有成員的存在
  • 缺點(diǎn)
  1. 閉包常駐內(nèi)存,會(huì)增大內(nèi)存使用量魁蒜,大量使用影響程序性能囊扳。
  2. 使用不當(dāng)很容易造成內(nèi)存泄露《悼矗可以查看: 內(nèi)存管理與垃圾回收锥咸。

一般函數(shù)執(zhí)行完畢后,局部活動(dòng)對(duì)象就被銷毀细移,內(nèi)存中僅僅保存全局作用域搏予。但閉包不會(huì)

為什么有閉包

我們考慮實(shí)現(xiàn)一個(gè)局部變量調(diào)用并自加的過程:

var a = 0;
function fun(){
  return a++;
}
fun(); //返回0
fun(); //返回1
fun(); //返回2

function func(){
  var a = 0;
  return a++;
}
func(); //返回0
func(); //返回0
func(); //返回0

看了上面代碼你會(huì)發(fā)現(xiàn)弧轧,當(dāng)a是全局變量的時(shí)候可以實(shí)現(xiàn)缔刹,但a成為了局部變量就不行了,當(dāng)然劣针,必須是閉包才可以實(shí)現(xiàn)這個(gè)功能:

var f = (function(){
  var a = 0;
  return function(){
    return a++;
  }
})();
f(); //返回0
f(); //返回1
f(); //返回2

這樣不僅實(shí)現(xiàn)了功能校镐,還防止了可能的全局污染。

上文舉了在循環(huán)內(nèi)定義函數(shù)訪問循環(huán)變量的例子捺典,可結(jié)果并不如意鸟廓,得到了十個(gè)10,下面我們用閉包修改這個(gè)代碼,使它可以產(chǎn)生0~9:

var arr = [];
for(var i = 0; i < 10; i++){
    arr[i] = (function(i){
    return function(){
      return i;
    };
  })(i);
}
for(var j = 0; j < arr.length; j++){
  console.log(arr[j]());
}//這樣就可以得到0~9了

當(dāng)然還以其他的解決方法:

//方法2
var arr = [];
for(var i = 0; i < 10; i++){
  arr[i] = console.log.bind(null, i);
}
for(var j = 0; j < arr.length; j++){
  console.log(arr[j]());

//方法3
var arr = [];
for(let i = 0; i < 10; i++){
  arr[i] = function(){
    console.log(i);
  };
}
for(var j = 0; j < arr.length; j++){
  console.log(arr[j]());
}//這樣也可以得到0~9了

迭代器

好了引谜,是時(shí)候放松一下了牍陌,看看下面這個(gè)代碼,這個(gè)會(huì)簡單一些

var inc = function(){
  var x = 0;
  return function(){
    console.log(x++);
  };
};
inc1 = inc();
inc1();  //0
inc1();  //1
inc2 = inc();
inc2();  //0
inc2();  //1
inc2 = null;  //內(nèi)存回收
inc2 = inc();
inc2();  //0

你會(huì)發(fā)現(xiàn)员咽,inc返回了一個(gè)函數(shù)毒涧,這個(gè)函數(shù)是個(gè)累加器,它們可以獨(dú)立工作互補(bǔ)影響贝室。這個(gè)就是js中迭代器next()的實(shí)現(xiàn)原理契讲。下面是一個(gè)簡單的迭代器:

//實(shí)現(xiàn)對(duì)數(shù)組遍歷
function iterator(arr){
  var num = 0;
  return {
    next: function(){
      if(num < arr.length)
        return arr[num++];
      else return null;
    }
  };
}
var a = [1,3,5,7,9];
var it = iterator(a);
var num = it.next()
while(num !== null){
  console.log(num)
   num = it.next();
}//依次輸出1,3滑频,5捡偏,7峡迷,9

如果你學(xué)了ES6银伟,那么你可以用現(xiàn)成的迭代器,就不用自定義迭代器了绘搞。

箭頭函數(shù)

箭頭函數(shù)本身也是一個(gè)函數(shù)彤避,具有自己的作用域。不過在箭頭函數(shù)里面的this上下文同函數(shù)定義所在的上下文夯辖,具體可以看我的另一篇文章:javascript中this詳解

典型實(shí)例

這個(gè)實(shí)例會(huì)涉及到對(duì)象的相關(guān)知識(shí)忠藤,如果不能完全理解,可以參考:javascript中this詳解javascript對(duì)象楼雹、類與原型鏈

function Foo() {
    getName = function () { console.log (1); };
    return this;
}
Foo.getName = function () { console.log (2);};
Foo.prototype.getName = function () { console.log (3);};
var getName = function () { console.log (4);};
function getName() { console.log (5);}

//請(qǐng)寫出以下輸出結(jié)果:
Foo.getName();     //2, 函數(shù)的靜態(tài)方法模孩,直接調(diào)用相關(guān)函數(shù)就可以了。
getName();    //4, 變量函數(shù)定義在調(diào)用之前贮缅,成功完成初始化榨咐,覆蓋函數(shù)聲明方式定義的同名函數(shù)
Foo().getName();   //1, 這里 Foo()返回的 this 是 window,在 Foo調(diào)用時(shí),對(duì)全局的變量型函數(shù) getName 重新定義了谴供,所以得到1块茁。
getName();   //1, 上一句改變了全局的 getName 函數(shù)為 cosnole.log(1)
new Foo.getName();   //2,無參數(shù) new 運(yùn)算比 . 運(yùn)算低桂肌,所以先運(yùn)行 Foo.getName数焊,得到2
new Foo().getName();   //3,有參數(shù) new 運(yùn)算和 . 運(yùn)算同一等級(jí)崎场,故從左到右佩耳,先運(yùn)算 new Foo() 得到一個(gè)匿名對(duì)象,在該對(duì)象上調(diào)用 getName 函數(shù)得到3
new new Foo().getName();    //3谭跨,同上干厚,先得到匿名對(duì)象李滴,然后將該對(duì)象的方法 getName 當(dāng)做構(gòu)造函數(shù)來調(diào)用,得到一個(gè)新對(duì)象蛮瞄,并輸出3;

Curry化

Curry化技術(shù)是一種通過把多個(gè)參數(shù)填充到函數(shù)體中所坯,實(shí)現(xiàn)將函數(shù)轉(zhuǎn)換為一個(gè)新的經(jīng)過簡化的(使之接受的參數(shù)更少)函數(shù)的技術(shù)。當(dāng)發(fā)現(xiàn)正在調(diào)用同一個(gè)函數(shù)時(shí)挂捅,并且傳遞的參數(shù)絕大多數(shù)都是相同的芹助,那么用一個(gè)Curry化的函數(shù)是一個(gè)很好的選擇.

下面利用閉包實(shí)現(xiàn)一個(gè)curry化的加法函數(shù)

function add(x,y){
  if(x && y) return x + y;
  if(!x && !y) throw Error("Cannot calculate");
  return function(newx){
    return x + newx;
  };
}
add(3)(4); //7
add(3, 4); //7
var newAdd = add(5);
newAdd(8); //13
var add2000 = add(2000);
add2000(100); //2100
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市闲先,隨后出現(xiàn)的幾起案子状土,更是在濱河造成了極大的恐慌,老刑警劉巖饵蒂,帶你破解...
    沈念sama閱讀 216,919評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件声诸,死亡現(xiàn)場(chǎng)離奇詭異酱讶,居然都是意外死亡退盯,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門泻肯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來渊迁,“玉大人,你說我怎么就攤上這事灶挟×鹦啵” “怎么了?”我有些...
    開封第一講書人閱讀 163,316評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵稚铣,是天一觀的道長箱叁。 經(jīng)常有香客問我,道長惕医,這世上最難降的妖魔是什么耕漱? 我笑而不...
    開封第一講書人閱讀 58,294評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮抬伺,結(jié)果婚禮上螟够,老公的妹妹穿的比我還像新娘。我一直安慰自己峡钓,他們只是感情好妓笙,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,318評(píng)論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著能岩,像睡著了一般寞宫。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上拉鹃,一...
    開封第一講書人閱讀 51,245評(píng)論 1 299
  • 那天淆九,我揣著相機(jī)與錄音统锤,去河邊找鬼。 笑死炭庙,一個(gè)胖子當(dāng)著我的面吹牛饲窿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播焕蹄,決...
    沈念sama閱讀 40,120評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼逾雄,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了腻脏?” 一聲冷哼從身側(cè)響起鸦泳,我...
    開封第一講書人閱讀 38,964評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎永品,沒想到半個(gè)月后做鹰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,376評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鼎姐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,592評(píng)論 2 333
  • 正文 我和宋清朗相戀三年钾麸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片炕桨。...
    茶點(diǎn)故事閱讀 39,764評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡饭尝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出献宫,到底是詐尸還是另有隱情钥平,我是刑警寧澤,帶...
    沈念sama閱讀 35,460評(píng)論 5 344
  • 正文 年R本政府宣布姊途,位于F島的核電站涉瘾,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏捷兰。R本人自食惡果不足惜立叛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,070評(píng)論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望寂殉。 院中可真熱鬧囚巴,春花似錦、人聲如沸友扰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽村怪。三九已至秽浇,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間甚负,已是汗流浹背柬焕。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評(píng)論 1 269
  • 我被黑心中介騙來泰國打工审残, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人斑举。 一個(gè)月前我還...
    沈念sama閱讀 47,819評(píng)論 2 370
  • 正文 我出身青樓搅轿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親富玷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子璧坟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,665評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容