函數進階

12章函數進階node

12-1當即執行函數表達式程序員

當即執行的函數表達式的英文全稱爲Immediately Invoked Function Expression,簡稱就爲IIFE。這是一個如它名字所示的那樣,在定義後就會被當即調用的函數。ajax

咱們在調用函數的時候須要加上一對括號,IIFE一樣如此。除此以外,咱們還須要將函數變爲一 個表達式,只須要將整個函數的聲明放進括號裏面就能夠實現。具體的語法以下:編程

(function(){

  //函數體

  })()

接下來咱們來看一個具體的示例:

(function(){

   console.log("Hello");

  })()

// Hello

 

IIFE能夠在執行一個任務的同時,將全部的變量都封裝到函數的做用域裏面,從而保證了全局的 命名空間不會被不少變量名污染。
數組

這裏我能夠舉一個簡單的例子,在之前咱們要交換兩個數的時候,每每須要聲明第三個臨時變量瀏覽器

temp緩存

|注:從ES6開始已經不須要這麼作了,直接使用結構就能夠交換兩個數了安全

let a = 3,b = 5;

let temp = a;

a = b;

b = temp;

console.log(a);//5

console.log(b);//3

console.log(temp);//3

 

這樣雖然咱們的兩個變量被交換了,可是存在一個問題,那就是咱們在全局環境下也存在了一個 temp變量,這就能夠被稱之爲污染了全局環境。因此咱們可使用IIFE來解決該問題,以下:微信

let a = 3,b = 5;

(function(a,b){

let temp = a;

a = b;

b = temp;

})(a,b)

console.log(a);//3

console.log(b);//5

console.log(temp);//報

 

這是一個很是方便的功能,特別是有些時候咱們在初始化一些信息時須要一些變量的幫助,可是 這些變量除了初始化以後就不再會用了,那麼這個時候咱們就能夠考慮使用IIFE來進行初始 化,這樣不會污染到全局環境。閉包

( function(){

let days =["星期天",」星期一","星期二",」星期三",」星期四",」星期五",」星期六"];

let date = new Date();

let today = [date.toLocaleDateString(),days[date.getDay()]];

console.log('今天是${today[0]}, ${today[1]},歡迎你回來! ');

})()

//今天是2017-12-20,星期三,歡迎你回來!

 

這裏咱們只是想要輸出一條歡迎信息,附上當天的日期和星期幾,可是有一個很尷尬的地方在於 上面定義的這些變量咱們都只使用一次,後面就不會再用了,因此這個時候咱們也是能夠考慮使 用IIFE來避免這些無用的變量聲明。

經過IIFE,咱們能夠對咱們的代碼進行分塊。而且塊與塊之間不會互相影響,哪怕有同名的變量 也沒問題,由於IIFE也是函數,在函數內部聲明的變量是一個局部變量,示例以下:

(function(){

//block A

let name = "xiejie";

console.log('my name is ${name}');

})();

( function(){

//block B

let name = "song";

console.log('my name is ${name}');

})();

// my name is xiejie

// my name is song

 

在var流行的時代,JS是沒有塊做用域的。什麼叫作塊做用域呢?目前咱們所知的做用域大概 有兩種:全局做用域和函數做用域。其中,全局做用域是指聲明的變量可在當前環境的任何地方 使用。函數做用域則只能在當前函數所創造的環境中使用。塊級做用域是指每一個代碼塊也能夠有 本身的做用域,好比在if塊中聲明一個變量,就只能在當前代碼塊中使用,外面沒法使用。而 用var聲明的變量是不存在塊級做用域的,因此即便在if塊中用var聲明變量,它也能在外 部的函數或者全局做用域中使用。

function show(valid){

  if(valid){

  var a = 100;

  }

  console.log('a:',a);

}

  show(true); // 輸出a的值爲100

 

這個例子中,a變量是在if塊中聲明,可是它的外部仍然能輸出它的結果。

解決這個問題有兩種方法,第一:使用ES6中的let關鍵字聲明變量,這樣它就有塊級做用域。 第二:使用IIFE,示例以下:

function show(valid){

  if(valid){

    ( function(){

    var a = 100;

  })();

}

console.log('a:',a);
}

show(true); // 報錯:a is not defined

 

固然,只要瀏覽器支持,創建儘可能使用let的方式來聲明變量。

 

12-2變量初始化

12-2-1執行上下文

在ECMAScript中代碼的運行環境分爲如下三種:

・全局級別的代碼:這是默認的代碼運行環境,一旦代碼被載入,JS引擎最早進入的就是這個 環境

・函數級別的代碼:當執行一個函數時,運行函數體中的代碼。

・EvaI級別的代碼:在EvaI函數內運行的代碼。

爲了便於理解,咱們能夠將"執行上下文"粗略的看作是當前代碼的運行環境或者說是做用域。下 面咱們來看一個例子,其中包括了全局以及函數級別的執行上下文,以下:

let one = "Hello";

let test = function(){

  let two = "Lucy",three = "Bill";

  let test2 = function(){

    console.log(one,two);

  }

  let test3 = function(){

    console.log(one,three);

    }

    test2();

    test3();

  }

test();

 

上面這段代碼,自己是沒有什麼意義的,咱們主要是要使用這段代碼來分析一下里面存在多少個 上下文。在上面的代碼中,一共存在4個上下文。一個全局上下文,一個test函數上下文,一個 test2函數上下文和一個test3函數上下文。

經過上面的例子,咱們就能夠得出下面的結論:

・無論什麼狀況下,只存在一個全局的上下文,該上下文能被任何其它的上下文所訪問到。也 就是說,咱們能夠在test的上下文中訪問到全局上下文中的o ne變量,固然在函數test2或者 test3中一樣能夠訪問到該變量。

・至於函數上下文的個數是沒有任何限制的,每到調用執行一個函數時,引擎就會自動新建出 —個函數上下文,換句話說,就是新建一個局部做用域,能夠在該局部做用域中聲明私有變 量等,在外部的上下文中是沒法直接訪問到該局部做用域內的元素的。

在上述例子中,內部的函數能夠訪問到外部上下文中的聲明的變量,反之則行不通。那麼,這到 底是什麼緣由呢?引擎內部是如何處理的呢?這須要瞭解執行上下文堆棧。

執行上下文堆棧

JS引擎的工做方式是單線程的。也就是說,某一個時候只有惟一的一個事件是處於被激活的,其 他的事件都是被放入隊列中,等待被處理的。下面的示例圖就描述了這樣一個堆棧,以下:

 

 

咱們已經知道,當JS代碼文件被JS引擎載入後,默認最早進入的是一個全局的執行上下文。當 在全局上下文中調用執行一個函數時,程序流就進入該被調用函數內,此時引擎就會爲該函數創 建一個新的執行上下文,而且將其壓入到執行上下文堆棧的頂部。

JS引擎老是執行當前在堆棧頂部的上下文,一旦執行完畢,該上下文就會從堆棧頂部被彈出,然 後,進入其下的上下文執行代碼。這樣,堆棧中的上下文就會被依次執行而且彈出堆棧,直到回 到全局的上下文。

來看下面這段代碼,分析其一共有多少個上下文:

(function foo(i){ 

  if(i==3){     return;   }else{     console.log(i);     foo(++i);   } })(0); //全局上下文 //函數上下文0 //函數上下文1 //函數上下文2 //函數上下文

 

上述foo被聲明後,經過0運算符強制直接運行了。函數代碼就是調用了其自身4次,每次是局部 變量i增長1。每次foo函數被自身調用時,就會有一個新的執行上下文被建立。每當一個上下文執 行完畢,該上下文就被彈出堆棧,回到上一個上下文,直到再次回到全局上下文。因此在本段代 碼中一共存在了 5個不一樣的執行上下文。

因而可知,對於執行上下文這個抽象的概念,能夠概括爲如下幾點:

・單線程

•同步執行

・惟一的一個全局上下文

・函數的執行上下文的個數沒有限制

・每次某個函數被調用,就會有個新的執行上下文爲其建立,即便是調用的自身函數,也是如此。

12-2-2函數上下文的創建與激活

咱們如今已經知道,每當咱們調用一個函數時,一個新的執行上下文就會被建立出來。然而,在 js引擎的內部,這個上下文的建立過程具體分爲兩個階段,分別是創建階段和代碼執行階段。這 兩個階段要作的事兒也不同。

創建階段:發生在當調用一個函數,可是在執行函數體內的具體代碼以前

•創建變量對象(arguments對象,形式參數,函數和局部變量)

•初始化做用域鏈

・肯定上下文中this的指向對象

代碼執行階段:發生在具體開始執行函數體內的代碼的時候

・執行函數體內的每一句代碼

咱們將創建階段稱之爲函數上下文的創建,將代碼執行階段稱之爲函數上下文的激活。

變量對象

在上面介紹函數兩個階段中的創建階段時,提到了一個詞,叫作變量對象。這實際上是將整個上下 文看作是一個對象之後獲得的一個詞語。具體來說,咱們能夠將整個函數上下文看作是一個對 象,那麼既然是對象,對象就應該有相應的屬性。對於咱們的執行上下文來講,有以下的三個屬 性:

executionContextObj = {

variableObject : {}, //象,裏面包含arguments象,形式參數,函和局部scopeChain : {},//做用域,包含部上下文全部象的列表 this : {}//上下文中this的指向

}

能夠看到,這裏咱們的執行上下文對象有3個屬性,分別是變量對象,做用域鏈以及this,這裏我 們重點來看一下變量對象裏面所擁有的東西。

在函數的創建階段,首先會創建arguments對象。而後肯定形式參數,檢查固然上下文中的函數 聲明,每找到一個函數聲明,就在variableObject下面用函數名創建一個屬性,屬性值就指向該 函數在內存中的地址的一個引用。若是上述函數名已經存在於variableObject(簡稱V0)下面,那 麼對應的屬性值會被新的引用給覆蓋。最後,是肯定當前上下文中的局部變量,若是遇到和函數 名同名的變量,則會忽略該變量。

好,接下來咱們來經過一個實際的例子來演示函數的這兩個階段以及變量對象是如何變化的。

let foo = function(i){

  var a = "Hello";

  var b = function privateB(){};

  function c(){}

  }

foo(10);

首先在創建階段的變量對象以下:

fooExecutionContext = {

variavleObject : {

ar guments : {0 : 10,length : 1}, // 肯定arguments 對象

i : \0,//肯定形式參數

c : pointe r to function c(),//肯定函數引用

a : undefined,//局部變量 初始值爲undefined

b : undefined //局部變量 初始值爲undefined

},

scopeChain : {},

this : {}

}

 

因而可知,在創建階段,除了arguments,函數的聲明,以及形式參數被賦予了具體的屬性值 外,其它的變量屬性默認的都是undefinedo而且普通形式聲明的函數的提高是在變量的上面 的。

—旦上述創建階段結束,引擎就會進入代碼執行階段,這個階段完成後,上述執行上下文對象如
下,變量會被賦上具體的值。

接下來咱們再經過一段代碼來加深對函數這兩個階段的過程的理解,代碼以下:

(function(){

  console.log(typeof foo);

  console.log(typeof bar);

  var foo = "Hello";

  var bar = function(){

    return "World";

  }

  function foo(){

    return "good";

  }

    console.log(foo,typeof foo);

})()

 

這裏,咱們定義了一個IIFE,該函數在創建階段的變量對象以下:

fooExecutionContext = { variavleObject : { arguments : {length : 0}, foo : pointer to function foo(), bar : undefined

}, scopeChain : {}, this : {}

首先肯定arguments對象,接下來是形式參數,因爲本例中不存在形式參數,因此接下來開始確 定函數的引用,找到foo函數後,建立foo標識符來指向這個foo函數,以後同名的foo變量不會再 被建立,會直接被忽略。而後建立bar變量,不過初始值爲undefined。

創建階段完成以後,接下來進入代碼執行階段,開始一句一句的執行代碼,結果以下:

(function(){

console.log(typeof foo);//function

console.log(typeof bar);//undefined

var foo = "Hello";//foo被從新賦值變成了一個字符串

var bar = function(){

return "World";

}

function foo()

{

return "good";

}

console.log(foo,typeof foo);//Hello string

})()

 

12-2-3做用域鏈

前面在講解函數上下文時,咱們將上下文看作了是一個對象,這個對象有3個屬性,分別是變量 對象,做用域鏈以及this指向。關於this指向咱們以前已經介紹過了,變量對象也在上面作了相關 的介紹,最後咱們就一塊兒來看一下這個做用域鏈。

所謂做用域鏈,就是內部上下文全部變量對象(包括父變量對象)的列表。此鏈主要是用於變量查 詢。

關於做用域鏈,有一^公式做用域鏈(ScopeChain) = AO + [[scope]]

其中A0,簡單來講就是VO, AO全稱爲active object(活動對象),對於當前的上下文來說,通常 將其稱之爲A0,對於不是當前的上下文,通常被稱爲V0

[[scope]]:全部父級變量對象的層級列表(也被稱之爲層級鏈) 舉個例子:

var x = 10;

function foo()

{

var y = 20;

console.log(x + y);

}

foo();//30
這裏,咱們來分析一下VO和AO

//全局

VO : x : 10

foo : pointer to foo()

//foo函數上下文

AO : y : 20

ScopeChain : AO(y) + [[scope]](VO:x)

 

這裏,在全局上下文下的VO就是一個x變量,而在foo函數上下文下面,AO有一個變量y,接下來 是做用域鏈。做用域鏈等於當前上下文的AO加上父級的VO,因此在函數內部雖然沒有變量x,但 是經過做用域鏈咱們找到了父級上下文下面有一個變量x,而後拿來使用。

關於[[scope]],有一個一個很是重要的特性,那就是[[scope]]是在函數建立的時候,就已經被存 儲了,是靜態的。所謂靜態,就是說永遠不會變,函數能夠永遠不被調用,可是[[scope]]在建立 的時候就已經被寫入了,而且存儲在函數做用域鏈對象裏面。咱們來舉一個例子說明,以下:

let food = "rice";
let eat = function(){
console.log('eat ${food}');
};
( function(){
let food = "noodle";
eat();//eat rice
})();

 

這裏的結果爲eat rice,緣由很是簡單。由於對於eat()函數來說,建立的時候它的父級是全局 上下文,因此[[scope]]裏面就存儲了全局上下文的VO,因此food的值爲rice。若是咱們將代碼稍 做修改,改爲以下:

let food = "rice";
( function(){
let food = "noodle";
let eat = function(){
console.log('eat ${food}');
};
eat();//eat noodle
})();

 

那麼這個時候打印出來的值就爲eat noodle。由於對於eat()函數來說,這個時候它的父級爲

IIFE,因此[[scope]]裏面存儲的是IIFE這個函數上下文的VO, food的值爲noodle。

最後,咱們用一個稍微複雜一些的例子來貫穿上面所介紹的做用域鏈。

var x = 10;
function foo()
{
var y = 20; function bar()
{
var z = 30; console.log(x + y + z);
} bar();
}
foo();// 60

 

在這裏,咱們來分析一下變量對象,函數的[[scope]]屬性以及上下文做用域鏈的變化。 首先,剛開始的時候是全局上下文的變量對象:

globalContext.VO = {

x : 10,

foo : pointer to foo()

}

在foo()函數被建立時,此時foo()函數的[[scope]]屬性爲:

foo.[[scope]] = [ globalContext.VO

];

以後,foo()函數會被激活,肯定foo()函數裏面的活動對象:

fooContext.AO = { y : 20, bar : pointer to bar()

}

此時foo()函數上下文裏面的做用域鏈的結構爲:

fooContext.Scope = fooContext.AO + foo.[[scope]] fooContext.Scope = [

fooContext.AO,

globalContext.VO

當內部函數bar ()函數被建立時,其[[scope]]爲:

bar.[[scope]] = [

fooContext.AO, globalContext.VO

];

當bar ()函數被激活,擁有活動對象時,bar()函數的活動對象以下:

barContext.AO = {

z : 30

}

此時bar ()函數上下文的做用域鏈的結構爲:

barContext.Scope = barContext.AO + bar.[[scope]] barContext.Scope = [ barContext.AO, fooContext.AO, globalContext.VO

]

最後總結一下:函數的[[scope]]屬性是在函數建立時就肯定了,而變量對象則是在函數激活時, 也就是說調用函數時纔會肯定。

12-3閉包

對於JavaScript程序員來講,閉包(closure)是一個難懂又必須征服的概念。接下來我將從四個方 面來描述閉包的概念:

・爲何要使用閉包

•什麼是閉包

•閉包的原理

•閉包的做用和使用

12-3-1閉包基本介紹

首先咱們來看看爲何要使用閉包,先看下面這個例子:

var eat = function(){

var food ="雞翅";

console.log(food);

}

eat();  

 

例子中聲明瞭一個名爲eat的函數,並對它進行調用。js引擎會建立一個eat的執行上下文,其中 聲明food變量並賦值。當該方法執行完後,上下文被銷燬,food變量也會跟着消失。這是由於 food變量屬於eat函數的局部變量,它做用於eat函數中,會隨着eat的執行上下文建立而建立,銷 毀而銷燬。

再看下面這個例子:

var eat = function(){

var food ="雞翅"return function(){

console.log(food);

}

}

var look = eat();

look();

look();

 

在這個例子中,eat函數返回一個函數,並在這個內部函數中訪問food這個局部變量。調用eat函 數並將結果賦給look變量,這個look指向了 eat函數中的內部函數,而後調用它,最終輸出food的 值。按照以前的說法,這個food變量應該當eat函數調用完後就銷燬,後續爲何還能經過調用
look方法訪問到這個變量呢?這是由於閉包起了做用。返回的內部函數和它外部的變量food實際 上就是一個閉包。咱們不由想問,爲何它們稱爲閉包?閉包又能作什麼呢?

咱們先來看看閉包的概念:

閉包是指引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即便離開了創造它的環境也不例外。這裏提到了自由變量,它又是什麼呢?

自由變量能夠理解成跨做用域的變量,好比子做用域訪問父做用域的變量。

這些概念提及來太過於專業,下面我用一個生活中的例子來解釋它。假如小王、小強是某某學校 計算機專業的學生。某一天他們的導師在外面接了一個項目,而後爲此成立一個項目組,拉了他 兩人加入。他們的這個項目組既在學校中創建,但又能夠獨立於學校存在。好比,小王和小強從 學校畢業了,他們的項目組仍然能夠繼續存在。因此,咱們能夠認爲他們組建的這個項目組就是 閉包。下面我把這個例子寫成了代碼:

var school = function(){

var si ="小強";

var s2 ="小王"var team = function(project){

console.log(s1 + s2 + project);

}

return team;

}

var team = school();

team(「作電商項且「);//小強、小王作電商項目

team(「作微信項且「);//小強、小王作微信項目

 

變量si和s2屬於school函數的局部變量,並被內部函數team使用,同時該函數也被返回出去。 在外部,經過調用scho ol獲得了內部函數的引用,後面屢次調用這個內部函數,仍然可以訪問到 si和s2變量。這樣si和s2做爲自由變量被tea m函數引用,即便創造它們的函數school執行完 了,這些變量依然存在,所以,這就是閉包。

看到這裏,咱們須要問本身兩個問題:

首先,爲何函數內部能夠訪問外部函數的變量?

這個問題其實很好解釋,在變量初始化那節中提到過,當一個函數在建立時其內部會產生一個 scope屬性,該屬性指向建立該函數的執行上下文中的做用域鏈對象。這句話提及來比較繞口, 看看下面這張圖:

 

 

其中做用域鏈對象包含了該上下文中的VO/AO對象,還有scope對象,好比schoo I上下文中的做 用域鏈對象就像這樣:

school.scopeChain = {

VO:{

si:"小強",

s2:"小王"

},

scope:[[scopeChain]]

}

接下來,我們就來回答這個問題。當內部函數中找不到對應的變量,它就會到scope指向的對象 中找。該對象保存着外部上下文中的做用域鏈對象,從該做用域鏈中就能找到對應的變量。這就 是爲何函數內部能夠訪問到外部函數變量的緣由。下面我們咱來看看第二個問題:

爲何當外部函數的上下文執行完之後,其中的局部變量仍是能經過閉包訪問到呢?

其實用上一個問題的答案再延伸一下,這個問題的答案就出來了。你想一想,外部函數的上下文即 使結束了,但內部的函數只要不銷燬(被外部引用了,就不會銷燬),它當中的scope就會一直 引用着剛纔上下文的做用域鏈對象,那麼包含在做用域鏈中的變量也就能夠一直被訪問到。

把這個理解了,閉包的原理也就明白了。

按照這種說法,在JS中每一個函數都有scope對象,而且都會保存外部上下文的做用域鏈對象。也 就是說,任什麼時候候外部上下文銷燬了,只要內部函數還在都能訪問到外部的變量。那豈不是任何 函數均可以稱爲閉包了嗎?事實上確實是這樣的,從廣義上講,JS的函數均可以稱爲閉包(由於 它們能訪問外部變量)。但咱們這裏要講的是狹義上的閉包,這樣閉包對於實際應用來說纔會有意

義。

狹義的閉包必須知足兩個條件:

・造成閉包環境的函數可以被外部變量引用,這樣就算它外部上下文銷燬,它依然存在。

・在內部函數中要訪問外部函數的局部變量。

後面我提到的閉包都是指要知足這兩個條件。下面咱們來看看閉包有哪些優缺點,先來看看優 點:

・經過閉包可讓外部環境訪問到函數內部的局部變量。

・經過閉包可讓局部變量持續保存下來,不隨着它的上下文環境一塊兒銷燬。看下面這個例 子:

let count = 0; //全局變量

let compute = function(){ //將計數器加 1

count++; console.log(count);

}

for( let i = 0 ;i < 100;i++){ compute(); // 循環100 次

}

這個例子是對一個全局變量進行加1的操做,一共加100次,獲得值爲100的結果。下面用閉包的 方式重構它:

var compute = function(){

var count = 0; //局部變量

return function(){

count++; //內部函數訪問外部變量

console.log(count);

}

}

var func = compute(); //引用了內部函數,造成閉包

for( var i = 0 ;i < 100;i++){

func();

}

 

這個例子就再也不使用全局變量,其中count這個局部變量依然能夠被保存下來。

下面來看看閉包的缺點: 其實閉包自己並無什麼明顯的缺點。但每每人們對閉包有種誤解:說閉包會將局部變量保存下
來,若是大量使用閉包,而其中的變量又未獲得清理,可能會形成內存泄漏。因此要儘可能減小閉 包的使用。

局部變量原本應該在函數退出時被解除引用,但若是局部變量被封閉在閉包造成的環境中,那麼 這個局部變量就能一直生存下去。從這個角度來看,閉包的確會使一些數據沒法被及時銷燬。使 用閉包的一部分緣由是咱們選擇主動把一些變量封閉在閉包中,由於可能在之後還須要使用這些 變量。把這些變量放在閉包中和放在全局做用域中,對內存方面的影響是同樣的,因此這裏並不 能說成是內存泄漏。若是在未來須要回收這些變量,咱們能夠手動把這些變量設置爲n ull。

若是非要說閉包和內存泄漏有關係的地方,那就是使用閉包的同時比較容易造成循環引用,若是 閉包的做用域中保存着一些DOM節點,這個時候就有可能形成內存泄漏。但這自己並不是閉包的 問題,也並不是JavaScript的問題。在IE瀏覽器中,因爲BOM和DOM中的對象是使用C++以COM 對象的方式實現的,而COM對象的垃圾收集機制採用的是引用計數策略。在基於引用計數策略 的垃圾回收機制中,若是兩個對象之間造成了循環引用,那麼這兩個對象都沒法被回收,但循環 引用形成的內存泄漏在本質上也不是閉包形成的。

一樣,若是要解決循環引用帶來的內存泄漏問題,咱們只須要把循環引用中的變量設爲n ull即 可。將變量設置爲n ull意味着切斷變量與它此前引用的值之間的鏈接。當垃圾收集器下次運行 時,就會刪除這些值並回收它們佔用的內存。

接下來咱們看看到底何時會用到閉包。好比咱們常常會使用時間函數對某一個變量進行操 做,看這個例子:

let a = 100;

setTimeout(function(){

console.log(++a);

},1000);

 

這個例子用到了時間函數setTimeout,並在等待1秒鐘後對變量a進行加1的操做。這是一個閉 包,由於setTimeout中的匿名函數對外部變量進行訪問,而後該函數又被setTimeout方法引用。 知足了造成閉包的兩個條件。因此你看,即便外部上下文結束了,1秒後仍然能對變量a進行加1 操做。

在DOM的事件操做中,也常常用到閉包,好比下面這個例子:

<input id="count" type="button" value="計數"<script>

(function(){

var cnt = 0;

var count = document.getElementById("count");

count.onclick = function(){

console.log(++cnt);
})()

</script>

 

onclick指向的函數中訪問了外部變量ent,同時該函數又被o nclick事件引用了,知足兩個條件, 是閉包。因此當外部上下文結束後,你繼續點擊按鈕,在觸發的事件處理方法中仍然能訪問到變 量 ent。

在有些時候閉包還會引發一些奇怪的問題,好比下面這個例子:

for(var i = 1;i <= 3;i++){

setTimeout(function(){

console.log(i);

},1000);

}

 

過1秒後分別輸出i變量的值爲1,2,3。可是,執行的結果是:4,4,4。實際上,問題就出在閉包身 上。你看,循環中的setTimeout訪問了它的外部變量i,造成閉包。而i變量只有1個,因此循環3 次的setTimeout中都訪問的是同一個變量。循環到第4次,i變量增長到4,不知足循環條件,循 環結束,代碼執行完後上下文結束。可是,那3個setTimeout等1秒鐘後才執行,因爲閉包的原 因,因此它們仍然能訪問到變量i,不過此時i變量值已是4了。

既然是閉包引發的問題,那麼解決的方法就是去掉閉包。咱們知道造成閉包有兩個條件,只要不 知足其一,那就再也不是閉包。條件之一,內部函數被外部引用,這個咱們沒辦法去掉。條件二, 內部函數訪問外部變量。這個條件咱們有辦法去掉,好比:

for(var i = 1;i <= 3;i++){

(function(index){

setTimeout(function(){

console.log(index);

},1000);

})(i)

}

 

這樣setTimeout中就能夠不用訪問for循環聲明的變量i了。而是採用調用函數傳參的方式把變量i 的值傳給了 setTimeout,這樣它們就再也不造成閉包。也就是說setTimeout中訪問的已經不是外部 的變量i,因此即便i的值增加到4,跟它內部也不要緊,最後達到了咱們想要的效果。

固然,解決這個問題還有個更簡單的方法,就是使用ES6中的let關鍵字。它聲明的變量有塊做用 域,若是將它放在循環中,那麼每次循環都會有一個新的變量i,這樣即便有閉包也沒問題,由於 每一個閉包保存的都是不一樣的i變量,那麼剛纔的問題也就迎刃而解。

for(let i = 1;i <= 3;i++){
setTimeout(function(){ console.log(i);

},1000);

}

 

12-3-2閉包的更多做用

  1. 1.      封裝變量

閉包能夠幫助把一些不須要暴露在全局的變量封裝成"私有變量"。假設有一個計算乘積的簡單函 數:

//入的字的乘

let mult = function(){

let a = 1;

for( let i=0;i<arguments.length;i++)

{

a *= arguments[i];

}

return a;

}

console.log(mult(1,2,3)); // 6

 

mult函數接受一些number類型的參數,並返回這些參數的乘積。如今咱們以爲對於那些相同的 參數來講,每次都進行計算是一種浪費,咱們能夠加入緩存機制來提升這個函數的性能,以下:

 

//象,用於對計
let cache = {}; let mult = function(){ //將傳入的數字組成字符串做爲緩存對象的鍵 let args = Array.prototype.join.call(arguments,','); if(cache[args]) { return cache[args]; } let a = 1; for( let i=0;i<arguments.length;i++) { a *= arguments[i]; } return cache[args] = a; } console.log(mult(1,2,3)); // 6 console.log(mult(1,2,3)); // 6

 

咱們看到,cache這個變量僅僅是在mult函數中被使用,與其讓cache變量跟mult函數一塊兒平行地 暴露在全局做用域下,不如把它封閉在mult函數內部,這樣能夠減小頁面中的全局變量,以免 這個變量在其餘對方被不當心修改而引起錯誤。代碼以下:

let mult = (function(){

//緩存對象,用於結果進行緩存

let cache = {};

return function(){

let args = Array.prototype.join.call(arguments,',');

if(cache[args])

{

return cache[args];

}

let a = 1;

for( let i=0;i<arguments.length;i++)

{

a *= arguments[i];

}

return cache[args] = a;

}

})();

console.log(mult(1,2,3)); // 6

console.log(mult(1,2,3)); // 6

 

提煉函數是代碼重構中的一種常見技巧。若是在一個大函數中有一些代碼塊可以獨立出來,那麼 咱們經常把這些代碼塊封裝在獨立的小函數裏面。獨立出來的小函數有助於代碼的複用,若是這 些小函數有一個良好的命名,它們自己也起到了註釋的做用。若是這些小函數不須要在程序的其 他地方使用,那麼最好是把它們用閉包封閉起來。重構後的代碼以下:

let mult = (function(){

//緩存對象,用於結果進行緩存

let cache = {};

//計算乘積函數和cache—樣,該函數一樣被閉包封閉了起來

let calc = function(){

let a = 1;

for( let i=0;i<arguments.length;i++)

{

a *= arguments[i];

}

return a;

}

return function(){

let args = Array.prototype.join.call(arguments,',');

if(cache[args])
{

return cache[args];

}

return cache[args] = calc.apply(null,arguments);

}

})();

console.log(mult(1,2,3)); // 6

console.log(mult(1,2,3)); // 6

 

  1. 2.      延續局部變量的壽命

img對象常常用於進行數據上報,以下所示:

let report = function(src){

  let img = new Image();

  img.src = src;

  }

report('http://xxx.com/getUserInfo');

 

可是經過查詢後臺的記錄咱們得知,由於一些低版本的瀏覽器的實現存在bug,在這些瀏覽器下 使用report函數進行數據上報時會丟失30%左右的數據,也就是說‘report函數並非每一次都 成功發起了 HTTP請求。丟失數據的緣由是img是report函數中的局部變量,當report函數在調用 結束後,img局部變量隨即被銷燬,而此時或許還沒來得及發出HTTP請求,因此這次請求就會丟 失掉。

如今咱們把img變量用閉包封閉起來,便能解決請求丟失的問題,以下:

let report = (function(){

  let imgs = [];

  return function(src){

    let img = new Image();

    imgs.push(img);

    img.src = src;

  }

})();

 

12-3-3閉包和麪向對象設計

過程與數據的結合是形容面向對象中的"對象"時常用的表達。對象以屬性的形式包含了數 據,以方法的形式包含了過程。而閉包則是在過程當中以環境的形式包含了數據。一般用面向對象 思想能實現的功能,用閉包也可以實現,反之亦然。

在JavaScript語言的祖先Scheme語言中,甚至都沒有提供面向對象的原生設計,但卻可使用 閉包來實現一個完整的面向對象的系統。下面咱們來看看這段跟閉包相關的代碼:

let Test = function(){

  let value = 0; return {

    call : function(){

      value++; console.log(value);

    }

  }

}

let test = new Test();

test.call();// 1

test.call(); // 2

test.call(); // 3

 

若是換成面向對象的寫法,那就是以下:

let test = {

  value : 0, call : function(){

    this.value++;

    console.log(this.value);

    }

  }

test.call(); // 1

test.call(); // 2

test.call(); // 3

 

或者

let Test = function(){

  this.value = 0;

  }

  Test.prototype.call = function(){

    this.value++;

    console.log(this.value);

  }

let test = new Test();

test.call(); // 1

test.call(); // 2

test.call(); // 3

 

12-4遞歸函數

遞歸函數是一個一直直接或者間接調用它本身自己,直到知足某個條件纔會退出的函數。當須要 設計到迭代過程時,這是一個頗有用的工具。下面咱們以計算階乘來進行示例:

let numCalc = function(i){

  if(i == 1){

    return 1;

  }else{

    return i * numCalc(i-1);

  }

}

console.log(numCalc(4));//24

 

這裏,咱們要計算4的階乘,那麼咱們能夠看做是4乘以3的階乘。而3的階乘又能夠看做是3乘以 2的階乘,以此類推。

下面是幾個常見的遞歸函數的例子,經過下面的例子能夠幫助咱們加深對遞歸函數的理解

1•使用遞歸計算從m加到n

let numCalc = function (m, n) {

  if (m === n) {

    return m;

  }else {

    return n + numCalc(m, m > n ? n + 1 : n - 1);

  }

}

console.log(numCalc(100, 1));//5050

 

2•使用遞歸計算出某一位的斐波那契數

let numCalc = function (i) {

  if (i == 1) {

    return 0;

  }else if (i == 2) {

    return 1;
  else {

    return numCalc(i - 1) + numCalc(i - 2); }

}

console.log(numCalc(8));//13

 

3•使用遞歸打印出多維數組裏面的每個數字

let arr = [1, 2, [3, 4, [5, 6], 7, 8], 9, 10];

   let test = function (arr) {

  for (let i = 0; i < arr.length; i++) {

     if (typeof arr[i] == 'object') {

      test(arr[i]);

  }else {

    console.log(arr[i]);

    }

  }

};

test(arr);

12-5高階函數

在本小結中,咱們將向你們介紹J avaScript中函數的高階用法。

12-5-1高階函數介紹

高階函數(higher-order-function)指的是操做函數的函數,通常有如下兩種狀況:

・函數能夠做爲參數被傳遞

・函數能夠做爲返回值輸出

JavaScript中的函數顯然知足高階函數的條件,在實際開發中,不管是將函數看成參數傳遞,還 是讓函數的執行結果返回另一個函數,這兩種情形都有不少應用場景。下面將對這兩種狀況進 行詳細介紹

12-5-2參數傳遞

把函數看成參數傳遞,表明能夠抽離出一部分容易變化的業務邏輯,把這部分業務邏輯放在函數 參數中,這樣一來能夠分離業務代碼中變化與不變的部分。其中一個常見的應用場景就是回調函 數。

1.回調函數

前面不管是在介紹函數基礎的時候,仍是在介紹異步編程的時候,咱們都有接觸過回調函數。回 調函數,就是典型的將函數做爲參數來進行傳遞。

在Ajax異步請求的應用中,回調函數的使用很是頻繁。想在Ajax請求返回以後作一些事情,但又 並不知道請求返回的確切時間時,最多見的方案就是把回調函數看成參數傳入發起Ajax請求的方 法中,待請求完成以後執行回調函數。俄們將在14章詳細介紹Ajax技術)

示例代碼以下:

let getUserInfo = function (userId, callback) {

  $.ajax('http://xx.com/getUserInfo?' + userId, function (data) {

    if (typeof callback === 'function') {

      callback(data);

    }

  });

}

getUserInfo(123, function (data) {

  alert(data.userName);
});

 

回調函數的應用不只只在異步請求中,當一個函數不適合執行一些請求時,也能夠把這些請求封 裝成一個函數,並把它做爲參數傳遞給另一個函數,"委託"給另一個函數來執行。好比,想 在頁面中建立100個div節點,而後把這些div節點都設置爲隱藏。下面是一種編寫代碼的方式:

let appendDiv = function () {

   for (var i = 0; i < 100; i++){

    var div = document.createElement('div');

     div.innerHTML = i;

    document.body.appendChild(div);

    div.style.display = 'none';

  }

};

appendDiv();

 

把div.style.display = 'none'的邏輯硬編碼在appendDiv裏顯然是不合理

的,appendDiv未免有點個性化,成爲了一個難以複用的函數,並非每一個人建立了節點以後 就但願它們馬上被隱藏,因而把div.style.display = 'none'這行代碼抽出來,用回調函數 的形式傳入appendDiv()方法

let appendDiv = function (callback) {

  for ( let i = 0; i < 100; i++){

    let div = document.createElement('div');

    div.innerHTML = i;

    document.body.appendChild(div);

    if (typeof callback === 'function'){

      callback(div);

    }

  }

};

appendDiv(function (node) {

  node.style.display = 'none';

});

 

能夠看到,隱藏節點的請求其實是由客戶發起的,可是客戶並不知道節點何時會建立好, 因而把隱藏節點的邏輯放在回調函數中,"委託"給appendDiv()方法。appendDiv()方法固然 知道節點何時建立好,因此在節點建立好的時候,appendDiv()會執行以前客戶傳入的回 調函數。

2.數組排序

前面在介紹數組排序時有介紹過sor t()方法,該方法就接收一個函數做爲參數。sort()方法 封裝了數組元素的排序方法。從sor t()方法的使用能夠看到,咱們的目的是對數組進行排序, 這是不變的部分;而使用什麼規則去排序,則是可變的部分。把可變的部分封裝在函數參數裏, 動態傳入sor t()方法,使sor t()方法方法成爲了一個很是靈活的方法。咱們這裏來複習一 下:

//小到大排列,出:[1, 3, 4 ]

[1, 4, 3].sort(function (a, b) {

  return a - b;

});

//從大到小排列,輸出:[4, 3, 1 ]

[1, 4, 3].sort(function (a, b) {

return b - a;

});

 

除了這個sort()方法之外,還有諸如for Each() , map() , eve ry() , some()等函數,也 是常見的回調函數。這些函數在前面都已經介紹過了,這裏再也不花篇幅進行介紹。

12-5-3返回值輸出

相比把函數看成參數傳遞,函數看成返回值輸出的應用場景也許更多。讓函數繼續返回一個可執 行的函數,意味着運算過程是可延續的。

1.判斷數據的類型

咱們來看下面的例子,判斷一個數據是否爲數組。在以往的實現中,能夠判斷這個數據有沒 有length屬性,有沒有sort方法或者slice方法等。可是更好的方法是使

用 Object,prototype.toString 來計算。Object.prototype.toString.call(obj)返回一^個 字符串,好比 Object.p rototype.toSt ri ng.call([1,2,3])老是返回"[object Array]", 而 Object.prototype.toString.call("str")老是返回"[object String]"。下面是使 用Object,prototype.toString.call()方法來判斷數據類型的一系列的isType函數。

let isString = function (obj) {

  return Object.prototype.toString.call(obj) === '[object String]';

};

let isArray = function (obj) {

  return Object.prototype.toString.call(obj) === '[object Array]';

};

let isNumber = function (obj) {
  return Object.prototype.toString.call(obj) === '[object Number]';

};

 

咱們發現,實際上這些函數的大部分實現都是相同的,不一樣的只

是Object.p rototype.toSt ring.call(obj)返回的字符串。爲了不多餘的代碼,咱們嘗試把 這些字符串做爲參數提早傳入isType函數。代碼以下:

let isType = function (type) {

  return function (obj) {

    return Object.prototype.toString.call(obj) === '[object ' + type + ]';

  }

};

let isString = isType('String');

let isArray = isType('Array');

let isNumber = isType('Number');

console.log(isArray([1, 2,引));// 輸出:true

 

固然,還能夠用循環語句,來批量註冊這些isType函數:

let Type = {};

for (let i = 0, type;type = ['String', 'Array', 'Number'][i++];){

( function (type) {

Type['is' + type] = function (obj) { return Object.prototype.toString.call(obj) === '[object ' + type +

]';

}

})(type)

}

console.log(Type.isArray([]));                     // 輸出:true

console.log(Type.isString("str")); // 輸出:true

 

2. getSingle

下面的例子是一個單例模式的例子。

|注:單例模式的定義是:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。

<body>

<script>

let getSingle = function(fn){

  let ret;
  return function(){

    return ret || (ret = fn.apply(this.arguments));

    }

}

let createDiv = getSingle(function(){

  return document.createElement('div');

});

let div1 = createDiv();

let div2 = createDiv();

console.log(div1 === div2); // true

</script>

</body>

 

在這個高階函數的例子中,既把函數看成參數傳遞,又讓函數執行後返回了另外一個函數。

12-5-4面向切面編程

AOP(面向切面編程)的主要做用是把一些跟核心業務邏輯模塊無關的功能抽離出來,這些跟業務 邏輯無關的功能一般包括日誌統計、安全控制、異常處理等。把這些功能抽離出來以後,再通 過"動態織入"的方式摻入業務邏輯模塊中。這樣作的好處首先是能夠保持業務邏輯模塊的純淨和 高內聚性,其次是能夠很方便地複用日誌統計等功能模塊

在Java語言中,能夠經過反射和動態代理機制來實現AOP技術。而在JavaScript這種動態語言 中,AOP的實現更加簡單,這是JavaScript與生俱來的能力。一般,在JavaScript中實現AOP, 都是指把一個函數"動態織入"到另一個函數之中。下面經過擴展Function.p rototype來實 現,示例以下:

Function.prototype.before = function (beforefn) {

  let _self = this;                 //保存原函數的引用

    return function () {                  //返回包含了原函數和新函數的"代理"函數

      beforefn.apply(this, arguments);                     // 先執行新函數,修正this

        return _self.apply(this,arguments); // 再執行原函數

      }

    };

    Function.prototype.after = function (afterfn) {

      let _self = this;

      return function () {

        let ret = _self.apply(this, ar guments); //先執行原函數

        afte rfn.apply(this, ar guments); //再執行新函數return ret;

    }
  
  };

let func = function () {

  console.log(2);
};

func = func.before(function () { console.log(1);

}).after(function () { console.log(3);

});

func();

// 1

// 2

// 3

 

把負責打印數字1和打印數字3的兩個函數經過AOP的方式動態植入func()函數。經過執行上面 的代碼,控制檯順利地返回了執行結果一、二、3。

相關文章
相關標籤/搜索