JavaScript閉包是個啥?

定義

函數與對其狀態即詞法環境(lexical environment)的引用共同構成閉包(closure)。也就是說,閉包可以讓你從內部函數訪問外部函數作用域。在JavaScript,函數在每次創建時生成閉包。
通俗的說就是:定義在一個函數內部的函數,並且該被函數訪問外部函數作用域
只有調用外部函數時纔會生成新閉包,不然就是同一個閉包

作用域鏈

對變量進行訪問時,最先在自己的作用域中查找,如果找到則停止查找,否則向向一級作用域中查找,直到全局作用域中。如果還是沒有找到就會報錯。
例子:

// var myName = 'global'
function fn(){
	// var myName = 'fn'
	function fn1(){
		// var myName = 'fn1' 
		console.log(myName)
	}
	fn1()
}
fn()

在上面的代碼裏依次從下面向上註釋,輸出結果依次是fn1,fn,global,Uncaught ReferenceError: myName is not defined,最後在全局作用域裏都沒找到就會報錯;

詞法作用域

詞法作用域:指函數在定義(或聲明)它們的作用域裏運行,而不是在執行(或調用)它們的作用裏運行;看個例子

var name = 'fn1_name'
function fn1(){
console.log(name)
}
function fn2(){
	var name = 'fn2_name'
	fn1()
}
fn2()// fn1_name

fn1是在全局中聲明的,因此fn1是在全局作用域中運行的,然後根據作用域鏈查找name,fn1函數中沒有向向一級作用域查找。最終在全局中找到它爲fn1_name

閉包

總所周知,一個函數運行完畢以後,該函數作用域裏的所有變量都會被垃圾機制回收;而閉包就是延長了作用域鏈;
閉包例子:

  1. 函數作爲返回值返回
function fn(){
var myName = 'fn'
	return function bar(){
		console.log(myName)
	}
}
var f = fn() //bar函數的引用賦值給變量f
f() //調用b函數 輸出fn

上面的函數就是閉包的一種,var f = fn()產生了閉包,延長myName的作用域鏈,導致fn函數運行完畢後,myName得不到釋放;
在這裏插入圖片描述
我們可以打個斷點就清楚,閉包產生Closure,記着只有引用了外部函數的變量纔會產生閉包。還有上面的Call Stack代表了調用棧,現在是bar,是函數名,如果該函數爲匿名函數則爲anonymous,如果點到下面的anonymous,就是匿名的全局,斷點就會跑到37行;
例子2:

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

var add5 = makeAdder(5); //返回匿名函數,x對外部函數有引用
var add10 = makeAdder(10);

console.log(add5(2));  // 7 相當於 function(2){return 5+2}
console.log(add10(2)); // 12

例子3:

for(var i=0;i<3;i++){
btn[i].onclick = function(){
		console.log(i) //輸出都是3
	}
}

上面的代碼運行了3次也就註冊了3個按鈕的點擊事件,但是隻有通過點擊按鈕纔會執行函數,所以那時已經變爲3了。
利用閉包:

for(var i=0;i<3;i++){
	(function(j){
		btn[i].onclick = function(){
			console.log(j)
		}
	})(i)
}

這樣操作的話,產生了3個閉包也保存了它們的值。當然我們開發時一般都是使用屬性保存索引,ES6中的let塊作用域也可以解決該問題。

用閉包模擬私有方法

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 */

上面的代碼,對象的privateCounter只能通過暴露的方法修改,從而有了私有屬性或者方法;模塊化應該的思想應該就是這樣,像JQuery,向外暴露一些方法。

試題

var name = 'The window'
var obj = {
	name:'my obj',
	sayName:function(){
		return function(){
			return this.name
		}
	}
}
alert(obj.sayName()())  //The window
alert(obj.sayName().call(obj)) //my obj

上述代碼輸出The window,因爲obj.sayName()()的調用對象是window ,所以this指向The window;第二個是修改this的指向爲obj;

var name = 'The window'
var obj = {
	name:'my obj',
	sayName:function(){
		var that = this
		return function(){
			return that.name
		}
	}
}
alert(obj.sayName()()) //my obj

這個是把this的指向的引用賦值給that,然後是閉包,內部函數引用了外部函數的變量;

function fun(n,o){
console.log(o)
	return{
		fun:function(m){
			return fun(m,n)
		}
	}
}
var a = fun(0);//返回一個對象{fun:function(){}} 輸出undefined,產生一個閉包
a.fun(1);//向function(m)傳1,n引用爲0,輸出0
a.fun(2);//向function(m)傳2,n引用爲0,輸出0
a.fun(3)//向function(m)傳3,n引用爲0,輸出0
var b = fun(0).fun(1).fun(2).fun(3)
//返回一個對象{fun:function(){}} 輸出undefined,產生閉包
//第二次再次調用了外部函數又產生一個閉包,輸出0
//第三次再次調用了外部函數又產生一個閉包,輸出1
//第四次再次調用了外部函數又產生一個閉包,輸出2

var c = fun(0).fun(1);
//在第二個閉包的基礎上開始,不再生成新閉包,使用先輸出undefined,0
c.fun(2);//輸出1
c.fun(3)//輸出1

上述代碼,注意有沒有生成新的閉包;

總結

閉包,我們可能都用過,但是也沒有刻意的去使用它,都是潛意識的。但是閉包可能會造成內存泄漏,所以要謹慎使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章