ES6詳細介紹及使用
一、 ES6概念及發展史
1、ES6概念
以前學習JavaScript的時候,對ES5是有了解過的,但是在學習Vue的時候,就會發現有很多新的寫法是ES6中的,真是讓人捉急。是以今天ES6他來了。原英文文檔:https://262.ecma-international.org/6.0/#sec-functiondeclarationinstantiation
W3C中對ECMAScript 6定義:ECMAScript 6.0(以下簡稱
ES6
)是 JavaScript 語言的下一代标準,已經在 2015 年 6 月正式釋出了(是以也被叫作ES2015)。它的目标,是使得 JavaScript 語言可以用來編寫複雜的大型應用程式,成為企業級開發語言。
ES6中大概做了哪些改變呢?
2、ES6發展史
版本 | 時間 | 描述 |
ECMA-262/ ECMAScript 1.0 | 1997 | 1996 年 11 月,JavaScript 的創造者 公司,決定将 JavaScript 送出給标準化組織 ,希望這種語言能夠成為國際标準。次年,ECMA 釋出 262 号标準檔案(ECMA-262)的第一版,規定了浏覽器腳本語言的标準,并将這種語言稱為 ECMAScript,這個版本就是 1.0 版 |
ECMAScript 2.0 | 1998 | 1998年6月誕生了ECMAScript 2.0,這一版的内容更新是為了與ISO/IEC-16262保持嚴格一緻,沒有作任何新增、修改或删節處理。 |
ECMAScript 3.0 | 1999 | 1999年12月誕生了ECMAScript 3.0,此版本應用非常廣泛,成為JavaScript文法基礎(JavaScript = ECMAScript(核心) + DOM + BOM)。新增了對正規表達式、新控制語句、try-catch異常處理的支援,修改了字元處理、錯誤定義和數值輸出等内容。 |
ECMAScript 4.0 | 2000 | 2000年,ES4.0 釋出,新标準幾乎是差別于ES3的新語言,由于4.0版的目标過于激進,各方對于是否通過這個标準,發生了嚴重分歧。争論了幾年...以Yahoo、Microsoft、Google為首的大公司,反對JavaScript的大幅更新,主張小幅改動;以JavaScript創造者Brendan Eich為首的Mozilla公司,則堅持目前的草案。 |
ECMAScript 3.1 | 2008 | 2008年7月,ES3.1 釋出,作為 ES4.0 的替代方案,中止了 ES4.0的釋出; |
ECMAScript5.0 | 2009 | 2009年,ES3.1 作為第五版(改名為ECMAScript 5)正式釋出,由于變動太大,未進行推廣應用; |
ECMAScript5.1 | 2011 | 2011年,ES5.1 釋出,成為國際标準;到了2012年底,所有主要浏覽器都支援ECMAScript 5.1版的全部功能。 |
ECMAScript2015 | 2015 | 2015年6月,ECMAScript 6正式釋出,并且更名為“ECMAScript 2015”,也是指 ES5.1 後的下一代JavaScript标準; S6是繼S5之後的一次主要改進,語言規範由ES5.1時代的245頁擴充至600頁。盡管ES6做了大量的更新,但是它依舊完全向後相容以前的版本。 ES6增添了許多必要的特性,新功能包括:子產品和類以及一些實用特性,例如Maps、Sets、Promises、生成器(Generators)等。 |
ECMAScript2016 | 2016 | TC39委員會計劃,以後每年釋出一個ECMAScirpt的版本,下一個版本在2016年釋出,稱為“ECMAScript 2016” |
二、 塊級作用域變量聲明(let、const)
先複習下ES5中的作用域:
在Java或C#中存在塊級作用域,即:大括号也是一個作用域。而在ES5中每個函數作為一個作用域,在外部無法通路内部作用域中的變量(通常認為ES5存在兩種作用域,即:全局作用域和函數作用域)。如果出現函數嵌套函數,就會出現作用域鍊。其尋找順序為根據作用域鍊從内到外的優先級尋找,如果内層沒有就逐漸向上找,直到沒找到抛出異常。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
</body>
<script>
var xo = '全局';
function Func(){
var xo = "局部";
function inner(){
var xo = '嵌套';
console.log(xo);
}
inner();
}
//在函數被調用之前作用域鍊已經存在:全局作用域 -> Func函數作用域 -> inner函數作用域
//當執行Func時,會調用inner函數,在函數中就已經存在了xo變量。
Func(); //嵌套
</script>
</html>
聲明提前:
在js中用var ,function聲明的變量都将被前;
函數聲明的優先級大于變量聲明的優先級(在指派聲明的時候首先會去方法域中尋找,而後才去變量域中尋找);
在函數内部變量提升的優先級會小于函數參數;
<script>
//聲明提前等效于var a;console.log(a);a = 3;
console.log(x);//undefined 而不會出現
var x = 3;
console.log(a);//function a(){console.log(x);}
function a(){
console.log(x);
}
var a = 1;
</script>
1、let指令
ES6 新增了
let
指令,用來聲明變量。它的用法類似于
var
,但是所聲明的變量,隻在
let
指令所在的代碼塊内有效。
1.1、let聲明的變量不存在變量提升
// var 的情況
console.log(foo); // 輸出undefined
var foo = 2;
// let 的情況
console.log(bar); // 報錯ReferenceError
let bar = 2;
1.2、let聲明的變量不允許重複
// 報錯
function func() {
let a = 10;
var a = 1;
}
// 報錯
function func() {
let a = 10;
let a = 1;
}
//不報錯
function func(arg) {
{
let arg;
}
}
1.3、let聲明的變量塊級作用域内有效
<script>
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
//按習慣來說,這裡的i,不應該是5
console.log(i); // 5
var s1 = 'hello';
for(let j=0;j<s.length;j++){
console.log(s[0]);
}
//j不存在,這樣比較符合我們的思想
console.log(j); // Uncaught ReferenceError: j is not defined
</script>
2、const指令
const
聲明一個隻讀的常量。一旦聲明,常量的值就不能改變。
const
的作用域與
let
指令相同:隻在聲明所在的塊級作用域内有效。
const
指令聲明的常量也是不提升。
const
實際上保證的,并不是變量的值不得改動,而是變量指向的那個記憶體位址所儲存的資料不得改動。
const PI = 3.1415;
PI // 3.1415
PI = 3;// TypeError: Assignment to constant variable.
const foo = {};
// 為 foo 添加一個屬性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一個對象,就會報錯
foo = {}; // TypeError: "foo" is read-only
注:頂層對象,在浏覽器環境指的是
window
對象,在 Node 指的是
global
對象。ES5 之中,頂層對象的屬性與全局變量是等價的。ES6 為了改變這一點,一方面規定,為了保持相容性,
var
指令和
function
指令聲明的全局變量,依舊是頂層對象的屬性;另一方面規定,
let
指令、
const
指令、
class
指令聲明的全局變量,不屬于頂層對象的屬性。
var a = 1;
// 如果在 Node 的 REPL 環境,可以寫成 global.a
// 或者采用通用方法,寫成 this.a
window.a // 1
let b = 1;
window.b // undefined
三、 解構與指派
先看一個栗子:
//在ES5中我們會這麼寫
var arr =[111,222,333];
var first = arr[0];
var second = arr[1];
var third = arr[2];
//但是在ES6中我們可以這樣寫
let [first, second, third] = arr;
//本質上,這種寫法屬于“模式比對”、“映射關系”
什麼是解構?從前,有一個叫庖丁的廚師,特别善于宰牛。庖丁心裡先設想把牛(Array、Object等)分解成很多塊,然後按照規劃好的想法,一刀刀對應起來,就把牛分解了。把解構指派說的更通俗點,有點類似于“庖丁解牛” 。
定義:ES6 允許按照一定模式,從數組和對象中提取值,對變量進行指派,這被稱為解構。
變量的解構指派用途很多:1)交換變量的值,寫法更簡潔;2)提取 JSON 資料;3)從函數傳回多個值;4)函數參數的定義;5)函數參數的預設值;6)周遊 Map 結構(for...of);7)導入與導出子產品(import/export);
變量的解構指派就是一種寫法,掌握了這種寫法可以讓我們在書寫 javascript 代碼時可以更加的簡單,迅捷。
1、數組的解構指派
1.1、完全比對模式
隻要等号兩邊模式一緻,左邊變量即可擷取右邊對應位置的值。
// ES6 之前
var a=1;
var b=2;
var c=3;
// ES6 之後
let [a,b,c] = [1,2,3];
1.2、支援對任意深度的嵌套數組解構
能夠非常迅速的擷取二維數組、三維數組,甚至多元數組中的值。
let [foo, [[bar], baz]] = [1, [[2], 3]];
console.log(foo); // 1
console.log(bar); // 2
console.log(baz); // 3
1.3、不需要比對的位置可以置空
[,,third] = [1, 2, 3];
console.log(third); // 3
1.4、支援使用...擴充運算符
var [head, ...body] = [1, 2, 3, 4];
console.log(body); // [2, 3, 4]
2、對象的解構指派
2.1、可以自定義屬性名稱
var {name, id: ID} = { name: 'jack', id: 1 };
ID // 1
id // Uncaught ReferenceError: id is not defined
2.2、可以對任意深度的嵌套對象進行解構
//解析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]
//解析嵌套對象
const node = {
loc: {
start: {
line: 1,
column: 5
}
}
};
let { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc // Object {start: Object}
start // Object {line: 1, column: 5}
2.3、字元串解構
字元串也可以解構指派。這是因為此時,字元串被轉換成了一個類似數組的對象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
let { length:len } = 'hello';
console.log(len); //5 (長度為5)
2.4、數值和布爾值的解構指派
解構指派時,如果等号右邊是數值和布爾值,則會先轉為對象。
// 數值和布爾值的包裝對象都有toString屬性
let {toString: str1} = 111;
str1 === Number.prototype.toString // true
let {toString: str2} = true;
str2 === Boolean.prototype.toString // true
let { prop: x } = undefined; // TypeError:Cannot destructure property 'prop' of 'undefined' as it is undefined.
let { prop: y } = null; // TypeError
上面代碼中,數值和布爾值的包裝對象都有
toString
屬性,是以變量
s
都能取到值。解構指派的規則是,隻要等号右邊的值不是對象或數組,就先将其轉為對象。由于
undefined
和
null
無法轉為對象,是以對它們進行解構指派,都會報錯。
2.5、函數參數的解構
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
四、 擴充運算符(spread)和rest運算符
1、數組的擴充運算符
1.1、将數組轉換為參數序列
function add(x, y) {
return x + y;
}
const numbers = [1, 2];
add(...numbers) // 3
1.2、複制/合并數組
const arr1 = [1, 2];
const arr2 = [...arr1]; //淺拷貝
const arr3 = [3, 4];
console.log([...arr1,...arr3]); // [1, 2, 3, 4]
// 本質也是:**将一個數組轉為用逗号分隔的參數序列,然後置于數組中**
//注:如果将擴充運算符用于數組指派,隻能放在參數的最後一位,否則會報錯。
const [first, ...rest, last] = [1, 2, 3, 4, 5]; //報錯
const [first, last, ...rest] = [1, 2, 3, 4, 5]; //正确
1.3、将字元串轉為真正的數組
任何 Iterator 接口的對象(參閱 Iterator 一章),都可以用擴充運算符轉為真正的數組。
[...'hello'] // [ "h", "e", "l", "l", "o" ]
2、對象的擴充運算符
對象的擴充運算符(...)用于取出 參數對象 所有 可周遊屬性 然後拷貝到目前對象。
2.1、合并對象
let age = {age: 15};
let name = {name: "Amy"};
let person = {...age, ...name};
person; //{age: 15, name: "Amy"}
3、rest運算符
rest運算符用來表示不确定參數個數,形如,...變量名,由...加上一個具名參數辨別符組成。
function addNumbers(...numbers){
let sum = 0;
for (let i=0;i<numbers.length;i++) {
sum+=numbers[i];
}
return sum;
}
console.log(addNumbers(1,2,3,4,5)); //15
擴充:實作原理
let bar = { a: 1, b: 2 };
let baz = Object.assign({}, bar); // { a: 1, b: 2 }
//Object.assign方法用于對象的合并,将源對象(source)的所有可枚舉屬性,複制到目标對象(target)。
//Object.assign方法的第一個參數是目标對象,後面的參數都是源對象。(如果目标對象與源對象有同名屬性,或多個源對象有同名屬性,則後面的屬性會覆寫前面的屬性)。
五、字元串的擴充
1、模闆字元串
1)模闆字元串使用(反引号):··
2)嵌入變量使用:
${}
傳統的 JavaScript 語言,輸出模闆通常是自己拼接字元串。模闆字元串(template string)是增強版的字元串,用反引号(`)辨別。它可以當作普通字元串使用,也可以用來定義多行字元串,或者在字元串中嵌入變量。
//傳統寫法(ES6前):
$('#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!
`);
1.1、對字元串的拼接
// ES6寫法:支援使用換行符
`In JavaScript \n is a line-feed.`
//ES6寫法:支援換行
console.log(`string text line 1
string text line 2`);
// ES6寫法:字元串中嵌入變量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
1.2、對運算符的支援
//ES6寫法
let result = `${1+2}`
console.log(result); //3
1.3、對html标簽的支援
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<div id="d"></div>
</body>
<script>
let die = document.getElementById("d");
die.innerHTML = `hello es6 <br \> hello world`
</script>
</html>
效果:
2、新方法
ES6對字元串對象,新增了幾個方法,includes(),startsWith(),endsWith(),repeat();
ES2017中:padStart(),padEnd(); ES2019中:trimStart(),trimEnd();
2.1、includes()
傳回布爾值,表示是否找到了參數字元串。
2.2、startsWith()
傳回布爾值,表示參數字元串是否在原字元串的頭部。
2.3、endsWith()
傳回布爾值,表示參數字元串是否在原字元串的尾部。
2.4、repeat()
傳回一個新字元串,表示将原字元串重複
n
次。
let s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true
//第二個參數,表示起始搜尋位置
let s = 'Hello world!';
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false
//表示将原字元串重複n次
'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""
六、數組的擴充
1、Array對象的新方法
ES6在内置對象Array中新增了兩個方法:Array.from() 和 Array.of()
1.1、Array.from()
Array.from
方法用于将兩類對象轉為真正的數組:類似數組的對象(array-like object)和可周遊(iterable)的對象(包括 ES6 新增的資料結構 Set 和 Map)。
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// ES5的寫法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
// ES6的寫法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']
let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']
//如果參數是一個真正的數組,Array.from會傳回一個一模一樣的新數組。
Array.from([1, 2, 3])
// [1, 2, 3]
1.2、Array.of()
Array.of()
方法用于将一組值,轉換為數組。
Array.of
基本上可以用來替代
Array()
或
new Array()。
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
Array.of() //[]
//比較
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
2、Array執行個體的新方法
Array執行個體中新增了一些方法: find()、findIndex()、fill()、entries()、keys()、values()、includes()、flat()、flatMap()、copyWithin()
2.1、find() 和 findIndex()
用于查找滿足特定條件的數組元素,均接受兩個參數,一個回調函數,一個可選值用于指定回調函數内部的this,該回調函數可接受三個參數,數組的某個元素,該元素對應的索引位置,以及該數組自身。若想查找特定值使用indexOf()與lastIndexOf()方法會是更好的選擇。
//查找第一個小于0的元素
[1, 4, -5, 10].find((n) => n < 0)
// -5
//查找第一個大于9的元素
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
//查找第一個大于9的元素的位置
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
2.2、fill()
fill
方法使用給定值,填充一個數組。
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
//第二個和第三個參數,用于指定填充的起始位置和結束位置
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
2.3、entries()、keys()、values()
ES6 提供三個新的方法——
entries()
,
keys()
和
values()
——用于周遊數組。它們都傳回一個周遊器對象(詳見《Iterator》一章),可以用
for...of
循環進行周遊,唯一的差別是
keys()
是對鍵名的周遊、
values()
是對鍵值的周遊,
entries()
是對鍵值對的周遊。
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
2.4、includes()
Array.prototype.includes
方法傳回一個布爾值,表示某個數組是否包含給定的值,與字元串的
includes
方法類似。沒有該方法之前,我們通常使用數組的
indexOf
方法,檢查是否包含某個值。
indexOf
方法有兩個缺點,一是不夠語義化,它的含義是找到參數值的第一個出現位置,是以要去比較是否不等于
-1
,表達起來不夠直覺。二是,它内部使用嚴格相等運算符(
===
)進行判斷,這會導緻對
NaN
的誤判。
[1, 2, 3].includes(2) // true
[1, 2, 3].includes(4) // false
[1, 2, NaN].includes(NaN) // true
[NaN].indexOf(NaN)
// -1
2.5、flat()、flatMap()
數組的成員有時還是數組,
Array.prototype.flat()
用于将嵌套的數組“拉平”,變成一維的數組。該方法傳回一個新數組,對原資料沒有影響。
[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]
//flat()的參數為2,表示要“拉平”兩層的嵌套數組。(預設是1)
[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]
flatMap()
方法對原數組的每個成員執行一個函數(相當于執行
Array.prototype.map()
),然後對傳回值組成的數組執行
flat()
方法。該方法傳回一個新數組,不改變原數組。它與 map 和 深度值1的 flat 幾乎相同,但 flatMap 通常在合并成一種方法的效率稍微高一些。
[2, 3, 4].flatMap((x) => [x, x * 2])
// 相當于 [[2, 4], [3, 6], [4, 8]].flat()
// [2, 4, 3, 6, 4, 8]
2.6、copyWithin()
數組執行個體的
copyWithin()
方法,在目前數組内部,将指定位置的成員複制到其他位置(會覆寫原有成員),然後傳回目前數組。也就是說,使用這個方法,會修改目前數組。
它接受三個參數:
- target(必需):從該位置開始替換資料。如果為負值,表示倒數。
- start(可選):從該位置開始讀取資料,預設為 0。如果為負值,表示從末尾開始計算。
- end(可選):到該位置前停止讀取資料,預設等于數組長度。如果為負值,表示從末尾開始計算。
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]
// -2相當于3号位,-1相當于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)
// [4, 2, 3, 4, 5]
3、Iterator和for...of 循環
3.1、Iterator和for...of
JavaScript 原有的表示“集合”的資料結構,主要是數組(
Array
)和對象(
Object
),ES6 又添加了
Map
和
Set
。這樣就有了四種資料集合,使用者還可以組合使用它們,定義自己的資料結構,比如數組的成員是
Map
,
Map
的成員是對象。以前我們周遊數組,我們通常會用for循環或者是forEach,周遊字元串甚至還會使用for...in,但是for...in他會周遊對象上所有可枚舉的屬性,包括自身的和原型鍊上的。而對于新資料類型Map,我們使用forEach周遊,也沒問題。周遊的方法固然很多,但是我們需要根據資料類型選擇周遊方法。能不能統一方法呢?這樣就需要一種統一的接口機制,來處理所有不同的資料結構。
Iterator(因為
javascript
語言裡沒有接口的概念,這裡我們可以了解成它是一種特殊的對象 - 疊代器對象,傳回此對象的方法叫做疊代器方法,此對象具有一個
next
方法)就是提供了一個統一的通路機制,不斷的調用next()才能周遊完成,如果Iterator像java那樣提供一個hasNext()方法的話,那麼我們可以通過while進行周遊,事實上js中是沒有的。之是以沒有是因為ES6使用for...of...實作了對具有Symbol.iterator(可周遊)的資料結構的周遊,也就是說隻要是包含Symbol.iterator屬性的結構都可以使用for...of...進行周遊。也就是說,
for...of
循環内部調用的是資料結構的
Symbol.iterator
方法。
原生具備 Iterator 對象的資料結構如下:
- Array
- Map
- Set
- String
- TypedArray
- 函數的 arguments 對象
- NodeList 對象
//使用next()方法
let a = [1,2,3];
let it_arr = a[Symbol.iterator]();
it_arr.next(); // { value: 1, done: false }
it_arr.next(); // { value: 2, done: false }
it_arr.next(); // { value: 3, done: false }
it_arr.next(); // { value: undefined, done: true }
//類數組對象:我們可以直接将數組的周遊器對象指派給對象
let obj = {
0: "a",
1: "b",
2: "c",
length: 3,
}
obj[Symbol.iterator] = Array.prototype[Symbol.iterator];
for (let i of obj) { //如果注釋上一行代碼:Uncaught TypeError: obj is not iterable at
console.log(i); // a b c
}
ES6
裡規定,隻要在對象的屬性上部署了
Iterator
接口,具體形式為給對象添加
Symbol.iterator
屬性,此屬性指向一個疊代器方法,這個疊代器會傳回一個特殊的對象 - 疊代器對象。(Symbol.iterator,它是一個表達式,傳回Symbol對象的iterator屬性,這是一個預定義好的、類型為 Symbol 的特殊值。)此時,這個對象就是可疊代的,也就是可以被
for of
周遊。
有人會很好奇的問Object對象為什麼沒有預設部署Iterator對象?因為obj可能有各種屬性,不像數組的值是有序的。是以周遊的時候根本不知道如何确定他們的先後順序,是以需要我們根據情況手動實作。比如:我們去改造一下Iterator疊代器對象:
var arr1 = [100, 200, 300];
for (var o of arr1) {
console.log(o); //100 200 300
}
let arr2 = [100, 200, 300];
arr2[Symbol.iterator] = function () {
var self = this;
var i = 0;
return {
next: function () {
var done = (i >= self.length);
var value = !done ? self[i++] : undefined;
return {
done: done,
value: i + "-" + value
};
}
};
}
for (var o of arr2) {
console.log(o); //1-100,2-200,3-300
}
七、新增資料類型/資料結構
ES5 的對象屬性名都是字元串,這容易造成屬性名的沖突。如果有一種機制,保證每個屬性的名字都是獨一無二的就好了,這樣就從根本上防止屬性名的沖突。ES6 引入了一種新的原始資料類型
Symbol
,表示獨一無二的值。它是 JavaScript 語言的第七種資料類型,前六種是:
undefined
、
null
、布爾值(Boolean)、字元串(String)、數值(Number)、對象(Object)。Symbol 值通過
Symbol
函數生成。這就是說,對象的屬性名現在可以有兩種類型,一種是原來就有的字元串,另一種就是新增的 Symbol 類型。凡是屬性名屬于 Symbol 類型,就都是獨一無二的,可以保證不會與其他屬性名産生沖突。
1、新增資料類型: Symbol
Symbol
1.1、Symbol類型的聲明
1.1、Symbol類型的聲明
let s = Symbol();
typeof s
// "symbol"
//可以接受一個字元串作為參數
let s1 = Symbol('foo');
s1.toString() // 可以顯式轉為字元串:"Symbol(foo)"
1.2、
不能與其他類型的值進行運算
1.2、
let sym = Symbol('My symbol');
"your symbol is " + sym
// TypeError: can't convert symbol to string
`your symbol is ${sym}`
// TypeError: can't convert symbol to string
1.3、作為屬性名
由于每一個 Symbol 值都是不相等的,這意味着 Symbol 值可以作為辨別符,用于對象的屬性名,就能保證不會出現同名的屬性。這對于一個對象由多個子產品構成的情況非常有用,能防止某一個鍵被不小心改寫或覆寫。(注意,Symbol 值作為對象屬性名時,不能用點運算符。)
let mySymbol = Symbol();
// 第一種寫法
let a = {};
a[mySymbol] = 'Hello!';
// 第二種寫法
let a = {
[mySymbol]: 'Hello!'
};
// 第三種寫法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上寫法都得到同樣結果
a[mySymbol] // "Hello!"
const mySymbol = Symbol();
const a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
//因為點運算符後面總是字元串,是以不會讀取mySymbol作為辨別名所指代的那個值,導緻a的屬性名實際上是一個字元串,而不是一個 Symbol 值。
a['mySymbol'] // "Hello!"
1.4、屬性名的周遊
Symbol 作為屬性名,周遊對象的時候,該屬性不會出現在
for...in
、
for...of
循環中,也不會被
Object.keys()
、
Object.getOwnPropertyNames()
、
JSON.stringify()
傳回。但是,它也不是私有屬性,有一個
Object.getOwnPropertySymbols()
方法,可以擷取指定對象的所有 Symbol 屬性名。該方法傳回一個數組,成員是目前對象的所有用作屬性名的 Symbol 值。
另一個新的 API,
Reflect.ownKeys()
方法可以傳回所有類型的鍵名,包括正常鍵名和 Symbol 鍵名。
const obj = {};
const foo = Symbol('foo');
obj[foo] = 'bar';
for (let i in obj) {
console.log(i); // 無輸出
}
Object.getOwnPropertyNames(obj) // []
Object.getOwnPropertySymbols(obj) // [Symbol(foo)]
let obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
};
Reflect.ownKeys(obj)
// ["enum", "nonEnum", Symbol(my_key)]
2、新增資料結構:Set、WeakSet、Map、WeakMap
2.1、Set
ES6 提供了新的資料結構 Set。它類似于數組,但是成員的值都是唯一的,沒有重複的值。
Set
本身是一個構造函數,用來生成 Set 資料結構。
2.1.1、Set的構造函數
1)Set() :無參數構造
2)Set([ iterable ]) :接受具有 iterable 接口的資料結構作為參數,如:Strin、數組等
//1、通過add方法向 Set 結構加入成員
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
//2、Set函數可以接受一個數組(或者具有 iterable 接口的其他資料結構)作為參數
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
//例二
const s = new Set("hello");
console.log(s.size);//4
console.log(s);//{"h", "e", "l", "o"}
2.1.2、Set執行個體的屬性
1)constructor 屬性--構造函數
2)size 屬性-執行個體成員的總數
let s = new Set("hello");
console.log(s.size);//4
console.log(s.constructor);//ƒ Set() { [native code] }
2.1.3、Set執行個體的方法
1)add(value):添加某個值
2)delete(value):删除某個值,傳回一個布爾值,表示删除是否成功。
3)has(value):傳回一個布爾值
4)clear():清除所有成員,沒有傳回值。
let s = new Set("hello");
s.add(1).add(2).add(2);
// 注意2被加入了兩次
s.size // 2
s.has(1) // true
s.has(2) // true
s.has(3) // false
s.delete(2);
s.has(2) // false
5)keys():傳回鍵名的周遊器
6)values():傳回鍵值的周遊器
7)entries():傳回鍵值對的周遊器
8)forEach():使用回調函數周遊每個成員
let set = new Set(['red', 'green', 'blue']);
for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9
2.2、WeakSet
WeakSet 結構與 Set 類似,也是不重複的值的集合。但是,它與 Set 有兩個差別:
1)WeakSet 的成員隻能是對象,而不能是其他類型的值。
2)WeakSet 中的對象都是弱引用,即垃圾回收機制不考慮 WeakSet 對該對象的引用,也就是說,如果其他對象都不再引用該對象,那麼垃圾回收機制會自動回收該對象所占用的記憶體。WeakSet 沒有
size
屬性,沒有辦法周遊它的成員。
const ws = new WeakSet();
const obj = {};
const foo = {};
ws.add(window);
ws.add(obj);
ws.has(window); // true
ws.has(foo); // false
ws.delete(window);
ws.has(window); // false
ws.size // undefined
ws.forEach // undefined
2.3、Map
JavaScript 的對象(Object),本質上是鍵值對的集合(Hash 結構),但是傳統上隻能用字元串當作鍵。為了解決這個問題,ES6 提供了 Map 資料結構。它類似于對象,也是鍵值對的集合,但是“鍵”的範圍不限于字元串,各種類型的值(包括對象)都可以當作鍵。如果你需要“鍵值對”的資料結構,Map 比 Object 更合适。
2.3.1、Map的構造函數
1)Map( ):無參數構造
2)Map( [ iterable ]):可以接收一個可疊代對象(前提是每個成員都是一個雙元素結構的疊代對象)作為參數
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
const map = new Map([
['F', 'no'],
['T', 'yes'],
]);
for (let [key, value] of map.entries()) {
console.log(key, value); //F no T yes
}
2.3.2、Map執行個體的屬性
1)constructor 屬性--構造函數
2)size 屬性-執行個體成員的總數
let maps = new Map();
maps.set("a", "1");
maps.set("b", "2");
console.log(maps.size);//2
console.log(maps.constructor);//ƒ Map() { [native code] }
2.3.3、Map執行個體的方法
1)set(key, value):添加鍵值
2)get(key):取值。
3)has(value):傳回一個布爾值
4)delete(key):删除某個鍵,傳回一個布爾值
5)clear():清除所有成員,沒有傳回值。
const m = new Map();
m.set('edition', 6); // 鍵是字元串
m.set(262, 'standard'); // 鍵是數值
m.set(undefined, 'nah'); // 鍵是 undefined
m.has(undefined); // true
m.delete(undefined);
m.has(undefined); // false
m.clear();
console.log(m.size);// 0
5)keys():傳回鍵名的周遊器
6)values():傳回鍵值的周遊器
7)entries():傳回鍵值對的周遊器
8)forEach():使用回調函數周遊每個成員
const map = new Map([
['F', 'no'],
['T', 'yes'],
]);
for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"
for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
2.4、WeakMap
WeakMap
結構與
Map
結構類似,也是用于生成鍵值對的集合。
WeakMap
與
Map
的差別有兩點:
1)
WeakMap
隻接受對象作為鍵名(
null
除外),不接受其他類型的值作為鍵名。
2)
WeakMap
的鍵名所指向的對象,不計入垃圾回收機制。
WeakMap
的設計目的在于,有時我們想在某個對象上面存放一些資料,但是這會形成對于這個對象的引用。一旦不再需要這兩個對象,我們就必須手動删除這個引用,否則垃圾回收機制就不會釋放。WeakMap 就是為了解決這個問題而誕生的,它的鍵名所引用的對象都是弱引用,即垃圾回收機制不将該引用考慮在内。是以,隻要所引用的對象的其他引用都被清除,垃圾回收機制就會釋放該對象所占用的記憶體。也就是說,一旦不再需要,WeakMap 裡面的鍵名對象和所對應的鍵值對會自動消失,不用手動删除引用。
運用場景:Angular、Nest等依賴注入架構都使用了反射技術。其中核心的依賴reflect-metadata就會用到WeakMap。
WeakMap
隻有四個方法可用:
get()
、
set()
、
has()
、
delete()
。
const wm = new WeakMap();
// size、forEach、clear 方法都不存在
wm.size // undefined
wm.forEach // undefined
wm.clear // undefined
八、函數的變化
1、函數參數的預設值
ES6 之前,不能直接為函數的參數指定預設值,隻能采用變通的方法。但是,下面的代碼,如果參數
y
等于空字元,結果被改為預設值。
function log(x, y) {
y = y || 'World';
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World 參數y等于空字元,結果被改為預設值。(參數對應的布爾值不能false)
//對于:|| 和 && 的用法
//與其他語言不同,在JS中,a&&b或者a||b傳回的是要麼是a,要麼是b;而其他語言中傳回的是true or false
//a&&b 如果a為true,則執行b并傳回b的值;如果a為false,則傳回false不執行b;
//如果a為true,則傳回a的值不執行b;如果a為false,則執行b并傳回b的值;
ES6 允許為函數的參數設定預設值,即直接寫在參數定義的後面。 (不傳或者傳入undefined的時候才會觸發預設值指派)
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
2、rest 參數
rest參數,這是一個新的概念,rest的中文意思是:剩下的部分。ES6 引入 rest 參數(形式為
...變量名
),用于擷取函數的多餘參數,這樣就不需要使用
arguments
對象了。rest 參數搭配的變量是一個數組,該變量将多餘的參數放入數組中。
注:rest 參數之後不能再有其他參數(即隻能是最後一個參數),否則會報錯。
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
// arguments變量的寫法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort();
}
// rest參數的寫法
const sortNumbers = (...numbers) => numbers.sort();
// 報錯:rest 參數之後不能再有其他參數
function f(a, ...b, c) {
// ...
}
3、箭頭函數
ES6 允許使用“箭頭”(
=>
)定義函數。箭頭函數的産生,主要有兩個目的:更簡潔的文法和與父作用域共享關鍵字
this。
箭頭函數适合于無複雜邏輯或者無副作用的純函數場景下,例如用在map、reduce、filter的回調函數定義中。在了解箭頭函數之前,需要先複習一下匿名函數及閉包等相關概念了。
3.1、ES5中的匿名函數及閉包
什麼是匿名函數?顧名思義:匿名函數就是沒有實際名字的函數。通過匿名函數可以實作閉包,在ES6之前隻有兩個作用域,全局作用域和函數内的局部作用域,閉包的優點是可以在全局作用域使用局部作用域的變量。匿名函數及閉包的寫法:
//聲明一個普通函數,函數的名字叫fn
function fn(){
console.log("穆瑾軒");
}
//匿名函數:形式一
(function (){
console.log(this); //[object Window] 注:這裡的this指向全局作用域
})();
//匿名函數:形式二
var obj={
name:"穆瑾軒",
age:18,
fn:function(){
return "我叫"+this.name+"今年"+this.age+"歲了!";
}
};
console.log(obj.fn());//我叫穆瑾軒今年18歲了!
//匿名函數:應用閉包
var name = "this is window"; //定義window的name屬性,看this.name是否會調用到
var testObj = {
name: "this is testObj",
getName: function() {
var self = this; //臨時儲存this對象
var handle = function() {
console.log(this); //輸出:Window(this指向的是全局對象--window對象)-->閉包會将其空間擴充到外層函數空間之外,止步于全局空間
console.log(this.name); //輸出: this is window
console.log(self); //這樣可以擷取到的this即指向testObj對象(對象擁有name屬性和getName() 方法)
}
handle();
}
}
testObj.getName();
//引入this的初衷就是想在原型繼承的情況下,拿到函數的調用者。JavaScript 中的 this 指向函數調用時的執行環境
var obj = {
name: "obj.name",
method: function () {
console.log("method:" + this.name);
return this;
}
}
console.log("obj.method() === obj:" + (obj.method() === obj)); //"obj.method() === obj:"true
3.2、ES6中為何引入箭頭函數
ES6新增了箭頭函數,一定是為了解決某個問題。上面我們說到了閉包。閉包中的this會将其空間擴充到外層函數空間之外,止步于全局空間(取決于函數的運作環境上下文)。
let obj = {
name: '張三',
fn: function () {
let _this = this //在定義是将this指派給一個變量_this
return function() {
//console.log(this.name) //undefined
console.log(_this.name) //調用被指派的_this去擷取obj裡的name
}
}
}
obj.fn()()
//列印結果: 張三
普通函數的
this
是運作時綁定,正常來說我們希望
this
指向的是
obj
中的
name。而箭頭函數它做到了,
箭頭函數的
this
是定義時綁定。這樣就輕松地解決了普通函數
this
随着運作環境的改變而改變的問題了。
let obj = {
name: '張三',
fn: function () {
//此處有個this,該this指向obj,并且被箭頭函數所綁定
return () => {
console.log(this.name) //這裡的this是向上尋找,找到function() {}内有一個this,并與之綁定,而這個this指向的就是obj
}
}
}
obj.fn()()
//列印結果: 張三
3.3、箭頭函數的基本使用
下面是ES6之前和ES6箭頭函數寫法的對比:
let f = v => v;
// 等同于
let f = function (v) {
return v;
};
let fn = (num1, num2) => num1 + num2;
console.log(fn(1, 2));//3
// 等同于
let fn = function(num1, num2) {
return num1 + num2;
};
文法格式規則總結:
1)一個參數對應一個表達式。形如:param => expression;【當隻有一個參數時,參數
()
可以省略(rest參數是一個例外,如
(...args) => ...
),
function
和
{}
都消失了,所有的回調函數都隻出現在了一行裡。注:當
{}
消失後,
return
關鍵字也跟着消失了。單行的箭頭函數會提供一個隐式的
return
(這樣的函數在其他程式設計語言中常被成為lamda函數)。】
2)多個參數對應一個表達式。形如:(param [, param]) => expression; 【參數
()不
可以省略】
3)一個參數對應多行代碼。形如:param => {statements;}【{}不可以省略,注:當箭頭函數伴随着
{}
被聲明,那麼即使它是單行的,它也不會有隐式
return。
】
4)多個參數對應多行代碼。形如:([param] [, param]) => {statements}【參數
()不
可以省略,{}不可以省略】
5)特殊情況一:表達式裡沒有參數。形如:() => expression; 或 () => {statements;} 【參數
()不
可以省略】
6)特殊情況二:傳回一個空對象或者對象省略return時,需要加上()。形如:() => ({}) 或 ([param]) => ({ key: value });
// 一個參數對應一個表達式:param => expression; 例如:
x => x+1;
// 多個參數對應一個表達式:(param [, param]) => expression; 例如:
(x,y) => (x + y);
// 一個參數對應多個表示式:param => {statements;} 例如
x = > { x++; return x;};
// 多個參數對應多個表達式:([param] [, param]) => {statements} 例如
(x,y) => { x++;y++;return x*y;};
//表達式裡沒有參數:() => expression; 例如
var flag = (() => 2)(); flag等于2
//() => {statements;} 例如
var flag = (() => {return 1;})(); flag就等于1
//傳入一個表達式,傳回一個空對象或者對象省略return時:([param]) => ({ key: value });例如:
() => ({}) // {}
var fuc = (x) => ({key:x})
alert(fuc(1));//{key:1}
箭頭函數有幾個使用注意點:
1)函數體内的
this
對象,就是定義時所在的對象,而不是使用時所在的對象。
2)不可以當作構造函數,也就是說,不可以使用
new
指令,否則會抛出一個錯誤。
3)不可以使用
arguments
對象,該對象在函數體内不存在。如果要用,可以用 rest 參數代替。
4)不可以使用
yield
指令,是以箭頭函數不能用作 Generator 函數。
//1)下面有幾個this
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1
//上面代碼之中,隻有一個this,就是函數foo的this,是以t1、t2、t3都輸出同樣的結果。因為所有的内層函數都是箭頭函數,都沒有自己的this,它們的this其實都是最外層foo函數的this。
//2)不能當做構造函數
let foo1 = () => ({});
console.log(foo1.prototype); //undefined--無構造函數
let foonew = function () {
return {}
}
console.log(foonew.prototype);//{constructor: ƒ}
九、Promise對象
1、基本概念
1.1、分布式、高并發和多線程簡訴
在如今這個網絡大環境下,在數量遞增的同時,也要追求效率。也許你經常會聽到分布式、高并發和多線程這些概念,他們三個總是相伴而生,但側重點又有不同。
分布式是從實體資源的角度去将不同的機器組成一個整體對外服務,技術範圍非常廣且難度非常大,有了這個基礎,高并發、高吞吐等系統很容易建構;
高并發是從業務角度去描述系統的能力,實作高并發的手段可以采用分布式,也可以采用諸如緩存、CDN等,當然也包括多線程;
多線程則聚焦于如何使用程式設計語言将CPU排程能力最大化。線程不是一個計算機硬體的功能,而是作業系統提供的一種邏輯功能,線程本質上是程序中一段并發運作的代碼。
線程同步:是多個線程同時通路同一資源,等待資源通路結束,浪費時間,效率低。
線程異步:通路資源時在空閑等待時同時通路其他資源,實作多線程機制。效率高,但也伴随着線程安全問題。
1.2、同步任務與異步任務
同步任務指的是,在主線程上排隊執行的任務,隻有前一個任務執行完畢,才能執行後一個任務;這種單線程很容易因為一個任務發生延遲,造成整體的耗時變長,為了解決這個問題,是以就有了
異步
這個概念。
異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,隻有等主線程任務執行完畢,"任務隊列"開始通知主線程,請求執行任務,該任務才會進入主線程執行。簡單說就是一個任務分成兩段,先執行第一段,然後轉而執行其他任務,等做好了準備,再回過頭執行第二段。
總結:這種不連續的執行,就叫做異步。異步程式設計的文法目标,就是怎樣讓它更像同步程式設計。相應地,連續的執行,就叫做同步。
javascript是運作在浏覽器端的語言,必須依賴javascript引擎來解析并執行代碼,js引擎是
單線程
,也就是一個任務接着一個任務來執行程式。異步執行的最終結果,依然需要回到 JS 線程上進行處理。在JS中,異步的結果回到 JS 主線程的方式采用的是 “ 回調函數 ” 的形式 ,這也是早期實作異步程式設計的主要形式。于是出現了很多異步流程控制的包,如async.js 和Promise等。
1.3、Promise 的含義
Promise,中文翻譯為
承諾,期約。
Promise 是異步程式設計的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。(有了
Promise
對象,就可以将異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。)它由社群最早提出和實作,ES6 将其寫進了語言标準,統一了用法,原生提供了
Promise
對象。所謂
Promise
,簡單說就是一個容器,裡面儲存着某個未來才會結束的事件(通常是一個異步操作)的結果。從文法上說,Promise 是一個對象,從它可以擷取異步操作的消息。Promise 提供統一的 API,各種異步操作都可以用同樣的方法進行處理。
Promise
對象有以下兩個特點:
1)核心概念是狀态,對象的狀态不受外界影響。狀态轉換就是Promise執行異步事件的
時機
,它有三種狀态:
pending
(進行中)、
fulfilled
(已成功)和
rejected
(已失敗)
2)Promise中的狀态是不可逆轉的。任何時候都可以得到這個結果。
Promise
對象的狀态改變,隻有兩種可能:從
pending
變為
fulfilled
和從
pending
變為
rejected
。
Promise的
缺點:
1)無法取消
Promise
,一旦建立它就會立即執行,無法中途取消。
2)如果不設定回調函數,
Promise
内部抛出的錯誤,不會反應到外部。第三,當處于
pending
狀态時,無法得知目前進展到哪一個階段。
2、Promise 的基本用法
ES6 規定,
Promise
對象是一個構造函數,用來生成
Promise
執行個體。使用時需要用
new
關鍵詞來建立執行個體對象。Promise構造函數中自帶
excutor
執行器,excutor執行器中有2個JavaScript中預設的函數參數resolve,reject。
resolve函數的作用是當Promise狀态從padding轉換到resolve時,可以把Promise中的對象或者變量當成參數傳遞出來供異步成功時調用,reject函數的作用是當Promise狀态從padding轉換到reject時候可以把Promise中的對象或者變量,以及系統報錯當成參數傳遞出來供異步失敗時調用。
then是Promise原型上的一個方法,
Promise.prototype.then()
是以通過構造函數建立的Promise執行個體對象也會自帶then( )方法。then( )方法接受2個函數參數,作為Promise中異步成功和異步失敗的2個回調函數。
2.1、Promise執行個體的基本代碼結構
1)構造函數接受一個函數作為參數,該函數接受兩個參數
resolve
和
reject,
它們是兩個函數。
2)原型方法then:Promise.prototype.then()
3)原型方法catch:Promise.prototype.catch()
4)原型方法all:Promise.all()等
//ES6 箭頭函數寫法
let promise = new Promise((resolve,reject)=>{
if(/判斷條件/){
resolve()//承諾實作
}else{
reject()//承諾失效
}
})
promise.then(res=>{
//處理承諾實作方法
},err=>{
//處理承諾失效方法
})
3、Promise 的方法
3.1、原型方法then-Promise.prototype.then()
Promise 執行個體具有
then
方法,也就是說,
then
方法是定義在原型對象
Promise.prototype
上的。它的作用是為 Promise 執行個體添加狀态改變時的回調函數。
then
方法傳回的是一個新的
Promise
執行個體(注意,不是原來那個
Promise
執行個體)。是以可以采用鍊式寫法,即
then
方法後面再調用另一個
then
方法。
注意:Promise函數本身不是一個異步函數,在excutor執行器中運作的代碼是同步的。執行異步的是then( )方法中的事件
//案例:
console.log('步驟1');
new Promise((resolve,reject)=>{
console.log('步驟2');
resolve()
}).then(res=>{
console.log('步驟3');
})
console.log('步驟4')
//執行結果:
//步驟1
//步驟2
//步驟4
//步驟3
//鍊式寫法
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
3.2、原型方法catch-Promise.prototype.catch()
Promise.prototype.catch()
方法是
.then(null, rejection)
或
.then(undefined, rejection)
的别名,用于指定發生錯誤時的回調函數。
p.then((val) => console.log('fulfilled:', val))
.catch((err) => console.log('rejected', err));
// 等同于
p.then((val) => console.log('fulfilled:', val))
.then(null, (err) => console.log("rejected:", err));
//案例:
// 寫法一
const promise = new Promise(function(resolve, reject) {
try {
throw new Error('test');
} catch(e) {
reject(e);
}
});
promise.catch(function(error) {
console.log(error);
});
// 寫法二
const promise = new Promise(function(resolve, reject) {
reject(new Error('test'));
});
promise.catch(function(error) {
console.log(error);
});
3.3、原型方法all-Promise.all()
Promise.all()
方法用于将多個 Promise 執行個體,包裝成一個新的 Promise 執行個體。
p
的狀态由
p1
、
p2
、
p3
決定,分成兩種情況。
1)隻有
p1
、
p2
、
p3
的狀态都變成
fulfilled
,
p
的狀态才會變成
fulfilled
,此時
p1
、
p2
、
p3
的傳回值組成一個數組,傳遞給
p
的回調函數。
2)隻要
p1
、
p2
、
p3
之中有一個被
rejected
,
p
的狀态就變成
rejected
,此時第一個被
reject
的執行個體的傳回值,會傳遞給
p
的回調函數。
const p = Promise.all([p1, p2, p3]);
案例:
function runAsync1() {
var p = new Promise(function (resolve, reject) {
//做一些異步操作
setTimeout(function () {
console.log('異步任務1執行完成');
resolve('随便什麼資料1');
}, 1000);
});
return p;
}
function runAsync2() {
var p = new Promise(function (resolve, reject) {
//做一些異步操作
setTimeout(function () {
console.log('異步任務2執行完成');
resolve('随便什麼資料2');
}, 2000);
});
return p;
}
function runAsync3() {
var p = new Promise(function (resolve, reject) {
//做一些異步操作
setTimeout(function () {
console.log('異步任務3執行完成');
resolve('随便什麼資料3');
}, 2000);
});
return p;
}
//runAsync1 runAsync2 runAsync3 的狀态都變成了fulfilled,才會傳遞給回調函數
Promise.all([runAsync1(), runAsync2(), runAsync3()]).then(function (results) {
console.log(results);
});
輸出結果:
3.4、原型方法race-Promise.race()
Promise.race()
方法同樣是将多個 Promise 執行個體,包裝成一個新的 Promise 執行個體。隻要
p1
、
p2
、
p3
之中有一個執行個體率先改變狀态,
p
的狀态就跟着改變。那個率先改變的 Promise 執行個體的傳回值,就傳遞給
p
的回調函數。
const p = Promise.race([p1, p2, p3]);
案例:
//把上面案例的all改成race
Promise.race([runAsync1(), runAsync2(), runAsync3()]).then(function (results) {
console.log(results);
});
輸出結果:
4、總結與應用
Promise
是
ECMAscript ES6
原生的對象,是解決javascript語言異步程式設計産生回調地獄的一種方法。但它的本質也沒有跳出回調問題,隻是把嵌套關系優化成類似層級結構的寫法來幫助開發者更容易處理異步中的邏輯代碼。
我們可以将圖檔的加載寫成一個
Promise
,一旦加載完成,
Promise
的狀态就發生變化。
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
十、Generator函數的引入
1、基本概念
1.1、Generator 函數簡介
Generator的中文名稱是生成器。Generator 函數是 ES6 提供的一種異步程式設計解決方案,文法行為與傳統函數完全不同。它最大特點就是可以交出函數的執行權(即暫停執行)。
Generator初衷應該
并不是為異步而設計出來的,它還有其他功能(對象疊代、控制輸出、部署
Interator
接口...),更多的是為了生成疊代器。在ES2017 标準引入了 async函數,它就是 Generator 函數的文法糖,使得異步操作變得更加友善。
形式上,Generator 函數是一個普通函數,但是有兩個特征。一是,
function
關鍵字與函數名之間有一個星号;二是,函數體内部使用
yield
表達式,定義不同的内部狀态(
yield
在英語裡的意思就是“産出”)。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
1.2、Generator 函數的資料交換和錯誤處理
Generator 函數可以暫停執行和恢複執行,這是它能封裝異步任務的根本原因。除此之外,它還有兩個特性,使它可以作為異步程式設計的完整解決方案:函數體内外的資料交換和錯誤處理機制。next 方法傳回值的 value 屬性,是 Generator 函數向外輸出資料;next 方法還可以接受參數,這是向 Generator 函數體内輸入資料。
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
//Generator 函數内部還可以部署錯誤處理代碼,捕獲函數體外抛出的錯誤。
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1);
g.next();
g.throw('出錯了');
// 出錯了
2、yield和yield*表達式的了解
yield
和
yield*
都是配合
Generator
進行使用的。
yield
是關鍵字,其文法如下:
[rv] = yield [expression];
//expression:是Generator函數傳回的周遊器對象調用next方法時所得到的值;
//rv:是周遊其對象調用next方法時傳遞給next方法的參數
yield*
是表達式,有傳回值,其文法如下:
yield* [[expression]];
//expression:是可周遊對象,可以是數組,也可以是另外一個Generator函數的執行表達式,等等
案例一:
function* _testYieldExpression() {
let value = '';
value = yield 'yield value'; //yield value:是Generator函數傳回的周遊器對象調用next方法時所得到的值;value 是周遊其對象調用next方法時傳遞給next方法的參數
console.log(`1 value is: ${value}`);//1 value is: params from next
value = yield 'yield value';
console.log(`2 value is: ${value}`);//2 value is: undefined
return 'over';
}
let _testIterator = _testYieldExpression();
let _res = _testIterator.next();
console.log(`1:no params to next, result is: ${_res.value}`);//1:no params to next, result is: yield value
_res = _testIterator.next('params from next');
console.log(`2:params to next, result is: ${_res.value}`);//2:params to next, result is: yield value
_res = _testIterator.next();
console.log(`3:params to next, result is: ${_res.value}`);//3:params to next, result is: over
輸出結果:
從上面的執行結果中看出:第一次調用next()方法輸出:1:no params to next, result is: yield value。第二次調用next()方法輸出:1 value is: params from next 和 2:params to next, result is: yield value 第三次調用next()方法輸出:2 value is: undefined 和 3:params to next, result is: over
案例二:
function* g3() {
yield* [1, 2]; //可以是數組,也可以是另外一個Generator函數的執行表達式,等等
yield* '34'; //
yield* Array.from(arguments);
}
var iterator = g3(5, 6);
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: "3", done: false}
console.log(iterator.next()); // {value: "4", done: false}
console.log(iterator.next()); // {value: 5, done: false}
console.log(iterator.next()); // {value: 6, done: false}
console.log(iterator.next()); // {value: undefined, done: true}
案例三:為什麼需要
yield*
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
// 手動周遊 foo()
for (let i of foo()) {
console.log(i);
}
yield 'y';
}
//for...of循環可以自動周遊 Generator 函數運作時生成的Iterator對象
for (let v of bar()){
console.log(v);
}
// x
// a
// b
// y
//原生的 JavaScript 對象沒有周遊接口,無法使用for...of循環,通過 Generator 函數為它加上這個接口,就可以用了。
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);
for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
foo
和
bar
都是 Generator 函數,在
bar
裡面調用
foo
,就需要手動周遊
foo
。如果有多個 Generator 函數嵌套,寫起來就非常麻煩。ES6 提供了
yield*
表達式,作為解決辦法,用來在一個 Generator 函數裡面執行另一個 Generator 函數。
十一、代理(Proxy)與反射(Reflect)
1、代理-Proxy
1.1、Proxy簡介
proxy的中文有代理的意思,在java中,也許你見過這兩種方式的代理:靜态代理和動态代理。代理模式(英語:Proxy Pattern)是程式設計中的一種設計模式,就是給某一個對象提供一個代理對象,并由代理對象控制對原對象的引用。通俗的來講代理模式就是我們生活中常見的中介。
在MDN上對于 Proxy 的解釋是:
Proxy
對象允許你攔截并定義基本語言操作的自定義行為(例如,屬性查找,指派,枚舉,函數調用等)。簡單來說: 也可以了解為在目标對象之前架設一層攔截,外部所有的通路都必須先通過這層攔截,是以提供了一種機制,可以對外部的通路進行過濾和修改。盡管它不像其他ES6功能用的普遍,但
Proxy
有許多用途,包括運算符重載,對象模拟,簡潔而靈活的API建立,對象變化事件,甚至Vue 3背後的内部響應系統提供動力。
在ES5出現以前,JS環境中的對象包含許多不可枚舉和不可寫的屬性,但開發者不能定義自己的不可枚舉或不可寫屬性,于是ES5引入了
Object.defineProperty()
方法來支援開發者去做JS引擎早就可以實作的事情。在vue2.x中就是通過Object.defineProperty()來實作資料劫持的,但是在vue3.x中使用ES6中的Proxy來替代Object.defineProperty()。
Proxy
本質上屬于元程式設計非破壞性資料劫持,在原對象的基礎上進行了功能的衍生而又不影響原對象,符合松耦合高内聚的設計理念。
Object.defineProperty隻能劫持對象的屬性,不能直接代理對象。雖然
Object.defineProperty
通過為屬性設定
getter/setter
能夠完成資料的響應式,但是它并不算是實作資料的響應式的完美方案,某些情況下需要對其進行修補,這也是它的缺陷,主要表現在兩個方面:1)無法檢測到對象屬性的新增或删除;2)不能監聽數組的變化。
ES6原生提供了
Proxy
構造函數,用來生成
Proxy
執行個體:
var proxy = new Proxy(target, handler);
//Proxy 對象的所有用法,都是上面這種形式,不同的隻是handler參數的寫法。
//其中,new Proxy()表示生成一個Proxy執行個體,target參數表示所要攔截的目标對象,handler參數也是一個對象,用來定制攔截行為。
//target: 要使用 Proxy 包裝的目标對象(可以是任何類型的對象,包括原生數組,函數,甚至另一個代理)
//handler: 對該代理對象的各種操作行為處理(為空對象的情況下,基本可以了解為是對第一個參數做的一次淺拷貝)
案例:
let obj = {
a: 1,
b: 2,
}
const p = new Proxy(obj, {
get(target, key, value) {
if (key === 'c') {
return '我是自定義的一個結果';
} else {
return target[key];
}
},
set(target, key, value) {
if (value === 4) {
target[key] = '我是自定義的一個結果';
} else {
target[key] = value;
}
}
})
//都有的屬性
console.log(obj.a) // 1
console.log(p.a) // 1
//都沒有的屬性
console.log(obj.c) // undefined
console.log(p.c) // 我是自定義的一個結果
//obj添加name屬性
obj.name = '李白';
console.log(obj.name); // 李白
console.log(p.name); // 李白
//p添加age屬性
p.age = 4;
console.log(obj.age); // 我是一個自定義結果
console.log(p.age); //我是一個自定義結果
1.2、Proxy 支援的攔截操作
Proxy 支援的攔截操作:
get(target, propKey, receiver):攔截對象屬性的讀取,比如proxy.foo和proxy['foo']。
set(target, propKey, value, receiver):攔截對象屬性的設定,比如proxy.foo = v或proxy['foo'] = v,傳回一個布爾值。
has(target, propKey):攔截propKey in proxy的操作,傳回一個布爾值。
deleteProperty(target, propKey):攔截delete proxy[propKey]的操作,傳回一個布爾值。
ownKeys(target):攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循環,傳回一個數組。該方法傳回目标對象所有自身的屬性的屬性名,而Object.keys()的傳回結果僅包括目标對象自身的可周遊屬性。
getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),傳回屬性的描述對象。
defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),傳回一個布爾值。
preventExtensions(target):攔截Object.preventExtensions(proxy),傳回一個布爾值。
getPrototypeOf(target):攔截Object.getPrototypeOf(proxy),傳回一個對象。
isExtensible(target):攔截Object.isExtensible(proxy),傳回一個布爾值。
setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),傳回一個布爾值。如果目标對象是函數,那麼還有兩種額外操作可以攔截。
apply(target, object, args):攔截 Proxy 執行個體作為函數調用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
construct(target, args):攔截 Proxy 執行個體作為構造函數調用的操作,比如new proxy(...args)。
//注意:如果一個屬性不可配置 || 不可寫,則該屬性不可被代理,通過 Proxy 通路該屬性會報錯。
1.2.1、Proxy 執行個體的方法-get()
get(target, propKey, receiver)
方法用于攔截某個屬性的讀取操作,可以接受三個參數,依次為目标對象、屬性名和 proxy 執行個體本身(嚴格地說,是操作行為所針對的對象),其中最後一個參數可選。
var person = {
name: "張三"
};
var proxy = new Proxy(person, {
get: function(target, propKey) {
if (propKey in target) {
return target[propKey];
} else {
throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
}
}
});
proxy.name // "張三"
proxy.age // 抛出一個錯誤
1.2.2、Proxy 執行個體的方法-set()
set(target, propKey, value, receiver)
set
方法用來攔截某個屬性的指派操作,可以接受四個參數,依次為目标對象、屬性名、屬性值和 Proxy 執行個體本身,其中最後一個參數可選。
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// 對于滿足條件的 age 屬性以及其他屬性,直接儲存
obj[prop] = value;
}
};
let person = new Proxy({}, validator);
person.age = 100;
person.age // 100
person.age = 'young' // 報錯
person.age = 300 // 報錯
1.2.3、Proxy 執行個體的方法-apply()
apply
方法攔截函數的調用、
call
和
apply
操作。
apply
方法可以接受三個參數,分别是目标對象、目标對象的上下文對象(
this
)和目标對象的參數數組。
var handler = {
apply (target, ctx, args) {
return Reflect.apply(...arguments);
}
};
1.2.4、Proxy 執行個體的方法-has()
has()
方法用來攔截
HasProperty
操作,即判斷對象是否具有某個屬性時,這個方法會生效。典型的操作就是
in
運算符。
has()
方法可以接受兩個參數,分别是目标對象、需查詢的屬性名。
var handler = {
has (target, key) {
if (key[0] === '_') {
return false;
}
return key in target;
}
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
'_prop' in proxy // false
1.2.5、Proxy 執行個體的方法-construct()
construct()
方法用于攔截
new
指令,下面是攔截對象的寫法。
const handler = {
construct (target, args, newTarget) {
return new target(...args);
}
};
1.2.6、Proxy 執行個體的方法-deleteProperty()
deleteProperty
方法用于攔截
delete
操作,如果這個方法抛出錯誤或者傳回
false
,目前屬性就無法被
delete
指令删除。
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
delete target[key];
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property
1.2.6、Proxy 執行個體的方法-defineProperty()
defineProperty()
方法攔截了
Object.defineProperty()
操作。
var handler = {
defineProperty (target, key, descriptor) {
return false;
}
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar' // 不會生效
1.2.7、Proxy 執行個體的方法-其他
getOwnPropertyDescriptor()、getPrototypeOf()、isExtensible()、ownKeys()、preventExtensions()、setPrototypeOf()
//getOwnPropertyDescriptor
var handler = {
getOwnPropertyDescriptor (target, key) {
if (key[0] === '_') {
return;
}
return Object.getOwnPropertyDescriptor(target, key);
}
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'wat')
// undefined
Object.getOwnPropertyDescriptor(proxy, '_foo')
// undefined
Object.getOwnPropertyDescriptor(proxy, 'baz')
// { value: 'tar', writable: true, enumerable: true, configurable: true }
//getPrototypeOf
var proto = {};
var p = new Proxy({}, {
getPrototypeOf(target) {
return proto;
}
});
Object.getPrototypeOf(p) === proto // true
1.2.8、Proxy.revocable()
Proxy.revocable()
方法傳回一個可取消的 Proxy 執行個體。
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
2、反射-Reflect
Reflect
對象與
Proxy
對象一樣,也是 ES6 為了操作對象而提供的新 API。其主要作用:
1)将
Object
對象的一些明顯屬于語言内部的方法(比如
Object.defineProperty
),放到
Reflect
對象上。
2)修改某些
Object
方法的傳回結果,讓其變得更合理。
3)讓
Object
操作都變成函數行為。
4)
Reflect
對象的方法與
Proxy
對象的方法一一對應,隻要是
Proxy
對象的方法,就能在
Reflect
對象上找到對應的方法。
Reflect
對象一共有 13 個靜态方法。
- Reflect.apply(target, thisArg, args)
- Reflect.construct(target, args)
- Reflect.get(target, name, receiver)
- Reflect.set(target, name, value, receiver)
- Reflect.defineProperty(target, name, desc)
- Reflect.deleteProperty(target, name)
- Reflect.has(target, name)
- Reflect.ownKeys(target)
- Reflect.isExtensible(target)
- Reflect.preventExtensions(target)
- Reflect.getOwnPropertyDescriptor(target, name)
- Reflect.getPrototypeOf(target)
- Reflect.setPrototypeOf(target, prototype)
上面這些方法的作用,大部分與
Object
對象的同名方法的作用都是相同的,而且它與
Proxy
對象的方法是一一對應的
十二、ES6中的Class介紹
1、Class基本用法
1.1、Class簡介
傳統的javascript中隻有對象,沒有類的概念。它是基于原型的面向對象語言。原型對象特點就是将自身的屬性共享給新對象。這樣的寫法相對于其它傳統面向對象語言(比如 C++ 和 Java)差異很大,很容易讓新學習這門語言的程式員感到困惑。ES6 提供了更接近傳統語言的寫法,引入了 Class(類)這個概念,作為對象的模闆。通過
class
關鍵字,可以定義類。
類中有:constructor構造,有static關鍵字(聲明靜态方法),也可以通過 extends 關鍵字實作繼承,也有super 關鍵字,這很像java中的類。基本上,ES6 的
class
可以看作隻是一個文法糖,它的絕大部分功能,ES5 都可以做到,新的
class
寫法隻是讓對象原型的寫法更加清晰、更像面向對象程式設計的文法而已。我們看個栗子:
ES5的寫法:
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
ES6的寫法:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
console.log(typeof Point); // "function"
console.log(Point === Point.prototype.constructor); // true
//類完全可以看作構造函數的另一種寫法:Point === Point.prototype.constructor
//實際上,類中所有的方法都定義在類的prototype屬性上
使用class關鍵字需要注意一下幾點:
1)class内部定義的方法都是不可枚舉的,ES5中函數的寫法是可以枚舉的;
2)生成類的寫法需使用 new 指令,否則會報錯;
3)constructor 方法預設傳回執行個體對象(即 this );
4)類和子產品的内部,預設就是嚴格模式,是以不需要使用 use strict 指定運作模式;
5)類不存在變量提升;
6)類的方法内部如果含有this,它預設指向類的執行個體。
class Point {
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
var Point = function (x, y) {
// ...
};
Point.prototype.toString = function () {
// ...
};
Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
// 報錯
var point = Point(2, 3);
// 正确 使用new
var point = new Point(2, 3);
//不存在變量提示
new Foo(); // ReferenceError
class Foo {}
1.2、constructor 方法和普通方法
constructor()
方法是類的預設方法,通過
new
指令生成對象執行個體時,自動調用該方法。一個類必須有
constructor()
方法,如果沒有顯式定義,一個空的
constructor()
方法會被預設添加。
與 ES5 一樣,在“類”的内部可以使用
get
和
set
關鍵字,對某個屬性設定存值函數和取值函數,攔截該屬性的存取行為。
class Point {
}
// 等同于
class Point {
constructor() {}
}
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter';
}
set prop(value) {
console.log('setter: '+value);
}
}
let inst = new MyClass();
inst.prop = 123;
// setter: 123
inst.prop
// 'getter'
2、Class 表達式
與函數一樣,類也可以使用表達式的形式定義。
const MyChild = class Child {
toString() {
console.log(Child.name); //name屬性總是傳回緊跟在class關鍵字後面的類名。
}
};
//類的名字是MyChild而不是Child,Child隻在Class内部代碼可用
let mychild = new MyChild();
mychild.toString(); // Child
//如果函數内部用不到Child,也可以省略
const MyChild = class {
// ...
};
3、Class 的靜态方法
類相當于執行個體的原型,所有在類中定義的方法,都會被執行個體繼承。如果在一個方法前,加上
static
關鍵字,就表示該方法不會被執行個體繼承,而是直接通過類來調用,這就稱為“靜态方法”。
class Foo {
static className() {
console.log("heyushuo");
}
}
Foo.className(); //heyushuo 不能通過執行個體調用會報錯
// 注意,如果靜态方法包含this關鍵字,這個this指的是類,而不是執行個體。
class Foo {
static bar() {
this.baz();
}
static baz() {
console.log("hello");
}
baz() {
console.log("world");
}
}
Foo.bar(); // hello
// 1.靜态方法bar調用了this.baz,這裡的this指的是Foo類,而不是Foo的執行個體,等同于調用Foo.baz。
// 2.靜态方法可以與非靜态方法重名。
// 父類的靜态方法,可以被子類繼承。
class Foo {
static classMethod() {
return "hello";
}
}
class Bar extends Foo {}
Bar.classMethod(); // 'hello'
4、Class 的繼承
Class 可以通過 extends 關鍵字實作繼承,這比 ES5 的通過修改原型鍊實作繼承,要清晰和友善很多。
class Parent {
constructor(name, age) {
this.name = name;
this.age = age;
}
toString() {
console.log("年齡:" + this.age + "姓名:" + this.name);
}
}
class Child extends Parent {
constructor(name, age, height) {
super(name, age); //調用父類的constructor(構造方法),子類必須在 constructor 方法中調用 super 方法,使子類獲得自己的 this 對象,否則建立執行個體時會報錯。
this.height = height;
}
sayInfo() {
super.toString(); // 調用父類的toString()
console.log(`身高:${this.height}`);
}
}
var person = new Child("heyushuo", 24, 180);
person.sayInfo(); //年齡:24姓名:heyushuo 身高:180
// 父類的靜态方法,也會被子類繼承。
class A {
static hello() {
console.log("hello world");
}
}
class B extends A {}
B.hello(); // hello world
// hello()是A類的靜态方法,B繼承A,也繼承了A的靜态方法。
5、super 關鍵字
super
這個關鍵字,既可以當作函數使用,也可以當作對象使用。在這兩種情況下,它的用法完全不同。
super 作為函數
- super 作為函數調用時,代表父類的構造函數。ES6 要求,子類的構造函數必須執行一次 super 函數。
- super 雖然代表了父類 A 的構造函數,但是傳回的是子類 B 的執行個體,即 super 内部的 this 指的是 B
- super()隻能用在子類的構造函數之中,用在其他地方就會報錯。
class A {}
class B extends A {
constructor() {
super();
}
}
// 注意, super雖然代表了父類A的構造函數, 但是傳回的是子類B的執行個體,
//即super内部的this指的是B,是以super() 在這裡相當于A.prototype.constructor.call(this)。
super 作為對象時
- 在普通方法中指向父類的原型對象,在靜态方法中指向父類
- super 指向父類的原型對象,是以定義在父類執行個體上的方法或屬性,是無法通過 super 調用的。
- 在子類普通方法中通過 super 調用父類的方法時,方法内部的this 指向目前的子類執行個體。
- super 作為對象,用在靜态方法之中,這時super 将指向父類,在普通方法之中指向父類的原型對象。
class A {}
class B extends A {
constructor() {
super();
console.log(super); // 報錯
}
}
class A {}
class B extends A {
constructor() {
super();
console.log(super.valueOf() instanceof B); // true
}
}
let b = new B();
//上面代碼中,super.valueOf()表明super是一個對象,是以就不會報錯。同時,由于super使得this指向B的執行個體,是以super.valueOf()傳回的是一個B的執行個體。
6、ES6中的Class實作
ES6中Class的底層還是通過構造函數去建立的。有興趣的可以使用Babel去轉換代碼(Babel是一個工具鍊,主要用于在目前和較舊的浏覽器或環境中将ECMAScript 2015+代碼轉換為JavaScript的向後相容版本-https://babeljs.io/repl)
轉換前:
class Parent {
constructor(a){
this.filed1 = a;
}
filed2 = 2;
func1 = function(){}
}
經過babel轉碼之後:
//調用_classCallCheck方法判斷目前函數調用前是否有new關鍵字,若構造函數前面沒有new則構造函數的prototype不會出現在this的原型鍊上,傳回false
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var Parent = function Parent(a) {
_classCallCheck(this, Parent);
_defineProperty(this, "filed2", 2);
_defineProperty(this, "func1", function () {});
this.filed1 = a;
};
執行過程:
1)調用_classCallCheck方法判斷目前函數調用前是否有new關鍵字,若構造函數前面沒有new則構造函數的prototype不會出現在this的原型鍊上,傳回false
2)将class内部的變量及函數指派給this
3)執行constructor内部的邏輯
十三、子產品(Module)
1、Module簡介
曆史上,JavaScript 一直沒有子產品(module)體系,無法将一個大程式拆分成互相依賴的小檔案,再用簡單的方法拼裝起來。其他語言都有這項功能,比如 Ruby 的
require
、Python 的
import
,甚至就連 CSS 都有
@import,
子產品化已是常态,但是 JavaScript 任何這方面的支援都沒有,這對開發大型的、複雜的項目形成了巨大障礙。
在 ES6 之前,社群制定了一些子產品加載方案,最主要的有 CommonJS 和 AMD 兩種。前者用于伺服器,後者用于浏覽器。ES6 在語言标準的層面上,實作了子產品功能,而且實作得相當簡單,完全可以取代 CommonJS 和 AMD 規範,成為浏覽器和伺服器通用的子產品解決方案。
子產品功能主要由兩個指令構成:
export
和
import
。
export
指令用于規定子產品的對外接口,
import
指令用于輸入其他子產品提供的功能。
//--------在CommonJs中------------
//file.js
module.exports = value;
// 引入value
const value = require('file.js')
//--------在ES6中------------
// const.js
export const value = 'xxx';
import { value } from 'const.js'
ES6 的子產品自動采用嚴格模式,不管你有沒有在子產品頭部加上
"use strict";
。
嚴格模式主要有以下限制:
- 變量必須聲明後再使用
- 函數的參數不能有同名屬性,否則報錯
- 不能使用
語句with
- 不能對隻讀屬性指派,否則報錯
- 不能使用字首 0 表示八進制數,否則報錯
- 不能删除不可删除的屬性,否則報錯
- 不能删除變量
,會報錯,隻能删除屬性delete prop
delete global[prop]
-
不會在它的外層作用域引入變量eval
-
和eval
不能被重新指派arguments
-
不會自動反映函數參數的變化arguments
- 不能使用
arguments.callee
- 不能使用
arguments.caller
- 禁止
指向全局對象this
- 不能使用
和fn.caller
擷取函數調用的堆棧fn.arguments
- 增加了保留字(比如
、protected
和static
)interface
2、export 指令
ES6子產品隻支援靜态導出,你隻可以在子產品的最外層作用域使用export,不可在條件語句中使用,也不能在函數作用域中使用。從分類上級講, exports 主要有三種:
1)Named Exports (Zero or more exports per module)-具名導出:這種方式導出多個函數/變量。
2)Default Exports (One per module)-預設導出:導出一個預設函數/類。
3)Hybrid Exports-混合導出:也就是 上面第一點和第二點結合在一起的情況。
2.1、Named Exports-具名導出
隻需要在變量或函數前面加
export
關鍵字即可。
//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
return x * x;
}
export function diag(x, y) {
return sqrt(square(x) + square(y));
}
//------ main.js 使用方式1 ------
import { square, diag } from './lib.js';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5
//------ main.js 使用方式2 ------
import * as lib from './lib.js';
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5
引用:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
</body>
<!--需要加上類型為:module-->
<script type="module" src="./main.js"></script>
</html>
如果直接使用浏覽器運作的話,會出現跨域問題:同源政策禁止讀取位于 file:///D:/vueTest/vue-test-big/src/... 的遠端資源,跨源請求僅支援協定方案:http、data、chrome、chrome擴充、https。
原因:HTML使用type="module"會預設産生跨域請求,我們是在本地打開的檔案,而file協定并不支援。
解決方案:Visual Studio Code下載下傳Live Server 插件
輸出結果:
2.2、Default exports-預設導出
這種方式比較簡單,一般用于一個類檔案,或者功能比較單一的函數檔案使用。一個子產品中隻能有一個export default預設輸出。export default與export的主要差別有兩個:不需要知道導出的具體變量名, 導入(import)時不需要{}。
//導出一個函數------ myFunc.js ------
export default function () {console.log("default導出函數")};
//------ main.js ------
import myFunc from './myFunc.js';
myFunc();
//------ MyClass.js ------
class MyClass{}
//導出一個Class
export default MyClass;
//------ Main.js ------
import MyClass from './MyClass.js';
2.3、Mixed exports-混合導出
混合導出,也就是 上面第一點和第二點結合在一起的情況。
//mixed.js
export function myFunc () {
console.log("混合導出");
}
export const MY_CONST = 'hh';
export default class MyClass {
say () {
console.log("混合類導出");
}
}
//main.js
import MyClass, { myFunc, MY_CONST } from './mixed.js';
new MyClass().say();
myFunc();
console.log(MY_CONST);
輸出結果:
注意:一般情況下,export輸出的變量就是在原檔案中定義的名字,但也可以用 as 關鍵字來指定别名,這樣做一般是為了簡化或者語義化export的函數名。
//------ lib.js ------
export function getUserName(){
// ...
};
export function setName(){
// ...
};
//輸出别名,在import的時候可以同時使用原始函數名和别名
export {
getName as get, //允許使用不同名字輸出兩次
getName as getNameV2,
setName as set
}
3、imoprt指令
import的用法和export是一一對應的,但是import支援靜态導入和動态導入兩種方式,動态import支援晚一些,相容性要差一些。
3.1、靜态-導入全部
當export有多個函數或變量時,可以使用 * as 關鍵字來導出所有函數及變量,同時 as 後面跟着的名稱做為該子產品的命名空間。
//導出lib的所有函數及變量
import * as lib from './lib.js';
//以 lib 做為命名空間進行調用,類似于object的方式
console.log(lib.square(11)); // 121
3.2、靜态-按需導入
從子產品檔案中導入單個或多個函數,與 * as namepage 方式不同,這個是按需導入,可指定需要導入的内容。
//導入square和 diag 兩個函數
import { square, diag } from './lib.js';
// 隻導入square 一個函數
import { square } from './lib.js';
// 導入預設子產品
import MyClass from './mixed.js';
注:和 export 一樣,import也可以用 as 關鍵字來設定别名,當import的兩個類的名字一樣時,可以使用 as 來重設導入子產品的名字,也可以用as 來簡化名稱。
// 用 as 來 簡化函數名稱
import {
reallyReallyLongModuleExportName as shortName,
anotherLongModuleName as short
} from './modules/my-module.js';
3.3、動态導入
靜态import在首次加載時候會把全部子產品資源都下載下傳下來。我們實際開發時候,有時候需要動态import(dynamic import)。ES2020提案 引入
import()
函數,支援動态加載子產品。
十四、其他
ES6中不僅僅對字元串和數組進行了擴充,還對對内置對象Object、Number和Math也進行了擴充。如(隻列出部分):
Object.is() 方法判斷兩個值是否為同一個值。
Object.assign() 方法用于将所有可枚舉屬性的值從一個或多個源對象配置設定到目标對象。
Number.isSafeInteger() 方法用來判斷傳入的參數值是否是一個“安全整數”(safe integer)。
Number.isNaN() 方法确定傳遞的值是否為 NaN,并且檢查其類型是否為 Number。它是原來的全局 isNaN() 的更穩妥的版本。
Math.acosh(x) 傳回x的反雙曲餘弦(inverse hyperbolic cosine)。
Math.imul()傳回兩個數以32位帶符号整數形式相乘的結果,傳回的也是一個32位的帶符号整數
Math.trunc() 用于除去一個數的小數部分,傳回整數部分。