做用域和做用域鏈是js中很是重要的特性,關係到理解整個js體系,閉包是對做用域的延伸,其餘語言也有閉包的特性。html
那什麼是做用域?做用域指的是一個變量和函數的做用範圍。es6
一、js中函數內聲明的全部變量在函數體內始終是可見的;面試
二、在ES6中有全局做用域和局部做用域,可是沒有沒有塊級做用域(catch只在其內部生效);segmentfault
三、局部變量的優先級高於全局變量。數組
咱們來舉幾個栗子:閉包
var scope="global"; function scopeTest(){ console.log(scope); var scope="local" } scopeTest(); //undefined
上面的代碼輸出是undefined,這是由於局部變量scope變量提高了,等效於下面函數
var scope="global"; function scopeTest(){ var scope; console.log(scope); scope="local" } scopeTest(); //undefined
注意,若是在局部做用域中忘記var,那麼變量就被聲明爲全局變量。ui
var scope="global"; function scopeTest(){ console.log(scope); scope="local" } scopeTest(); //global var scope="global"; function scopeTest(){ scope="local" console.log(scope); } scopeTest(); //local
和咱們其餘經常使用語言不一樣的是,js中沒有塊級做用域code
var data = []; for (var i = 0; i < 3; i++) { data[i] = function () { console.log(i); }; } data[0](); // 3 data[1](); // 3 data[2](); // 3
每一個函數都有本身的執行上下文環境,當代碼在這個環境中執行時候,會建立變量對象的做用域鏈,htm
那什麼是做用域鏈?做用域鏈式是一個對象列表。
做用域鏈的做用?他保證了變量對象的有序訪問。
做用域鏈開始的地方:當前代碼執行環境的變量對象,常被稱之爲「活躍對象」(AO),變量的查找會從第一個鏈的對象開始,若是對象中包含變量屬性,那麼就中止查找,若是沒有就會繼續向上級做用域查找,直到找到全局對象中,若是找不到就會報ReferenceError。
function createClosure(){ var name = "jack"; return { setStr:function(){ name = "rose"; }, getStr:function(){ return name + ":hello"; } } } var builder = new createClosure(); builder.setStr(); console.log(builder.getStr()); //rose:hello
上面在函數中反悔了兩個閉包,這兩個閉包都維持着對外部做用域的引用,所以無論在哪調用都是可以訪問外部函數中的變量。在一個函數內部定義的函數,閉包中會將外部函數的自由對象添加到本身的做用域中,因此能夠經過內部函數訪問外部函數的屬性,這就是js模擬私有變量的一種方式。
注意:因爲閉包會額外的附帶函數的做用域(內部匿名函數攜帶外部函數的做用域),所以,閉包會比其餘函數多佔用些內存空間,過分使用會致使內存佔用增長。
因爲做用域鏈機制的影響,閉包只能取得內部函數的最後一個值,這引發了一個反作用,若是內部函數在一個循環中,那麼變量的值始終爲最後一個值。
var data = []; for (var i = 0; i < 3; i++) { data[i] = function () { console.log(i); }; } data[0](); // 3 data[1](); // 3 data[2](); // 3
若是想強制返回逾期結果,怎麼整?
方法一:當即執行函數
for (var i = 0; i < 3; i++) { (function(num) { setTimeout(function() { console.log(num); }, 1000); })(i); } // 0 // 1 // 2
方法二:返回一個匿名函數賦值
var data = []; for (var i = 0; i < 3; i++) { data[i] = (function (num) { return function(){ console.log(num); } })(i); } data[0](); // 0 data[1](); // 1 data[2](); // 2
不管上是當即執行函數仍是返回一個匿名函數賦值,原理上都是由於變量的按值傳遞,因此會將變量i的值賦值給實參num,在匿名函數的內部又建立了一個用於訪問num的匿名函數,這樣每個函數都有一個num的副本,互不影響。
方法三:使用es6的let
var data = []; for (let i = 0; i < 3; i++) { data[i] = function () { console.log(i); }; } data[0](); data[1](); data[2]();
解釋一下原理:
var data = [];// 建立一個數組data;
// 進入第一次循環
{
let i = 0; // 注意:由於使用let使得for循環爲塊級做用域 // 這次 let i = 0 在這個塊級做用域中,而不是在全局環境中 data[0] = function() { console.log(i); };
}
循環時,let聲明瞭i,因此整個塊是塊級做用域,那麼data[0]這個函數就成了一個閉包,這裏用{}表述,只是但願經過它來講明let存在的時候,這個for循環塊是塊級做用域,而不是全局做用域。 上面的塊級做用域,就像函數做用域同樣,寒暑表執行完畢,其中的變量會被銷燬,可是由於這個代碼塊中存在一個閉包,閉包的做用域鏈中引用着塊級做用域,因此在閉包被調用以前,這個塊級做用域內部的變量不會被銷燬。
// 進入第二次循環
{
let i = 1; // 由於 let i = 1 和上面的 let i = 0 // 在不一樣的做用域中,因此不會相互影響 data[1] = function(){ console.log(i); };
}
當執行data[1]()時,進入下面的執行環境。
{
let i = 1; data[1] = function(){ console.log(i); };
}
在上面這個執行環境中,它會首先尋找該執行環境中是否存在i,沒有找到,就沿着做用域鏈繼續向上找,在其所在的塊級做用域執行環境中,找到i=1,因而輸出1。 ## 4、思考題 代碼1:
var scope = "global scope";
function checkscope(){
var scope = "local scope"; function f(){ return scope; } return f;
}
checkscope()(); //local scope
代碼2:
var scope = "global scope";
function checkscope(){
var scope = "local scope"; function f(){ return scope; } return f;
}
var foo = checkscope();
foo(); //local scope
## 4、參考 一、https://segmentfault.com/a/1190000000618597 二、https://www.cnblogs.com/zhuzhenwei918/p/6131345.html