天天看點

閉包、閉包作用及缺點

閉包:是指有權通路另一個函數作用域中的變量的函數。建立閉包的常見形式:就是在一個函數内部建立另一個函數。

詞法作用域

function outer(){
	var localVal = 30;
	function inner(){
		return localVal;
	};
        inner()
}
outer();
           

詞法(lexical)一詞指的是,詞法作用域根據源代碼中聲明變量的位置來确定該變量在何處可用。嵌套函數可通路聲明于它們外部作用域的變量。

閉包

function outer(){
	var localVal = 30;
	function inner(){
		return localVal
	};
        return inner;
}
var func = outer();
func();
           

運作這段代碼的效果和之前

outer()

 函數的示例完全一樣。其中不同的地方(也是有意思的地方)在于内部函數

inner()

在執行前,從外部函數傳回。

可以看到在JavaScript中的函數形成了閉包。閉包是由函數以及聲明該函數的詞法環境組合而成的。該環境包含了這個閉包建立時作用域内的任何局部變量。在本例子中,func 是執行 outer 時建立的 inner 函數執行個體的引用。inner 的執行個體維持了一個對它的詞法環境(變量 localVal 存在于其中)的引用。是以,當 func 被調用時,變量 localVal 仍然可用,其值 name 就被傳回了。

閉包的作用

1.閉包可以封裝這個函數内的私有變量:

function create_counter(initial) {
    var x = initial || 0;
    return {
        inc: function () {
            x += 1;
            return x;
        }
    }
}
var c1 = create_counter();
c1.inc(); // 1
c1.inc(); // 2
c1.inc(); // 3

var c2 = create_counter(10);
c2.inc(); // 11
c2.inc(); // 12
c2.inc(); // 13
           

在傳回的對象中,實作了一個閉包,該閉包攜帶了局部變量

x

,并且,從外部代碼根本無法通路到變量

x

。換句話說,閉包就是攜帶狀态的函數,并且它的狀态可以完全對外隐藏起來。

2.閉包還可以把多參數的函數變成單參數的函數。

例如,要計算xy可以用

Math.pow(x, y)

函數,不過考慮到經常計算x2或x3,我們可以利用閉包建立新的函數

pow2

pow3

function make_pow(n) {
    return function (x) {
        return Math.pow(x, n);
    }
}
// 建立兩個新函數:
var pow2 = make_pow(2);
var pow3 = make_pow(3);

console.log(pow2(5)); // 25
console.log(pow3(7)); // 343
           

從本質上講,make_pow 是一個函數工廠 — 他建立了将指定的值和它的參數相加求和的函數。

循環錯誤

閉包隻能取得包含函數中任何變量的最後一個值。

下面是一個使用閉包的示例:

function count() {
    var arr = [];
    for (var i=1; i<=3; i++) {
        arr.push(function () {
            return i * i;
        });
    }
    return arr;
}

var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];
           

傳回結果:

f1(); // 16
f2(); // 16
f3(); // 16
           

原因就在于傳回的函數引用了變量

i

,但它并非立刻執行。等到3個函數都傳回時,它們所引用的變量

i

已經變成了

4

,是以最終結果為

16

修改的方法一:

需要在函數内部添加立即執行函數,否則每次循環都會在循環的最後一位:

function count() {
    var arr = [];
    for (var i=1; i<=3; i++) {
        arr.push((function (n) {
            return function () {
                return n * n;
            }
        })(i));
    }
    return arr;
}

var results = count();
var f1 = results[0];
var f2 = results[1];
var f3 = results[2];

f1(); // 1
f2(); // 4
f3(); // 9
           

這個方法是再建立一個函數,用該函數的參數綁定循環變量目前的值,無論該循環變量後續如何更改,已綁定到函數參數的值不變。

修改的方法二:

直接将for循環内的聲明變量i的 var變為 let。

因為var在for循環内部聲明的值,在外部可以擷取到。而let會使for循環變成一個封閉的作用域。

閉包的缺點

如果不是某些特定任務需要使用閉包,在其它函數中建立函數是不明智的,因為閉包在處理速度和記憶體消耗方面對腳本性能具有負面影響。

1.性能考量

例如,在建立新的對象或者類時,方法通常應該關聯于對象的原型,而不是定義到對象的構造器中。原因是這将導緻每次構造器被調用時,方法都會被重新指派一次(也就是說,對于每個對象的建立,方法都會被重新指派)。

考慮以下示例:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}
           

在上面的代碼中,我們并沒有利用到閉包的好處,是以可以避免使用閉包。修改成如下:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};
           

但我們不建議重新定義原型。可改成如下例子:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

           

在前面的兩個示例中,繼承的原型可以為所有對象共享,不必在每一次建立對象時定義方法。

2.this指向

this對象是在運作時基于函數函數的執行環境綁定的,在全局函數中,this等于window,而當函數被當作某個對象的方法調用時,this等于那個對象。但是匿名函數具有全局性,是以其this對象通常指向window。

例如:

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

這裡沒擷取到這個對象的name,而是拿到了全局的name。因為每個函數在被調用時會自動取得兩個特殊變量:this和arguments。内部函數在搜尋這兩個變量時,隻會搜尋到其活動對象為止,是以永遠不可能直接通路外部函數中的這兩個變量。

把外部作用域的this對象儲存在一個閉包能夠通路到的變量裡,就可以讓閉包通路該對象了。

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

3.記憶體洩漏

閉包會引用包含函數的整個變量對象,如果閉包的作用域鍊中儲存着一個HTML元素,那麼就意味着該元素無法被銷毀。是以我們有必要在對這個元素操作完之後主動銷毀。

function assignHandler(){
	var element = document.getElementById("someElement");
	var id = element.id;
	
	element.onclick = function(){
		alert(id);
	}
	element = null;
}
           

閉包會引用包含函數的整個活動對象,其中包含element,包含函數的活動對象中也包含着一個引用,是以需要把element設定為null。這樣可以解除對DOM對象的引用,減少引用次數,確定正常回收其占用的記憶體。

參考:高階函數 閉包

          MDN中級教程 Closures

繼續閱讀