理解es6中的暫時性死區

引入

什麼是作用域?

一段程序代碼中所用到的名字並不總是有效/可用的,而限定這個名字的可用性的代碼範圍就是這個名字的作用域。

全局作用域

JS中沒有明確的全局作用域的概念,只有局部作用域以及全局執行環境的概念,全局執行環境被認爲是window對象,是最外圍的一個執行環境。因爲作用域的概念只是給後續聲明語句的闡述做一個鋪墊,所以這裏就不贅述了。

局部作用域

在外部無法訪問局部作用域中的變量

1、函數作用域

變量在聲明它們的函數體以及這個函數體嵌套的任意函數體內都是有定義的。在函數中聲明的變量只能在函數內部訪問。

function fn(){
	var  a = 1;
}
fn()
console.log(a)//ReferenceError: a is not defined
function fn(){
	let  a = 1;
}
fn()
console.log(a)//ReferenceError: a is not defined

在函數作用域中有一個特殊情況

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

在函數中沒有聲明,直接賦值一個變量時,這個變量會在函數執行之後成爲一個全局變量。

2、塊級作用域(ES6)

{}內部就是一個塊級作用域,ES5中沒有塊級作用域的概念,塊級作用域的概念是在ES6中出現的。
塊級作用域的概念只和let/const所聲明的變量有關,與var聲明的變量無關。

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

聲明變量的方式

1. var

在函數作用域或全局作用域中通過關鍵字var聲明的變量,無論在哪裏聲明的,都會被當成在當前作用域頂部聲明的變量。這就是我們常說的變量提升

function fn(){
	if(false){
		var a = 1;
	}else{
		console.log(a)//undeined
	}
}
fn();

等價於

function fn(){
	var a;
	if(false){
		a = 1;
	}else{
		console.log(a)//undeined
	}
}
fn();

var在全局執行環境下聲明的變量會成爲window對象的屬性

var  i =  1;
console.log(window.i);//1

2. let

ES6 新增了let命令,用來聲明變量。它的用法類似於var
官方說法是,let沒有變量提升。還有一個說法是,let存在變量提升,變量的聲明的過程爲,1.創建2.初始化(undefined)3.賦值,用let聲明的變量,它的創建提升了,但是它的初始化沒有提升。而用var聲明的變量,它的創建和初始化都進行了提升,這個點在後面我們會提到。

function fn(){
	if(false){
	    let a = 1;
	}else{
		console.log(a)//undeined
	}
}
fn();//ReferenceError: a is not defined

let所聲明的變量,只在let命令所在的代碼塊內有效。外界訪問不到塊級作用域中用let/const所聲明的變量。

{
    let a = 1
}
console.log(a)//a is not defined

{
    const  a = 1;
}
console.log(a)//a is not defined

與此同時,let/const所聲明的變量也會“綁定”這個塊級作用域,不再受外部的影響。

let a  = 2
{
    console.log(a);//1
    let a = 1;
}

這裏就符合了之前說的,用let聲明的變量,它的創建提升了,因此console.log(a)纔會知道,我這個塊級作用域裏有一個被聲明的變量a,但是它的初始化沒有提升,因此它會報錯,因爲要等到執行let a時,a變量纔會被初始化。

並且let不允許在相同作用域內,重複聲明同一個變量。

// 報錯
function func() {
  let a = 10;
  var a = 1;
}

// 報錯
function func() {
  let a = 10;
  let a = 1;
}

因此,不能在函數內部重新聲明參數。

function func(arg) {
  let arg;
}
func() // 報錯

function func(arg) {
  {
    let arg;
  }
}
func() // 不報錯

但是,可以在for循環內部重新聲明參數,因爲for循環有一個特別之處,就是設置循環變量的那部分是一個父作用域,而循環體內部是一個單獨的子作用域

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

接下來大家來看看這兩段代碼,猜測一下結果

let a = 2;
{
    console.log(a);
    var a = 1;
}


let a = 2;
{
    console.log(a);
    let a = 1;
}

結果揭曉:

let a = 2;
{
    console.log(a);
    var a = 1;
}
//Identifier 'a' has already been declared

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

第一段代碼報錯是因爲,對於var聲明的變量,是不存在塊級作用域的,因此我們用let和var在全局執行環境中聲明瞭a變量兩次,從而報錯。

第二段代碼報錯是因爲let聲明的變量a綁定了{},使{}成爲塊級作用域,塊級作用域內部的變量不再受外部的影響,又因爲變量a的調用在變量a的聲明之前,所以產生了暫時性死區的問題,這個問題我們等下會討論,這裏就不仔細講了。
上面代碼中,計數器i只在for循環體內有效,在循環體外引用就會報錯。

在全局執行環境中,用let聲明的變量不會成爲window的屬性

let i = 1;
console.log(window.i)//undefined

for循環的let,具有閉包的機制

for (let i = 0; i < 10; i++) {
	  liList[i].onclick = function(){
	    console.log(i)
	  }
}

如果上面的let用var代替,那麼每一個li被點擊之後,輸出的肯定是10。因爲函數綁定肯定在函數點擊之前被執行完畢,在那個時候,i的值已經變成了10。
但是由於let有閉包的性質,所以它會爲每一個執行的i創建一個獨立的作用域
等價於

for (var i = 0; i < 10; i++) {
	~function(i){//這個i是自執行函數的i
		  liList[i].onclick = function(){
	    	console.log(i)
	  }
	}(i)//這個i是傳進去的i
}

這樣,在點擊事件執行時,就會通過作用域鏈找到保存在函數上層執行環境中的變量i

3. const

const聲明一個只讀的常量。一旦聲明,常量的值就不能改變。

const PI = 3.1415;
PI // 3.1415

PI = 3;
// TypeError: Assignment to constant variable.

上面代碼表明改變常量的值會報錯。
因此,每個通過const聲明的變量必須進行初始化

const foo;
// SyntaxError: Missing initializer in const declaration

上面代碼表示,對於const來說,只聲明不賦值,就會報錯。

const的作用域與let命令相同:只在聲明所在的塊級作用域內有效。

if (true) {
  const MAX = 5;
}

MAX // Uncaught ReferenceError: MAX is not defined

暫時性死區

暫時性死區就是由於,let/const聲明變量時沒有變量提升所導致的。或者我們可以理解爲,在變量僅創建,還沒有初始化之時就使用了變量

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

var tmp = 123;

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

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

ES6 明確規定,如果區塊中存在let和const命令,這個區塊對這些命令聲明的變量,從一開始就形成了封閉作用域。凡是在聲明之前就使用這些變量,就會報錯。

總之,在代碼塊內,使用let命令聲明變量之前,該變量都是不可用的。這在語法上,稱爲“暫時性死區”(temporal dead zone,簡稱 TDZ)。

有些“死區”比較隱蔽,不太容易發現。

function bar(x = y, y = 2) {
  return [x, y];
}

bar(); // 報錯

上面代碼中,調用bar函數之所以報錯(某些實現可能不報錯),是因爲參數x默認值等於另一個參數y,而此時y還沒有聲明,屬於“死區”。
這說明默認賦值有可能導致暫時性死區
我看到網上有一個說法說,上面的代碼出現暫時性死區的原因是因爲,函數參數的默認賦值,其實是用let聲明的
即等價於下面的代碼

function bar(let x = y, let y = 2) {
  return [x, y];
}

bar(); // 報錯

經過我的探究,這個說法是不正確的,首先我們來做一個小測試

function fn(x = 1){
    let x = 2;
    console.log(x);
}
fn()//Identifier 'x' has already been declared

function fn(x = 1){
    var x = 2;
    console.log(x);
}
fn()//2

第一個測試可以說明,函數參數的默認賦值和函數內部是同一作用域,這樣函數纔會因爲變量x的重複聲明而報錯
第二個測試可以說明,函數參數的默認賦值不是用let聲明的,這樣函數內部用var重複聲明變量x的時候纔不會報錯。

那麼,如果函數參數的默認賦值不是用let聲明的,那麼是用var聲明的嗎?首先我們在函數內部使用var聲明試試

function fn(){
    var x = 1;
    var x = 3;
    var x = 2;
    console.log(x)
}
fn()//2

我們試試函數的默認賦值

function fn(x = 1,x = 3){
    var x = 2;
    console.log(x);
}
fn()//Duplicate parameter name not allowed in this context
//意思是此上下文中不允許重複的參數名

因此我們可以做出以下總結,函數參數的默認賦值,在函數的參數括號列表內部類似於let聲明,因爲不允許有重複的參數名,且存在暫時性死區的現象
而在函數作用域中,又類似於var聲明,因爲允許重複聲明。並且參數已經顯示聲明在函數頂部了,類似於變量提升。
以上。

本文參考
《深入理解es6》
《ECMAScript 6 入門》http://es6.ruanyifeng.com/#docs/object
https://blog.csdn.net/nicexibeidage/article/details/78144138
https://www.zhihu.com/people/zhihusucks/activities

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章