一、let和const命令
1. let命令
只在let所在的代碼塊內有效
-
例子(識別var和let的作用域區別,for說明)
for循環的計數器,就很合適使用let命令。for (let i = 0; i < 10; i++) {
// …
}
console.log(i);
// ReferenceError: i is not defined
下面的代碼如果使用var,最後輸出的是10。
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
如果使用let,聲明的變量僅在塊級作用域內有效,最後輸出的是 6。
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
for循環還有一個特別之處,就是設置循環變量的那部分是一個父作用域,而循環體內部是一個單獨的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
ES6變量定義不存在變量提升
var在腳本開始運行時變量就存在,在定義前使用變量則undefined,這就是變量提升。
let則不會發生變量提升,變量聲明前不存在,使用則報錯。
// var 的情況
console.log(foo); // 輸出undefined
var foo = 2;
// let 的情況
console.log(bar); // 報錯ReferenceError
let bar = 2;
暫時性死區
暫時性死區的本質就是,只要一進入當前作用域,所要使用的變量就已經存在了,但是不可獲取,只有等到聲明變量的那一行代碼出現,纔可以獲取和使用該變量。語法上,稱爲“暫時性死區”(temporal dead zone,簡稱 TDZ)
理解:
- 在作用域內於定義let/const變量前使用該變量則發生暫時性死區error報錯
- typeof直接使用則變量提升得到undefined。但如之後用let/const定義變量則報錯——typeof不再是百分比安全。
if (true) {
// TDZ開始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ結束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
typeof x; // ReferenceError
let x;
typeof undeclared_variable // "undefined"
例子:
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 報錯
// 不報錯
var x = x;
// 報錯
let x = x;
// ReferenceError: x is not defined
不允許重複聲明
顧名思義,let不允許在相同作用域內重複聲明同一變量
function func(arg) {
let arg;
}
func() // 報錯
function func(arg) {
{
let arg;
}
}
func() // 不報錯
2. 塊級作用域
爲什麼需要塊級作用域(ES5作用域的弊端)
ES5只有全局作用域和函數作用域,會發生(1)變量覆蓋(2)變量泄露爲全局
- 內層變量覆蓋外層變量
//if中的變量聲明導致發送了變量提升,打印的tmp覆蓋了外層的tmp變成undefined
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
- 循環中泄露爲全局變量
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
ES6的塊級作用域
(1)外層代碼不受內層代碼影響,即無法讀取(2)內層可以定義和外層作用域同名
- 外層不受內層代碼影響
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
- 內層可以定義外層作用域
{{{{
let insane = 'Hello World';
{let insane = 'Hello'}
}}}};
塊級作用域與函數聲明
- 區別
- ES5只能在頂層作用域和函數作用域之中聲明,不能在塊級作用域聲明。但瀏覽器不遵守這個規定,爲了兼容以前舊代碼
- ES6引入塊級作用域,明確允許在塊級作用域之中聲明函數,類似let,在塊級作用域之外不可引用該函數
ES5:
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重複聲明一次函數f
function f() { console.log('I am inside!'); }
}
f();
}());
ES6:
// 瀏覽器的 ES6 環境
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重複聲明一次函數f
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
但ES6規定瀏覽器可不遵守上面的規定,有自己的行爲方式
- 運行在塊級作用域內聲明函數
- 函數聲明類似於var,即會提升到全局作用域或函數作用域的頭部
- 同時,函數聲明還會提升到所在的塊級作用域的頭部
所以上面ES6的例子報錯可以這麼翻譯(發生了變量提升):
// 瀏覽器的 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
ES6的塊級作用域必須有大括號,沒有則不存在塊級作用域
// 第一種寫法,報錯
if (true) let x = 1;
// 第二種寫法,不報錯
if (true) {
let x = 1;
}
// 不報錯
'use strict';
if (true) {
function f() {}
}
// 報錯
'use strict';
if (true)
function f() {}
3. const命令
const實際上保證的,並不是變量的值不得改動,而是變量指向的那個內存地址所保存的數據不得改動。
(1)簡單類型數值/字符串/布爾值而言就等於常量,但對於複合類型的數據對象/數組,保存的只是一個指向實際數據的指針
(2)const聲明一個只讀的常量,一旦聲明必須初始化
(3)和let一樣是塊級作用域。其他也要,不可提升/同樣存在暫時性死區
(2)
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
(3)
const foo;
// SyntaxError: Missing initializer in const declaration
(1)
const foo = {};
// 爲 foo 添加一個屬性,可以成功
foo.prop = 123;
foo.prop // 123
// 將 foo 指向另一個對象,就會報錯
foo = {}; // TypeError: "foo" is read-only
const a = [];
a.push('Hello'); // 可執行
a.length = 0; // 可執行
a = ['Dave']; // 報錯
如果真想把對象變成常量,可以使用freeze進行凍結。之後的對象及其屬性都不可改變,嚴格模式下還會報錯
const foo = Object.freeze({});
// 常規模式時,下面一行不起作用;
// 嚴格模式時,該行會報錯
foo.prop = 123;
//連對象的屬性也凍結
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
ES6聲明變量的六種方法
ES5:var和function
ES6:var、function、let、const、import、class
4. 頂層對象的屬性
頂層對象:(1)瀏覽器環境——window對象(2)Node環境指的是global對象
- ES5
(1)頂層對象的屬性window和全局變量var的賦值是同一件事。
(2)頂層對象的屬性是隨處可寫,容易不知不覺創建全局變量var。
問題:一不利於模塊化編程,二頂層對象有實體含義指定是瀏覽器的窗口對象window
window.a = 1;
a // 1
a = 2;
window.a // 2
- ES6
(1)爲保持兼容var和function命令聲明的全局變量依舊是頂層對象的屬性
(2)let、const、class命令聲明的全局變量不屬於頂層對象window的屬性
var a = 1;
// 如果在 Node 的 REPL 環境,可以寫成 global.a
// 或者採用通用方法,寫成 this.a
window.a // 1
let b = 1;
window.b // undefined
5. globalThis對象
JavaScript 語言存在一個頂層對象,它提供全局環境(即全局作用域),所有代碼都是在這個環境中運行。
-
頂層對象在各種實現中是不統一的:
self:瀏覽器、Web Worker
window:瀏覽器
global:Node -
同樣的代碼爲能在各種環境都去到頂層對象,一般是使用this變量。但存在侷限性:
(1)全局環境中,this會返回頂層對象。但Node、ES6模塊中,this返回的是當前模塊
(2)函數中的this不作爲對象的方法運行,單純爲function則會指向頂層對象,但嚴格模式下會指向undefined
(3)不管是嚴格模式,還是普通模式,new Function(‘return this’)(),總是會返回全局對象。但是,如果瀏覽器用了 CSP(Content Security Policy,內容安全策略),那麼eval、new Function這些方法都可能無法使用。
墊片庫global-this,可以在所有環境拿到globalThis
https://github.com/ungap/global-this