天天看點

var,let和const深入解析(一)

es6有許多特别棒的特性,你可能對該語言的整體非常熟悉,但是你知道它在内部是如何工作的嗎?當我們知道它的内部原理以後,我們使用起來也會更加的安心一些。這裡我們想逐漸的引導你,讓你對其有一個更深入,更淺顯的認識。讓我們就先從es6中的變量開始講起吧。

let和const

在es6中新引入了兩種方式來申明變量,我們仍然可以使用廣為傳誦的var變量(然而你不應該繼續使用它了,繼續閱讀來了解其中原因),但是現在我們有了兩種更牛的工具去使用:let和const。

let

let和var非常的相似,在使用方面,你可以使用完全相同的方式來聲明變量,例如:

let myNewVariable = 2;
var myOldVariable = 3;

console.log(myNewVariable); // 2
console.log(myOldVariable); // 3
           

但是實際上,他們之間有幾處明顯的不同。他們不僅僅是關鍵字變了,而且實際上它還讓會簡化我們的一些工作,防止一些奇怪的bug,其中這些不同點是:

let是塊狀作用域(我将會在文章後面着重講一下作用域相關的東西),而var是函數作用域。

let不能在定義之前通路該變量(var是可以的,它确實是js世界中許多bug和困擾的源頭)。

let不能被重新定義。

在我們講解這些不同點之前,首先我們看一個更酷的變量:const

const

const和let非常像(跟var相比來說,他們之間有許多相同點),但是他們有一個主要的不同點:let可以重新指派,但是const不能。是以const定義的變量隻能有一個值,并且這個值在聲明的時候就會被指派。是以我們來看下下面的例子。

const myConstVariable = 2;
let myLetVariable = 3;

console.log(myConstVariable); // 2
myLetVariable = 4;  // ok
myConstVariable = 5;  //wrong - TypeError thrown
           

但是const是完全不可變的嗎?

有一個常見的問題:雖然變量不能被重新指派,但是也不能讓他們真正的變為不可變的狀态。如果const變量有一個數組或者對象作為其值的話,你可能會像下面的代碼一樣改變它的值。

const myConstObject = {mutableProperty: 2};

// myConstObject = {}; - TypeError
myConstObject.mutableProperty = 3; //ok
console.log(myConstObject.mutableProperty); // 3
const myConstArray = [1];

// myConstArray = []; - TypeError
myConstArray.push(2) //ok
console.log(myConstArray); // [1, 2]
           

當然你不能用原始資料類型的值,比如string,number,boolean等,因為他們本質上是不可變的。

真正的不可變

如果你想讓我們的變量真正的不可變的話,可以使用Object.freeze(), 它可以讓你的對象保持不可變。不幸的是,他僅僅是淺不可變,如果你對象裡嵌套着對象的話,它依然是可變的。

const myConstNestedObject = {
  immutableObject: {
    mutableProperty: 1
  }
};

Object.freeze(myConstNestedObject);

myConstNestedObject.immutableObject = 10; // won't change
console.log(myConstNestedObject.immutableObject); // {mutableProperty: 1}
myConstNestedObject.immutableObject.mutableProperty = 10; // ok
console.log(myConstNestedObject.immutableObject.mutableProperty); // 10
           

變量的作用域

在介紹了一些基礎知識以後,下面我們要進入一個更進階的話題。現在我們要開始講解es5和es6變量中的第一個不同-作用域

注意:下面的例子都用的是let,它的規則在const上同樣也适用

全局變量和函數作用域變量

在js中,究竟什麼是作用域呢?本文不會給出一個關于作用域的完整解釋。簡單來說,變量的作用域決定了變量的可用位置。從不同的角度來看,可以說作用域是你可以在特定區域内使用的那些變量(或者是函數)的聲明。作用域可以是全局的(是以在全局作用域中定義的變量可以在你代碼中任何部分通路)或者是局部的。

很顯然,局部作用域隻能在内部通路。在ES6以前,它僅僅允許一種方式來定義局部作用域 - function,咱們來看一下下面的例子:

// global scope
var globalVariable = 10;

function functionWithVariable() {
  // local scope
  var localVariable = 5;
  console.log(globalVariable);  // 10
  console.log(localVariable);   // 5
}

functionWithVariable();

//global scope again
console.log(globalVariable);  // 10
console.log(localVariable);   // undefined
           

上面的例子中,變量globalVariable是全局變量,是以它可以在我們代碼中的函數内或者是其他區域内被通路到,但是變量localVariable定義在函數内,是以它隻在函數内可通路。

是以,所有在函數内建立的内容都可以在函數内被通路到,包括函數内部裡所有的嵌套函數(可能會被嵌套多層)。在這裡可能要感謝閉包了,但是在文章裡我們并不打算介紹它。不過請繼續關注,因為我們在未來的博文中,會更多的介紹它。

提升

簡單來說,提升是一種吧所有的變量和函數聲明“移動”到作用域的最前面的機制。讓我們看一下下面的例子。

function func() {
  console.log(localVariable);   // undefined
  var localVariable = 5;

  console.log(localVariable);   // 5
}

func();
           

它為什麼依然會正常工作呢?我們還沒有定義這個變量,但是它依然通過console.log()列印出了undefined。為什麼不會報出一個變量未定義的錯誤呢?讓我們再仔細看一下。

編譯變量

Javascript解析器要周遊這個代碼兩次。第一次被稱為編譯狀态,這一次的話,代碼中定義的變量就會提升。在他之後,我們的代碼就變成類似于下面的這樣子的(我已經做了一些簡化,隻展示出相關的部分)。

function func() {
  var localVariable = undefined;

  console.log(localVariable); // undefined
  localVariable = 5;

  console.log(localVariable); // 5
}

func();
           

我們看到的結果是,我們的變量localVariable已經被移動到func函數的作用域的最前面。嚴格來說,我們變量的聲明已經被移動了位置,而不是聲明的相關代碼被移動了位置。我們使用這個變量并列印出來。它是undefined是因為我們還沒有定義它的值,它預設使用的undefined。

提升的例子 - 會出什麼問題

來讓我們看一個令人讨厭的例子,我們的作用域範圍對于我們來說,是弊大于利的。也不是說函數作用域是不好的。而是說我們必須要警惕一些由于提升而引起的一些陷進。我們來看看下面的代碼:

var callbacks = [];
for (var i = 0; i < 4; i++) {
  callbacks.push(() => console.log(i));
}

callbacks[0]();
callbacks[1]();
callbacks[2]();
callbacks[3]();
           

你認為輸出的值是多少呢?你猜可能是0 1 2 3,是嗎?如果是的話,對于你來說,可能會有一些驚喜。實際上,他真實的結果是4 4 4 4。等等,它到底發生了什麼?我們來“編譯”一下代碼,代碼現在看起來就像這樣:

var callbacks;
var i;

callbacks = [];
for (i = 0; i < 4; i++) {
  callbacks.push(() => console.log(i));
}

callbacks[0]();
callbacks[1]();
callbacks[2]();
callbacks[3]();
           

你看出問題所在了嗎?變量i在整個作用域下都是可以被通路到的,它不會被重新定義。它的值隻會在每次的疊代中不斷地被改變。然後呢,當我們随後想通過函數調用列印它的值得時候,他實際上隻有一個值 - 就是在最後一次循環賦給的那個值。

我們隻能這樣了嗎?不是的

Let和Const的拯救

除了定義變量的新方式以外,還引入了一種新的作用域:塊級作用域。塊就是由花括号括起來的所有的内容。是以它可以是if,while或者是for聲明中的花括号,也可以是單獨的一個花括号甚至是一個函數(對,函數作用域是塊狀作用域)。let和const是塊作用域。意味着無論你在塊中無論定義了什麼變量,什麼時候定義的,它都不會跑到塊作用域外面去。我們來看一下下面的例子:

function func() {
  // function scope
  let localVariable = 5;
  var oldLocalVariable = 5;

  if (true) {
    // block scope
    let nestedLocalVariable = 6;
    var oldNestedLocalVariable = 6;

    console.log(nestedLocalVariable); // 6
    console.log(oldNestedLocalVariable); // 6
  }

  // those are stil valid
  console.log(localVariable); // 5
  console.log(oldLocalVariable); // 5
  // and this one as well
  console.log(oldNestedLocalVariable); // 6
  // but this on isn't
  console.log(nestedLocalVariable); // ReferenceError: nestedLocalVariable is not defined
           

你能看出來差别嗎?你能看出來怎麼使用let來解決早些時候提出問題的嗎?我們的for循環包含一組花括号,是以它是塊作用域。是以如果在定義循環變量的時候,使用的是let或者是const來代替var的話,代碼會轉為下面的形式。注意:我實際上已經簡化了很多,不過我确定你能了解我的意思。

let callbacks = [];
for (; i < 4; i++) {
  let i = 0 //, 1, 2, 3
  callbacks.push(() => console.log(i));
}

callbacks[0]();
callbacks[1]();
callbacks[2]();
callbacks[3]();
           

現在的每一次循環都有它自己的變量定義,是以變量不會被重寫,我們确信這行代碼可以完成讓他做的任何事情。

這是這一部分結束的例子,但是我們再看一下下面的例子,我相信你明白列印出來的值的原因,以及對應的表現是什麼。

function func() {
  var functionScopedVariable = 10;
  let blockScopedVariable = 10;

  console.log(functionScopedVariable);  // 10
  console.log(blockScopedVariable);  // 10
  if (true) {
    var functionScopedVariable = 5;
    let blockScopedVariable = 5;

    console.log(functionScopedVariable);  // 5
    console.log(blockScopedVariable);  // 5
  }

  console.log(functionScopedVariable);  // 5
  console.log(blockScopedVariable);  // 10
}

func();
           

本文翻譯自:

https://blog.pragmatists.com/let-your-javascript-variables-be-constant-1633e56a948d

本文轉載自:

http://www.lht.ren/article/15/

繼續閱讀