本期的主題是做用域閉包,本計劃一共28期,每期重點攻克一個面試重難點,若是你還不瞭解本進階計劃,文末點擊查看所有文章。javascript
若是以爲本系列不錯,歡迎點贊、評論、轉發,您的支持就是我堅持的最大動力。html
做用域指的是一個變量和函數的做用範圍,JS中函數內聲明的全部變量在函數體內始終是可見的,在ES6前有全局做用域和局部做用域,可是沒有塊級做用域(catch只在其內部生效),局部變量的優先級高於全局變量。前端
var scope="global";
function scopeTest(){
console.log(scope);
var scope="local"
}
scopeTest(); //undefined
複製代碼
上面的代碼輸出是undefined
,這是由於局部變量scope
變量提高了,等效於下面java
var scope="global";
function scopeTest(){
var scope;
console.log(scope);
scope="local"
}
scopeTest(); //undefined
複製代碼
注意,若是在局部做用域中忘記var,那麼變量就被聲明爲全局變量。webpack
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0](); // 3
data[1](); // 3
data[2](); // 3
複製代碼
上篇文章已經介紹過了,【進階2-2期】JavaScript深刻之從做用域鏈理解閉包git
每一個函數都有本身的執行上下文環境,當代碼在這個環境中執行時,會建立變量對象的做用域鏈,做用域鏈是一個對象列表或對象鏈,它保證了變量對象的有序訪問。github
做用域鏈的開始是當前代碼執行環境的變量對象,常被稱之爲「活躍對象」(AO),變量的查找會從第一個鏈的對象開始,若是對象中包含變量屬性,那麼就中止查找,若是沒有就會繼續向上級做用域鏈查找,直到找到全局對象中web
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
複製代碼
上面在函數中返回了兩個閉包,這兩個閉包都維持着對外部做用域的引用。閉包中會將外部函數的自由對象添加到本身的做用域鏈中,因此能夠經過內部函數訪問外部函數的屬性,這也是javascript模擬私有變量的一種方式。面試
因爲做用域鏈機制的影響,閉包只能取得內部函數的最後一個值,這引發的一個反作用就是若是內部函數在一個循環中,那麼變量的值始終爲最後一個值。算法
這個代碼已經貼過了,怕大家忘記,就再貼一遍
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
的副本,互不影響了。
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
。
代碼1:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo();
複製代碼
代碼2:
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope();
複製代碼
上面的兩個代碼中,checkscope()
執行完成後,閉包f
所引用的自由變量scope
會被垃圾回收嗎?爲何?
進階系列文章彙總:github.com/yygmind/blo…,內有優質前端資料,歡迎領取,以爲不錯點個star。
我是木易楊,網易高級前端工程師,跟着我每週重點攻克一個前端面試重難點。接下來讓我帶你走進高級前端的世界,在進階的路上,共勉!