JS三座大山再學習(二、作用域和閉包)
原文地址
作用域
JS中有兩種作用域:全域性作用域|區域性作用域
栗子1
console.log(name); //undefined var name = '波妞'; var like = '宗介' console.log(name); //波妞 function fun(){ console.log(name); //波妞 console.log(eat) //ReferenceError: eat is not defined (function(){ console.log(like) //宗介 var eat = '肉' })() } fun();
- name定義在全域性,在全域性可以訪問到,所以 (2) 列印能夠正確列印;
- 在函式fun中,如果沒有定義name屬性,那麼會到它的父作用域去找,所以 (3) 也能正確列印。
- 內部環境可以通過作用域鏈訪問所有外部環境,但外部環境不能訪問內部環境的任何變數和函式。類似單向透明,這就是作用域鏈,所以 (4) 不行而 (5) 可以。
那麼問題來了,為什麼第一個列印是"undefined",而不是"ReferenceError: name is not defined"。原理簡單的說就是JS的變數提升
變數提升:JS在解析程式碼時,會將所有的宣告提前到所在作用域的最前面
栗子2
console.log(name); //undefined var name = '波妞'; console.log(name); //波妞 function fun(){ console.log(name) //undefined console.log(like) //undefined var name = '大西瓜'; var like = '宗介' } fun();
相當於
var name;
console.log(name); //undefined
name = '波妞';
console.log(name); //波妞
function fun(){
var name;
var like;
console.log(name) //undefined
console.log(like) //undefined
name = '大西瓜';
like = '宗介'
console.log(name) //大西瓜
console.log(like) //宗介
}
fun();
注意:是提前到當前作用域的最前面
栗子3
printName(); //printName is not a function
var printName = function(){
console.log('波妞')
}
printName(); //波妞
相當於
var printName;
printName(); //printName is not a function
printName = function(){
console.log('波妞')
}
printName(); //波妞
這樣一來就好理解了,函式表示式在宣告的時候還只是個變數
栗子4
{
var name = '波妞';
}
console.log(name) //波妞
(function(){
var name = '波妞';
})()
console.log(name) //ReferenceError: name is not defined
{
let name = '波妞';
}
console.log(name) //ReferenceError: name is not defined
從上面的栗子可以看出,不可以草率的認為JS中var宣告的變數的作用範圍就是大括號的起止範圍,ES5並沒有塊級作用域,實質是函式作用域;ES6中有了let、const定義後,才有了塊級作用域。
栗子5
function p1() {
console.log(1);
}
function p2() {
console.log(2);
}
(function () {
if (false) {
function p1() {
console.log(3);
}
}else{
function p2(){
console.log(4)
}
}
p2();
p1()
})();
//4
//TypeError: print is not a function
這是一個非常經典的栗子,宣告提前了,但是因為判斷條件為否,所以沒有執行函式體。所以會出現"TypeError: print is not a function"。while,switch,for同理
閉包
函式與對其狀態即詞法環境(lexical environment)的引用共同構成閉包(closure)。也就是說,閉包可以讓你從內部函式訪問外部函式作用域。在JavaScript中,函式在每次建立時生成閉包。
上面的定義來自MDN,簡單講,閉包就是指有權訪問另一個函式作用域中變數的函式。
- 閉包的關鍵在於:外部函式呼叫之後其變數物件本應該被銷燬,但閉包的存在使我們仍然可以訪問外部函式的變數物件.,
//舉個例子
function makeFunc() {
var name = "波妞";
function displayName() {
console.log(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();
JavaScript中的函式會形成閉包。 閉包是由函式以及建立該函式的詞法環境組合而成。這個環境包含了這個閉包建立時所能訪問的所有區域性變數
在例子中,myFunc 是執行 makeFunc 時建立的 displayName 函式例項的引用,而 displayName 例項仍可訪問其詞法作用域中的變數,即可以訪問到 name 。由此,當 myFunc 被呼叫時,name 仍可被訪問,其值 '波妞' 就被傳遞到console.log中。建立閉包最常見方式,就是在一個函式內部建立另一個函式
- 通常,函式的作用域及其所有變數都會在函式執行結束後被銷燬。但是,在建立了一個閉包以後,這個函式的作用域就會一直儲存到閉包不存在為止
//例二
function makeAdder(x) {
return function(y) {
return x + y;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
//釋放對閉包的引用
add5 = null;
add10 = null;
從本質上講,makeAdder 是一個函式工廠 — 他建立了將指定的值和它的引數相加求和的函式。在上面的示例中,我們使用函式工廠建立了兩個新函式 — 一個將其引數和 5 求和,另一個和 10 求和。
add5 和 add10 都是閉包。它們共享相同的函式定義,但是儲存了不同的詞法環境。在 add5 的環境中,x 為 5。而在 add10 中,x 則為 10。
閉包的作用域鏈包含著它自己的作用域,以及包含它的函式的作用域和全域性作用域。
- 閉包只能取得包含函式中的任何變數的最後一個值
//栗子1
function arrFun1(){
var arr = [];
for(var i = 0 ; i < 10 ; i++){
arr[i] = function(){
return i
}
}
return arr
}
console.log(arrFun1()[9]()); //10
console.log(arrFun1()[1]()); //10
//栗子2
function arrFun2(){
var arr = [];
for(var i = 0 ; i < 10 ; i++){
arr[i] = function(num){
return function(){
return num
};
}(i)
}
return arr
}
console.log(arrFun2()[9]()); //9
console.log(arrFun2()[1]()); //1
栗子 1 中,arr陣列中包含10個匿名函式,每個函式都可以訪問外部的變數 i , arrFun1 執行後,其作用域被銷燬,但它的變數依然存在記憶體中,能被迴圈中的匿名函式訪問,這是的 i 為 10;
栗子 2 中,arr陣列中有是個匿名函式,其匿名函式內還有匿名函式,最內層匿名函式訪問的 num 被 上一級匿名函式儲存在了記憶體中,所以可以訪問到每次的 i 的值。
如有錯誤,請斧正
以上