let const var 比較說明


現在先來做兩道練習題

for(var i=0;i<10;i++){
    var a='a'
    let b = 'b'
}
console.log(a)
console.log(b)
for(var i=0;i<3;i++){
    setTimeout(function(){
        console.log(i)
    },1000)
}
function(){
    console.log(a)
    var a =1;
    function a(){}
    console.log(a)
}

理解js作用域

作用域我自己的理解是變量在某個的范圍內可訪問,那這個范圍就是這個變量的作用域
在ES5中,js只有兩種形式的作用域:全局作用域函數作用域

  • 全局作用域:變量在程序中任意地方都可以訪問到
  • 函數作用域:變量在函數內部可以訪問到,在函數外部無法訪問
for(var i=0;i<10;i++){
    var a='a'
    let b = 'b'
}
console.log(a) //'a'
console.log(b) //'b' is not defined

上述代碼中,變量a為全局變量

function test(){
    var a = 'a'
}
test()
console.log(a) // 函數外部無法直接訪問函數內部變量,報錯

上述代碼中,變量a為局部變量,控制台打印報錯信息a is not defined

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

函數內部未使用var關鍵字定義變量,此時a為全局變量

小結

  • ES5中, js的作用域分為全局作用域和函數作用域
  • 函數內部可以訪問函數外部的全局變量,函數外部卻無法直接訪問函數內部的局部變量
  • 未使用var關鍵字定義的變量是全局變量

現在我們知道函數內部可以訪問函數外部的全局變量,函數外部卻無法直接訪問函數內部的局部變量。但有的時候我們需要讀取函數內部的局部變量,該怎么辦呢?


閉包

舉個例子:

function f1(){
    var a = 1;
    function f2(){
        return a;
    }
    return f2;
}
console.log(f1()()) //1

上述代碼就創建了一個閉包,我們可以在f1函數外部訪問到f1函數內部的值。
我們看到上述代碼有兩個特點:

  • f1函數嵌套了f2函數
  • f1函數返回了f2函數

閉包的用處:

1.很多js流行框架都是使用匿名自執行函數來避免變量污染
;(function(){
    //todo
})()
2.緩存:閉包可以讓變量的值始終保存在內存中,因此在使用時也要注意不要濫用閉包
function f1(){
    var n=999;
    nAdd = function(){n+=1}
    function f2(){alert(n)}
    return f2;
}
var result = f1(); //注意只有f1的返回值被外部引用,才不會被回收
result(); // 999
nAdd();
result(); //1000
3.封裝
var person = function(){
    var name = ‘default’
    return {
        getName:function(){ return name},
        setName:function(newName){ name=newName}
    }
}()

既然ES5中有定義變量的方法,那為什么ES6中又定義了let,const關鍵字呢?


js中的變量提升

var定義變量存在變量提升:只提升聲明語句,不提升賦值語句

var foo = {n:1};
(function(foo){
    console.log(foo.n);
    foo.n = 3;
    var foo = {n:2};
    console.log(foo.n);
})(foo)
console.log(foo.n); 

執行上述代碼,我們可以看到控制台中按順序依次打印:1,2,3。這是因為Javascript先編譯后執行。編譯階段,先聲明變量,所以引擎會將上面的代碼理解為以下格式

var foo = {n:1};
(function(foo){
    var foo;
    console.log(foo.n)
    foo.n = 3;
    foo = {n:2};
    console.log(foo.n)
})(foo);
console.log(foo.n)

說明:

  1. 函數內部定義變量foo時,因為當前作用域中已經存在名為foo的變量,所以編譯器忽略當前聲明,繼續進行編譯,因此第一次打印的內容為外部變量foo的屬性n值:1
  2. foo.n=3 改變的是外部變量foo,foo={n:2}將foo指向了內部變量,並重新賦值為{n:2},所以第二次打印的內容為內部重新賦值的變量foo的屬性n值:2
  3. 第三次打印內容是外部變量foo.n,因為函數內容已經更改了外部變量foo,所以打印結果為:3

js中先提升函數,后提升變量。

思考以下代碼:

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

執行上述代碼,我們可以看到控制台中按順序依次打印:a(){},1。按照剛才的理解,js引擎將上面的代碼會理解為下面的格式

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

那打印的結果應該為 undefined , f(){},這是因為我們忽略了一點,js先提升函數,后提升變量。所以正確的格式為

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

說明:
1.定義變量a時,因為已經存在命名為a的函數,所以第一次打印結果為a(){}
2.a=1,將變量a重新賦值,所以第二次打印結果為1

小結

  • ES5中,使用var定義變量,變量的作用域有兩種:全局作用域、函數作用域
  • var定義變量存在變量提升,此外,先提升函數,后提升變量

但是開發過程中,變量提升往往會對開發造成困擾,幸好ES6中引入了let語法。


let

塊級作用域

我們剛才提到,ES5中,js只用兩種作用域:全局作用域與函數作用域。在ES6中,let關鍵字會隱式地創建一個塊級作用域(通常是{}內部),變量只能在這個作用域中被訪問。例如題目一中

for(var i=0;i<10;i++){
    var a='a'
    let b = ‘b'
}
console.log(a)
console.log(b)

我們在循環的內部,使用let創建了變量b,在循環外部訪問時報錯,b is not defined.就是這個原因。
塊級作用域的引入大大改善了代碼中由於全局變量而引發的錯誤,比如文章開頭提出的第二題:

for(var i=0;i<3;i++){
    setTimeout(function(){
        console.log(i)
    },1000)
}

上述代碼由於變量i是用var聲明的,所以全局范圍有效 ,當循環體執行完時,i=2,所以定時器中console.log(i)中的i是指向全局變量i的,所以打印結果為2,2,2
如果我們將代碼改為

for(let i=0;i<3;i++){
    setTimeout(function(){
        console.log(i)
    },1000)
}

上述代碼中,變量i使用let定義,所以只在本輪for循環中有效,所以打印結果為0,1,2。

let不存在變量提升,其所聲明的變量一定要在聲明語句之后使用。

例如:

console.log(bar);
let bar = 2;

打印結果報錯:bar is not defined

此外,let 聲明的變量不能重復聲明,例如

let foo = {n:1};
(function(foo){
    console.log(foo.n);
    foo.n = 3;
    let foo = {n:2};
    console.log(foo.n);
})(foo)
console.log(foo.n);

函數內部定義變量foo時,因為當前作用域中已經存在命名為foo的變量,所以報錯:’foo’ has already been declared.

const

ES6中新增了let關鍵字的同時,也新增了const關鍵字。
let與const有很多共同點

  • 都支持塊級作用域
  • 都不支持變量提升
  • 都不支持重復聲明

此外,我們知道var聲明全局變量時,變量是掛在window上的。而let,const聲明變量,卻不是。這樣子便避免了window對象的變量污染問題。

當然,const與let也有區別。const與let的區別在於:

  • let聲明變量時無需賦值,const聲明變量時必須賦值
  • let聲明變量,變量可重新賦值,const聲明變量,完成初始化后,值不得更改 (基本類型)

剛剛提到const聲明變量后,如果值類型是基本類型,則不得更改,如果是引用類型呢?

如圖所示,可修改。如果我用const定義變量,值為對象,但是想讓對象的屬性無法修改應該怎么做呢?


對象屬性的保護方法

Object.defineProperty

Object.defineProperty()方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回這個對象。
更多關於Object.defineProperty方法的信息,推薦閱讀 MDN:Object.defineProperty

function setConst(obj) {
      Object.keys(obj).forEach(function (t,i) {
        Object.defineProperty(obj,t,{
          value:obj[t],
          writable:false, // 是否可重新賦值
          enumerable:false, //是否可枚舉
          configurable:false // 是否可刪除,其他設置屬性,是否可修改
        })
      })
    }

如圖所示,雖然實現了需求,對象屬性無法修改,無法刪除。可是控制台無提示,不是很人性化。幸好,ES6中提供了Proxy方法

Proxy對象代理

function setConst(obj) {
      return new Proxy(obj,{
        set:function (obj,prop,value) {
          throw new TypeError('Assignment to constant variable')
        },
        deleteProperty(target, key) {
          throw Error(`Error! ${key} cannot be deleted.`);
        }
      })
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM