神馬是閉包
關於閉包的概念,是婆說婆有理。
閉包是指有權存取另外一個函數作用域中的變數的函數
這概念有點繞,拆分一下。從概念上來說,閉包有兩個特點:
在ES 6之前,Javascript只有函數作用域的概念,沒有區塊級作用域(但catch捕獲的異常 只能在catch區塊中存取)的概念(IIFE可以創建局部作用域)。每個函數作用域都是封閉的,即外部是存取不到函數作用域中的變數。
function getName() { var name = "美女的名字"; console.log(name); //"美女的名字" } function displayName() { console.log(name); //报错 }
但是為了得到美女的名字,不死心的單身汪把代碼改成了這樣:
function getName() { var name = "美女的名字"; function displayName() { console.log(name); } return displayName; } var 美女 = getName(); 美女() //"美女的名字"
這下,美女是一個閉包了,單身汪想怎麼玩就怎麼玩了。 (但不建議單身汪用中文做變數名的寫法,大家不要學)。
關於閉包呢,還想再說三點:
1、閉包可以存取目前函數以外的變數
function getOuter(){ var date = '815'; function getDate(str){ console.log(str + date); //访问外部的date } return getDate('今天是:'); //"今天是:815" } getOuter();
getDate是一個閉包,當函數執行時,會形成一個作用域A,A中並沒有定義變數date,但它能在父一級作用域中找到該變數的定義。
2、即使外部函數已經回傳,閉包仍能存取外部函數定義的變數
function getOuter(){ var date = '815'; function getDate(str){ console.log(str + date); //访问外部的date } return getDate; //外部函数返回 } var today = getOuter(); today('今天是:'); //"今天是:815" today('明天不是:'); //"明天不是:815"
3、閉包可以更新外部變數的值
function updateCount(){ var count = 0; function getCount(val){ count = val; console.log(count); } return getCount; //外部函数返回 } var count = updateCount(); count(815); //815 count(816); //816
作用域鏈
為毛閉包就能存取外部函數的變數呢?這就要說說Javascript中的作用域鏈了。
Javascript中有一個執行環境(execution context)的概念,它定義了變數或函數有權存取的其它數據,決定了他們各自的行為。每個執行環境都有一個與之關聯的變數對象,環境中定義的所有變數和函數都保存在這個對像中。你可以把它當作Javascript的一個普通對象,但是你只能修改它的屬性,卻不能引用它。
變數物件也是有父作用域的。當存取變數時,解釋器會先在目前作用域尋找標示符,如果沒有找到,就去父作用域找,直到找到該變數的標示符或不再存在父作用域了,這就是作用域鏈。
作用域鍊和原型繼承有點類似,但又有點小區別:如果去查找一個普通對象的屬性時,在當前對象和其原型中都找不到時,會返回undefined;但查找的屬性在作用域鏈中不存在的話就會拋出ReferenceError。
作用域鏈的頂端是全域物件。對於全域環境中的程式碼,作用域鏈只包含一個元素:全域物件。所以,在全域環境中定義變數的時候,它們就會被定義到全域物件中。當函數被呼叫的時候,作用域鏈就會包含多個作用域物件。
關於作用域鏈講得略多(紅皮書上有關於作用域及執行環境的詳細解釋),看一個簡單地例子:
// my_script.js "use strict"; var foo = 1; var bar = 2;
在全域環境中,建立了兩個簡單地變數。如前面所說,此時變數物件是全域物件。
改動一下程式碼,建立一個沒有函數巢狀的函數:
"use strict"; var foo = 1; var bar = 2; function myFunc() { //-- define local-to-function variables var a = 1; var b = 2; var foo = 3; console.log("inside myFunc"); } console.log("outside"); //-- and then, call it: myFunc();
当myFunc被定义的时候,myFunc的标识符(identifier)就被加到了当前的作用域对象中(在这里就是全局对象),并且这个标识符所引用的是一个函数对象(function object)。函数对象中所包含的是函数的源代码以及其他的属性。其中一个我们所关心的属性就是内部属性[[scope]]。[[scope]]所指向的就是当前的作用域对象。也就是指的就是函数的标识符被创建的时候,我们所能够直接访问的那个作用域对象(在这里就是全局对象)。
比较重要的一点是:myFunc所引用的函数对象,其本身不仅仅含有函数的代码,并且还含有指向其被创建的时候的作用域对象。
当myFunc函数被调用的时候,一个新的作用域对象被创建了。新的作用域对象中包含myFunc函数所定义的本地变量,以及其参数(arguments)。这个新的作用域对象的父作用域对象就是在运行myFunc时我们所能直接访问的那个作用域对象。
如前面所说,当函数返回没有被引用的时候,就会被垃圾回收器回收。但是对于闭包(函数嵌套是形成闭包的一种简单方式)呢,即使外部函数返回了,函数对象仍会引用它被创建时的作用域对象。
"use strict"; function createCounter(initial) { var counter = initial; function increment(value) { counter += value; } function get() { return counter; } return { increment: increment, get: get }; } var myCounter = createCounter(100); console.log(myCounter.get()); // 返回 100 myCounter.increment(5); console.log(myCounter.get()); // 返回 105
当调用createCounter(100)时,内嵌函数increment和get都有指向createCounter(100) scope的引用。如果createCounter(100)没有任何返回值,那么createCounter(100) scope不再被引用,于是就可以被垃圾回收。但是因为createCounter(100)实际上是有返回值的,并且返回值被存储在了myCounter中,所以对象之间的引用关系发生变化。
需要用点时间思考的是:即使createCounter(100)已经返回,但是其作用域仍在,并能且只能被内联函数访问。可以通过调用myCounter.increment() 或 myCounter.get()来直接访问createCounter(100)的作用域。
当myCounter.increment() 或 myCounter.get()被调用时,新的作用域对象会被创建,并且该作用域对象的父作用域对象会是当前可以直接访问的作用域对象。
当执行到return counter;时,在get()所在的作用域并没有找到对应的标示符,就会沿着作用域链往上找,直到找到变量counter,然后返回该变量,调用increment(5)则会更有意思。当单独调用increment(5)时,参数value会存贮在当前的作用域对象。函数要访问value,能马上在当前作用域找到该变量。但是当函数要访问counter时,并没有找到,于是沿着作用域链向上查找,在createCounter(100)的作用域找到了对应的标示符,increment()就会修改counter的值。除此之外,没有其他方式来修改这个变量。闭包的强大也在于此,能够存贮私有数据。
Similar function objects, different scope objects
对于上面的counter示例,再说点扩展的事。看代码:
//myScript.js "use strict"; function createCounter(initial) { /* ... see the code from previous example ... */ } //-- create counter objects var myCounter1 = createCounter(100); var myCounter2 = createCounter(200);
myCounter1 和 myCounter2创建之后,关系图是酱紫的:
在上面的例子中,myCounter1.increment和myCounter2.increment的函数对象拥有着一样的代码以及一样的属性值(name,length等等),但是它们的[[scope]]指向的是不一样的作用域对象。
这才有了下面的结果:
var a, b; a = myCounter1.get(); // a 等于 100 b = myCounter2.get(); // b 等于 200 myCounter1.increment(1); myCounter1.increment(2); myCounter2.increment(5); a = myCounter1.get(); // a 等于 103 b = myCounter2.get(); // b 等于 205
作用域和this
作用域会存储变量,但this并不是作用域的一部分,它取决于函数调用时的方式。关于this指向的总结,可以看这篇文章:JavaScript面试问题:事件委托和this