紅寶書筆記-第7章-私有變量

本章內容前端

  • 函數表達式的特徵
  • 使用函數實現遞歸
  • 使用閉包定義私有變量

函數聲明提高

函數聲明的一個重要特徵是函數聲明提高(function declaration hoisting),在執行代碼前會先讀取函數聲明。意味着能夠把函數聲明放在調用它的語句後面。
但函數表達式不存在函數聲明提高,所以若是這麼使用會報錯,這也是「函數聲明」和「函數表達式」的區別。閉包

例如:函數

sayHi();
function sayHi() {
    //...
}

匿名函數(anonymous function)

又叫拉姆達函數,其 name 屬性是空字符串。this

function 關鍵字後沒有標識符。spa

var functionName = function() {
    // ...
}

函數表達式

既然可以建立函數再複製給變量,也就可以把函數做爲其餘函數的值返回。prototype

function foo() {
    return function() {
        // do sth.
        return 1;
    }
}

把函數當成值來使用的狀況下,均可以使用匿名函數。指針

7.1 遞歸

遞歸函數是在一個函數內部經過名字調用自身的狀況下構成的:code

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

上面是個經典的「遞歸階乘」函數,但這個函數還有些缺陷:對象

var anotherFoo = foo;
foo = null;
alert(anotherFoo(4)); // 出錯

由於 foo 再也不是函數,因此致使了錯誤。能夠用 arguments.callee 解決:blog

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

在編寫遞歸函數時,使用 arguments.callee 比使用函數名更保險。

但在嚴格模式下,不能經過腳本訪問 arguments.callee,訪問的話會致使錯誤。不過,可使用命名函數表達式來達到一樣的效果。

var foo = (function f(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * f(num - 1);
    }
});

以上代碼建立了一個名爲 f() 的命名函數表達式,而後將它賦給了變量 foo,即便把函數賦給了另外一個變量,函數的名字 f 仍然有效,因此遞歸調用可以正常完成。這種方式在嚴格模式和非嚴格模式都可以執行。

7.2 閉包

閉包是指有權訪問另外一個函數做用域中的變量的函數。

建立閉包

在一個函數內部建立另外一個函數。

function foo(num) {
    return function () {
        console.log(num)
    }
}
  1. 當某個函數被調用時,會建立一個執行環境(execution context)及相應的做用域鏈。
  2. 使用 arguments 和其餘命名參數的值來初始化函數的活動對象(activation object)。但在做用域鏈中,外部函數的活動對象始終處於第二位,外部函數的外部函數的活動對象處於第三位,以此類推,直到做爲做用域鏈終點的全局執行環境。

例子:

function compare(a, b) {
    if (a < b) {
        return -1;
    } else if (a > b) {
        return 1;
    } else {
        return 0;
    }
}

後臺的每一個執行環境都有一個表示變量的對象——變量對象。

建立函數時,會建立一個預先包含全局變量對象的做用域鏈,這個做用域鏈被保存在內部的 [[Scope]] 屬性中。

調用函數時,會爲函數建立一個執行環境,經過複製函數的 [[Scope]] 屬性中的變量對象來構建執行環境的做用域鏈。

接着,一個活動對象被建立並被推入執行環境做用域鏈的前端。

做用域鏈本質上是一個指向變量對象的指針列表,它只引用但不實際包含變量對象。

7.2.1 閉包與變量

做用域有個反作用:閉包只能取得包含函數中任何變量的最後一個值,由於閉包所保存的是整個變量對象,而不是某個特殊的變量。

function foo() {
    var result = [];
    
    for(var i = 0; i < 10; i++) {
        result[i] = function(num) {
            return function() {
                return num;
            }
        }(i);
    }
}

在調用每一個匿名函數時,咱們傳入了變量 i,因爲函數參數是按值傳遞的,因此就會將變量 i 的當前值複製給參數 num。而在這個匿名函數內部,又建立並返回了一個訪問 num 的閉包。因此,result 中的每一個函數都有本身 num 變量的一個副本,所以就能夠返回各自不一樣的數值。

7.2.2 關於 this 對象

this 對象是在運行時基於函數的執行環境綁定的:在全局函數中,this 等於 window,而當函數被做爲某個對象的方法調用時,this 等於調用它的對象。不過,匿名函數的執行環境具備全局性,所以 this 對象一般指向 window。

爲何匿名函數不能取得其包含做用域(外部做用域)的this呢?

每一個函數在被調用時,都會自動得到兩個特殊變量:this,arguments。內部函數在搜索這兩個變量時,只會搜索到其活動對象爲止,所以永遠不可能直接訪問外部函數中的這兩個變量。不過,把外部做用域中的 this 對象保存在一個閉包可以訪問到的變量裏,就可讓閉包訪問該對象了。

var name = "The Window";
var obj = {
    name: "My Object",
    getNameFunc: function() {
        var that = this;
        return function() {
            return this.name;
        };
    }
}
alert(obj.getNameFunc()()) // My Object

7.2.3 內存泄露

function foo() {
    var element = document.getElementById('test');
    var id = element.id;
    
    element.onclick = function() {
        alert(id);
    };
    element = null;
}

閉包會引用包含函數的整個活動對象,而其中包含着 element。即便閉包不直接引用 element,包含函數的活動對象仍然會保存一個引用。所以,有必要把 element 變量設置爲 null。這樣就可以接觸對 DOM 對象的引用,順利地減小其引用數,確保正常回收其佔用的內存。

7.3 模仿塊級做用域

ES6 以前,js 沒有塊級做用域。

同名變量聲明時,後續的變量聲明將被忽略。

var a = 1;
var a = 2; // var a 被忽略,只執行初始化賦值 a = 2

使用匿名函數能夠模仿塊級做用域:

(function() {
  // 塊級做用域
  var i = 1;
})()
// 括號包括的 function 實際上是一個函數表達式,不是一個函數聲明。

alert(i) // 報錯

這種作法能夠減小閉包占用的內存問題,由於沒有指向匿名函數的引用。只要函數執行完畢,就能夠當即銷燬其做用域鏈了。

7.4 私有變量

嚴格說:JS 沒有私有成員的概念(ts 語法有),全部對象屬性都是共有的。但有一個私有變量的概念。

任何在函數中定義的變量,均可以認爲是私有變量,由於不能在函數的外部訪問這些變量。

咱們把有權訪問私有變量和函數的公有方法稱爲「特權方法」(privileged method)。有兩種在對象上建立特權方法的方式。

第一個種在構造函數中定義特權方法:

function MyObject() {
    // 私有變量(函數)
    var privateVariable = 10;
    
    function privateFunction() {
        return false;
    }
    
    // 特權方法,是一個閉包,因此能夠訪問構造函數中的私有變量和函數,外部環境只能經過 MyObject.publicMethod() 來訪問內部私有變量和函數。
    this.publicMethod = function() {
        privateVariable++;
        return privateFunction();
    }
    
}

可是這有個弊端,弊端來自構造函數,它針對每一個實例都會建立一樣一組新方法。

7.4.1 靜態私有變量

(function() {

var name = '';
Person = function(value) {
    name = value;
}
Person.prototype.getName = function() {
    return name;
}
Person.prototype.setName = function(value) {
    name = value;
}

})();

以這種方式建立靜態私有變量,會由於使用原型而增進代碼複用,但每一個實例都沒有本身的私有變量。變量 name 變成了一個靜態的、由全部實例共享的屬性,在某個實例上調用 setName() 後,會影響全部實例。

7.4.2 模塊模式(module pattern)

模塊模式是爲單例建立私有變量和特權方法。單例,指的是隻有一個實例的對象。

建立單例:

var singleton = {
    name: value,
    method: function() {
        // ...
    }
}

模塊模式:

var singleton = function() {
    // 私有變量和私有函數
    var privateVariable = 10;
    var privateFunction() {
        return false;
    };
    return {
        publicProperty: 10,
        publicFunction: function() {
            privateVariable++;
            privateFunction();
        }
    }
}

若是必須建立一個對象並以某些數據對其進行初始化,同時還要公開一些可以訪問這些私有數據的方法,那麼就可使用模塊模式。

以這種模式建立的每一個單例都是 Object 的實例,由於最終要經過一個對象字面量來表示它。

相關文章
相關標籤/搜索