成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

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

ssshooter / 977人閱讀

摘要:而外層的函數(shù)不能訪問內(nèi)層的變量或函數(shù),這樣的層層嵌套就形成了作用域鏈。閉包閉包是指有權(quán)訪問另一個(gè)函數(shù)作用域中的變量的函數(shù),創(chuàng)建閉包的最常見的方式就是在一個(gè)函數(shù)內(nèi)創(chuàng)建另一個(gè)函數(shù),通過另一個(gè)函數(shù)訪問這個(gè)函數(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í)行函的返回值。

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è)方式很糟糕,帶來性能下降和安全風(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

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

這是一個(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è)必要條件

函數(shù)(作用域)嵌套函數(shù)

函數(shù)(作用域)內(nèi)部可以引用外部的參數(shù)和變量

參數(shù)和變量不會(huì)被垃圾回收機(jī)制回收??梢圆榭? 內(nèi)存管理與垃圾回收

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

優(yōu)點(diǎn)

希望一個(gè)變量長期駐扎在內(nèi)存中(如同c++中static局部變量)

避免全局變量的污染

私有成員的存在

缺點(diǎn)

閉包常駐內(nèi)存,會(huì)增大內(nèi)存使用量,大量使用影響程序性能。

使用不當(dāng)很容易造成內(nèi)存泄露(關(guān)于內(nèi)存管理和垃圾回收的細(xì)節(jié)以后會(huì)專門講一篇的)。

一般函數(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)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/97461.html

相關(guān)文章

  • 前端基礎(chǔ)進(jìn)階(四):詳細(xì)圖解作用鏈與閉包

    摘要:之前一篇文章我們?cè)敿?xì)說明了變量對(duì)象,而這里,我們將詳細(xì)說明作用域鏈。而的作用域鏈,則同時(shí)包含了這三個(gè)變量對(duì)象,所以的執(zhí)行上下文可如下表示。下圖展示了閉包的作用域鏈。其中為當(dāng)前的函數(shù)調(diào)用棧,為當(dāng)前正在被執(zhí)行的函數(shù)的作用域鏈,為當(dāng)前的局部變量。 showImg(https://segmentfault.com/img/remote/1460000008329355);初學(xué)JavaScrip...

    aikin 評(píng)論0 收藏0
  • JavaScript作用鏈與閉包的理解

    摘要:作用域是最重要的概念之一,想要學(xué)好就需要理解作用域和作用域鏈的工作原理。腳本錯(cuò)誤腳本錯(cuò)誤由此可以引發(fā)作用域鏈的概念在中,函數(shù)也是對(duì)象,實(shí)際上,里一切都是對(duì)象。當(dāng)一個(gè)函數(shù)創(chuàng)建后,它的作用域鏈會(huì)被創(chuàng)建此函數(shù)的作用域中可訪問的數(shù)據(jù)對(duì)象填充。 作用域是JavaScript最重要的概念之一,想要學(xué)好JavaScript就需要理解JavaScript作用域和作用域 鏈的工作原理。 1. 全局作...

    darcrand 評(píng)論0 收藏0
  • JavaScript作用鏈與閉包的理解

    摘要:作用域是最重要的概念之一,想要學(xué)好就需要理解作用域和作用域鏈的工作原理。腳本錯(cuò)誤腳本錯(cuò)誤由此可以引發(fā)作用域鏈的概念在中,函數(shù)也是對(duì)象,實(shí)際上,里一切都是對(duì)象。當(dāng)一個(gè)函數(shù)創(chuàng)建后,它的作用域鏈會(huì)被創(chuàng)建此函數(shù)的作用域中可訪問的數(shù)據(jù)對(duì)象填充。 作用域是JavaScript最重要的概念之一,想要學(xué)好JavaScript就需要理解JavaScript作用域和作用域 鏈的工作原理。 1. 全局作...

    BaronZhang 評(píng)論0 收藏0
  • JavaScript作用鏈與閉包的理解

    摘要:作用域是最重要的概念之一,想要學(xué)好就需要理解作用域和作用域鏈的工作原理。腳本錯(cuò)誤腳本錯(cuò)誤由此可以引發(fā)作用域鏈的概念在中,函數(shù)也是對(duì)象,實(shí)際上,里一切都是對(duì)象。當(dāng)一個(gè)函數(shù)創(chuàng)建后,它的作用域鏈會(huì)被創(chuàng)建此函數(shù)的作用域中可訪問的數(shù)據(jù)對(duì)象填充。 作用域是JavaScript最重要的概念之一,想要學(xué)好JavaScript就需要理解JavaScript作用域和作用域 鏈的工作原理。 1. 全局作...

    tianhang 評(píng)論0 收藏0
  • 前端基礎(chǔ)進(jìn)階(六):在chrome開發(fā)者工具中觀察函數(shù)調(diào)用棧、作用鏈與閉包

    摘要:在的開發(fā)者工具中,通過斷點(diǎn)調(diào)試,我們能夠非常方便的一步一步的觀察的執(zhí)行過程,直觀感知函數(shù)調(diào)用棧,作用域鏈,變量對(duì)象,閉包,等關(guān)鍵信息的變化。其中表示當(dāng)前的局部變量對(duì)象,表示當(dāng)前作用域鏈中的閉包。 showImg(https://segmentfault.com/img/remote/1460000008404321); 在前端開發(fā)中,有一個(gè)非常重要的技能,叫做斷點(diǎn)調(diào)試。 在chrome...

    draveness 評(píng)論0 收藏0
  • “動(dòng)靜結(jié)合” 小白初探靜態(tài)(詞法)作用域,作用鏈與執(zhí)行環(huán)境(EC)

    摘要:圖片中的作用域鏈,是全局執(zhí)行環(huán)境中的作用域鏈。然后此活動(dòng)對(duì)象被推入作用域鏈的最前端。在最后調(diào)用的時(shí)候,創(chuàng)建先構(gòu)建作用域鏈,再創(chuàng)建執(zhí)行環(huán)境,再創(chuàng)建執(zhí)行環(huán)境的時(shí)候發(fā)現(xiàn)了一個(gè)變量標(biāo)識(shí)符。 從圖書館翻過各種JS的書之后,對(duì)作用域/執(zhí)行環(huán)境/閉包這些概念有了一個(gè)比較清晰的認(rèn)識(shí)。 栗子說明一切 第一個(gè)栗子 來看一個(gè)來自ECMA-262的栗子: var x = 10; (function foo(...

    Drummor 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<