1. 程式人生 > >ES6學習筆記一之js發展、let、const、解構賦值

ES6學習筆記一之js發展、let、const、解構賦值

ECMAScript和JavaScript的關係?

1996年11月,JavaScript的創造者—-Netscape公司,決定將JavaScript提交給國際標準化組織ECMA,希望這種語言能成為國際標準。次年,ECMA釋出262號檔案(ECMA-262)的第一版,規定了瀏覽器指令碼語言的標準,並將這種語言成為ECMAScript,這個版本就是1.0版。

該標準一開始就是針對JavaScript語言制定的,但是沒有稱為JavaScript,原因有二:

  • Java時Sun公司的註冊商標,根據授權協議,只有NetScape公司可以合法使用JavaScript這個名字,而且JavaScript本身已被NetScape公司註冊為商標。
  • 體現這門語言的制定者時ECMA,而不是NetScape,這樣有利於保證這門語言的開放性和中立性。

因此ECAM和JavaScript的關係是,前者是後者的規格,後者是前者的一種實現(另外還有JScript和ActionScript)。

下表是各個版本ES釋出時間:

標準標題 釋出時間
ECMA-262 1997年7月
ECMA-262 Edition2 1998年8月
ECMA-262 Edition3 1999年12月
ECMA-262 Edition5 2009年12月
ECMA-262 Edition6 2015年6月
ECMA-262 Edition7 2016年7月

ECMAScript 2016(ES7)開始,版本的釋出將會變得更加頻繁, 這也意味著未來每個新的發行版本都會包含儘可能少的特性,而發行週期則縮短為1年,並且每年只發行確保一年期限內能夠完成的所有特性。

let和const

let命令

基本用法

ES6新增了let命令,用法和var類似,區別是let宣告的變數只在其所在的作用域有效。

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

一個典型的例子,看看var和let的區別:

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

上述i是全域性宣告的,a[0]到a[10]繫結的都是同一個函式function(){console.log(i)},所以在執行的時候i變成10了。會打印出10.

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

使用let宣告變數,let劫持了for迴圈的作用域,所以每個i都是有自己的作用域的。於是最後輸出的結果是6。

不存在變數提升

let不像var會存在變數提升。所以必須先宣告變數才能使用,否則報錯。

console.log(x); // undefined 
var x;
console.log(x); // ReferenceError: x is not defined
let x;

暫時性死區(temporal dead zone, TDZ)

ES6明確規定,如果區塊中存在let和const命令,則這個區塊對這些命令宣告的變數從一開始就形成封閉作用域。只要在宣告之前使用這些變數,就報錯。

上面那個例子也算TDZ。

if(true) {
  tmp = 'abc'; // ReferenceError: tmp is not defined
  console.log(tmp); // ReferenceError: tmp is not defined

  let tmp; // TDZ結束
  console.log(tmp);
}

還有些閉包較為隱蔽,不易察覺。函式引數預設賦值時es6才出現的,使用let。:)

function foo(x = y, y = 2) {
  console.log(x);
}
foo(); // ReferenceError: y is not defined

因為函式引數的賦值從左到右,給x賦值時,y還處於TDZ。所以賦值失敗。

ES6規定暫時性死區和不存在變數提升,主要是為了減少執行時錯誤,畢竟這樣的失誤在ES5是存在的。現在有了這種規定,瀏覽器會自動避免此類錯誤。

關於是否存在變數提升,這裡有一篇文章

不允許重複宣告

{
  var a = 1;
  let a = 1; // SyntaxError: Identifier 'a' has already been declared
}

塊級作用域

ES5沒有塊級作用域,只有函式作用域和全域性作用域。比如形成變數覆蓋。

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

  if(false) {
    var a = 2; // 變數提升
  }
}
foo(); // undefined

再比如迴圈中的變數洩露到全域性變數。

ES6新增了塊級作用域,使用let可以形成塊級作用域。

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

上述如果使用var定義輸出結果是10。
ES6允許作用域任意巢狀。

{{{{ let a = 1; }}}}

函式能不能在塊級作用域之中宣告,是一個相當令人混淆的問題。

ES5 規定,函式只能在頂層作用域和函式作用域之中宣告,不能在塊級作用域宣告。

if (true) {
  function f() {}
}

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

上面兩種函式宣告,根據 ES5 的規定都是非法的。

但是,瀏覽器沒有遵守這個規定,為了相容以前的舊程式碼,還是支援在塊級作用域之中宣告函式,因此上面兩種情況實際都能執行,不會報錯。ES不過,“嚴格模式”下還是會報錯。(我在chrome50.0沒報錯,預設轉換為es6了嗎???)

// ES5嚴格模式
'use strict';
if (true) {
  function f() {}
}

ES6 引入了塊級作用域,明確允許在塊級作用域之中宣告函式。

// ES6
if (true) {
  function f() {} // 不報錯
}

ES6 規定,塊級作用域之中,函式宣告語句的行為類似於let,在塊級作用域之外不可引用。

function f() { console.log('I am outside!'); }
(function () {
  if (false) {
    // 重複宣告一次函式f
    function 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 版本
function f() { console.log('I am outside!'); }
(function () {
  f();
}());

但是,如果你真的在 ES6 瀏覽器中執行一下上面的程式碼,是會報錯的,這是為什麼呢?

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

  • 允許在塊級作用域內宣告函式。
  • 函式宣告類似於var,即會提升到全域性作用域或函式作用域的頭部。
  • 同時,函式宣告還會提升到所在的塊級作用域的頭部。

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

根據這三條規則,在瀏覽器的 ES6 環境中,塊級作用域內宣告的函式,行為類似於var宣告的變數。

// 瀏覽器的 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 的瀏覽器中,都會報錯,因為實際執行的是下面的程式碼。

// 瀏覽器的 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

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

// 函式宣告語句
{
  let a = 'secret';
  function f() {
    return a;
  }
}

// 函式表示式
{
  let a = 'secret';
  let f = function () {
    return a;
  };
}

另外,還有一個需要注意的地方。ES6 的塊級作用域允許宣告函式的規則,只在使用大括號的情況下成立,如果沒有使用大括號,就會報錯。

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

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

const命令

const定義一個常量,定義的同時必須賦值,不賦值就會報錯。其值不會改變,給它重新賦值會引起錯誤。

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

但是對於一個複雜資料型別(物件),我們知道它在記憶體中是有一個地址的(即指標),const即是指向這個地址,保證這個地址是常量即可。對於物件的屬性,卻是可以定義的。

const obj = {};
obj.a = 3; // 這是可以的

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

const foo = Object.freeze({});
foo.prop = 123; // 不起作用

const的作用域和let命令相同:只在宣告所在的塊級作用域內有效。

if(true) {
  const Max = 5;
}
console.log(Max); // ReferenceError

const命令宣告的變數也不提升,同樣存在暫時性死區。

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

const也不可重複宣告變數,這點和let一樣。無論是用var還是let宣告。

var a = 1;
const a = 1; // SyntaxError: Identifier 'a' has already been declared

ES5只有兩種宣告變數的黨閥,var命令和function命令。ES6除添加了let和const命令。還有import命令和class命令。所以,ES6一共有6中宣告變數的方法。

全域性物件的屬性

ES6規定,var和function宣告的全域性變數依舊是全域性物件的屬性,而const和let命令宣告的變數不屬於全域性物件的屬性。

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

解構賦值

陣列的解構賦值

基本用法

以前為變數賦值,只能直接指定值,一個等號只能賦值一次。而在ES6中,

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

本質上,這種寫法屬於“模式匹配”,只要等號兩邊等邊的模式相同,就可以賦值。

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

如果變數沒有對應到值(解構不成功),那麼變數預設賦值為undefined。

let [a, b, c] = [1,2];
console.log(a,b,c); // 1 2 undefined

另一個例子是不完全解構,解構是可以成功的。

var [a, [b]] = [1, [2, 3]];
console.log(a, b); // 1 2

解構對var、let、const命令都適合。

事實上,只要某種資料結構具有Iterator介面,都可以採用陣列形式的解構賦值。

預設值

解構賦值允許制定預設值。

var [foo = true] = [];
console.log(foo); // true

注意,ES6內部使用嚴格相等運算子(===)判斷一個位置是否有值。所以,只有一個數組成員嚴格等於undefined,預設值才會生效(如上述的什麼都不寫)。

var [x = 1, y = 2] = [undefined, null];
console.log(x, y); // 1, null 

物件的解構賦值

解構不僅可以用於物件,還可以用於陣列。

var { foo, bar } = { foo: "aaa", bar: "bbb" } ;
console.log(foo, bar); // aaa bbb

物件的解構賦值與陣列有一個重要的不同。陣列的元素是有序的,變數的取值有它的位置決定;而物件的屬性沒有次序,變數必須與屬性同名,才能取到正確的值。

var { foo, bar } = { bar: 'bbb', foo: 'aaa' };
console.log(foo, bar); // aaa bbb

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

var {foo: baz} = {foo: 1};
console.log(baz); // 1
console.log(foo); // ReferenceError: foo is not defined

這實際上說明,物件的解構賦值時一下形式的縮寫:

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

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

對於解構賦值這種寫法,變數的宣告和賦值時一體的。對於let和const而言,變數不能重新宣告,否則會報錯。

let foo;
let { foo } = { foo: 1 }; // SyntaxError: Identifier 'foo' has already been declared

陣列和物件 的結構賦值可以結合起來使用。

字串的解構賦值

const [a, b] = 'hi';
console.log(a, b); // h i
let {length: len} = 'hello';
console.log(len); // 5

數值和布林值的解構賦值

let { toString: s } = 123;
console.log(s === Number.prototype.toString); // true

let { toString: s } = true;
console.log(s === Boolean.prototype.toString); // true

解構賦值的規則是:如果等號右邊不是物件,就先將右邊轉換為物件。對於undefined和null無法轉為物件,所以對它們解構賦值會報錯。

函式引數的解構賦值

function add([x, y]) {
  return x + y;
}
console.log(add([2 ,3])); // 5

引數的解構賦值也可以使用預設值,只有undefined會觸發預設值。

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

對於上述程式碼,我的理解是呼叫函式時,先將函式實參與實參匹配,如果可以匹配,即前三個。實參物件取代了預設值,然後對x,y進行賦值,正如第三個,實參中的x,y都是undefined。如果沒匹配上,就是用預設引數,再對x、y賦值。

圓括號問題

不能使用圓括號的情況

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

  • 變數宣告語句中,不能帶有圓括號。
// 全部報錯
let [(a)] = [1];

let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};

let { o: ({ p: p }) } = { o: { p: 2 } };

上面三個語句都會報錯,因為它們都是變數宣告語句,模式不能使用圓括號。

  • 函式引數中,模式不能帶有圓括號。

函式引數也屬於變數宣告,因此不能帶有圓括號。

// 報錯
function f([(z)]) { return z; }
  • 賦值語句中,不能將整個模式,或巢狀模式中的一層,放在圓括號之中。
// 全部報錯
({ p: a }) = { p: 42 };
([a]) = [5];

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

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

上面程式碼將巢狀模式的一層,放在圓括號之中,導致報錯。

可以使用圓括號的情況

  • 可以使用圓括號的情況只有一種:賦值語句的非模式部分,可以使用圓括號。
[(b)] = [3]; // 正確
({ p: (d) } = {}); // 正確
[(parseInt.prop)] = [3]; // 正確

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

用途

  • 交換變數的值
[x, y] = [y, x];
  • 從函式返回多個值
  • 函式引數的定義
  • 提取JSON資料
  • 函式引數的預設值
  • 遍歷Map結構
    任何部署了Iterator介面的物件,都可以用for…of迴圈遍歷。Map結構原生支援Iterator介面,配合變數的解構賦值,獲取鍵名和鍵值就非常方便。
  • 輸入模組
const { Component } = require('react-native');

參考