ES5和ES6做用域詳解

ES5的做用域

變量起做用的範圍,js中能建立做用域的只能是函數javascript

{
  let a = 1;
  var b = 2;
}
console.log(a); // a is not defined
console.log(b); // 2

var的做用域就是所在的函數體html

let的做用域就是所在的代碼塊前端

詞法做用域和函數做用域

當代碼寫好的時候,可以根據代碼的結構肯定變量的做用域,這種狀況下的做用域就是詞法做用域。js就是此法做用域,不是動態做用域。java

在某個函數中使用var聲明變量,那個變量就將被視做一個局部變量,只存在於函數中.es6

函數在調用結束時,該函數<wiz_tmp_highlight_tag class="cm-searching" style="margin-top: 0px; background: yellow;">做用域</wiz_tmp_highlight_tag>會被銷燬,裏面的全部局部變量也會被銷燬。web

做用域鏈

console.log(a); // a is not defined

function test() {
   a = 1;
}
test();

(根據javascript高級程序設計第四章)解析上面的代碼面試

  1. js中存在全局執行環境和由函數造成的局部執行環境這兩種,統稱爲執行環境(這裏能夠理解成做用域);
  2. 執行環境都會對應一個變量對象,包含當前環境的變量和函數(函數中的參數也做爲函數執行環境的變量,即函數所在做用域的局部變量,函數的活動對象包括this、arguments以及內部的變量和函數);
  3. 只有當函數執行時會造成做用域鏈,做用域鏈的前端始終是當前執行代碼所在的執行環境對應的變量對象,日後是下一個(外部)變量對象,直到最外邊的全局執行環境的變量對象(所謂的做用域鏈就是變量對象組成的一條線);
  4. 變量對象中變量的解析查找就是沿着做用域鏈一級一級查找;
  5. 若是執行環境中的變量沒有用var聲明,那麼在函數執行時(這樣纔會造成做用域鏈)會沿着做用域鏈一級一級查找變量對象,若是沒有找到則會在全局變量對象中聲明該變量並初始化。

綜上,上面的代碼能夠改寫成下面這樣數組

function test() {
   a = 1;
}
test();
console.log(a); // 1

閉包

閉包是指有權訪問另外一個函數做用域中的變量的函數瀏覽器

首先區分一點就是函數內部定義的函數,其做用域鏈會包括外部函數。請看下面兩個例子對比閉包

// 案例一

function foo(){
  var num = '123';
  function bar(){
    console.log(num); // '123'
  }
  bar();
}
foo();

案例二
function bar(){
  console.log(num);// num is not defined
}
function foo(){
  bar();
}
foo();
<script>
    var arr = [];
    for(var i = 0; i<10; i++) {
        arr.push(function(){
        console.log(i);
    })
}
    arr[0]();
    arr[1]();

</script>

上述答案是輸出10

分析: 數組中每一個函數若是執行時其做用域鏈都會保存全局執行環境對應的變量對象,因此函數執行時函數內部的i變量會沿着做用域鏈找到全局執行環境中的變量,此時全局執行環境中的變量i爲10,因此都會輸出10.

若是想輸出1,2,3...,
第一種方法能夠把for循環中的var變爲let,變量ilet聲明的,當前的i只在本輪循環有效,因此每一次循環的i其實都是一個新的變量,因此最後輸出的是6。你可能會問,若是每一輪循環的變量i都是從新聲明的,那它怎麼知道上一輪循環的值,從而計算出本輪循環的值?這是由於 JavaScript 引擎內部會記住上一輪循環的值,初始化本輪的變量i時,就在上一輪循環的基礎上進行計算。摘自《ECMAScript 6 入門》
第二種方法就象下面案例一下再寫一個for循環;
第三個方法就如同案例三在函數內部再定義一個函數,並當即執行外部函數,使函數的活動對象中變量i的值每次都不一樣,從而保證內部函數在執行時其做用域鏈會包括外部函數的變量對象。
(通常來講函數執行完畢該函數的做用域和活動變量會被銷燬,可是由於函數裏面定義的函數它的做用域鏈始終會包括外部函數的活動對象,因此外面的函數即便當即執行了,可是活動對象還在內存中,沒有被銷燬)

下面的案例再次鞏固知識點,第二個案例再執行時,全局執行環境的變量對象i又從新被動態賦值,for循環中函數當即執行,由於函數的參數是按值傳遞的,因此每一個函數獲得的是不一樣的i值。

// 案例一
var arr = [ { name: '張三1'},
            { name: '張三2' },
            { name: '張三3' },
            { name: '張三4' } ];
for ( var i = 0; i < arr.length; i++) {
     arr[ i ].sayHello = function () {
         console.log(i);
      };
}
arr[0].sayHello();
arr[1].sayHello();
// 案例二
var arr = [ { name: '張三1'},
            { name: '張三2' },
            { name: '張三3' },
            { name: '張三4' } ];
for ( var i = 0; i < arr.length; i++) {
    arr[ i ].sayHello = function () {
        console.log(i);
    };
}
for ( var i = 0; i < arr.length; i++ ) {
    arr[ i ].sayHello();
}
// 案例三
var arr = [ { name: '張三1'},
            { name: '張三2' },
            { name: '張三3' },
            { name: '張三4' } ];
for ( var i = 0; i < arr.length; i++) {
    arr[ i ].sayHello = (function (i) {
       return function(){
          console.log(i);
       }
    })(i);
}

變量提高

分爲預解析階段和執行階段

在預解析階段,會將全部的變量聲明(只提高聲明不提高賦值)以及函數聲明(指整個函數),提高到其所在的做用域的最頂上,通常會先提高函數聲明再提高變量聲明。

注意區分函數聲明和函數表達式聲明的區別
在變量提高條件下函數表達式和通常變量的聲明的規則是同樣的。下面的條件式聲明章節還會用案例做對比

函數聲明變量提高

  • 函數聲明會被提高(是指整個函數都會被提高)
// 函數聲明

fn();

 function fn() {
   console.log('hello world');
}
  • 函數表達式不會被提高(是指只會提高聲明該函數的變量)
// 函數表達式
fn();

var fn = function() {
  console.log('nihao');
}

如下是變量提高中的特別狀況

在變量提高狀況下,變量通常被分紅兩種,即通常變量和函數名變量

變量和函數同名

console.log(typeof f);  // function

var f;

console.log(typeof f); // undefined

function f(){};

console.log(typeof f); // undefined
console.log(typeof a); // function

function a() { }

console.log(typeof a); // function

var a = '';

console.log(typeof a);  // string

<font color="red">只提高函數對應變量,其餘變量直接不提高,同時將變量的聲明var去掉(在通常定義過程當中不推薦使用同名變量)</font>

函數和函數同名

都提高,可是後面的會覆蓋前面的

func(); // second func
function func(){
    console.log("first func");
}

function func(){
    console.log("second func");
}

變量和變量同名

// 通常的變量同名對於變量的提高沒有影響,由於提高的只是變量的聲明,不會提高變量的賦值

console.log(typeof a); // undefined

var a = 'abc';

console.log(a); // 'abc'

var a = 1;

console.log(a); // 1

下面有兩個小栗子

var a = 1;
var a = 2;
console.log(a); // 2
var a = 1;
var a;
console.log(a); //1

剛剛去查了資料,參考《JavaScript高級程序設計》第7.3章節,原話以下

JavaScript歷來不會告訴你是否屢次聲明瞭同一個變量;遇到這種狀況,它只會對後續的聲明視而不見(不過,它會執行後續聲明中的變量初始化)。

變量提高是分段的

段是指<script></script>標籤,代碼執行時不分段的

<script>

        var num = 10;

        func(); // 第二個func
        
        console.log(str); // 報錯

        function func(){
            console.log("第一個func");
        }

        function func(){
            console.log("第二個func");
        }

    </script>

    <script>
        var str = 'abc';
        
        console.log(num); // 10
        
        func(); // 第三個func
        function func(){
            console.log("第三個func");
        }
    </script>

ES5塊級做用域中的函數聲明

ES5 規定,函數只能在頂層做用域和函數做用域之中聲明,不能在塊級做用域聲明。若是確實須要,也應該寫成函數表達式,而不是函數聲明語句。

test();  //報錯

if(true){
    function test(){
    console.log("我是在if語句中聲明的函數");
  }
}
// 各個瀏覽器執行結果不一樣,不建議這麼寫
if(flag){
   functiont test(){
      console.log("flag爲true時執行");
  }
}else{
    function test(){
      console.log("flag爲false時執行");
    }
}

上面兩種狀況在ES5中都是非法的,能夠將上面的demo改寫成下面這樣

// 下面會根據flag狀態決定執行哪段代碼
if(flag){
    test = functiont(){
      console.log("flag爲true時執行");
  }
}else{
    test = function(){
      console.log("flag爲false時執行");
    }
}

條件式變量聲明能夠被提高。

console.log( num ); // undefined
if ( false ) {
   var num = 123;
}
console.log( num ); // undefined

ES6的做用域

塊級做用域

塊級做用域就是包含在{}裏面的

function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}

ES6塊級做用域中函數聲明

ES6 引入了塊級做用域,明確容許在塊級做用域之中聲明函數。ES6 規定,塊級做用域之中,函數聲明語句的行爲相似於let,在塊級做用域以外不可引用。

原來,若是改變了塊級做用域內聲明的函數的處理規則,顯然會對老代碼產生很大影響。爲了減輕所以產生的不兼容問題,ES6在附錄B裏面規定,瀏覽器的實現能夠不遵照上面的規定,有本身的行爲方式

  • 容許在塊級做用域內聲明函數。
  • 函數聲明相似於var,即會提高到全局做用域或函數做用域的頭部。
  • 同時,函數聲明還會提高到所在的塊級做用域的頭部。

下面的例子可以很好的區分解釋ES5和ES6兩個環境下處理塊級做用域中函數聲明的區別

function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重複聲明一次函數f
    function f() { 
      console.log('I am inside!'); 
    }
  }
  f();
}());
// ES5 環境
function f() { console.log('I am outside!'); }

(function () {
  function f() { console.log('I am inside!'); }
  if (false) {
  }
  f(); // 輸出I am inside!
}());
// 瀏覽器的 ES6 環境
function f() { console.log('I am outside!'); }
(function () {
  var f = undefined;
  if (false) {
    function f() { console.log('I am inside!'); }
  }

  f();
}());
// Uncaught TypeError: f is not a function

let和const都存在暫時性死區

只要塊級做用域內存在let命令,它所聲明的變量就「綁定」(binding)這個區域,再也不受外部的影響。

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

上面代碼中,存在全局變量tmp,可是塊級做用域內let又聲明瞭一個局部變量tmp,致使後者綁定這個塊級做用域,因此在let聲明變量前,對tmp賦值會報錯。

或者

{
  var a = 1;
  let a = 1;
}
// 報錯 Uncaught SyntaxError: Identifier 'a' has already been declared

「暫時性死區」是指在使用let命令聲明變量以前,該變量都是不可用的。

if (true) {
  let tmp;
  tmp = 'abc'; // abc  
}

let&const相同點

  1. 支持塊級做用域;
  2. 變量不能提高;
  3. 存在暫時性死區
  4. 不可重複聲明

let&const區別

  • let聲明變量,const聲明常量
  • const聲明時必須賦值
const foo;
// SyntaxError: Missing initializer in const declaration
  • 聲明基本數據類型必須是寫死的常量
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
  • const聲明引用數據類型必須是變量的內存地址不變

ES5 只有兩種聲明變量的方法:var命令和function命令。ES6 除了添加letconst命令,還有import命令和class命令。因此,ES6 一共有6種聲明變量的方法。

ES6中頂層對象

  • 現狀: ES5頂層對象很混亂
    瀏覽器裏面,頂層對象是window,但 Node 和 Web Worker 沒有window

瀏覽器和 Web Worker 裏面,self也指向頂層對象,可是Node沒有self
Node 裏面,頂層對象是global,但其餘環境都不支持。(詳見http://es6.ruanyifeng.com/#do...

  • ES6的變更
    ES6 爲了改變這一點,一方面規定,爲了保持兼容性,var命令和function命令聲明的全局變量,依舊是頂層對象的屬性;另外一方面規定,let命令、const命令、class命令聲明的全局變量,不屬於頂層對象的屬性。也就是說,從 ES6 開始,全局變量將逐步與頂層對象的屬性脫鉤。

ES6中也存在做用域鏈

let test = 'out';
function  f(){
  test = 'in';
  console.log(window.test);// undefined
}
f();
console.log(test);// in

上下兩個demo的區別就是test有沒有用let聲明,當使用let聲明時,瀏覽器會認爲當前的環境是ES6環境,因此聲明的變量不會複製給window;相反若是沒用let聲明test,瀏覽器就會默認當前環境是ES5。

test = 'out';
function  f(){
  test = 'in';
  onsole.log(window.test); // in
}
f();
console.log(test);// in

經典面試題案例

案例一

var num = 123;
   function f1() {
       console.log( num );
    }

   function f2() {
        num = 456;
        f1();
    }

f2();

上述執行結果爲456

相關文章
相關標籤/搜索