ES6常用的新語法學習筆記
簡介
ES6是 JavaScript 語言的下一代標準,已經在 2015 年 6 月正式釋出了。它的目標,是使得 JavaScript 語言可以用來編寫複雜的大型應用程式,成為企業級開發語言。
1.let和const
let
let和var 差不多,都是用來宣告變數,但是let的宣告只能在{}內,以下程式碼輸入都是10
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {//將a[i]賦予一個方法,輸出i
console
};
}a[2](); // 10
但是如果把var i=0換成let i=0;的話當呼叫a[數字i的範圍]的時候輸出的就是你填入的數字,把let宣告理解為區域性物件,像下面這段程式碼:
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);}
只會輸出三次abd,這個輸出的i是最近的i,而不是迴圈條件中的i
另外,如果使用var未定義的物件會輸出undefined,而使用let的話則會報錯
暫時性死區
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
上方程式碼會tmp is not defined的錯誤,存在全域性變數tmp,但是塊級作用域內let又聲明瞭一個區域性變數tmp,導致後者繫結這個塊級作用域,所以在let宣告變數前,對tmp賦值會報錯。
ES6 明確規定,如果區塊中存在let和const命令,這個區塊對這些命令宣告的變數,從一開始就形成了封閉作用域。凡是在宣告之前就使用這些變數,就會報錯。
在程式碼塊內,使用let命令宣告變數之前,該變數都是不可用的。這在語法上,稱為“暫時性死區”
總之,暫時性死區的本質就是,只要一進入當前作用域,所要使用的變數就已經存在了,但是不可獲取,只有等到宣告變數的那一行程式碼出現,才可以獲取和使用該變數。
let不允許在相同作用域內{},重複宣告同一個變數。
// 報錯function func() {
let a = 10;
var a = 1;}
// 報錯function func() {
let a = 10;
let a = 1;}
允許在塊級作用域內宣告函式。
函式宣告類似於var,即會提升到全域性作用域或函式作用域的頭部。
同時,函式宣告還會提升到所在的塊級作用域的頭部。
像以下程式碼執行只會輸出 I am inside
function f() {
console.log('I am outside!');
}
(function () {
function f() {
console.log('I am inside!');
}
if (false) {
}
f();
}());
const命令
const宣告一個只讀的常量。一旦宣告,常量的值就不能改變。const一旦宣告變數,就必須立即初始化,不能留到以後賦值。對於const來說,只宣告不賦值,就會報錯。
const宣告的常量,也與let一樣不可重複宣告。
const實際上保證的,並不是變數的值不得改動,而是變數指向的那個記憶體地址所儲存的資料不得改動。對於簡單型別的資料(數值、字串、布林值),值就儲存在變數指向的那個記憶體地址,因此等同於常量。但對於複合型別的資料(主要是物件和陣列),變數指向的記憶體地址,儲存的只是一個指向實際資料的指標,const只能保證這個指標是固定的(即總是指向另一個固定的地址),至於它指向的資料結構是不是可變的,就完全不能控制了。因此,將一個物件宣告為常量必須非常小心。
const foo = {};
// 為 foo 新增一個屬性,可以成功foo.prop = 123;
foo.prop // 123
// 將 foo 指向另一個物件,就會報錯foo = {}; // TypeError: "foo" is read-only
ES6 宣告變數的六種方法
ES5 只有兩種宣告變數的方法:var命令和function命令。ES6 除了新增let和const命令,後面章節還會提到,另外兩種宣告變數的方法:import命令和class命令。
var命令和function命令宣告的全域性變數,依舊是頂層物件的屬性;另一方面規定,let命令、const命令、class命令宣告的全域性變數,不屬於頂層物件的屬性。
2.變數的解構賦值
陣列的解構賦值
es6允許寫成這樣:let [a, b, c] = [1, 2, 3];
上面程式碼表示,可以從陣列中提取值,按照對應位置,對變數賦值。
let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"
這是一個set集合,事實上,只要某種資料結構具有 Iterator 介面,都可以採用陣列形式的解構賦值。
ES6 內部使用嚴格相等運算子(===),判斷一個位置是否有值。所以,只有當一個數組成員嚴格等於undefined,預設值才會生效。
let [x = 1] = [undefined];
x // 1let [x = 1] = [null];
x // null
上面程式碼中,如果一個數組成員是null,預設值就不會生效,因為null不嚴格等於undefined。
物件的解構賦值
let { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"bar // "bbb"
物件的解構與陣列有一個重要的不同。陣列的元素是按次序排列的,變數的取值由它的位置決定;而物件的屬性沒有次序,變數必須與屬性同名,才能取到正確的值。
如果變數名與屬性名不一致,必須寫成下面這樣。
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: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"foo // error: foo is not defined
上面程式碼中,foo是匹配的模式,baz才是變數。真正被賦值的是變數baz,而不是模式foo。
物件的解構也可以指定預設值。預設值生效的條件是,物件的屬性值嚴格等於undefined。
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
上面程式碼中,屬性x等於null,因為null與undefined不嚴格相等,所以是個有效的賦值,導致預設值3不會生效。
如果解構失敗,變數的值等於undefined。
let {foo} = {bar: 'baz'};
foo // undefined
字串的解構賦值
字串也可以解構賦值。這是因為此時,字串被轉換成了一個類似陣列的物件。
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 // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
上面程式碼中,數值和布林值的包裝物件都有toString屬性,因此變數s都能取到值。
解構賦值的規則是,只要等號右邊的值不是物件或陣列,就先將其轉為物件。由於undefined和null無法轉為物件,所以對它們進行解構賦值,都會報錯。
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
函式引數的解構賦值
函式的引數也可以使用解構賦值。
function add([x, y]){
return x + y;}
add([1, 2]); // 3
上面程式碼中,函式add的引數表面上是一個數組,但在傳入引數的那一刻,陣列引數就被解構成變數x和y。對於函式內部的程式碼來說,它們能感受到的引數就是x和y。
下面是另一個例子。
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]
可以使用圓括號的情況只有一種:賦值語句的非模式部分,可以使用圓括號。
用途
(1)交換變數的值
let x = 1;let y = 2;
[x, y] = [y, x];
(2)從函式返回多個值
函式只能返回一個值,如果要返回多個值,只能將它們放在陣列或物件裡返回。有了解構賦值,取出這些值就非常方便。
// 返回一個數組
function example() {
return [1, 2, 3];
}let [a, b, c] = example();
// 返回一個物件
function example() {
return {
foo: 1,
bar: 2
};}
let { foo, bar } = example();
任何部署了 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) {
// ...}
3.字串的擴充套件
使得字串可以被for...of迴圈遍歷。把這個字串當做集合遍歷輸出
for (let codePoint of 'foo') {
console.log(codePoint)
}
String 的新方法:
includes():返回布林值,表示是否找到了引數字串。
startsWith():返回布林值,表示引數字串是否在原字串的頭部。
endsWith():返回布林值,表示引數字串是否在原字串的尾部。
這三個方法都支援第二個引數,表示開始搜尋的位置。
repeat方法返回一個新字串,表示將原字串重複n次。
padStart()用於頭部補全,padEnd()用於尾部補全。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
如果原字串的長度,等於或大於指定的最小長度,則返回原字串。
'xxx'.padStart(2, 'ab') // 'xxx' 'xxx'.padEnd(2, 'ab') // 'xxx'
matchAll方法返回一個正則表示式在當前字串的所有匹配
String.raw方法,往往用來充當模板字串的處理函式,返回一個斜槓都被轉義(即斜槓前面再加一個斜槓)的字串,對應於替換變數後的模板字串:
String.raw`Hi\n${2+3}!`;
// 返回 "Hi\\n5!"
String.raw`Hi\u000A!`;
// 返回 "Hi\\u000A!"
模板字串
$('#result').append(
'There are <b>' + basket.count + '</b> ' +
'items in your basket, ' +
'<em>' + basket.onSale +
'</em> are on sale!');
上面這種寫法相當繁瑣不方便,ES6 引入了模板字串解決這個問題。
$('#result').append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);
模板字串(template string)是增強版的字串,用反引號(`)標識。它可以當作普通字串使用,也可以用來定義多行字串,或者在字串中嵌入變數。大括號內部可以放入任意的 JavaScript 表示式,可以進行運算,以及引用物件屬性。模板字串之中還能呼叫函式:
function fn() {
return "Hello World";}
`foo ${fn()} bar`
let template = `
<ul>
<% for(let i=0; i < data.supplies.length; i++) { %>
<li><%= data.supplies[i] %></li>
<% } %>
</ul>
`;
上面程式碼在模板字串之中,放置了一個常規模板。該模板使用<%...%>放置 JavaScript 程式碼,使用<%= ... %>輸出 JavaScript 表示式。
4.數值的擴充套件
Math.trunc()
Math.trunc方法用於去除一個數的小數部分,返回整數部分。
Math.trunc(4.1) // 4
Math.trunc(4.9) // 4
Math.trunc(-4.1) // -4
Math.trunc(-4.9) // -4
Math.trunc(-0.1234) // -0
對於非數值,Math.trunc內部使用Number方法將其先轉為數值。
Math.trunc('123.456') // 123
Math.trunc(true) //1
Math.trunc(false) // 0
Math.trunc(null) // 0
對於空值和無法擷取整數的值,返回NaN。
Math.trunc(NaN); // NaN
Math.trunc('foo'); // NaN
Math.trunc(); // NaN
Math.trunc(undefined) // NaN
Math.sign()
Math.sign方法用來判斷一個數到底是正數、負數、還是零。對於非數值,會先將其轉換為數值。
它會返回五種值。
- 引數為正數,返回+1;
- 引數為負數,返回-1;
- 引數為 0,返回0;
- 引數為-0,返回-0;
- 其他值,返回NaN。
Math.cbrt()
Math.cbrt方法用於計算一個數的立方根。
Math.cbrt(-1) // -1Math.cbrt(0) // 0Math.cbrt(1) // 1Math.cbrt(2) // 1.2599210498948734
對於非數值,Math.cbrt方法內部也是先使用Number方法將其轉為數值。
Math.cbrt('8') // 2Math.cbrt('hello') // NaN
Math.clz32()
JavaScript 的整數使用 32 位二進位制形式表示,Math.clz32方法返回一個數的 32 位無符號整數形式有多少個前導 0。
Math.clz32(0) // 32Math.clz32(1) // 31Math.clz32(1000) // 22Math.clz32(0b01000000000000000000000000000000) // 1Math.clz32(0b00100000000000000000000000000000) // 2
Math.imul()
Math.imul方法返回兩個數以 32 位帶符號整數形式相乘的結果,返回的也是一個 32 位的帶符號整數。
Math.imul(2, 4) // 8Math.imul(-1, 8) // -8Math.imul(-2, -2) // 4
Math.fround()
Math.fround方法返回一個數的32位單精度浮點數形式。
對於32位單精度格式來說,數值精度是24個二進位制位(1 位隱藏位與 23 位有效位),所以對於 -224 至 224 之間的整數(不含兩個端點),返回結果與引數本身一致。
Math.hypot()
Math.hypot方法返回所有引數的平方和的平方根。
指數運算子
ES2016 新增了一個指數運算子(**)。
2 ** 2 // 4
2 ** 3 // 8
這個運算子的一個特點是右結合,而不是常見的左結合。多個指數運算子連用時,是從最右邊開始計算的。
// 相當於 2 ** (3 ** 2)
2 ** 3 ** 2
// 512
上面程式碼中,首先計算的是第二個指數運算子,而不是第一個。
指數運算子可以與等號結合,形成一個新的賦值運算子(**=)。
let a = 1.5;
a **= 2;
// 等同於 a = a * a;
let b = 4;
b **= 3;
// 等同於 b = b * b * b;
在js中的<<和>>位運算子也能使用
5.函式的擴充套件
ES6 允許為函式的引數設定預設值,即直接寫在引數定義的後面。
function log(x, y = 'World') {
console.log(x, y);}
log('Hello') // Hello Worldlog('Hello', 'China') // Hello Chinalog('Hello', '') // Hello
ES6 引入 rest 引數(形式為...變數名),用於獲取函式的多餘引數,這樣就不需要使用arguments物件了。rest 引數搭配的變數是一個數組,該變數將多餘的引數放入陣列中。和java中的類似
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;}
add(2, 5, 3) // 10
上面程式碼的add函式是一個求和函式,利用 rest 引數,可以向該函式傳入任意數目的引數。
函式的name屬性返回函式的名字.
箭頭函式
var f = v => v;
// 等同於
var f = function (v) {
return v;
};
如果箭頭函式不需要引數或需要多個引數,就使用一個圓括號代表引數部分。
var f = () => 5;
// 等同於var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同於var sum = function(num1, num2) {
return num1 + num2;};
如果箭頭函式不需要引數或需要多個引數,就使用一個圓括號代表引數部分。
var f = () => 5;
// 等同於
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同於
var sum = function(num1, num2) {
return num1 + num2;
};
如果箭頭函式的程式碼塊部分多於一條語句,就要使用大括號將它們括起來,並且使用return語句返回。
var sum = (num1, num2) => { return num1 + num2; }
由於大括號被解釋為程式碼塊,所以如果箭頭函式直接返回一個物件,必須在物件外面加上括號,否則會報錯
// 報錯let getTempItem = id => { id: id, name: "Temp" };
// 不報錯let getTempItem = id => ({ id: id, name: "Temp" });
如果箭頭函式只有一行語句,且不需要返回值,可以採用下面的寫法,就不用寫大括號了。
let fn = () => void doesNotReturn();
箭頭函式可以與變數解構結合使用。
const full = ({ first, last }) => first + ' ' + last;
// 等同於function full(person) {
return person.first + ' ' + person.last;}
箭頭函式有幾個使用注意點。
(1)函式體內的this物件,就是定義時所在的物件,而不是使用時所在的物件。
(2)不可以當作建構函式,也就是說,不可以使用new命令,否則會丟擲一個錯誤。
(3)不可以使用arguments物件,該物件在函式體內不存在。如果要用,可以用 rest (...var)引數代替。
(4)不可以使用yield命令,因此箭頭函式不能用作 Generator 函式。
上面四點中,第一點尤其值得注意。this物件的指向是可變的,但是在箭頭函式中,它是固定的。
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);}
var id = 21;
foo.call({ id: 42 });
// id: 42
上面程式碼中,setTimeout的引數是一個箭頭函式,這個箭頭函式的定義生效是在foo函式生成時,而它的真正執行要等到 100 毫秒後。如果是普通函式,執行時this應該指向全域性物件window,這時應該輸出21。但是,箭頭函式導致this總是指向函式定義生效時所在的物件(本例是{id: 42}),所以輸出的是42。
箭頭函式可以讓setTimeout裡面的this,繫結定義時所在的作用域,而不是指向執行時所在的作用域。
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭頭函式
setInterval(() => this.s1++, 1000);
// 普通函式
setInterval(function () {
this.s2++;
}, 1000);}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3// s2: 0
上面程式碼中,Timer函式內部設定了兩個定時器,分別使用了箭頭函式和普通函式。前者的this繫結定義時所在的作用域(即Timer函式),後者的this指向執行時所在的作用域(即全域性物件)。所以,3100 毫秒之後,timer.s1被更新了 3 次,而timer.s2一次都沒更新。
什麼是尾呼叫?
尾呼叫(Tail Call)是函數語言程式設計的一個重要概念,本身非常簡單,一句話就能說清楚,就是指某個函式的最後一步是呼叫另一個函式。但是必須是一個函式最後的一個操作是return 另一個函式才叫尾呼叫.
比如:函式m和n都屬於尾呼叫,因為它們都是函式f的最後一步操作。
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);}
尾呼叫優化
function f() {
let m = 1;
let n = 2;
return g(m + n);}
f();
// 等同於function f() {
return g(3);}
f();
// 等同於g(3);
如果函式g不是尾呼叫,函式f就需要儲存內部變數m和n的值、g的呼叫位置等資訊。但由於呼叫g之後,函式f就結束了,所以執行到最後一步,完全可以刪除f(x)的呼叫幀,只保留g(3)的呼叫幀。
這就叫做“尾呼叫優化”
6.陣列的擴充套件
擴充套件運算子
擴充套件運算子(spread)是三個點(...)。它好比 rest 引數的逆運算,將一個數組轉為用逗號分隔的引數序列。
該運算子主要用於函式呼叫。
function push(array, ...items) {
array.push(...items);}
function add(x, y) {
return x + y;}
const numbers = [4, 38];add(...numbers) // 42
上面程式碼中,array.push(...items)和add(...numbers)這兩行,都是函式的呼叫,它們的都使用了擴充套件運算子。該運算子將一個數組,變為引數序列。
7.class, extends, super
ES6提供了更接近傳統語言的寫法,引入了Class(類)這個概念。新的class寫法讓物件原型的寫法更加清晰、更像面向物件程式設計的語法,也更加通俗易懂。
上面程式碼首先用class定義了一個“類”,可以看到裡面有一個constructor方法,這就是構造方法,而this關鍵字則代表例項物件。簡單地說,constructor內定義的方法和屬性是例項物件自己的,而constructor外定義的方法和屬性則是所有例項物件可以共享的。 extends和super和java中的類似,就不說了
8.template string
大家可以先看下面一段程式碼:
$("#result").append(
"There are <b>" + basket.count + "</b> " +
"items in your basket, " +
"<em>" + basket.onSale +
"</em> are on sale!"
);
我們要用一堆的'+'號來連線文字與變數,而使用ES6的新特性模板字串``後,我們可以直接這麼來寫:
$("#result").append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);
用反引號(\)來標識起始,用${}`來引用變數,而且所有的空格和縮排都會被保留在輸出之中
9.import export
假設我們有兩個js檔案: index.js和content.js,現在我們想要在index.js中使用content.js返回的結果,我們要怎麼做呢?
//index.js
import animal from './content'
//content.js
export default 'A cat'
ES6 module的其他高階用法
//content.js
export default 'A cat'
export function say(){
return 'Hello!'
}
export const type = 'dog'
上面可以看出,export命令除了輸出變數,還可以輸出函式,甚至是類(react的模組基本都是輸出類)
//index.js
import { say, type } from './content'
let says = say()
console.log(`The ${type} says ${says}`) //The dog says Hello
這裡輸入的時候要注意:大括號裡面的變數名,必須與被匯入模組(content.js)對外介面的名稱相同。
如果還希望輸入content.js中輸出的預設值(default), 可以寫在大括號外面。