JavaScript函數(shù)、this以及閉包

JavaScript筆記(三)

函數(shù)

理解函數(shù)

Javascript函數(shù)的參數(shù)與大多數(shù)其他語言中的函數(shù)的參數(shù)不同。Javascript函數(shù)不介意傳遞進(jìn)來多少個參數(shù),也不介意傳遞進(jìn)來的參數(shù)是什么數(shù)據(jù)類型。
之所以會這樣,原因是Javascript中的參數(shù)在內(nèi)部是用一個數(shù)組來表示的。函數(shù)接收到的始終是這個數(shù)組,而不關(guān)心這個數(shù)組包含多少參數(shù)。
實際上,在函數(shù)體內(nèi)可以通過arguments對象來訪問這個參數(shù)數(shù)組,從而獲取傳遞給函數(shù)的每一個參數(shù)。
其實arguments對象只是與數(shù)組類似(它并不是Array的實例),它可以使用方括號來訪問它的每一個元素(第一個元素是arguments[0],第二個元素是arguments[1],以此類推),使用length屬性來確定傳進(jìn)來多少個參數(shù)。

可以利用這一點讓函數(shù)能夠接受任意個參數(shù)并分別實現(xiàn)適當(dāng)?shù)墓δ堋?/p>

所以,可以得到另一個結(jié)論:js的函數(shù)沒有重載

函數(shù)聲明與函數(shù)表達(dá)式

有兩種寫法:

函數(shù)聲明:

function add(a, b) {
    return a + b;
}

函數(shù)表達(dá)式:

var add = function(a, b) {
    return a + b;
}

實際上,解析器在向執(zhí)行環(huán)境中加載數(shù)據(jù)時,對函數(shù)聲明和函數(shù)表達(dá)式并非一視同仁。解析器會率先讀取函數(shù)聲明,并使其在執(zhí)行任何代碼之前可用(可以訪問),這稱為一種函數(shù)聲明提前的過程;至于函數(shù)表達(dá)式,則必須等到解析器執(zhí)行到它所在的代碼行,才會真正被解釋執(zhí)行。

函數(shù)作參數(shù)傳遞

function也是一種object。所以可以作為普通對象進(jìn)行傳遞。
要訪問函數(shù)的指針而不執(zhí)行函數(shù)的話,必須去掉函數(shù)后面那對圓括號。
也可以作為返回值,return一個function即可。

function add(num){
   return num + 10;
}

function greeting(f,num){
    console.log(f(num))
}

greeting(add,10);
20

arguments

在函數(shù)內(nèi)部有兩個特殊對象分別是arguments和this。
arguments前面已經(jīng)提到過,它是一個類數(shù)組的對象,包含著傳入函數(shù)中的所有參數(shù)。雖然arguments的主要用途是保存函數(shù)參數(shù),但是這個對象還有一個名叫callee的屬性,該屬性是一個指針,指向擁有這個arguments對象的函數(shù),請看下面非常經(jīng)典的階乘函數(shù)。

function factorial(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * factorial(num - 1);
    }
}

定義階乘函數(shù)一般都要用到遞歸算法;如上面代碼所示,在函數(shù)有名字,而且名字以后也不會變的情況下,這樣定義沒有問題。但是問題是這個函數(shù)的執(zhí)行與函數(shù)名factorial緊緊耦合在了一起。為了消除這種緊密耦合的現(xiàn)象,可以像下面這樣使用arguments.callee。

function factorial(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * arguments.callee(num - 1);
    }
}

在這個重寫后的factorial()函數(shù)體內(nèi),沒有再引用函數(shù)名factorial。這樣,無論引用函數(shù)時使用的是什么名字,都可以保證正常完成遞歸調(diào)用。例如:

var trueFactorial = factorial;
factorial = function() {
    return 0;
}

alert(trueFactorial(5));   // 120
alert(factorial(5));        // 0

在此,變量trueFactorial獲得了factorial的值,實際上是在另一個位置上保存了一個函數(shù)的指針。然后,我們又將一個返回了0的函數(shù)賦值給factorial變量。如果將原來的factorial()那樣不使用arguments.callee,調(diào)用trueFactorial會返回0.可是,在解除了函數(shù)體內(nèi)的代碼與函數(shù)名的耦合狀態(tài)后,trueFactorial()仍能正常地計算階乘;至于factorial(),它現(xiàn)在只是個返回0的函數(shù)。

this

在JavaScript中,函數(shù)的this關(guān)鍵字的行為與其他語言相比有很多不同。在絕大多數(shù)情況下,函數(shù)的調(diào)用方式?jīng)Q定了this的值。this不能在執(zhí)行期間被賦值,在每次函數(shù)被調(diào)用時this的值也可能會不同。ES5引入了bind方法來設(shè)置函數(shù)的this值,而不用考慮函數(shù)如何被調(diào)用的。

  • 全局上下文
    在全局運行上下文中(在任何函數(shù)體外部),this 指代全局對象

      console.log(this.document === document); // true
    
      // 在瀏覽器中,全局對象為 window 對象:
      console.log(this === window); // true
      
      this.a = 37;
      console.log(window.a); // 37
    
  • 函數(shù)上下文
    在函數(shù)內(nèi)部,this的值取決于函數(shù)是如何調(diào)用的。
    直接調(diào)用

      function f1(){
        return this;
      }
      
      f1() === window; // true
    

    在上面的例子中,this的值不是由函數(shù)調(diào)用設(shè)定。this 的值總是一個對象且默認(rèn)為全局對象。

  • 對象方法中的 this
    當(dāng)以對象里的方法的方式調(diào)用函數(shù)時,它們的 this 是調(diào)用該函數(shù)的對象. 下面的例子中,當(dāng) o.f() 被調(diào)用時,函數(shù)內(nèi)的this將綁定到o對象。

      var o = {
        prop: 37,
        f: function() {
          return this.prop;
        }
      };
      
      console.log(o.f()); // logs 37
    

注意,在何處或者如何定義調(diào)用函數(shù)完全不會影響到this的行為。在上一個例子中,我們在定義o的時候為其成員f定義了一個匿名函數(shù)。但是,我們也可以首先定義函數(shù)然后再將其附屬到o.f。這樣做this的行為也一致:

```
var o = {prop: 37};

function independent() {
  return this.prop;
}

o.f = independent;

console.log(o.f()); // logs 37
```

這說明this的值只與函數(shù) f 作為 o 的成員被調(diào)用有關(guān)系。
類似的,this 的綁定只受最靠近的成員引用的影響。在下面的這個例子中,我們把一個方法g當(dāng)作對象o.b的函數(shù)調(diào)用。在這次執(zhí)行期間,函數(shù)中的this將指向o.b。事實上,這與對象本身的成員沒有多大關(guān)系,最靠近的引用才是最重要的。

```
o.b = {
  g: independent,
  prop: 42
};
console.log(o.b.g()); // logs 42
```
  • 原型鏈中的 this
    相同的概念在定義在原型鏈中的方法也是一致的。如果該方法存在于一個對象的原型鏈上,那么this指向的是調(diào)用這個方法的對象,表現(xiàn)得好像是這個方法就存在于這個對象上一樣。
    var o = {
      f : function(){
        return this.a + this.b;
      }
    };
    var p = Object.create(o);
    p.a = 1;
    p.b = 4;
    
    console.log(p.f()); // 5
在這個例子中,對象p沒有屬于它自己的f屬性,它的f屬性繼承自它的原型。但是這對于最終在o中找到f屬性的查找過程來說沒有關(guān)系;查找過程首先從p.f的引用開始,所以函數(shù)中的this指向p。也就是說,因為f是作為p的方法調(diào)用的,所以它的this指向了p。這是JavaScript的原型繼承中的一個有趣的特性。
  • getter 與 setter 中的 this
    再次,相同的概念也適用函數(shù)作為一個 getter 或者 一個setter調(diào)用。作為getter或setter函數(shù)都會綁定 this 到從設(shè)置屬性或得到屬性的那個對象。
    function modulus(){
      return Math.sqrt(this.re * this.re + this.im * this.im);
    }
    
    var o = {
      re: 1,
      im: -1,
      get phase(){
        return Math.atan2(this.im, this.re);
      }
    };
    
    Object.defineProperty(o, 'modulus', {
      get: modulus, enumerable:true, configurable:true});
    
    console.log(o.phase, o.modulus); // logs -0.78 1.4142
  • 構(gòu)造函數(shù)中的 this
    當(dāng)一個函數(shù)被作為一個構(gòu)造函數(shù)來使用(使用new關(guān)鍵字),它的this與即將被創(chuàng)建的新對象綁定。
    注意:當(dāng)構(gòu)造器返回的默認(rèn)值是一個this引用的對象時,可以手動設(shè)置返回其他的對象,如果返回值不是一個對象,返回this。

      function C(){
        this.a = 37;
      }
      
      var o = new C();
      console.log(o.a); // logs 37
      
      
      function C2(){
        this.a = 37;
        return {a:38};
      }
      
      o = new C2();
      console.log(o.a); // logs 38
    

在最后的例子中(C2),因為在調(diào)用構(gòu)造函數(shù)的過程中,手動的設(shè)置了返回對象,與this綁定的默認(rèn)對象被取消(本質(zhì)上這使得語句“this.a = 37;”成了“僵尸”代碼,實際上并不是真正的“僵尸”,這條語句執(zhí)行了但是對于外部沒有任何影響,因此完全可以忽略它)。

  • call 和 apply
    當(dāng)一個函數(shù)的內(nèi)部使用了this關(guān)鍵字時,通過從Function對象的原型中繼承的call()方法和apply()方法調(diào)用這個函數(shù)時,this的值可以綁定到一個指定的對象上。
    function add(c, d){
      return this.a + this.b + c + d;
    }
    
    var o = {a:1, b:3};
    
    // The first parameter is the object to use as 'this', subsequent parameters are passed as
    // arguments in the function call
    add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
    
    // The first parameter is the object to use as 'this', the second is an array whose
    // members are used as the arguments in the function call
    add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34

使用 call 和 apply 函數(shù)的時候要注意,如果傳遞的 this 值不是一個對象,JavaScript 將會嘗試使用內(nèi)部 ToObject 操作將其轉(zhuǎn)換為對象。因此,如果傳遞的值是一個原始值比如 7 或 'foo' ,那么就會使用相關(guān)構(gòu)造函數(shù)將它轉(zhuǎn)換為對象,所以原始值 7 通過new Number(7)被轉(zhuǎn)換為對象,而字符串'foo'使用 new String('foo') 轉(zhuǎn)化為對象,例如:

    unction bar() {
      console.log(Object.prototype.toString.call(this));
    }
    
    bar.call(7); // [object Number]
  • bind 方法
    ECMAScript 5 引入了 Function.prototype.bind。調(diào)用f.bind(someObject)會創(chuàng)建一個與f具有相同函數(shù)體和作用域的函數(shù),但是在這個新函數(shù)中,this將永久地被綁定到了bind的第一個參數(shù),無論這個函數(shù)是如何被調(diào)用的。
    function f(){
      return this.a;
    }
    
    var g = f.bind({a:"azerty"});
    console.log(g()); // azerty
    
    var o = {a:37, f:f, g:g};
    console.log(o.f(), o.g()); // 37, azerty
  • DOM事件處理函數(shù)中的 this
    當(dāng)函數(shù)被用作事件處理函數(shù)時(以addEventListener綁定事件),它的this指向觸發(fā)事件的元素。
    // 被調(diào)用時,將關(guān)聯(lián)的元素變成藍(lán)色
    function bluify(e){
      console.log(this === e.currentTarget); // 總是 true
    
      // 當(dāng) currentTarget 和 target 是同一個對象是為 true
      console.log(this === e.target);
      this.style.backgroundColor = '#A5D9F3';
    }
    
    // 獲取文檔中的所有元素的列表
    var elements = document.getElementsByTagName('*');
    
    // 將bluify作為元素的點擊監(jiān)聽函數(shù),當(dāng)元素被點擊時,就會變成藍(lán)色
    for(var i=0 ; i<elements.length ; i++){
      elements[i].addEventListener('click', bluify, false);
    }
  • 內(nèi)聯(lián)事件處理函數(shù)中的 this
    當(dāng)代碼被內(nèi)聯(lián)處理函數(shù)調(diào)用時,它的this指向監(jiān)聽器所在的DOM元素:

      <button onclick="alert(this.tagName.toLowerCase());">
        Show this
      </button>
    

上面的alert會顯示button。

作用域

  • 函數(shù)內(nèi)部,未聲明(即沒有var)而直接賦值的變量,是全局作用域。

  • 作用域鏈
    因為全局變量總是存在于運行期上下文作用域鏈的最末端,因此在標(biāo)識符解析的時候,查找全局變量是最慢的。所以,在編寫代碼的時候應(yīng)盡量少使用全局變量,盡可能使用局部變量。一個好的經(jīng)驗法則是:如果一個跨作用域的對象被引用了一次以上,則先把它存儲到局部變量里再使用。例如下面的代碼:

      function changeColor(){
          document.getElementById("btnChange").onclick=function(){
              document.getElementById("targetCanvas").style.backgroundColor="red";
          };
      }
    

這個函數(shù)引用了兩次全局變量document,查找該變量必須遍歷整個作用域鏈,直到最后在全局對象中才能找到。這段代碼可以重寫如下:

    function changeColor(){
        var doc=document;
        doc.getElementById("btnChange").onclick=function(){
            doc.getElementById("targetCanvas").style.backgroundColor="red";
        };
    }

這段代碼比較簡單,重寫后不會顯示出巨大的性能提升,但是如果程序中有大量的全局變量被從反復(fù)訪問,那么重寫后的代碼性能會有顯著改善。

閉包

  • 原理

閉包是一種特殊的對象。它由兩部分構(gòu)成:函數(shù),以及創(chuàng)建該函數(shù)的環(huán)境。環(huán)境由閉包創(chuàng)建時在作用域中的任何局部變量組成。

function makeFunc()
{
    var name = 'yunsheng';
    function theFunc(){
    console.log(name);
    }
return theFunc;
}

makeFunc就是一個閉包,它由函數(shù)theFunc和函數(shù)的環(huán)境--局部變量name組成。

function mySum(x){
return function(y){
        return x + y;
    }
}

var add5 = mySum(5);
var add10 = mySum(10);
add5(2);   // 7
add10(2);  // 12

這個例子更明顯,add5 和 add10 都是閉包。它們共享相同的函數(shù)定義,但是保存了不同的環(huán)境。在 add5 的環(huán)境中,x 為 5。而在 add10 中,x 則為 10。

  • 閉包的實際使用案例。
  1. 在 Web 中,您可能想這樣做的情形非常普遍。大部分我們所寫的 Web JavaScript 代碼都是事件驅(qū)動的 — 定義某種行為,然后將其添加到用戶觸發(fā)的事件之上(比如點擊或者按鍵)。我們的代碼通常添加為回調(diào):響應(yīng)事件而執(zhí)行的函數(shù)。
    以下是一個實際的示例:假設(shè)我們想在頁面上添加一些可以調(diào)整字號的按鈕。一種方法是以像素為單位指定 body 元素的 font-size,然后通過相對的 em 單位設(shè)置頁面中其它元素(例如頁眉)的字號:
        body {
          font-family: Helvetica, Arial, sans-serif;
          font-size: 12px;
        }
        
        h1 {
          font-size: 1.5em;
        }
        h2 {
          font-size: 1.2em;
        }   

我們的交互式的文本尺寸按鈕可以修改 body 元素的 font-size 屬性,而由于我們使用相對的單位,頁面中的其它元素也會相應(yīng)地調(diào)整。
以下是 JavaScript:

    function makeSizer(size) {
      return function() {
        document.body.style.fontSize = size + 'px';
      };
    }
    
    var size12 = makeSizer(12);
    var size14 = makeSizer(14);
    var size16 = makeSizer(16); 

size12,size14 和 size16 為將 body 文本相應(yīng)地調(diào)整為 12,14,16 像素的函數(shù)。我們可以將它們分別添加到按鈕上(這里是鏈接)。如下所示:

    document.getElementById('size-12').onclick = size12;
    document.getElementById('size-14').onclick = size14;
    document.getElementById('size-16').onclick = size16;
    
    <a href="#" id="size-12">12</a>
    <a href="#" id="size-14">14</a>
    <a href="#" id="size-16">16</a>
  1. 用閉包模擬私有方法
    諸如 Java 在內(nèi)的一些語言支持將方法聲明為私有的,即它們只能被同一個類中的其它方法所調(diào)用。
    對此,JavaScript 并不提供原生的支持,但是可以使用閉包模擬私有方法。私有方法不僅僅有利于限制對代碼的訪問:還提供了管理全局命名空間的強大能力,避免非核心的方法弄亂了代碼的公共接口部分。
    下面的示例展現(xiàn)了如何使用閉包來定義公共函數(shù),且其可以訪問私有函數(shù)和變量。這個方式也稱為 模塊模式(module pattern):
    var Counter = (function() {
      var privateCounter = 0;
      function changeBy(val) {
        privateCounter += val;
      }
      return {
        increment: function() {
          changeBy(1);
        },
        decrement: function() {
          changeBy(-1);
        },
        value: function() {
          return privateCounter;
        }
      }
    })();
    
    console.log(Counter.value()); /* logs 0 */
    Counter.increment();
    Counter.increment();
    console.log(Counter.value()); /* logs 2 */
    Counter.decrement();
    console.log(Counter.value()); /* logs 1 */

這里有很多細(xì)節(jié)。在以往的示例中,每個閉包都有它自己的環(huán)境;而這次我們只創(chuàng)建了一個環(huán)境,為三個函數(shù)所共享:Counter.increment,Counter.decrement 和 Counter.value。
該共享環(huán)境創(chuàng)建于一個匿名函數(shù)體內(nèi),該函數(shù)一經(jīng)定義立刻執(zhí)行。環(huán)境中包含兩個私有項:名為 privateCounter 的變量和名為 changeBy 的函數(shù)。 這兩項都無法在匿名函數(shù)外部直接訪問。必須通過匿名包裝器返回的三個公共函數(shù)訪問。
這三個公共函數(shù)是共享同一個環(huán)境的閉包。多虧 JavaScript 的詞法范圍的作用域,它們都可以訪問 privateCounter 變量和 changeBy 函數(shù)。
您應(yīng)該注意到了,我們定義了一個匿名函數(shù)用于創(chuàng)建計數(shù)器,然后直接調(diào)用該函數(shù),并將返回值賦給 Counter 變量。也可以將這個函數(shù)保存到另一個變量中,以便創(chuàng)建多個計數(shù)器。

    ```
    var makeCounter = function() {
      var privateCounter = 0;
      function changeBy(val) {
        privateCounter += val;
      }
      return {
        increment: function() {
          changeBy(1);
        },
        decrement: function() {
          changeBy(-1);
        },
        value: function() {
          return privateCounter;
        }
      }
    };
    
    var Counter1 = makeCounter();
    var Counter2 = makeCounter();
    console.log(Counter1.value()); /* logs 0 */
    Counter1.increment();
    Counter1.increment();
    console.log(Counter1.value()); /* logs 2 */
    Counter1.decrement();
    console.log(Counter1.value()); /* logs 1 */
    console.log(Counter2.value()); /* logs 0 */
    
    ```

請注意兩個計數(shù)器是如何維護(hù)它們各自的獨立性的。每次調(diào)用 makeCounter() 函數(shù)期間,其環(huán)境是不同的。每次調(diào)用中, privateCounter 中含有不同的實例。
這種形式的閉包提供了許多通常由面向?qū)ο缶幊趟碛械囊嫣帲绕涫菙?shù)據(jù)隱藏和封裝。

  • 如果不是因為某些特殊任務(wù)而需要閉包,在沒有必要的情況下,在其它函數(shù)中創(chuàng)建函數(shù)是不明智的,因為閉包對腳本性能具有負(fù)面影響,包括處理速度和內(nèi)存消耗。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 作用域和閉包是 JavaScript 最重要的概念之一,想要進(jìn)一步學(xué)習(xí) JavaScript,就必須理解 Java...
    劼哥stone閱讀 1,240評論 1 13
  • 1.函數(shù)參數(shù)的默認(rèn)值 (1).基本用法 在ES6之前,不能直接為函數(shù)的參數(shù)指定默認(rèn)值,只能采用變通的方法。
    趙然228閱讀 829評論 0 0
  • 第5章 引用類型(返回首頁) 本章內(nèi)容 使用對象 創(chuàng)建并操作數(shù)組 理解基本的JavaScript類型 使用基本類型...
    大學(xué)一百閱讀 3,667評論 0 4
  • 曾經(jīng)一度 在昔日中迷離, 昔日也妄想 逃離過往之門, 汗與血, 終究只是 嘲笑的過客。 漆黑天宇里奔跑的藍(lán)色駿馬,...
    4fde37bccee7閱讀 213評論 0 0
  • - - 李亞菲 曾渴望站在最美的花前聽你許下的諾言, 曾渴望你拉著我的手散步在清冷的...
    南宮冰茹閱讀 399評論 0 1

友情鏈接更多精彩內(nèi)容