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');