ES6專題——整理自阮一峰老師的ECMAScript 6入門

這里我僅僅是記錄了那些我認為值得注意的ES6知識點,詳細版請挪步https://es6.ruanyifeng.com/#docs/let


let和const命令

let聲明的變量只在它所在的代碼塊有效。

var a = [];
for (let i = 0; i < 10; i++) {a[i] = function () {console.log(i);};
}
a[6](); // 6

上面代碼中,變量ilet聲明的,當前的i只在本輪循環有效,所以每一次循環的i其實都是一個新的變量,所以最后輸出的是6。你可能會問,如果每一輪循環的變量i都是重新聲明的,那它怎么知道上一輪循環的值,從而計算出本輪循環的值?這是因為 JavaScript 引擎內部會記住上一輪循環的值,初始化本輪的變量i時,就在上一輪循環的基礎上進行計算

另外,for循環還有一個特別之處,就是在條件中設置循環變量的那部分是一個父作用域,而循環體內部是一個單獨的子作用域。(感覺實現機制是閉包,將參數都初始化之后return一個函數)(我想因為在for中初始化的變量只會執行一次并隨后記錄在引擎中,所以for循環內部聲明的變量與for條件中的變量并不會產生沖突,這里我的想法是錯誤的,原因請看下去)

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

上面代碼正確運行,輸出了 3 次abc。這表明函數內部的變量i與循環變量i不在同一個作用域,有各自單獨的作用域。

不存在變量提升

let所聲明的變量一定要在聲明后使用,否則報錯。

// var 的情況
console.log(foo); // 輸出undefined
var foo = 2;// let 的情況
console.log(bar); // 報錯ReferenceError
let bar = 2;

變量barlet命令聲明,不會發生變量提升。這表示在聲明它之前,變量bar是不存在的,這時如果用到它,就會拋出一個錯誤。

暫時性死區

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

var tmp = 123;if (true) {tmp = 'abc'; // ReferenceErrorlet tmp;
}

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

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

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

if (true) {// TDZ開始tmp = 'abc'; // ReferenceErrorconsole.log(tmp); // ReferenceErrorlet tmp; // TDZ結束console.log(tmp); // undefinedtmp = 123;console.log(tmp); // 123
}

上面代碼中,在let命令聲明變量tmp之前,都屬于變量tmp的“死區”。

“暫時性死區”也意味著typeof不再是一個百分之百安全的操作。

typeof x; // ReferenceError
let x;

上面代碼中,變量x使用let命令聲明,所以在聲明之前,都屬于x的“死區”,只要用到該變量就會報錯。因此,typeof運行時就會拋出一個ReferenceError

作為比較,如果一個變量根本沒有被聲明,使用typeof反而不會報錯。

typeof undeclared_variable // "undefined"

上面代碼中,undeclared_variable是一個不存在的變量名,結果返回“undefined”。所以,在沒有let之前,typeof運算符是百分之百安全的,永遠不會報錯。現在這一點不成立了。這樣的設計是為了讓大家養成良好的編程習慣,變量一定要在聲明之后使用,否則就報錯。

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

function bar(x = y, y = 2) { //語句從左到右按順序執行,一個, 分隔兩個語句return [x, y];
}bar(); // 報錯

上面代碼中,調用bar函數之所以報錯(某些實現可能不報錯),是因為參數x默認值等于另一個參數y,而此時y還沒有聲明,屬于“死區”。如果y的默認值是x,就不會報錯,因為此時x已經聲明了。( 這里會報錯我猜由于此刻參數是存在默認值的,如果你調用函數的時候沒有傳參,那么就會使用默認值,此刻(x = y)執行,但是y還沒有被聲明賦值過,所以會報錯,而下面的例子里,x = 2已經聲明賦值過了,所以執行 y = x 的時候能正確執行)

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

另外,下面的代碼也會報錯,與var的行為不同。

// 不報錯
var x = x;// 報錯
let x = x;
// ReferenceError: x is not defined

上面代碼報錯,也是因為暫時性死區。使用let聲明變量時,只要變量在還沒有聲明完成前使用,就會報錯。上面這行就屬于這個情況,在變量x的聲明語句還沒有執行完成前,就去取x的值,導致報錯”x 未定義“。(一個語句的執行順序是從右往左,所以x為聲明而報錯)

ES6 規定暫時性死區和letconst語句不出現變量提升,主要是為了減少運行時錯誤,防止在變量聲明前就使用這個變量,從而導致意料之外的行為。這樣的錯誤在 ES5 是很常見的,現在有了這種規定,避免此類錯誤就很容易了。

總之,暫時性死區的本質就是,只要一進入當前作用域,所要使用的變量就已經存在了,但是不可獲取,只有等到聲明變量的那一行代碼出現,才可以獲取和使用該變量。

不允許重復聲明

let不允許在相同作用域內重復聲明同一個變量。

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

因此,不能在函數內部重新聲明參數。(正好對應上面的for循環中的父作用域與子作用域的解說!)

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

ES6 的塊級作用域

let實際上為 JavaScript 新增了塊級作用域。

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

上面的函數有兩個代碼塊,都聲明了變量n,運行后輸出 5。這表示外層代碼塊不受內層代碼塊的影響。如果兩次都使用var定義變量n,最后輸出的值才是 10。

ES6 允許塊級作用域的任意嵌套。

{{{{{let insane = 'Hello World'}console.log(insane); // 報錯
}}}};

上面代碼使用了一個五層的塊級作用域,每一層都是一個單獨的作用域。第四層作用域無法讀取第五層作用域的內部變量。

內層作用域可以定義外層作用域的同名變量。

{{{{let insane = 'Hello World';{let insane = 'Hello World'}
}}}};

塊級作用域的出現,實際上使得獲得廣泛應用的匿名立即執行函數表達式(匿名 IIFE)不再必要了。

// IIFE 寫法
(function () {var tmp = ...;...
}());// 塊級作用域寫法
{let tmp = ...;...
}

塊級作用域與函數聲明

函數能不能在塊級作用域之中聲明?這是一個相當令人混淆的問題。

ES5 規定,函數只能在頂層作用域函數作用域之中聲明,不能在塊級作用域聲明。

// 情況一
if (true) {function f() {}
}// 情況二
try {function f() {}
} catch(e) {// ...
}

上面兩種函數聲明,根據 ES5 的規定都是非法的。

但是,瀏覽器沒有遵守這個規定,為了兼容以前的舊代碼,還是支持在塊級作用域之中聲明函數,因此上面兩種情況實際都能運行,不會報錯。

ES6 引入了塊級作用域,明確允許在塊級作用域之中聲明函數。ES6 規定,塊級作用域之中,函數聲明語句的行為類似于let,在塊級作用域之外不可引用。

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

上面代碼在 ES5 中運行,會得到“I am inside!”,因為在if內聲明的函數f會被提升到函數頭部,實際運行的代碼如下。

// ES5 環境
function f() { console.log('I am outside!'); }(function () {function f() { console.log('I am inside!'); }if (false) {}f();
}());

ES6 就完全不一樣了,理論上會得到“I am outside!”。因為塊級作用域內聲明的函數類似于let,對作用域之外沒有影響。但是,如果你真的在 ES6 瀏覽器中運行一下上面的代碼,是會報錯的,這是為什么呢?

// 瀏覽器的 ES6 環境
function f() { console.log('I am outside!'); }(function () {if (false) {// 重復聲明一次函數ffunction f() { console.log('I am inside!'); }}f();
}());
// Uncaught TypeError: f is not a function

上面的代碼在 ES6 瀏覽器中,都會報錯。

原來,如果改變了塊級作用域內聲明的函數的處理規則,顯然會對老代碼產生很大影響。為了減輕因此產生的不兼容問題,ES6 在附錄 B里面規定,瀏覽器的實現可以不遵守上面的規定,有自己的行為方式。

  • 允許在塊級作用域內聲明函數。
  • 函數聲明類似于var,即會提升到全局作用域或函數作用域的頭部。
  • 同時,函數聲明還會提升到所在的塊級作用域的頭部。(是函數聲明還是整個函數都提升到塊級作用域?)

注意,上面三條規則只對 ES6 的瀏覽器實現有效,其他環境的實現不用遵守,還是將塊級作用域的函數聲明當作let處理。

根據這三條規則,瀏覽器的 ES6 環境中,塊級作用域內聲明的函數,行為類似于var聲明的變量。上面的例子實際運行的代碼如下。

// 瀏覽器的 ES6 環境
function f() { console.log('I am outside!'); }
(function () {var f = undefined; // 函數聲明類似于var,即會提升到全局作用域或函數作用域的頭部if (false) {function f() { console.log('I am inside!'); } //同時,函數還會提升到所在的塊級作用域的頭部。}f();
}());
// Uncaught TypeError: f is not a function

考慮到環境導致的行為差異太大,應該避免在塊級作用域內聲明函數。如果確實需要,也應該寫成函數表達式,而不是函數聲明語句。

// 塊級作用域內部的函數聲明語句,建議不要使用
{let a = 'secret';function f() {return a;}
}// 塊級作用域內部,優先使用函數表達式
{let a = 'secret';let f = function () {return a;};
}

另外,還有一個需要注意的地方。ES6 的塊級作用域必須有大括號,如果沒有大括號,JavaScript 引擎就認為不存在塊級作用域

// 第一種寫法,報錯
if (true) let x = 1;// 第二種寫法,不報錯
if (true) {let x = 1;
}

上面代碼中,第一種寫法沒有大括號,所以不存在塊級作用域,而let只能出現在當前作用域的頂層,所以報錯。第二種寫法有大括號,所以塊級作用域成立。

函數聲明也是如此,嚴格模式下,函數只能聲明在當前作用域的頂層。(我猜是因為如果沒有{},變量或者函數都存在當前作用域頂層的下一級非塊級作用域中(不知名作用域……),所以會報錯?)

// 不報錯
'use strict';
if (true) {function f() {}
}// 報錯
'use strict';
if (true)function f() {}

const

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

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

上面代碼表明改變常量的值會報錯。

const聲明的變量不得改變值,這意味著,const一旦聲明變量,就必須立即初始化不能留到以后賦值。

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

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

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

if (true) {const MAX = 5;
}MAX // Uncaught ReferenceError: MAX is not defined

const命令聲明的常量也是不提升,同樣存在暫時性死區,只能在聲明的位置后面使用。

if (true) {console.log(MAX); // ReferenceErrorconst MAX = 5;
}

上面代碼在常量MAX聲明之前就調用,結果報錯。

const聲明的常量,也與let一樣不可重復聲明。

var message = "Hello!";
let age = 25;// 以下兩行都會報錯
const message = "Goodbye!";
const age = 30;

本質

const實際上保證的,并不是變量的值不得改動,而是變量指向的那個內存地址所保存的數據不得改動。對于簡單類型的數據(數值、字符串、布爾值),值就保存著變量指向的那個內存地址,因此等同于常量。但對于復合類型的數據(主要是對象和數組),變量指向的內存地址,保存的只是一個指向實際數據的指針,const只能保證這個指針是固定的(即總是指向另一個固定的地址),至于它指向的數據結構是不是可變的,就完全不能控制了。因此,將一個對象聲明為常量必須非常小心。

const foo = {};// 為 foo 添加一個屬性,可以成功
foo.prop = 123;
foo.prop // 123// 將 foo 指向另一個對象,就會報錯
foo = {}; // TypeError: "foo" is read-only

上面代碼中,常量foo儲存的是一個地址,這個地址指向一個對象。不可變的只是這個地址,即不能把foo指向另一個地址,但對象本身是可變的,所以依然可以為其添加新屬性。

下面是另一個例子。

const a = [];
a.push('Hello'); // 可執行
a.length = 0;    // 可執行
a = ['Dave'];    // 報錯

上面代碼中,常量a是一個數組,這個數組本身是可寫的,但是如果將另一個數組賦值給a,就會報錯。

如果真的想將對象凍結,應該使用Object.freeze方法。

const foo = Object.freeze({});// 常規模式時,下面一行不起作用;
// 嚴格模式時,該行會報錯
foo.prop = 123;

上面代碼中,常量foo指向一個凍結的對象,所以添加新屬性不起作用,嚴格模式時還會報錯。

除了將對象本身凍結,對象的屬性也應該凍結。下面是一個將對象徹底凍結的函數。(遞歸)

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 除了添加letconst命令,后面章節還會提到,另外兩種聲明變量的方法:import命令和class命令。所以,ES6 一共有 6 種聲明變量的方法。

頂層對象的屬性

頂層對象,在瀏覽器環境指的是window對象,在 Node 指的是global對象。ES5 之中,頂層對象的屬性與全局變量是等價的。

window.a = 1;
a // 1a = 2;
window.a // 2

這樣的設計帶來了幾個很大的問題,首先是沒法在編譯時就報出變量未聲明的錯誤,只有運行時才能知道因為全局變量可能是頂層對象的屬性創造的,而屬性的創造是動態的);其次,程序員很容易不知不覺地就創建了全局變量(比如打字出錯);最后,頂層對象的屬性是到處可以讀寫的,這非常不利于模塊化編程。另一方面,window對象有實體含義,指的是瀏覽器的窗口對象,頂層對象是一個有實體含義的對象,也是不合適的。

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

var a = 1;
// 如果在 Node 的 REPL 環境,可以寫成 global.a
// 或者采用通用方法,寫成 this.a
window.a // 1let b = 1;
window.b // undefined

上面代碼中,全局變量avar命令聲明,所以它是頂層對象的屬性;全局變量blet命令聲明,所以它不是頂層對象的屬性,返回undefined

globalThis 對象

JavaScript 語言存在一個頂層對象,它提供全局環境(即全局作用域),所有代碼都是在這個環境中運行。但是,頂層對象在各種實現里面是不統一的。

  • 瀏覽器里面,頂層對象是window,但 Node 和 Web Worker 沒有window
  • 瀏覽器和 Web Worker 里面,self也指向頂層對象,但是 Node 沒有self
  • Node 里面,頂層對象是global,但其他環境都不支持。

同一段代碼為了能夠在各種環境,都能取到頂層對象,現在一般是使用this變量,但是有局限性。

  • 全局環境中,this會返回頂層對象。但是,Node 模塊和 ES6 模塊中,this返回的是當前模塊。
  • 函數里面的this,如果函數不是作為對象的方法運行,而是單純作為函數運行,this會指向頂層對象。但是,嚴格模式下,這時this會返回undefined
  • 不管是嚴格模式,還是普通模式,new Function('return this')(),總是會返回全局對象。但是,如果瀏覽器用了 CSP(Content Security Policy,內容安全策略),那么evalnew Function這些方法都可能無法使用。

綜上所述,很難找到一種方法,可以在所有情況下,都取到頂層對象。下面是兩種勉強可以使用的方法。

// 方法一
(typeof window !== 'undefined'? window: (typeof process === 'object' &&typeof require === 'function' &&typeof global === 'object')? global: this);// 方法二
var getGlobal = function () {if (typeof self !== 'undefined') { return self; }if (typeof window !== 'undefined') { return window; }if (typeof global !== 'undefined') { return global; }throw new Error('unable to locate global object');
};

現在有一個提案,在語言標準的層面,引入globalThis作為頂層對象。也就是說,任何環境下,globalThis都是存在的,都可以從它拿到頂層對象,指向全局環境下的this

墊片庫global-this模擬了這個提案,可以在所有環境拿到globalThis


變量的解構賦值

基本用法

ES6 允許按照一定模式,從數組對象中提取值,對變量進行賦值,這被稱為解構(Destructuring)。

let [a, b, c] = [1, 2, 3];

上面代碼表示,可以從數組中提取值,按照對應位置,對變量賦值。

本質上,這種寫法屬于“模式匹配”,只要等號兩邊的模式相同,左邊的變量就會被賦予對應的值。下面是一些使用嵌套數組進行解構的例子。

let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3let [ , , third] = ["foo", "bar", "baz"];
third // "baz"let [x, , y] = [1, 2, 3];
x // 1
y // 3let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []

另一種情況是不完全解構,即等號左邊的模式,只匹配一部分的等號右邊的數組。這種情況下,解構依然可以成功。

let [x, y] = [1, 2, 3];
x // 1
y // 2let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4

如果等號的右邊不是數組(或者嚴格地說,不是可遍歷的結構,參見《Iterator》一章),那么將會報錯。

// 報錯
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

上面的語句都會報錯,因為等號右邊的值,要么轉為對象以后不具備 Iterator 接口(前五個表達式),要么本身就不具備 Iterator 接口(最后一個表達式)。

對于 Set 結構,也可以使用數組的解構賦值。

let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"

事實上,只要某種數據結構具有 Iterator 接口,都可以采用數組形式的解構賦值。

function* fibs() {let a = 0;let b = 1;while (true) {yield a;[a, b] = [b, a + b];}
}let [first, second, third, fourth, fifth, sixth] = fibs();
sixth // 5

上面代碼中,fibs是一個 Generator 函數(參見《Generator 函數》一章),原生具有 Iterator 接口。解構賦值會依次從這個接口獲取值

默認值

解構賦值允許指定默認值。

let [foo = true] = [];
foo // truelet [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

注意,ES6 內部使用嚴格相等運算符(===)判斷一個位置是否有值。所以,只有當一個數組成員嚴格等于undefined,默認值才會生效

let [x = 1] = [undefined];
x // 1let [x = 1] = [null];
x // null

上面代碼中,如果一個數組成員是null,默認值就不會生效,因為null不嚴格等于undefined

console.log(undefined === undefined) // true
console.log(null === null) // true
console.log(NaN === NaN) // false
console.log(null)//null
console.log(typeof null)//object

如果默認值是一個表達式,那么這個表達式是惰性求值的,即只有在用到的時候,才會求值。

function f() {console.log('aaa');
}let [x = f()] = [1];

上面代碼中,因為x能取到值,所以函數f根本不會執行。上面的代碼其實等價于下面的代碼。

let x;
if ([1][0] === undefined) {x = f();
} else {x = [1][0];
}

默認值可以引用解構賦值的其他變量,但該變量必須已經聲明。

let [x = 1, y = x] = [];     // x=1; y=1
let [x = 1, y = x] = [2];    // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = [];     // ReferenceError: y is not defined

上面最后一個表達式之所以會報錯,是因為xy做默認值時,y還沒有聲明。

對象的解構賦值

解構不僅可以用于數組,還可以用于對象。

let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

對象的解構與數組有一個重要的不同。數組的元素是按次序排列的,變量的取值由它的位置決定;而對象的屬性沒有次序,變量必須與屬性同名,才能取到正確的值。

let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined

上面代碼的第一個例子,等號左邊的兩個變量的次序,與等號右邊兩個同名屬性的次序不一致,但是對取值完全沒有影響。第二個例子的變量沒有對應的同名屬性,導致取不到值,最后等于undefined

如果解構失敗,變量的值等于undefined

let {foo} = {bar: 'baz'};
foo // undefined

對象的解構賦值,可以很方便地將現有對象的方法,賦值到某個變量。

// 例一
let { log, sin, cos } = Math;// 例二
const { log } = console;
log('hello') // hello

上面代碼的例一將Math對象的對數、正弦、余弦三個方法,賦值到對應的變量上,使用起來就會方便很多。例二將console.log賦值到log變量。

如果變量名與屬性名不一致,必須寫成下面這樣。

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

這實際上說明,對象的解構賦值是下面形式的簡寫(參見《對象的擴展》一章)。

let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };

也就是說,對象的解構賦值的內部機制,是先找到同名屬性,然后再賦給對應的變量。真正被賦值的是后者,而不是前者。

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined

上面代碼中,foo是匹配的模式,baz才是變量。真正被賦值的是變量baz,而不是模式foo

與數組一樣,解構也可以用于嵌套結構的對象。

let obj = {p: ['Hello',{ y: 'World' }]
};let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"

注意,這時p是模式,不是變量,因此不會被賦值。如果p也要作為變量賦值,可以寫成下面這樣。

let obj = {p: ['Hello',{ y: 'World' }]
};let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]

下面是嵌套賦值的例子。

let obj = {};
let arr = [];({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });obj // {prop:123}
arr // [true]

如果解構模式是嵌套的對象,而且子對象所在的父屬性不存在,那么將會報錯。

// 報錯
let {foo: {bar}} = {baz: 'baz'};

上面代碼中,等號左邊對象的foo屬性,對應一個子對象。該子對象的bar屬性,解構時會報錯。原因很簡單,因為foo這時等于undefined,再取子屬性就會報錯。

注意,對象的解構賦值可以取到繼承的屬性。

const obj1 = {};
const obj2 = { foo: 'bar' };
Object.setPrototypeOf(obj1, obj2);const { foo } = obj1;
foo // "bar"

上面代碼中,對象obj1的原型對象是obj2foo屬性不是obj1自身的屬性,而是繼承自obj2的屬性,解構賦值可以取到這個屬性。

默認值

對象的解構也可以指定默認值。

var {x = 3} = {};
x // 3var {x, y = 5} = {x: 1};
x // 1
y // 5var {x: y = 3} = {};
y // 3var {x: y = 3} = {x: 5};
y // 5var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"

默認值生效的條件是,對象的屬性值嚴格等于undefined

var {x = 3} = {x: undefined};
x // 3var {x = 3} = {x: null};
x // null

上面代碼中,屬性x等于null,因為nullundefined不嚴格相等,所以是個有效的賦值,導致默認值3不會生效。

注意點

(1)如果要將一個已經聲明的變量用于解構賦值,必須非常小心。

// 錯誤的寫法
let x;
{x} = {x: 1};
// SyntaxError: syntax error

上面代碼的寫法會報錯,因為 JavaScript 引擎會將{x}理解成一個代碼塊,從而發生語法錯誤(這里可以理解為代碼塊 = 代碼塊,所以會報錯)。只有不將大括號寫在行首,避免 JavaScript 將其解釋為代碼塊,才能解決這個問題。

// 正確的寫法
let x;
({x} = {x: 1});

上面代碼將整個解構賦值語句,放在一個圓括號里面,就可以正確執行。關于圓括號與解構賦值的關系,參見下文。

(2)解構賦值允許等號左邊的模式之中,不放置任何變量名。因此,可以寫出非常古怪的賦值表達式。

({} = [true, false]);
({} = 'abc');
({} = []);

上面的表達式雖然毫無意義,但是語法是合法的,可以執行。

(3)由于數組本質是特殊的對象,因此可以對數組進行對象屬性的解構。

let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3

上面代碼對數組進行對象解構。數組arr0鍵對應的值是1[arr.length - 1]就是2鍵,對應的值是3。方括號這種寫法,屬于“屬性名表達式”(數組默認屬性名都是下標)(參見《對象的擴展》一章)。

字符串的解構賦值

字符串也可以解構賦值。這是因為此時,字符串被轉換成了一個類似數組的對象

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

類似數組的對象都有一個length屬性,因此還可以對這個屬性解構賦值。

let {length : len} = 'hello';
len // 5

數值和布爾值的解構賦值

解構賦值時,如果等號右邊是數值和布爾值,則會先轉為對象

let {toString: s} = 123;
s === Number.prototype.toString // truelet {toString: s} = true;
s === Boolean.prototype.toString // true

上面代碼中,數值和布爾值的包裝對象都有toString屬性,因此變量s都能取到值。

解構賦值的規則是,只要等號右邊的值不是對象或數組,就先將其轉為對象。由于undefinednull無法轉為對象(可是null又稱為空對象,typeof null = object……),所以對它們進行解構賦值,都會報錯。

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

函數參數的解構賦值

函數的參數也可以使用解構賦值。

function add([x, y]){return x + y;
}add([1, 2]); // 3

上面代碼中,函數add的參數表面上是一個數組,但在傳入參數的那一刻,數組參數就被解構成變量xy。對于函數內部的代碼來說,它們能感受到的參數就是xy

下面是另一個例子。

[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]

函數參數的解構也可以使用默認值。

function move({x = 0, y = 0} = {}) {return [x, y];
}move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

上面代碼中,函數move的參數是一個對象,通過對這個對象進行解構,得到變量xy的值。如果解構失敗,xy等于默認值。

注意,下面的寫法會得到不一樣的結果。

function move({x, y} = { x: 0, y: 0 }) {return [x, y];
}move({x: 3, y: 8}); // [3, 8] => move({x, y} = { x: 0, y: 0 } = {x: 3, y: 8})
move({x: 3}); // [3, undefined] => move({x, y} = { x: 0, y: 0 } = {x: 3, y: undefined})
move({}); // [undefined, undefined] => move({x, y} = { x: 0, y: 0 } = {x: undefined, y: undefined})
move(); // [0, 0] => move({x, y} = { x: 0, y: 0 })

上面代碼是為函數move的參數指定默認值,而不是為變量xy指定默認值,所以會得到與前一種寫法不同的結果。

undefined就會觸發函數參數的默認值。

[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]

圓括號問題

解構賦值雖然很方便,但是解析起來并不容易。對于編譯器來說,一個式子到底是模式,還是表達式,沒有辦法從一開始就知道,必須解析到(或解析不到)等號才能知道。

由此帶來的問題是,如果模式中出現圓括號怎么處理。ES6 的規則是,只要有可能導致解構的歧義,就不得使用圓括號。

但是,這條規則實際上不那么容易辨別,處理起來相當麻煩。因此,建議只要有可能,就不要在模式中放置圓括號。

不能使用圓括號的情況

以下三種解構賦值不得使用圓括號。

(1)變量聲明語句

// 全部報錯
let [(a)] = [1];let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};let { o: ({ p: p }) } = { o: { p: 2 } };

上面 6 個語句都會報錯,因為它們都是變量聲明語句,模式不能使用圓括號。

(2)函數參數

函數參數也屬于變量聲明,因此不能帶有圓括號。

// 報錯
function f([(z)]) { return z; }
// 報錯
function f([z,(x)]) { return x; }

(3)賦值語句的模式

// 全部報錯,這樣就等于 語句 = 代碼塊
({ p: a }) = { p: 42 };
([a]) = [5];

上面代碼將整個模式放在圓括號之中,導致報錯。

// 報錯
[({ p: a }), { x: c }] = [{}, {}];

上面代碼將一部分模式放在圓括號之中,導致報錯。

可以使用圓括號的情況

可以使用圓括號的情況只有一種:賦值語句的非模式部分,可以使用圓括號。

[(b)] = [3]; // 正確
({ p: (d) } = {}); // 正確
[(parseInt.prop)] = [3]; // 正確

上面三行語句都可以正確執行,因為首先它們都是賦值語句,而不是聲明語句;其次它們的圓括號都不屬于模式的一部分。第一行語句中,模式是取數組的第一個成員,跟圓括號無關;第二行語句中,模式是p,而不是d;第三行語句與第一行語句的性質一致。

用途

變量的解構賦值用途很多。

(1)交換變量的值

let x = 1;
let y = 2;[x, y] = [y, x];

上面代碼交換變量xy的值,這樣的寫法不僅簡潔,而且易讀,語義非常清晰。

(2)從函數返回多個值

函數只能返回一個值,如果要返回多個值,只能將它們放在數組或對象里返回。有了解構賦值,取出這些值就非常方便。

// 返回一個數組function example() {return [1, 2, 3];
}
let [a, b, c] = example();// 返回一個對象function example() {return {foo: 1,bar: 2};
}
let { foo, bar } = example();

(3)函數參數的定義

解構賦值可以方便地將一組參數與變量名對應起來。

// 參數是一組有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);// 參數是一組無次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});

(4)提取 JSON 數據

解構賦值對提取 JSON 對象中的數據,尤其有用。

let jsonData = {id: 42,status: "OK",data: [867, 5309]
};let { id, status, data: number } = jsonData;console.log(id, status, number);
// 42, "OK", [867, 5309]

上面代碼可以快速提取 JSON 數據的值。

(5)函數參數的默認值

jQuery.ajax = function (url, {async = true,beforeSend = function () {},cache = true,complete = function () {},crossDomain = false,global = true,// ... more config
} = {}) { <=!!!!// ... do stuff
};

指定參數的默認值,就避免了在函數體內部再寫var foo = config.foo || 'default foo';這樣的語句。

(6)遍歷 Map 結構

任何部署了 Iterator 接口的對象,都可以用for...of循環遍歷。Map 結構原生支持 Iterator 接口,配合變量的解構賦值,獲取鍵名和鍵值就非常方便。

const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');for (let [key, value] of map) {console.log(key + " is " + value);
}
// first is hello
// second is world

如果只想獲取鍵名,或者只想獲取鍵值,可以寫成下面這樣。

// 獲取鍵名
for (let [key] of map) {// ...
}// 獲取鍵值
for (let [,value] of map) {// ...
}

(7)輸入模塊的指定方法

加載模塊時,往往需要指定輸入哪些方法。解構賦值使得輸入語句非常清晰。

const { SourceMapConsumer, SourceNode } = require("source-map");

?

?

?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/251328.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/251328.shtml
英文地址,請注明出處:http://en.pswp.cn/news/251328.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

開發測試比

1.服務器已經開啟了CORS跨域支持 瀏覽器有同源策略限制&#xff1a;協議、域名、端口號其中無法向非同源地址發送ajax請求 跨域解決方法&#xff1a;JSONP&#xff08;只支持get不支持post&#xff09;&#xff0c;不是ajax 凡是有src屬性的標簽都有跨域能力 前端定義一個處理…

map函數用法詳解

map函數是Python內置的高階函數&#xff0c;它是一個典型的函數式編程例子。它的參數為: 一個函數function、一個或多個sequence。通過把函數function依次作用在sequence的每個元素上&#xff0c;得到一個新的sequence并返回。注意&#xff1a;map函數不改變原有的sequence&…

2018暑假集訓測試六總結

拿到試題沒幾分鐘&#xff0c;就有人說會做T1QAQ。第一題感覺似曾相識&#xff0c;其實不同。梳理出本質后發現有兩個限制&#xff0c;便想用枚舉遞推來快速求解&#xff0c;發現要么是不會推&#xff0c;要么是時空超限&#xff0c;不會優化。期間也想過通過離線做&#xff0c…

css3 --- 使用媒體查詢進行響應式布局

css3引入media,可以根據設備特性進行不同的布局, 本文展示的是根據不同屏幕的寬度進行不同的布局,代碼如下: <!DOCTYPE html> <html> <head><meta http-equiv"Content-Type" content"text/html; charsetutf-8" /><title> 針…

node項目正常啟動后不能訪問(防火墻未放行端口)

今天打開個人站點&#xff0c;發現登陸不了&#xff0c;原以為是pm2的問題&#xff0c;先停了pm2用node app.js的方式運行后端代碼&#xff0c;項目能正常啟動但是依然不能登陸。 1 檢查ecs的安全組規則&#xff0c;node項目端口3000、8888是否放行 2 確認node正常運行 輸入…

[轉載]dbms_lob用法小結

http://blog.sina.com.cn/s/blog_713978a50100prkt.html CLOB里存的是2進制 判定長度 DBMS_LOB.GETLENGTH(col1)獲取文本 DBMS_LOB.SUBSTR(col1,n,pos)DBMS_LOB.SUBSTR(col1,10,1)表示從第1個字節開始取出10個字節 DBMS_LOB.SUBSTR(CLOB_VAR,32767)表示截取CLOB變量保存的全…

javascript --- 利用節點關系訪問HTML元素

<input type"button" value"父節點"onclick"change(curTarget.parentNode);" /><input type"button" value"第一個"onclick"change(curTarget.parentNode.firstChild.nextSibling);" /><input typ…

mysql中列屬性

mysql列屬性包括&#xff1a;NULL 、default、comment、primary key、unique key 一、NULL定義方式&#xff1a;NULL&#xff08;默認&#xff09;  NOT NULL 空屬性有2個值&#xff0c;mysql數據庫默認字段都是為null的&#xff0c;但是在實際開發過程中&#xff0c;盡可能保…

前端知識點整理(三)不定時更新~

目錄 一、移動端跨平臺開發方案 Hybrid App React Native Weex Flutter PWA &#xff08;Progressive Web App&#xff09; 小程序 Cordova html5 組件和模塊的區別 組件化 模塊化 前端代碼規范 前端工程化理解 網站性能監測與優化策略 1.網絡傳輸性能優化 頁…

前端試題(一)

2020-03-28 金卡智能 *1. 腳手架 vue-cli現在用的什么版本&#xff0c;2版本了解多少&#xff0c;2 3有什么區別 絕對路徑與相對路徑 ./ 當前路徑 …/父路徑 / 絕對路徑 某文件里引用其他路徑下的資源&#xff1a; 判斷該文件所在文件夾與其他資源路徑間的關系。 什么&#…

html5 --- 利用localStorage進行本地存儲

首先做一個提交到本地存儲的表單及一個用來顯示本地localStorage信息的表格…代碼如下: <h2> 本地存儲用 </h2>標題: <input id"title" name"title" type"text" size"60" style"margin-left:32px;margin-bottom:…

Tomcat啟動阻塞變慢

Tomcat 熵池阻塞變慢詳解 Tomcat 啟動很慢&#xff0c;且日志上無任何錯誤&#xff0c;在日志中查看到如下信息&#xff1a; Log4j:[2015-10-29 15:47:11] INFO ReadProperty:172 - Loading properties file from class path resource [resources/jdbc.properties] Log4j:[201…

項目總結

123轉載于:https://www.cnblogs.com/kehuaihan/p/9284858.html

前端試題(二)

1. 數組方法、reduce()的第二個參數 reduce() MDN文檔 accumulator 累計器currentValue 當前值currentIndex 當前索引array 數組 在沒有初始值的空數組上調用 reduce 將報錯&#xff08;如果有initialValue不報錯&#xff09;。回調函數第一次執行時&#xff0c;accumulator…

項目中遇到問題的解決方法合集

以下內容主要是為了方便記錄自己在工作中遇到的項目問題搜尋到的解決方法&#xff0c;肯定方法不唯一&#xff0c;這里只是給出解決了我的問題的方法&#xff0c;大家走過路過隨便瞧瞧較好啦嘻嘻 1、使用vue/cli 4.x 創建vue項目時使用iconfont 圖標無法顯示——前者版本問題 …

JS展示預覽PDF。

剛好遇到需求&#xff0c;需要在手機端--展示一個電子收據&#xff0c;電子收據返回是PDF格式的&#xff0c;所以需要在前端上面去做PDF預覽。 在學習過程中&#xff0c;了解到一種很簡單&#xff0c;不需要任何插件的方法做PDF預覽&#xff0c;但是這方法有局限性. 代碼如下&a…

html5 --- IDBDatabase創建對象存儲和索引

代碼如下: <!DOCTYPE html> <html> <head><meta http-equiv"Content-Type" content"text/html; charsetutf-8" /><title> CRUD操作 </title><style type"text/css">table{width: 830px;border: 1px …

查詢數據庫 收集 (如某個字段不是中文)

查詢某個字段不為中文的 select * from 表名 where 字段名 REGEXP [\u0391-\uFFE5] 查詢某個字段以數字開頭 select * from 表名 where 字段名 REGEXP [0-9].*轉載于:https://www.cnblogs.com/spicy/p/8038442.html

2019大疆PC軟件開發筆試——開關和燈泡兩個電路板

題目描述: 小A是一名DIY愛好者&#xff0c;經常制作一些有趣的東西。 今天&#xff0c;小A突然想要來做這樣一個東西。小A現在有兩塊同樣大小為nm&#xff0c;有nm塊大小為11小電路板拼成的矩形電路板&#xff0c;假設叫做電路板A和電路板B。電路板A上每個小電路板都是一個開關…