做用域、做用域鏈、閉包

1、做用域

做用域,是指變量的生命週期。javascript

  • 全局做用域
  • 函數做用域
  • 塊級做用域
  • 詞法做用域
  • 動態做用域

一、全局做用域前端

全局變量:生命週期將存在於整個程序以內。能被程序中任何函數或者方法訪問。在javascript內默認是能夠被修改的。java

1.1顯示聲明node

帶有關鍵字var的聲明:面試

var a = 1;
var f = function(){
    console.log("come on");
};
//全局變量會掛在到window對象上
console.log(window.a);
console.log(window.f);複製代碼


1.2隱式聲明編程

function f(num){
    result = num + 1;
    return result;
}
f(1);
console.log(window.result);複製代碼


不帶有var關鍵字的變量result,也被隱式聲明爲一個全局變量。掛載在window對象上。數組

二、函數做用域瀏覽器

function f(){
    var a = 1;
}
console.log(a);複製代碼


2.1如何訪問函數做用域內的變量呢?bash

法一:經過return返問函數內部變量閉包

function f(num){
    var result = num++;
    return result;
}
console.log(f(1));複製代碼


function f(num){
    var result = num + 1;
    return result;
}
console.log(f(1));複製代碼


function f(num){
    var result = ++num;
    return result;
}
console.log(f(1));複製代碼


以上三段程序也體現了i++和++i的區別。

法2、經過閉包訪問函數內部的變量

function outer(){
    var value = 'inner';
    return function inner(){
        return value;
    }
}
console.log(outer()());複製代碼


2.2當即執行函數

當即執行函數可以自動執行(function(){})()裏面包裹的內容,可以很好地消除全局變量的影響。

(function(){
    var a = 1;
    var foo = function(){
        console.log("haha");
    }
})();
console.log(window.a);
console.log(window.foo);複製代碼


三、塊級做用域

在 ES6 以前,是沒有塊級做用域的概念的。

for(var i = 0; i < 3; i++){

}
console.log(i);   複製代碼


很明顯,用 var 關鍵字聲明的變量,存在變量提高,至關於:

var i;
for(i = 0; i < 3; i++){

}
console.log(i);   複製代碼

若是須要實現塊級做用域,可使用let關鍵字,let關鍵字是不存在變量提高的。

for(let i = 0; i < 3; i++){

}
console.log(i);
複製代碼


一樣能造成塊級做用域的還有const關鍵字。

if(true){
    const a = 1;
}
console.log(a);複製代碼


塊級做用域的做用以及常考的面試題

for(var i = 0; i < 3; i++){
    setTimeout(function(){
        console.log(i);
    },200);
}複製代碼


爲何i是3呢?

緣由由於var聲明的變量能夠進行變量提高,i是在全局做用域裏面的,for()循環是同步函數,setTimeout是異步操做,異步操做必須等到全部的同步操做執行完畢後才能執行,執行異步操做以前i已是3,因此以後會輸出同一個值3。

如何讓它按咱們想要的結果輸出呢?

法一:最簡單使用let

for(let i = 0; i < 3; i++){
    setTimeout(function(){
        console.log(i);
    },200);
}
複製代碼


法二:調用函數,建立函數做用域;

for(var i = 0; i < 3; i++){
    f(i);
}
function f(i){
    setTimeout(function(){
        console.log(i);
    },200);
}複製代碼


法3、當即執行函數

for(var i = 0; i < 3; i++){
    (function(j){
        setTimeout(function(){
            console.log(j);
        },200) 
    })(i);
}複製代碼


當即執行函數同函數調用,先把for循環中的i記錄下來,而後把i賦值給j,而後輸出0,1,2。

四、詞法做用域

函數的做用域在函數定義的時候就決定了。

var value = 'outer';
function foo(){
    var value = 'middle';
    console.log(value);   //middle
    function bar(){
        var value = 'inner';
        console.log(value);  //innner
    }
    return bar();
}
foo();
console.log(value);   //outer複製代碼

當咱們要使用聲明的變量時:JS引擎總會從最近的一個域,向外層域查找;


例:面試題

var a = 2;
function foo(){
    console.log(a);
}
function bar(){
    var a = 3;
    foo();
}
bar();複製代碼


若是是詞法做用域,也就是如今的javascript環境。變量a首先會在foo()函數裏面查找,若是沒有找到,會根據書寫的位置,查找上一層的代碼,在這裏是全局做用域,找到並賦值爲2,因此控制檯輸出2。

咱們說過,詞法做用域是寫代碼的時候就靜態肯定下來的。Javascript中的做用域就是詞法做用域(事實上大部分語言都是基於詞法做用域的),因此這段代碼在瀏覽器中運行的結果是輸出 2

做用域的"遮蔽"

做用域查找從運行時所處的最內部做用域開始,逐級向外或者說向上進行,直到碰見第一個匹配的標識符爲止。在多層的嵌套做用域中能夠定義同名的標識符,這叫做「遮蔽效應」,內部的標識符「遮蔽」了外部的標識符。

var a = 0;
function test(){
    var a = 1;
    console.log(a);//1
}
test();複製代碼

var a = 0;
function test(){
    var a = 1;
    console.log(window.a);//0
}
test();複製代碼

經過這種技術能夠訪問那些被同名變量所遮蔽的全局變量。但非全局的變量若是被遮蔽了,不管如何都沒法被訪問到。

五、動態做用域

而動態做用域並不關心函數和做用域是如何聲明以及在何處聲明的,只關心它們從何處調用

動態做用域,做用域是基於調用棧的,而不是代碼中的做用域嵌套;

請聽面試題:

var x = 3;
var y = 3;
function A(y){
    var x = 2;
    var num = 3;
    num++;    //4
    function B(num){
        return x * y * num;    //x,y,num:1,5,4
    }
    x = 1;
    return B;
}
console.log(A(5)(4));
複製代碼


解析:

本題的關鍵在於肯定x的值,函數B是在 return B;時執行的,因此x的值在函數調用前已經修改成1;因此返回20。

2、做用域鏈

每個 javaScript 函數都表示爲一個對象,更確切地說,是 Function 對象的一個實例。

Function 對象同其餘對象同樣,擁有可編程訪問的屬性。和一系列不能經過代碼訪問的屬性,而這些屬性是提供給 JavaScript 引擎存取的內部屬性。其中一個屬性是 [[Scope]] ,由 ECMA-262標準第三版定義。

內部屬性 [[Scope]] 包含了一個函數被建立的做用域中對象的集合。

這個集合被稱爲函數的 做用域鏈,它能決定哪些數據能被訪問到。

來源於:《 高性能JavaScript 》;

例:

function add(x,y){
    return x + y;
}
console.log(add.prototype);
複製代碼


[[Scope]] 屬性下是一個數組,裏面保存了,做用域鏈,此時只有一個 global

理解詞法做用域的原理

var a = 2;
function foo(){
    console.log(a);   //2
    console.log(foo.prototype);
}
function bar(){
    var a = 3;
    console.log(a);   //3
    foo();
    console.log(bar.prototype);
}
bar();複製代碼

node環境下:



瀏覽器下:



疑惑:爲何在node中scopes數組中有兩個對象,在瀏覽器中scopes數組中只有一個對象。

緣由:node的模塊化,本質上也是在外層添加一個匿名函數,由於node的模塊化,在編譯的時候,給每一個JS文件外部包裹一個匿名函數。因此會出現scopes中有兩個對象。展開scopes[0],會發現裏面確實包含在瀏覽器中是全局的變量或全局的函數,而在node環境下因爲多包裹了一層匿名函數,會讓它存在於closure中。

var a = 2;
function bar(){
    var a = 3;
    console.log(a);   //3
    foo();
    console.log(bar.prototype);
    function foo(){
        console.log(a);   //3
        console.log(foo.prototype);
    }
}
bar();複製代碼

node環境下:



瀏覽器下:



全局做用域鏈是在全局執行上下文初始化時就已經肯定了。

證實:

console.log(add.prototype);     //1聲明前
function add(x,y){
    console.log(add.prototype);  //2運行時
    return x + y;
}
add(1,2);
console.log(add.prototype);   //3執行後
複製代碼

1聲明前


2運行時


3執行後


做用域鏈是在 JS 引擎完成初始化執行上下文環境就已經肯定了。

理解做用域鏈的好處:若是做用域鏈越深, [0] => [1] => [2] => [...] => [n],咱們調用的是全局變量,它永遠在最後一個(這裏是第 n 個),這樣的查找到咱們須要的變量會引起多大的性能問題,因此,儘可能將 全局變量局部化 ,避免做用域鏈的層層嵌套。

理解執行上下文


  • 在函數未調用以前,add 函數的[[Scope]]屬性的做用域鏈裏面已經有以上內容。
  • 當執行此函數時,會創建一個稱爲 執行上下文 (execution context) 的內部對象。一個 執行上下文 定義了一個函數執行時的環境,每次調用函數,就會建立一個 執行上下文 ;一旦初始化 執行上下文 成功,就會建立一個 活動對象 ,裏面會產生 this arguments 以及咱們聲明的變量,這個例子裏面是 xy
創建執行上下文階段


結束執行上下文階段


做用域鏈和執行上下文的關係

做用域鏈本質上是指向一個指針,指向變量對象列表。建立函數時,會把全局變量對象的做用域鏈添加在[[Scope]]屬性中。調用函數時,會爲函數建立一個執行環境的做用域鏈,而且建立一個活動對象,並將其推入執行環境做用域鏈的前端。函數局部環境的變量對象只有在函數執行的過程當中才存在。通常來說,當函數執行完畢後,局部活動對象就會被銷燬,內存中僅保存全局做用域。可是閉包的狀況有所不一樣。

3、閉包

function createComparisonFunction(propertyName){
    return function(object1,object2){
        var value1 = object1[propertyName];
        var value2 = object2[propertyName];
        if(value1 < value2){
            return -1;
        }else if(value1 > value2){
            return 1;
        }else{
            return 0;
        }
    }
}
//建立函數
var compareNames = createComparisonFunction('name');
//調用函數
var result = compareNames({name:"Nicholas"},{name:"Greg"});
//解除對匿名函數的引用
compareNames = null;複製代碼

調用compareNames()函數的過程當中產生的做用域鏈之間的關係圖以下


createComparisonFunction執行完畢後,其執行環境的做用域鏈會被銷燬,但其活動對象不會被銷燬,由於匿名函數的做用域鏈還在引用這個活動對象。直到匿名函數被銷燬後,createComparisonFunction()的活動對象纔會被銷燬。

閉包與變量

function createFunctions(){
    var result = new Array();
    for(var i = 0; i < 10; i++){
        result[i] = function(){
            return i;
        };
    }
    return result;
}
console.log(createFunctions()[0]());   //10複製代碼

實際上每一個函數都會返回10。緣由:由於每一個函數的做用域鏈中都會保存着createFunctions()函數的活動對象,因此它們引用的都是同一個變量。

如何讓閉包輸出想要的結果呢?

function createFunctions(){
    var result = new Array();
    for(var i = 0; i < 10; i++){
        result[i] = function(num){
            return function(){
                return num;
            }
        }(i);
    }
    return result;
}
for(var j = 0; j < 10; j++){
    console.log(createFunctions()[j]());
}複製代碼


理解:咱們沒有直接把閉包賦值給數組,而是定義了一個匿名函數,而後給該匿名函數傳入參數,因爲參數是按值傳遞的,因此至關於把當前的i賦值給num,再在該匿名函數內生成一個閉包,返回num。

參考:理解 JS 做用域鏈與執行上下文

相關文章
相關標籤/搜索