這片文章主要是基於阮一峰老師的ECMAScript 6 入門。在看了阮一峰老師的這ES6入門之後,自己做了一下總結,將一些覺得對自己目前有用的東西整理出來方便日後再來鞏固複習。總覺得看別人的東西當時懂了過了一段時間就忘記了,所以我總是會將別人的東西驗證一遍,這樣對知識的理解是能提升一個層次的。
- 1、ECMAScript和JavaScript的關係以及ES6的含義?
- 2、let和const
- 3、變數的解構賦值
- 4、字串的擴充套件
- 5、數值的擴充套件
- 6、函式的擴充套件
- 7、陣列的擴充套件
- 8、物件的擴充套件
- 9、物件的新增方法
- 10、Symbol
- 11、Set
- 12、Map
- 13、Proxy
- 14、Reflect
- 15、Promise
- 16、Iterator和for…of迴圈
1、ECMAScript和JavaScript的關係
前者是後者的規格,後者是前者的一種實現。ES6 既是一個歷史名詞,也是一個泛指,含義是 5.1 版以後的 JavaScript 的下一代標準,涵蓋了 ES2015、ES2016、ES2017 等等,而 ES2015 則是正式名稱,特指該年釋出的正式版本的語言標準。
2、let和const
2.1 let
let用來宣告變數。它的用法類似於var,但是所宣告的變數,只在let命令所在的程式碼塊內有效。
let和const有幾個特點:
1、 不存在變數宣告提升;
2、 暫時性死區(只要一進入當前作用域,所要使用的變數就已經存在了,但是不可獲取,只有等到宣告變數的那一行程式碼出現,才可以獲取和使用該變數。);
3、 不允許重複宣告。不允許在相同作用域內,重複宣告同一個變數。
4、 塊級作用域
塊級作用域的例子
{
let a=12;
var b=23;
}
console.log(b);//23
console.log(a);// a is not defined
for(let i=0;i<10;i++){
}
console.log(i);// is not defined
-----------------------------------------------------------------
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
複製程式碼
分析:變數i是let宣告的,所以i只在let宣告的程式碼塊內有效。for迴圈一共迴圈了10次,每一次都是一個獨立的程式碼塊——{},所以每次迴圈中的i都是獨立的,當前的i只在當前迴圈有效,所以每一次迴圈的i其實都是一個新的變數,所以最後輸出的是6。
for迴圈還有一個特別之處,就是設定迴圈變數的那部分是一個父作用域,而迴圈體內部是一個單獨的子作用域。JavaScript 引擎內部會記住上一輪迴圈的值,初始化本輪的變數i時,就在上一輪迴圈的基礎上進行計算。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
複製程式碼
2.2 不存在變數宣告提升
// var 的情況
console.log(foo); // 輸出undefined,變數宣告提升,相當於在輸出之前就var foo;
var foo = 2;
// let 的情況
console.log(bar); // 報錯ReferenceError,沒有變數宣告提升
let bar = 2;
————————————————————————————————————————————————————————————
複製程式碼
2.3 暫時性死區(temporal dead zone,簡稱 TDZ)
在區塊中使用let和const命令宣告的變數,從一開始就形成了封閉作用域。凡是在宣告之前就使用這些變數,就會報錯(宣告之前都是死區)。本質:只要一進入當前作用域,所要使用的變數就已經存在了,但是不可獲取,只有等到宣告變數的那一行程式碼出現,才可以獲取和使用該變數。
2.4 不允許重複宣告
let不允許在相同作用域內,重複宣告同一個變數。
function func() {
let a = 10;
var a = 1;
}
funb()// // 報錯 Identifier 'a' has already been declared
function func() {
let a = 10;
let a = 1;
}
func()// 報錯 Identifier 'a' has already been declared
————————————————————————————————————————————————————————
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 報錯 引數x預設值等於另一個引數y,而此時y還沒有宣告,屬於"死區"(引數讀取從左至右)。
複製程式碼
不能在函式內部重新宣告引數。
function funb(arg) {
let arg;
}
func() // 報錯Identifier 'arg' has already been declared 形參arg跟區域性變數arg在同一個{}內,所以報錯
function func(arg) {
{
let arg;
console.log(arg);//undefined
}
console.log(arg);//34
}
func(34)
複製程式碼
2.5 塊級作用域
優點:
1、沒有塊級作用域,內層變數可能會覆蓋外層變數(變數宣告提升)。
2、用來計數的迴圈變數會洩露為全域性變數。
特點:
1、 允許任意巢狀。
2、 外層作用域無法讀取內層作用域的變數。
3、 使得立即執行函式不再必要了。
4、 允許在塊級作用域中宣告函式,函式宣告類似於var,函式宣告會提升到所在的塊級作用域的頭部。
2.6 const
const一旦宣告變數,就必須立即初始化,不能留到以後賦值,且變數的值也不能改變。本質:並不是變數的值不得改動,而是變數指向的那個記憶體地址所不得改動。
對於簡單型別的資料(數值、字串、布林值),值就儲存在變數指向的那個記憶體地址,因此等同於常量。但對於複合型別的資料(主要是物件和陣列),變數指向的記憶體地址,儲存的只是一個指向實際資料的指標,const只能保證這個指標是固定的(即總是指向一個固定的地址),至於它指向的資料結構是不是可變的,就完全不能控制了。
1、變數指向的是物件時,可以改變該物件的屬性。但是不可將該變數指向另一個物件。
const obj = {}
// 為 foo 新增一個屬性,可以成功
obj.prop = 123;
// 將 obj 指向另一個物件,就會報錯,此時已經改變了obj所指向的記憶體地址了
obj = {}; // TypeError: "foo" is read-only
1、變數指向的是陣列時,可以改變該陣列中的元素及陣列的屬性。但是不可將該變數指向另一個陣列。
const a = [];
a.push('Hello'); // 可執行
a.length = 0; // 可執行
a = ['Dave']; // 報錯,指向了另一個陣列
複製程式碼
2.7 ES6宣告變數的6種方式
var、function、let、const、class、import。es5只有var和function兩種。
頂層物件的差異: 在瀏覽器環境指的是window物件,在 Node中 指的是global物件,在Web Worker 裡面,self也指向頂層物件。
ES5 之中,頂層物件的屬性與全域性變數是等價的。ES6中的var命令和function命令宣告的全域性變數,依舊是頂層物件的屬性;let命令、const命令、class命令宣告的全域性變數,不屬於頂層物件的屬性。
var a = 1;
window.a // 1
let b = 1;
window.b // undefined
複製程式碼
3、變數的解構賦值
ES6允許按照一定模式,從陣列和物件中提取值,對變數進行賦值,這被稱為解構。本質上,這種寫法屬於模式匹配,只要等號兩邊的模式相同,左邊的變數就會被賦予對應的值。
如果解構不成功,變數的值就等於undefined。
3、1 陣列的結構賦值
事實上,只要某種資料結構具有 Iterator 介面,都可以採用陣列形式的解構賦值。 let [foo = true] = [];foo // true
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
let [x = 1] = [null]; x //null null不嚴格等於undefined,但是null==undefined
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] = []; // y is not undefined
從左到右的讀取。
let [x , y] = [];//[undefined,undefined]
複製程式碼
注意:ES6 內部使用嚴格相等運算子(===),判斷一個位置是否有值。所以,只有當一個陣列成員嚴格等於undefined,預設值才會生效。如果一個陣列成員是null,預設值就不會生效,因為null不嚴格等於undefined。
3、2 物件的解構賦值
物件的解構與陣列有一個重要的不同。陣列的元素是按次序排列的,變數的取值由它的位置決定;而物件的屬性沒有次序,變數必須與屬性同名,才能取到正確的值。
let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: "aaa", bar: "bbb" };baz // undefined
let { foo: baz } = { foo: "aaa", bar: "bbb" };
baz//"aaa",foo是匹配的模式,baz才是變數名
let { foo, bar } = { foo: "aaa", bar: "bbb" };
是下面表示的簡寫。
let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
複製程式碼
由於陣列本質是特殊的物件,因此可以對陣列進行物件屬性的解構。陣列arr的0鍵對應的值是1,[arr.length - 1]就是2鍵,對應的值是3。方括號這種寫法,屬於屬性名錶達式。
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
複製程式碼
3、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
複製程式碼
3、4 數值和布林值的解構賦值
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
複製程式碼
解構賦值的規則是,只要等號右邊的值不是物件或陣列,就先將其轉為物件。 由於undefined和null無法轉為物件,所以對它們進行解構賦值,都會報錯。
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
複製程式碼
3、5 函式引數的解構賦值
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為變數x和y指定預設值,函式move的引數是一個物件,通過對這個物件進行解構,得到變數x和y的值。如果解構失敗,x和y等於預設值。用實參將{}覆蓋。
function move({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined],相當於{x,y}={x,3}
move({}); // [undefined, undefined],相當於{x,y}={};
move(); // [0, 0]
複製程式碼
上面程式碼是為函式move的引數(形參)指定預設值,而不是為變數x和y指定預設值,所以會得到與前一種寫法不同的結果。這種寫法直接是將所傳引數將預設引數進行覆蓋。用實參將{x:0,y:0}覆蓋。
上面兩種寫法本質上都是用所傳引數將預設引數進行覆蓋。
3、6 解構賦值的用處
(1) 交換變數的值。 let{x,y}={y,x};
複製程式碼
(2) 從函式返回多個值;
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
複製程式碼
(3) 函式引數的定義;
function f([x, y, z]) { ... }
f([1, 2, 3]);
複製程式碼
(4) 提取json資料
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
複製程式碼
(5) 輸入模組的指定方法;
const { SourceMapConsumer, SourceNode } = require("source-map");
複製程式碼
(6) 函式引數的預設值(這樣避免了在函式內部再設定預設值)
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
} = {}) {
// ... do stuff
};
複製程式碼
(7) 遍歷map結構(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) {
// ...
}
複製程式碼
4、字串的擴充套件
4、1 includes()、startsWith()、endsWith()
includes():返回布林值,表示是否找到了引數字串。
startsWith():返回布林值,表示引數字串是否在原字串的頭部。 endsWith():返回布林值,表示引數字串是否在原字串的尾部。
這三個方法都支援第二個引數,表示開始搜尋的位置。endsWith的行為與其他兩個方法有所不同,n表示的是結束搜尋的位置,而其他兩個方法針n個表示的是開始搜尋的位置。
4、2 repeat()
repeat方法返回一個新字串,表示將原字串重複n次。 引數如果是小數,會被取整。相當於呼叫了parseInt()。
”a”.repeat(1.9)==>”a”.
複製程式碼
如果repeat的引數是字串,則會先轉換成數字。
'na'.repeat('na') // "" Number("na")等於NAN
'na'.repeat('3') // "nanana"
複製程式碼
4、3 padStart(),padEnd()
padStart()用於頭部補全,padEnd()用於尾部補全。padStart()和padEnd()一共接受兩個引數,第一個引數是字串補全生效的最大長度,第二個引數是用來補全的字串。 (1) 如果原字串的長度,等於或大於最大長度,則字串補全不生效,返回原字串。
'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'
'xxx'.padStart(5, 'ab') // 'abxxx'
'xxx'.padEnd(5, 'ab') // 'xxxab'
複製程式碼
(2) 如果用來補全的字串與原字串,兩者的長度之和超過了最大長度,則會截去超出位數的補全字串。
'abc'.padStart(10, '0123456789') // '0123456abc'
複製程式碼
(3) 如果省略第二個引數,預設使用空格補全長度。
'x'.padStart(4) // ' x'
'x'.padEnd(4) // 'x '
複製程式碼
4、4 模板字串
是增強版的字串,用反引號(`)標識。
(1) 如果在模板字串中需要使用反引號,則前面要用反斜槓轉義。
let greeting = `\`Yo\` World!`;
複製程式碼
(2) 所有模板字串的空格和換行,都是被保留的,比如ul標籤前面會有一個換行。如果你不想要這個換行,可以使用trim方法消除它。
$('#list').html(`
<ul>
<li>first</li>
<li>second</li>
</ul>
`.trim());
複製程式碼
(3)模板字串中嵌入變數,需要將變數名寫在${}之中。
(4)大括號內部可以放入任意的 JavaScript 表示式,可以進行運算,以及引用物件屬性。如果大括號中的值不是字串,將按照一般的規則轉為字串。本質:模板字串的大括號內部,就是執行 JavaScript 程式碼。
let x = 1;
let y = 2;
`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"
`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"
let obj = {x: 1, y: 2};
`${obj.x + obj.y}`
// "3"
複製程式碼
(5)模板字串之中還能呼叫函式。
function fn() {
return "Hello World";
}
`foo ${fn()} bar`
// foo Hello World bar
複製程式碼
5、數值的擴充套件
5、1 Number.isFinite()、Number.isNaN()
這兩個新方法只對數值有效,不會先呼叫Number()方法。
Number.isFinite(): 用來檢查一個數值是否為有限的(finite)。如果引數型別不是數值,Number.isFinite一律返回false。
Number.isNaN(): 用來檢查一個值是否為NaN。Number.isNaN()只有對於NaN才返回true,非NaN一律返回false。
5、2 Number.isSafeInteger
Number.isSafeInteger()則是用來判斷一個整數是否落在這個範圍之內。javaScript 能夠準確表示的整數範圍在-2^53到2^53之間(不含兩個端點),超過這個範圍,無法精確表示這個值。Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER這兩個常量,用來表示這個範圍的上下限。
5、3 Math物件的擴充套件
擴充套件方法在使用時都會引數使用Number()轉為數值來來處理。
Math.trunc(): 用於去除一個數的小數部分,返回整數部分。對於非數值,Math.trunc內部使用Number方法將其先轉為數值(本質上就是parseInt()方法)。
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('123.456') // 123
Math.trunc(true) //1
Math.trunc(false) // 0
Math.trunc(null) // 0
Math.trunc('123.456') // 123
Math.trunc(true) //1
Math.trunc(false) // 0
Math.trunc(null) // 0
複製程式碼
Math.sign(): 用來判斷一個數到底是正數、負數、還是零。
• 引數為正數,返回+1;
• 引數為負數,返回-1;
• 引數為 0,返回0;
• 引數為-0,返回-0;
• 其他值,返回NaN。
Math.sign(-5) // -1
Math.sign(5) // +1
Math.sign(0) // +0
Math.sign(-0) // -0
Math.sign(NaN) // NaN
複製程式碼
Math.cbrt(): 用於計算一個數的立方根。對於非數值,Math.cbrt方法內部也是先使用Number方法將其轉為數值。
Math.cbrt(-1) // -1
Math.cbrt(0) // 0
Math.cbrt(1) // 1
Math.cbrt(2) // 1.2599210498948734
Math.cbrt("8")//2
複製程式碼
Math.imul(): 方法返回兩個數以 32 位帶符號整數形式相乘的結果,返回的也是一個 32 位的帶符號整數。
Math.imul(2, 4) // 8
Math.imul(-1, 8) // -8
Math.imul(-2, -2) // 4
複製程式碼
Math.hypot(): 方法返回所有引數的平方和的平方根。
Math.hypot(3, 4); // 5
Math.hypot(3, 4, 5); // 7.0710678118654755
Math.hypot(); // 0
Math.hypot(NaN); // NaN
Math.hypot(3, 4, 'foo'); // NaN
Math.hypot(3, 4, '5'); // 7.0710678118654755
Math.hypot(-3); // 3
複製程式碼
6、函式的擴充套件
6、1 函式引數的預設值
引數變數是預設宣告的,所以不能用let或const再次宣告。
function foo(x = 5) {
let x = 1; // error
const x = 2; // error
}
foo()//Identifier 'x' has already been declared
複製程式碼
6、2 函式的length屬性
指定了預設值以後,函式的length屬性,將返回沒有指定預設值之前的的引數的個數。 也就是說,指定了預設值後,length屬性將失真。預設值後面的引數將不參加計算。函式的length屬性,不包括 rest 引數。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
(function(...args) {}).length // 0
複製程式碼
6、3 作用域
一旦設定了引數的預設值,函式進行宣告初始化時,引數會形成一個單獨的作用域(context)。等到初始化結束,這個作用域就會消失。(本質上是暫時性死區和不能重複宣告)
var x = 1;
function f(x, y = x) {
//let x=3;Identifier 'x' has already been declared
//let y=7;// Identifier 'y' has already been declared
console.log(y);//2
}
f(2)
複製程式碼
6、4 rest引數
形式為(...變數名),用於獲取函式的多餘引數,這樣就不需要使用arguments物件了。rest引數搭配的變數是一個陣列,該變數將多餘的引數放入陣列中。注意:rest引數之後不能再有其他引數(即只能是最後一個引數),否則會報錯。
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
console.log(item);
});
}
var a = [];
push(a, 1, 2, 3)
複製程式碼
6、5 嚴格模式
只要函式引數使用了預設值、解構賦值、或者擴充套件運算子(ES6語法預設是嚴格模式),那麼函式內部就不能顯式設定為嚴格模式,否則會報錯。6、6 name屬性
如果將一個匿名函式賦值給一個變數,ES5的name屬性,會返回空字串,而 ES6 的name屬性會返回實際的函式名。 如果將一個具名函式賦值給一個變數,則 ES5 和 ES6 的name屬性都返回這個具名函式原本的名字。
const bar = function baz() {};
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"
複製程式碼
6、7 箭頭函式
ES6 允許使用“箭頭”(=>)定義函式。如果箭頭函式不需要引數或需要多個引數,就使用一個圓括號代表引數部分。如果箭頭函式的程式碼塊部分多於一條語句,就要使用大括號將它們括起來,並且使用return語句返回。 如果箭頭函式直接返回一個物件,必須在物件外面加上括號 ,否則會報錯。
// 報錯
let getTempItem = id => { id: id, name: "Temp" };
// 不報錯
let getTempItem = id => ({ id: id, name: "Temp" });
複製程式碼
箭頭函式需要注意的地方有以下幾點
1. 函式體內的this物件,就是定義時所在的物件(固定不變),而不是使用時所在的物件。
2. 不可以當作建構函式, 也就是說,不可以使用new命令,否則會丟擲一個錯誤。
3. 不可以使用arguments物件, 該物件在函式體內不存在。如果要用,可以用 rest 引數代替。
4. 不可以使用yield命令, 因此箭頭函式不能用作 Generator 函式。
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭頭函式
setInterval(() => this.s1++, 1000);
// 普通函式
setInterval(function () {
this.s2++;//this表示window,setInterval是window的屬性
}, 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一次都沒更新。 this固定化的本質:並不是因為箭頭函式內部有繫結this的機制,實際原因是箭頭函式根本沒有自己的this,導致內部的this就是外層程式碼塊的this。 正常情況下,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。
箭頭函式不適用場合:
1、定義函式的方法(此時應該用普通函式的方式)
var lives=18;
const cat = {
lives: 9,
a:this.lives,//this指向的是window,
say:function(){
console.log(this.lives);//this指的是cat
},
jumps: () => {
this.lives--;//this指的是window,定義時的this指的就是window
}
}
cat.say();
cat.jumps();
console.log(cat.a);
複製程式碼
2、需要動態this的時候
var button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on');//this指的是window
})
複製程式碼
適用場合:回撥
var handler = {
id: '123456',
init: function() {
document.addEventListener('click',
event => this.doSomething(event.type), false);//this指的是handler
},
doSomething: function(type) {
console.log('Handling ' + type + ' for ' + this.id);//this指的是hander
}
};
複製程式碼
6、8 尾呼叫
是指某個函式的最後一步是呼叫另一個函式。 尾呼叫由於是函式的最後一步操作,所以不需要保留外層函式的呼叫幀,因為呼叫位置、內部變數等資訊都不會再用到了,只要直接用內層函式的呼叫幀,取代外層函式的呼叫幀就可以了。(就是說外層函式的作用域鏈會被銷燬,但它的活動物件任然會留在記憶體中)
function f(x){
return g(x);
}
複製程式碼
6、9 尾遞迴
尾呼叫自身,就稱為尾遞迴。缺點:把所有用到的內部變數改寫成函式的引數。優點:不會發生棧溢位,相對節省記憶體。
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
複製程式碼
採用es6語法(引數的預設值)可以解決這個缺點
function factorial(n, total=1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5) // 120
複製程式碼
7、陣列的擴充套件
7、1 擴充套件運算子
擴充套件運算子(spread)是三個點(...)。它好比 rest 引數的逆運算,將一個陣列轉為用逗號分隔的引數序列。 擴充套件運算子背後呼叫的是遍歷器介面(Symbol.iterator),如果一個物件沒有部署這個介面,就無法轉換。
注意:擴充套件運算子(spread)是三個點(...)。它好比 rest 引數的逆運算,將一個陣列轉為用逗號分隔的引數序列。本質上就是rest引數
console.log(…[1,2,3])// 1 2 3
複製程式碼
擴充套件運算子的應用:
1、 替代函式的apply用法
// ES5 的寫法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f.apply(null, args);
// ES6的寫法
function f(x, y, z) {
// ...
}
let args = [0, 1, 2];
f(...args);
複製程式碼
2、求取陣列中的最大值:
// ES5 的寫法
Math.max.apply(null, [14, 3, 77])
// ES6 的寫法
Math.max(...[14, 3, 77])
// 等同於
Math.max(14, 3, 77);
複製程式碼
3、 簡化push函式的用法
// ES5的 寫法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);
// ES6 的寫法
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);
複製程式碼
4、 複製陣列
const a1 = [1, 2];
// 寫法一
const a2 = [...a1];
// 寫法二
const [...a2] = a1;
寫法一二相當於把陣列中a1的元素複製到a2中
const a1 = [1, 2];
const a2 = a1.concat();
a2[0] = 2;
a1 // [1, 2]
上面兩個方法修改a2都不會對a1產生影響。
複製程式碼
5、 合併陣列
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];
// ES5 的合併陣列
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]
// ES6 的合併陣列
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
這兩種方法都是淺拷貝,使用的時候需要注意。
const a1 = [{ foo: 1 }];
const a2 = [{ bar: 2 }];
const a3 = a1.concat(a2);
const a4 = [...a1, ...a2];
console.log(a3[0] === a1[0]) // true 指向相同的記憶體地址
console.log(a4[0] === a1[0]) // true 指向相同的記憶體地址
a3[0].foo=2;
console.log(a1)//{foo: 2}
複製程式碼
a3和a4是用兩種不同方法合併而成的新陣列,但是它們的成員都是對原陣列成員的引用,這就是淺拷貝。如果修改了原陣列的成員,會同步反映到新陣列。
6、 與解構賦值結合
const [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(rest) // [2, 3, 4, 5]
const [first, ...rest] = [];
console.log(first) // undefined
console.log(rest) // []
const [first, ...rest] = ["foo"];
console.log(first) // "foo"
console.log(rest) // []
複製程式碼
將擴充套件運算子用於陣列賦值,只能放在引數的最後一位,否則會報錯。跟rest引數一樣。
const [...butLast, last] = [1, 2, 3, 4, 5];// 報錯
const [first, ...middle, last] = [1, 2, 3, 4, 5];//報錯,Rest element must be last element
複製程式碼
7、 字串
擴充套件運算子還可以將字串轉為真正的陣列。
[...'hello']; // [ "h", "e", "l", "l", "o" ]
[…'hello'].length;//5
複製程式碼
8、 實現了 Iterator 介面的物件
任何定義了遍歷器(Iterator)介面的物件,都可以用擴充套件運算子轉為真正的陣列。
let nodeList = document.querySelectorAll('div');
let array = [...nodeList];//實現了Iterator介面
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
let arr = [...arrayLike];// // TypeError: Cannot spread non-iterable object.
//可以改成下面這樣
let arr=Array.form(arrayLike)//把物件變成陣列,把類似陣列的變成陣列
複製程式碼
map結構:
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]
Generator 函式:
const go = function*(){
yield 1;
yield 2;
yield 3;
};
[...go()] // [1, 2, 3]
複製程式碼
7、2 Array.from()
Array.from方法用於將兩類物件轉為真正的陣列:類似陣列的物件(array-like object)和可遍歷(iterable)的物件(包括 ES6 新增的資料結構 Set 和 Map)。
任何有length屬性的物件,都可以通過Array.from方法轉為陣列,而此時擴充套件運算子就無法轉換。
Array.from({ length: 3 });
// [ undefined, undefined, undefined ]
複製程式碼
1、類似於陣列的物件
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']
複製程式碼
2、可遍歷的物件:
Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']
let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']
複製程式碼
3、不支援該方法的瀏覽器,可以用下面這種方法來進行相容:
const toArray = (() =>
Array.from ? Array.from : obj => [].slice.call(obj)
)();
複製程式碼
4、Array.from還可以接受第二個引數,作用類似於陣列的map方法,用來對每個元素進行處理,將處理後的值放入返回的陣列。
Array.from(arrayLike, x => x * x);
// 等同於
Array.from(arrayLike).map(x => x * x);
Array.from([1, 2, 3], (x) => x * x)// [1, 4, 9]
let spans = document.querySelectorAll('span.name');
// map()
let names1 = Array.prototype.map.call(spans, s => s.textContent);
// Array.from()
let names2 = Array.from(spans, s => s.textContent)
複製程式碼
5、Array.from()可以將各種值轉為真正的陣列。
Array.from({ length: 2 }, () => 'jack')// ['jack', 'jack']
複製程式碼
6、將字串轉為陣列,然後返回字串的長度
function countSymbols(string) {
return Array.from(string).length;
}
複製程式碼
7、3 Array.of()
用於將一組值,轉換為陣列。這個方法的主要目的,是彌補陣列建構函式Array()的不足。Array.of基本上可以用來替代Array()或new Array()。 Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
複製程式碼
7、4 copywithin()
陣列例項的copyWithin方法,在當前陣列內部,將指定位置的成員複製到其他位置(會覆蓋原有成員),然後返回當前陣列。
Array.prototype.copyWithin(target, start = 0, end = this.length)
target(必需):從該位置開始替換資料。如果為負值,表示倒數。
start(可選):從該位置開始讀取資料,預設為 0。如果為負值,表示倒數。
end(可選):到該位置前停止讀取資料,預設等於陣列長度。如果為負值,表示倒數。
[1, 2, 3, 4, 5].copyWithin(0, 3)// [4, 5, 3, 4, 5]
上面程式碼表示將從 3 號位直到陣列結束的成員(4 和 5),複製到從 0 號位開始的位置,結果覆蓋了原來的 1 和 2。
[1, 2, 3, 4, 5].copyWithin(0, 2)// [3, 4, 5, 4, 5]
[1, 2, 3, 4, 5].copyWithin(0, 2,3)// [3, 2, 3, 4, 5]
複製程式碼
7、5 find()和findIndex()
陣列例項的find方法,用於找出第一個符合條件的陣列成員。它的引數是一個回撥函式,所有陣列成員依次執行該回撥函式,直到找出第一個返回值為true的成員,然後返回該成員。如果沒有符合條件的成員,則返回undefined。 [1, 4, -5, 10].find((n) => n < 0)// -5
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
複製程式碼
陣列例項的findIndex(),返回第一個符合條件的陣列成員的位置,如果所有成員都不符合條件,則返回-1。
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
複製程式碼
這兩個方法都可以接受第二個引數,用來繫結回撥函式的this物件。
function f(v){
return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person); // 26
複製程式碼
這兩個方法都可以發現NaN,彌補了陣列的indexOf方法的不足。
[NaN].indexOf(NaN)
// -1
[NaN].findIndex(y => Object.is(NaN, y))
// 0
複製程式碼
Object.is()用來比較兩個值是否嚴格相等,與嚴格相等運算子(===)一樣。
7、6 fill()
//使用給定值,填充一個陣列,並返回填充後的陣列。
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
複製程式碼
陣列中已有的元素,會被全部抹去。 fill方法還可以接受第二個和第三個引數,用於指定填充的起始位置和結束位置。這根splice()很像,只是splice方法沒有返回值。
['a', 'b', 'c'].fill(7, 1, 2)// ['a', 7, 'c']
複製程式碼
注意:如果填充的型別為物件,那麼被賦值的是同一個記憶體地址的物件,而不是深拷貝物件。
let arr = new Array(3).fill({name: "Mike"});
arr[0].name = "Ben";
console.log(arr)// [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
複製程式碼
7、7 entries()、keys()、values()
它們都返回一個遍歷器物件(Iterator),可以用for...of迴圈進行遍歷。 let letter = ['a', 'b', 'c'];
let keys=letter.keys();
let values=letter.values()
let entries = letter.entries();
console.log(keys,values,entries)
// Array Iterator {} Array Iterator {} Array Iterator {}
for (let index of keys) {
console.log(index);
} //0, //1,//2
複製程式碼
7、8 includes()
返回一個布林值,表示某個陣列是否包含給定的值,與字串的includes方法類似。 [1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true
複製程式碼
該方法的第二個參數列示搜尋的起始位置,預設為0。如果第二個引數為負數,則表示倒數的位置,如果這時它大於陣列長度(比如第二個引數為-4,但陣列長度為3),則會重置為從0開始。
indexOf方法有兩個缺點。
1、不夠語義化,它的含義是找到引數值的第一個出現位置,所以要去比較是否不等於-1。
2、它內部使用嚴格相等運算子(===)進行判斷,這會導致對NaN的誤判。
[NaN].includes(NaN) //true
複製程式碼
可以用如下方法來判斷當前環境是否支援該方法。
const contains = (() =>
Array.prototype.includes ?
(arr, value) => arr.includes(value) :
(arr, value) => arr.some(el => el === value)
)();
console.log(contains(['foo', 'bar'], 'baz')); // => false
複製程式碼
7、9 flat()、flatMap()
用於將巢狀的陣列“拉平”,變成一維的陣列。該方法返回一個新陣列,對原資料沒有影響。flat()預設只會“拉平”一層,如果想要“拉平”多層的巢狀陣列,可以將flat()方法的引數寫成一個整數,表示想要拉平的層數,預設為1。
[1, 2, [3, [4, 5]]].flat() // [1, 2, 3, [4, 5]]
[1, 2, [3, [4, 5]]].flat(2) // [1, 2, 3, 4, 5]
[1, [2, [3]]].flat(Infinity) // [1, 2, 3],這種方式不管巢狀多少層,都會被拉平。
複製程式碼
如果原陣列有空位,flat()方法會跳過空位。
[1, 2, , 4, 5].flat() // [1, 2, 4, 5]
複製程式碼
flatMap()方法對原陣列的每個成員執行一個函式(相當於執行Array.prototype.map()),然後對返回值組成的陣列執行flat()方法。該方法返回一個新陣列,不改變原陣列。flatMap()只能展開一層陣列。
// 相當於 [[[2]], [[4]], [[6]], [[8]]].flat()
[1, 2, 3, 4].flatMap(x => [[x * 2]])
// [[2], [4], [6], [8]]
複製程式碼
7、10 陣列的空位
注意,空位不是undefined,一個位置的值等於undefined,依然是有值的。空位是沒有任何值。ES5:
1、forEach(), filter(), reduce(), every() 和some()都會跳過空位。
2、map()會跳過空位,但會保留這個值。
3、join()和toString()會將空位視為undefined,而undefined和null會被處理成空字串。
ES6:明確將空位轉為undefined。
8、物件的擴充套件
8、1 屬性的簡潔表示法
const foo = 'bar';
const baz = {foo};
console.log(baz) // {foo: "bar"}
// 等同於
const baz = {foo: foo};
複製程式碼
ES6 允許在物件之中,直接寫變數。這時,屬性名為變數名, 屬性值為變數的值。
function f(x, y) {
return {x, y};
}
// 等同於
function f(x, y) {
return {x: x, y: y};
}
f(1, 2) // {x: 1, y: 2}
複製程式碼
8、2 屬性名錶達式
JavaScript 定義物件的屬性,有兩種方法。 // 方法一
obj.foo = true;
// 方法二
obj['a' + 'bc'] = 123;
複製程式碼
ES6 允許字面量定義物件時,用方法二(表示式)作為物件的屬性名,即把表示式放在方括號內。
let lastWord = 'last word';
const a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
複製程式碼
表示式還可以用於定義方法名。
let obj = {
['h' + 'ello']() {
return 'hi';
}
};
obj.hello() // hi
複製程式碼
注意,屬性名錶達式如果是一個物件,預設情況下會自動將物件轉為字串[object Object],這一點要特別小心。
const keyA = {a: 1};
const keyB = {b: 2};
const myObject = {
[keyA]: 'valueA',
[keyB]: 'valueB'
};
myObject // Object {[object Object]: "valueB"}
複製程式碼
8、3 方法的name屬性
const person = {
sayName() {
console.log('hello!');
},
};
person.sayName.name // "sayName"
複製程式碼
如果物件的方法使用了取值函式(getter)和存值函式(setter),則name屬性不是在該方法上面,而是該方法的屬性的描述物件的get和set屬性上面,返回值是方法名前加上get和set。
const obj = {
get foo() {},
set foo(x) {}
};
obj.foo.name
// TypeError: Cannot read property 'name' of undefined
const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
descriptor.get.name // "get foo"
descriptor.set.name // "set foo"
複製程式碼
如果物件的方法是一個 Symbol 值,那麼name屬性返回的是這個 Symbol 值的描述。
const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
[key1]() {},
[key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""
複製程式碼
有兩種特殊情況:bind方法創造的函式,name屬性返回bound加上原函式的名字;Function建構函式創造的函式,name屬性返回anonymous(匿名)。
(new Function()).name // "anonymous"
var doSomething = function() {
// ...
};
doSomething.bind().name // "bound doSomething"
複製程式碼
8、4 屬性的可列舉性和遍歷
1、可列舉性: 物件的每個屬性都有一個描述物件(Descriptor),用來控制該屬性的行為。Object.getOwnPropertyDescriptor方法可以獲取該屬性的描述物件。 let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,//可列舉性
// configurable: true
// }
複製程式碼
目前,有四個操作會忽略enumerable為false的屬性。
1、 for...in迴圈:只遍歷物件自身的和繼承的可列舉的屬性。
2、 Object.keys():返回物件自身的可列舉的屬性的屬性。
3、 JSON.stringify():只序列化物件自身的可列舉的屬性。
4、 Object.assign(): 忽略enumerable為false的屬性,只拷貝物件自身的可列舉的屬性。
總結:儘量不要用for...in迴圈,而用Object.keys()代替。
2、屬性的遍歷
1、 for…in:for...in迴圈遍歷物件自身的和繼承的可列舉屬性(不含 Symbol 屬性)。
2、 Object.keys(obj)
返回一個陣列,包括物件自身的所有可列舉屬性(不含 Symbol 屬性)的鍵名。
3、 Object.getOwnPropertyNames(obj)
返回一個陣列,包含物件自身的所有屬性(不含 Symbol 屬性,但是包括不可列舉屬性)的鍵名。
4、 Object.getOwnPropertySymbols(obj)
返回一個陣列,包含物件自身的所有 Symbol 屬性的鍵名。
5、 Reflect.ownKeys(obj)
返回一個陣列,包含物件自身的所有鍵名,不管鍵名是 Symbol 或字串,也不管是否可列舉。
以上的 5 種方法遍歷物件的鍵名,都遵守同樣的屬性遍歷的次序規則。
1、首先遍歷所有數值鍵,按照數值升序排列。
2、其次遍歷所有字串鍵,按照加入時間升序排列。
3、最後遍歷所有 Symbol 鍵,按照加入時間升序排列。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 }) // ['2', '10', 'b', 'a', Symbol()]
複製程式碼
8、5 物件的擴充套件運算子
物件的解構賦值 (在=賦值左邊) 用於從一個物件取值,相當於將目標物件自身的所有可遍歷的(enumerable)、但尚未被讀取的屬性,分配到指定的物件上面。所有的鍵和它們的值,都會拷貝到新物件上面。屬於淺拷貝。
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1 y // 2 z // { a: 3, b: 4 }
複製程式碼
物件的解構賦值的注意事項
1、由於解構賦值要求等號右邊是一個物件,所以如果等號右邊是undefined或null,就會報錯,因為它們無法轉為物件。
let { x, y, ...z } = null; // 執行時錯誤
let { x, y, ...z } = undefined; // 執行時錯誤
複製程式碼
2、解構賦值必須是最後一個引數,否則會報錯。
let { ...x, y, z } = someObject; // 句法錯誤
let { x, ...y, ...z } = someObject; // 句法錯誤
複製程式碼
3、擴充套件運算子的解構賦值,不能複製繼承自原型物件的屬性。
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3 // { b: 2 }
o3.a // undefined
Object.create({ x: 1, y: 2 });建立的是原型物件
const o = Object.create({ x: 1, y: 2 });
o.z = 3;
let { x, ...newObj } = o;//newObj只能獲取z的值
let { y, z } = newObj;
x // 1
y // undefined
z // 3
複製程式碼
4、 變數宣告語句之中,如果使用解構賦值,擴充套件運算子後面必須是一個變數名,而不能是一個解構賦值表示式。
let { x, ...{ y, z } } = o;
// SyntaxError: ... must be followed by an identifier in declaration contexts
複製程式碼
擴充套件運算子: (在等號=右邊) 物件的擴充套件運算子(...)用於取出引數物件的所有可遍歷屬性,拷貝到當前物件之中。
物件的擴充套件運算子的注意事項
1、由於陣列是特殊的物件,所以物件的擴充套件運算子也可以用於陣列。
let foo = { ...['a', 'b', 'c'] };
console.log(foo);// {0: "a", 1: "b", 2: "c"}
複製程式碼
2、 如果擴充套件運算子後面不是物件,則會自動將其轉為物件。
// 等同於 {...Object(1)}
{...1} // {} 由於該物件沒有自身屬性,所以返回一個空物件。
// 等同於 {...Object(true)}
{...true} // {}
// 等同於 {...Object(undefined)}
{...undefined} // {}
// 等同於 {...Object(null)}
{...null} // {}
複製程式碼
3、 如果擴充套件運算子後面是字串,它會自動轉成一個類似陣列的物件,因此返回的不是空物件。
{...'hello'} // {0: "h", 1: "e", 2: "l", 3: "l", 4: "o"}
複製程式碼
4、 物件的擴充套件運算子等同於使用Object.assign()方法。
let aClone = { ...a };
// 等同於
let aClone = Object.assign({}, a);
複製程式碼
5、 如果想完整克隆一個物件,還拷貝物件原型的屬性,可以採用下面的寫法。
// 寫法1
const clone2 = Object.assign(
Object.create(Object.getPrototypeOf(obj)),
obj
);
// 寫法2
const clone3 = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
)
複製程式碼
6、 擴充套件運算子可以用於合併兩個物件。
let ab = { ...a, ...b };
// 等同於
let ab = Object.assign({}, a, b);
複製程式碼
7、 如果使用者自定義的屬性,放在擴充套件運算子後面,則擴充套件運算子內部的同名屬性會被覆蓋掉。
let a={x:2,y:3;z:4}
let aWithOverrides = { ...a, x: 1, y: 2 };
console.log(aWithOverrides)// {x: 1, y: 2, z: 4}
let arr={a:1,b:2}
let arr1={b:3,c:4}
let arr2={...arr,...arr1}
console.log(arr2) // {a: 1, b: 3, c: 4}
複製程式碼
9、物件的新增方法
9、1 Object.is()
用來比較兩個值是否嚴格相等,與嚴格比較運算子(===)的行為基本一致。與ES5的不同之處只有兩個:一是+0不等於-0,二是NaN等於自身。 +0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
複製程式碼
ES5 可以通過下面的程式碼,部署Object.is。
Object.defineProperty(Object, 'is', {
value: function(x, y) {
if (x === y) {
// 針對+0 不等於 -0的情況
return x !== 0 || 1 / x === 1 / y;
}
// 針對NaN的情況
return x !== x && y !== y;
},
configurable: true,
enumerable: false,
writable: true
});
複製程式碼
9、2 Object.assign()
用於物件的合併,將源物件(source)的所有可列舉屬性,複製到目標物件(target)。 如果目標物件與源物件有同名屬性,或多個源物件有同名屬性,則後面的屬性會覆蓋前面的屬性。 const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
const target={};
Object.assign(target, source1, source2);
Console.log(target); // {a:1, b:2, c:3}
複製程式碼
注意1:由於undefined和null無法轉成物件,所以如果它們作為引數,就會報錯。如果undefined和null不在首引數,就不會報錯。
Object.assign(undefined) // 報錯
Object.assign(null) // 報錯
let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
複製程式碼
注意2:其他型別的值(即數值、字串和布林值)不在首引數,也不會報錯。但是,除了字串會以陣列形式,拷貝入目標物件,其他值都不會產生效果。
const v1 = 'abc';
const v2 = true;
const v3 = 10;
const obj = Object.assign({}, v1, v2, v3);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
複製程式碼
注意3:Object.assign拷貝的屬性是有限制的,只拷貝源物件的自身屬性(不拷貝繼承屬性),也不拷貝不可列舉的屬性(enumerable: false)。
Object.assign的特點
1、 Object.assign()是淺拷貝。
2、 同名屬性的替換;(後者替換前者)
3、 陣列的處理。Object.assign把陣列視為屬性名為 0、1、2 的物件,因此源陣列的 0 號屬性4覆蓋了目標陣列的 0 號屬性1。
Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
複製程式碼
4、 取值函式的處理;(求值後再複製)
const source = {
get foo() { return 1 }
};
const target = {};
Object.assign(target, source)
// { foo: 1 }
複製程式碼
Object.assign的常見用途:
1、 為物件新增屬性
class Point {
constructor(x, y) {
Object.assign(this, {x, y});
}
}
複製程式碼
2、 為物件新增方法
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
},
anotherMethod() {
}
});
// 等同於下面的寫法
SomeClass.prototype.someMethod = function (arg1, arg2) {
};
SomeClass.prototype.anotherMethod = function () {
};
複製程式碼
3、 克隆物件(克隆自身與其繼承的值)
function clone(origin) {
let originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto), origin);
}
複製程式碼
4、 合併多個物件
const merge = (target, ...sources) => Object.assign(target, ...sources);
複製程式碼
5、 為屬性指定預設值
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
options = Object.assign({}, DEFAULTS, options);
}
複製程式碼
9、3 Object.getOwnPropertyDescriptors()
返回指定物件所有自身屬性(非繼承屬性)的描述物件。主要是為了解決Object.assign()無法正確拷貝get屬性和set屬性的問題。
9、4. __proto__屬性,Object.setPrototypeOf(),Object.getPrototypeOf()
__proto__屬性:
用來讀取或設定當前物件的prototype物件。目前,所有瀏覽器(包括 IE11)都部署了這個屬性。建議不要使用此屬性。使用下面的Object.setPrototypeOf()(寫操作)、Object.getPrototypeOf()(讀操作)、Object.create()(生成操作)代替。
Object.setPrototypeOf(): 用來設定一個物件的prototype物件。如果第一個引數不是物件,會自動轉為物件。但是由於返回的還是第一個引數,所以這個操作不會產生任何效果。 由於undefined和null無法轉為物件,所以如果第一個引數是undefined或null,就會報錯。
Object.setPrototypeOf(1, {}) === 1 // true
Object.setPrototypeOf('foo', {}) === 'foo' // true
Object.setPrototypeOf(true, {}) === true // true
Object.setPrototypeOf(undefined, {})
// TypeError: Object.setPrototypeOf called on null or undefined
Object.setPrototypeOf(null, {})
// TypeError: Object.setPrototypeOf called on null or undefined
複製程式碼
Object.getPrototypeOf() 用於讀取一個物件的原型物件。如果引數不是物件,會被自動轉為物件。如果引數是undefined或null,它們無法轉為物件,所以會報錯。
Object.getPrototypeOf(1) === Number.prototype // true
Object.getPrototypeOf('foo') === String.prototype // true
Object.getPrototypeOf(true) === Boolean.prototype // true
Object.getPrototypeOf(null)
// TypeError: Cannot convert undefined or null to object
Object.getPrototypeOf(undefined)
// TypeError: Cannot convert undefined or null to object
複製程式碼
Object.create() 從指定原型物件建立一個新的物件.
function Person(name,age){
this.name=name;
this.age=age;
}
Person.prototype.sayName=function(){
console.log(this.name)
}
function Teacher(subject,name,age){
this.subject=subject;
return Person.call(this,name,age);//繼承Person例項屬性
}
//繼承原型屬性,指向同一引用地址
Teacher.prototype=Object.create(Person.prototype);
var person1=new Person();
var person2=Object.create(person1);
person2.__proto__===person1;//true;
複製程式碼
9、5 Object.keys(),Object.values(),Object.entries()
Object.keys(): 返回一個陣列,成員是引數物件自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵名。
Object.values(): 返回一個陣列,成員是引數物件自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵值。屬性名為數值的屬性,是按照數值大小,從小到大遍歷的,因此返回的順序是b、c、a。Object.values會過濾屬性名為 Symbol 值的屬性。
const obj = { 100: 'a', 2: 'b', 7: 'c' };
Object.values(obj)
// ["b", "c", "a"]
Object.values({ [Symbol()]: 123, foo: 'abc' });
// ['abc'] 會過濾屬性名為 Symbol 值的屬性。
如果引數不是物件,Object.values會先將其轉為物件。如果Object.values方法的引數是一個字串,會返回各個字元組成的一個陣列。
Object.values('foo') // ['f', 'o', 'o']
Object.values(42) // []
Object.values(true) // []
複製程式碼
Object.entries(): 返回一個陣列,成員是引數物件自身的(不含繼承的)所有可遍歷(enumerable)屬性的鍵值對陣列。除了返回值不一樣,該方法的行為與Object.values基本一致。
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
複製程式碼
主要用途: 1、 遍歷物件的屬性。 2、 將物件轉為真正的Map結構。
const obj = { foo: 'bar', baz: 42 };
const map = new Map(Object.entries(obj));
map // Map { foo: "bar", baz: 42 }
複製程式碼
Object.fromEntries(): 是Object.entries()的逆操作,用於將一個鍵值對陣列轉為物件。
9、6 Obj.hasOwnProperty(obj.prop)
返回一個布林值,指示物件自身屬性中是否具有指定的屬性。和 in 運算子不同,該方法會忽略掉那些從原型鏈上繼承到的屬性。 o = new Object();
o.prop = 'exists';
o.hasOwnProperty('prop'); // 返回 true
o.hasOwnProperty('toString'); // 返回 false
o.hasOwnProperty('hasOwnProperty'); // 返回 false
複製程式碼
10、Symbol
新的原始資料型別Symbol,表示獨一無二的值,是javascript的第七種資料型別。前六種是:undefined、null、布林值(Boolean)、字串(String)、數值(Number)、物件(Object)。
注意事項:
1、Symbol函式前不能使用new命令,否則會報錯。由於 Symbol 值不是物件,所以不能新增屬性。基本上,它是一種類似於字串的資料型別。 Symbol函式可以接受一個字串作為引數,表示對 Symbol 例項的描述。
let s1 = Symbol('foo');
let s2 = Symbol('bar');
console.log(s1) // Symbol(foo)
console.log(s1.toString()) // Symbol(foo)
複製程式碼
2、如果 Symbol 的引數是一個物件,就會呼叫該物件的toString方法,將其轉為字串,然後才生成一個 Symbol 值。
const obj = {
toString() {
return 'abc';
}
};
const sym = Symbol(obj);
console.log(sym) // Symbol(abc)
複製程式碼
3、Symbol函式的引數只是表示對當前 Symbol 值的描述,因此相同引數的Symbol函式的返回值是不相等的。
// 沒有引數的情況
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false
// 有引數的情況
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false
複製程式碼
4、Symbol 值不能與其他型別的值進行運算,會報錯。
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
複製程式碼
5、Symbol 值可以顯式轉為字串。也可以轉為布林值,但是不能轉為數值。
let sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
let sym = Symbol();
Boolean(sym) // true
!sym // false
if (sym) {
}
Number(sym) // TypeError
Console.log(sym + 2) // TypeError
複製程式碼
10.1、 作為屬性名
Symbol 值作為物件屬性名時,不能用點運算子。一般使用方括號。 const mySymbol = Symbol();
const a = {};
a.mySymbol = 'Hello!';//此處的mySymbol只能看成是字串,而不能看成是一個Symbol值
console.log(a[mySymbol]) // undefined
consoe.log(a['mySymbol']) // "Hello!"
複製程式碼
在物件的內部,使用 Symbol 值定義屬性時,Symbol 值必須放在方括號之中。
let s = Symbol();
let obj = {
[s]: function (arg) { ... }
};
obj[s](123);
複製程式碼
Symbol 型別還可以用於定義一組常量,保證這組常量的值都是不相等的。 Symbol 值作為屬性名時,該屬性還是公開屬性,不是私有屬性。 魔術字串:在程式碼之中多次出現、與程式碼形成強耦合的某一個具體的字串或者數值。利用Symbol來消除。
const shapeType = {
triangle: Symbol('Triangle')
};
function getArea(shape, options) {
let area = 0;
switch (shape) {
case shapeType.triangle:
area = 5 * options.width * options.height;
break;
}
return area;
}
getArea(shapeType.triangle, { width: 100, height: 100 });
複製程式碼
10、2 屬性名的遍歷
Symbol 作為屬性名,該屬性不會出現在for...in、for...of迴圈中,也不會被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是,它也不是私有屬性,有一個Object.getOwnPropertySymbols方法,可以獲取指定物件的所有 Symbol 屬性名。 可以利用這個特性,為物件定義一些非私有的、但又希望只用於內部的方法。10、3 Symbol.for()、Symbol.keyFor()
Symbol.for(“foo”): 接受一個字串作為引數,然後搜尋有沒有以該引數作為名稱的 Symbol 值。如果有,就返回這個 Symbol 值,否則就新建並返回一個以該字串為名稱的 Symbol 值。
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
console.log(s1 === s2) // true
複製程式碼
Symbol.keyFor: 返回一個已登記的 Symbol 型別值的key。返回一個使用Symbol.for()方法定義的key。
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
複製程式碼
注意:Symbol.for為 Symbol 值登記的名字,是全域性環境的,可以在不同的 iframe 或 service worker 中取到同一個值。
10、4 內建的Symbol值
Symbol.hasInstance: 物件的Symbol.hasInstance屬性,指向一個內部方法。當其他物件使用instanceof運算子,判斷是否為該物件的例項時,會呼叫這個方法。比如,foo instanceof Foo在語言內部,實際呼叫的是Foo[Symbol.hasInstance](foo)。
class MyClass {
[Symbol.hasInstance](foo) {
return foo instanceof Array;
}
}
Console.log([1, 2, 3] instanceof new MyClass()) // trueSymbol.isConcatSpreadable
複製程式碼
Symbol.isConcatSpreadable: 等於一個布林值,表示該物件用於Array.prototype.concat()時,是否可以展開。
let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr1[Symbol.isConcatSpreadable] // undefined
let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']
複製程式碼
類似陣列的物件正好相反,預設不展開。它的Symbol.isConcatSpreadable屬性設為true,才可以展開。
let obj = {length: 2, 0: 'c', 1: 'd'};
['a', 'b'].concat(obj, 'e') // ['a', 'b', obj, 'e']
obj[Symbol.isConcatSpreadable] = true;
['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']
複製程式碼
Symbol.species: 指向一個建構函式。建立衍生物件時,會使用該屬性。下面例子中b、c是a的衍生物件。
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
}
const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
b instanceof MyArray // false
b instanceof Array //true
c instanceof MyArray // false
複製程式碼
主要的用途: 有些類庫是在基類的基礎上修改的,那麼子類使用繼承的方法時,作者可能希望返回基類的例項,而不是子類的例項。
還有其他屬性就不一一例舉了:Symbol.match、Symbol.replace、Symbol.search、Symbol.split、Symbol.iterator、Symbol.toPrimitive、Symbol.toStringTag(toString())、Symbol.unscopables(with)
11、Set
11.1、 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
複製程式碼
Set函式可以接受一個陣列(或者具有 iterable 介面的其他資料結構)作為引數,用來初始化。
// 例一
const set = new Set([1, 2, 3, 4, 4]);
console.log([...set])
// [1, 2, 3, 4]
let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)];
// [3, 5, 2]
// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
// 例三
const set = new Set(document.querySelectorAll('div'));
set.size // 56
複製程式碼
將set轉為陣列的方式
[...new Set('ababbc')]
複製程式碼
可以用於去重
[...new Set('ababbc')].join('') //abc
複製程式碼
向 Set 加入值的時候,不會發生型別轉換,所以5和"5"是兩個不同的值。在 Set 內部,兩個NaN是相等。
11.2、 Set例項的屬性和方法
例項屬性:
1、Set.prototype.constructor:建構函式,預設就是Set函式。
2、Set.prototype.size:返回Set例項的成員總數。
操作方法:
1、add(value):新增某個值,返回 Set 結構本身。
2、delete(value):刪除某個值,返回一個布林值,表示刪除是否成功。
3、has(value):返回一個布林值,表示該值是否為Set的成員。
4、clear():清除所有成員,沒有返回值。
Array.from方法可以將 Set 結構轉為陣列。
const items = new Set([1, 2, 3, 4, 5]);
//方法1
const array = Array.from(items);
//方法2
const arr=[…items]
複製程式碼
遍歷方法:
1、keys():返回鍵名的遍歷器。
2、values():返回鍵值的遍歷器。
3、entries():返回鍵值對的遍歷器。
4、forEach():使用回撥函式遍歷每個成員。
由於 Set 結構沒有鍵名,只有鍵值(或者說鍵名和鍵值是同一個值),所以keys方法和values方法的行為完全一致。 Set 結構的例項預設可遍歷,它的預設遍歷器生成函式就是它的values方法。
Set.prototype[Symbol.iterator] === Set.prototype.values
// true
複製程式碼
這意味著,可以省略values方法,直接用for...of迴圈遍歷 Set。
let set = new Set(['red', 'green', 'blue']);
for (let x of set) {
console.log(x);
}
// red // green // blue
複製程式碼
陣列的map和filter方法也可以間接用於 Set 了。
let set = new Set([1, 2, 3]);
set = new Set([...set].map(x => x * 2));
// 返回Set結構:{2, 4, 6}
let set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter(x => (x % 2) == 0));
// 返回Set結構:{2, 4}
複製程式碼
11.3、 WeakSet
WeakSet 結構與 Set 類似,但是,它與 Set 有兩個區別。
首先:WeakSet 的成員只能是物件,而不能是其他型別的值。
const ws = new WeakSet();
ws.add(1)
// TypeError: Invalid value used in weak set
ws.add(Symbol())
// TypeError: invalid value used in weak set
複製程式碼
其次:WeakSet 中的物件都是弱引用,垃圾回收機制不考慮 WeakSet 對該物件的引用,也就是說,如果其他物件都不再引用該物件,那麼垃圾回收機制會自動回收該物件所佔用的記憶體,不考慮該物件還存在於 WeakSet 之中。 因此,WeakSet 適合臨時存放一組物件。由於上面這個特點,WeakSet 的成員是不適合引用的,因為它會隨時消失。WeakSet 不可遍歷。
作為建構函式,WeakSet 可以接受一個陣列或類似陣列的物件作為引數。注意,是a陣列的成員成為 WeakSet 的成員,而不是a陣列本身。這意味著,陣列的成員只能是物件。
const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}
const b = [3, 4];
const ws = new WeakSet(b);
// Uncaught TypeError: Invalid value used in weak set(…)
複製程式碼
有以下幾個例項方法:add()、delete()、has()。
12、Map
12.1、Map
JavaScript 的物件(Object),本質上是鍵值對的集合(Hash 結構),但是傳統上只能用字串當作鍵。
Map: 一種資料結構,類似於物件,也是鍵值對的集合,但是“鍵”的範圍不限於字串,各種型別的值(包括物件)都可以當作鍵。
作為建構函式,Map 也可以接受一個陣列作為引數。該陣列的成員是一個表示鍵值對的陣列。任何具有 Iterator 介面、且每個成員都是一個雙元素的陣列的資料結構都可以當作Map建構函式的引數。這就是說,Set和Map都可以用來生成新的 Map。
const map = new Map([
['name', '張三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "張三"
map.has('title') // true
map.get('title') // "Author"
複製程式碼
Map建構函式接受陣列作為引數,實際上執行的是下面的演算法。
const items = [
['name', '張三'],
['title', 'Author']
];
const map = new Map();
items.forEach(
([key, value]) => map.set(key, value)
);
複製程式碼
如果對同一個鍵多次賦值,後面的值將覆蓋前面的值。只有對同一個物件的引用,Map 結構才將其視為同一個鍵
const map = new Map();
map.set(1, 'aaa').set(1, 'bbb');
map.get(1) // "bbb"
const map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined //這兩個[“a”]的記憶體地址不一樣
複製程式碼
注意: 雖然NaN不嚴格相等於自身,但 Map 將其視為同一個鍵。
12.2、例項的屬性和操作方法:
size、set()、get()、has()、delete()、clear()
12.3、遍歷方法
1、 keys():返回鍵名的遍歷器。
2、values():返回鍵值的遍歷器。
3、entries():返回所有成員的遍歷器。
4、forEach():遍歷 Map 的所有成員。
Map 結構轉為陣列結構,比較快速的方法是使用擴充套件運算子(...)。
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...map.keys()] // [1, 2, 3]
[...map.values()] // ['one', 'two', 'three']
[...map.entries()] // [[1,'one'], [2, 'two'], [3, 'three']]
[...map] // [[1,'one'], [2, 'two'], [3, 'three']]
複製程式碼
結合陣列的map方法、filter方法,可以實現 Map 的遍歷和過濾(Map 本身沒有map和filter方法)。
const map0 = new Map().set(1, 'a').set(2, 'b').set(3, 'c')
const map1 = new Map(
[...map0].filter(([k, v]) => k < 3)
);
// 產生 Map 結構 {1 => 'a', 2 => 'b'}
const map2 = new Map(
[...map0].map(([k, v]) => [k * 2, '_' + v])
);
// 產生 Map 結構 {2 => '_a', 4 => '_b', 6 => '_c'}
複製程式碼
12.4、與其他資料結構的互相轉換:
1、 Map與陣列的轉換: const myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
[...myMap] //Map轉陣列
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
複製程式碼
陣列轉Map
new Map([
[true, 7],
[{foo: 3}, ['abc']]
])
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }
複製程式碼
2、 Map與物件的轉換
Map轉物件
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;
}
const myMap = new Map() .set('yes', true).set('no', false);
strMapToObj(myMap) // { yes: true, no: false }
複製程式碼
物件轉Map
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
objToStrMap({yes: true, no: false}) // Map {"yes" => true, "no" => false}
複製程式碼
3、 Map與Json的轉換 Map 的鍵名都是字串,這時可以選擇轉為物件 JSON。
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'
複製程式碼
Map 的鍵名有非字串,這時可以選擇轉為陣列 JSON。
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
複製程式碼
Json轉Map,所有鍵名都是字串。
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
複製程式碼
Json轉Map,整個 JSON 就是一個陣列,且每個陣列成員本身,又是一個有兩個成員的陣列。
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
複製程式碼
12.5、WeakMap:
WeakMap與Map的區別有兩點。首先,WeakMap只接受物件作為鍵名(null除外),不接受其他型別的值作為鍵名。
其次,WeakMap的鍵名所指向的物件,不計入垃圾回收機制。 WeakMap的專用場合就是,它的鍵所對應的物件,可能會在將來消失。WeakMap結構有助於防止記憶體洩漏。
注意 ,WeakMap 弱引用的只是鍵名,而不是鍵值。鍵值依然是正常引用。
const wm = new WeakMap();
let key = {};
let obj = {foo: 1};
wm.set(key, obj);
obj = null;//解除引用,相當於切斷聯絡,但是{foo:1}所佔記憶體空間依然存在。
console.log(wm.get(key))
// Object {foo: 1}
複製程式碼
WeakMap 與 Map 在 API 上的區別主要是兩個,一是沒有遍歷操作(即沒有keys()、values()和entries()方法),也沒有size屬性。WeakMap只有四個方法可用:get()、set()、has()、delete()。
weakMap的用途:
1、 就是 DOM 節點作為鍵名
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
myWeakmap.set(myElement, {timesClicked: 0});
myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
}, false);
複製程式碼
2、 部署私有屬性
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
if (counter < 1) {
return;
}
_counter.set(this, --counter);
if (counter === 0) {
_action.get(this)();
}
}
}
const c = new Countdown(2, () => console.log('DONE'));
c.dec()
c.dec()
// DONE
複製程式碼
13、Proxy
Proxy 用於修改某些操作的預設行為。 Proxy 可以理解成,在目標物件之前架設一層“攔截”,外界對該物件的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。
var proxy = new Proxy(target, handler);
複製程式碼
target參數列示所要攔截的目標物件,handler引數也是一個物件,用來定製攔截行為。
注意:要使得Proxy起作用,必須針對Proxy例項進行操作,而不是針對目標物件進行操作。如果handler沒有設定任何攔截,那就等同於直接通向原物件。
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
複製程式碼
Proxy 例項也可以作為其他物件的原型物件。
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
複製程式碼
13.1、 Proxy 支援的攔截操作一覽,一共 13 種。
1、 get(target, propKey,receiver):攔截物件屬性的讀取,比如proxy.foo和proxy['foo']。2、 set(target, propKey, value, receiver):攔截物件屬性的設定,比如proxy.foo = v或proxy['foo'] = v,返回一個布林值。
3、has(target, propKey):攔截propKey in proxy的操作,返回一個布林值。但是has攔截對for...in迴圈不生效。
4、deleteProperty(target, propKey):攔截delete proxy[propKey]的操作,返回一個布林值。
5、ownKeys(target):攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in迴圈,返回一個陣列。該方法返回目標物件所有自身的屬性的屬性名,而Object.keys()的返回結果僅包括目標物件自身的可遍歷屬性。
6、getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述物件。
7、defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個布林值。
8、preventExtensions(target):攔截Object.preventExtensions(proxy),返回一個布林值。
9、getPrototypeOf(target):攔截Object.getPrototypeOf(proxy),返回一個物件。
10、isExtensible(target):攔截Object.isExtensible(proxy),返回一個布林值。否則返回值會被自動轉為布林值。
11、setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),返回一個布林值。如果目標物件是函式,那麼還有兩種額外操作可以攔截。
12、apply(target, object, args):攔截 Proxy 例項作為函式呼叫的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
13、construct(target, args):攔截 Proxy 例項作為建構函式呼叫的操作,比如new proxy(...args)。回的必須是一個物件,否則會報錯。
13.2、 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
複製程式碼
13.3、 this問題
即不做任何攔截的情況下,也無法保證與目標物件的行為一致。主要原因就是在 Proxy 代理的情況下,目標物件內部的this關鍵字會指向 Proxy 代理。 const _name = new WeakMap();
class Person {
constructor(name) {
_name.set(this, name);
}
get name() {
return _name.get(this);
}
}
const jane = new Person('Jane');
console.log(jane.name) // 'Jane'
const proxy = new Proxy(jane, {});
console.log(proxy.name) // undefined
複製程式碼
如果handler沒有設定任何攔截,那就等同於直接通向原物件。因為通過呼叫建構函式時this指的是Person,而proxy.name獲取name時的this指的是proxy,所以此時取不到值。
例項:通過攔截使得this繫結原始物件。
const target = new Date('2015-01-01');
const handler = {
get(target, prop) {
if (prop === 'getDate') {
return target.getDate.bind(target);
}
return Reflect.get(target, prop);
}
};
const proxy = new Proxy(target, handler);
proxy.getDate() // 1
複製程式碼
14、Reflect
與Proxy物件一樣,也是 ES6 為了操作物件而提供的新 API。Reflect物件的設計目的有這樣幾個。1. 將Object物件的一些明顯屬於語言內部的方法(比如Object.defineProperty),放到Reflect物件上。
2. 修改某些Object方法的返回結果,讓其變得更合理。
3. 讓Object操作都變成函式行為。
// 老寫法
'assign' in Object // true
// 新寫法
Reflect.has(Object, 'assign') // true
複製程式碼
4. Reflect物件的方法與Proxy物件的方法一一對應,只要是Proxy物件的方法,就能在Reflect物件上找到對應的方法。
5. Reflect物件的靜態方法有13個,跟Proxy的靜態方法時一一對應的。
1)Reflect.get(target, name, receiver):查詢並返回target物件的name屬性,如果沒有該屬性,則返回undefined。
var myObject = {
foo: 1,
bar: 2,
get baz() {
return this.foo + this.bar;
},
}
console.log(Reflect.get(myObject, 'foo') )// 1
console.log(Reflect.get(myObject, 'baz') )// 3
var myReceiverObject = {
foo: 4,
bar: 4,
};
console.log((Reflect.get(myObject, 'baz', myReceiverObject)) // 8
複製程式碼
如果name屬性部署了讀取函式(getter),則讀取函式的this繫結receiver。
2)Reflect.set(target, name, value, receiver): Reflect.set方法設定target物件的name屬性等於value。如果name屬性設定了賦值函式,則賦值函式的this繫結receiver。如果第一個引數不是物件,Reflect.set會報錯。
var myObject = {
foo: 1,
set bar(value) {
return this.foo = value;
},
}
myObject.foo // 1
Reflect.set(myObject, 'foo', 2);
myObject.foo // 2
Reflect.set(myObject, 'bar', 3)
myObject.foo // 3
複製程式碼
3)Reflect.has(obj,name):對應name in obj裡面的in運算子。如果第一個引數不是物件,Reflect.has和in運算子都會報錯。
var myObject = {
foo: 1,
};
// 舊寫法
'foo' in myObject // true
// 新寫法
Reflect.has(myObject, 'foo') // true
複製程式碼
….
例項:利用proxy實現觀察者模式
const queuedObservers = new Set();//觀察者佇列
const observe = fn => queuedObservers.add(fn);//新增觀察者
const observable = obj => new Proxy(obj, {set});//新增代理
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result;
}
//觀察目標
const person = observable({
name: '張三',
age: 20
});
//觀察者
function print() {
console.log(`${person.name}, ${person.age}`)
}
observe(print);
person.name = '李四';
複製程式碼
15、Promise
是非同步程式設計的一種解決方案,比傳統的解決方案——回撥函式和事件——更合理和更強大。所謂Promise,簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。Promise物件有以下兩個特點。
1) 物件的狀態不受外界影響。 Promise物件代表一個非同步操作,有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。
2) 一旦狀態改變,就不會再變, 任何時候都可以得到這個結果。Promise物件的狀態改變,只有兩種可能:從pending變為fulfilled和從pending變為rejected。只要這兩種情況發生,狀態就凝固了,不會再變了,會一直保持這個結果,這時就稱為 resolved(已定型)。如果改變已經發生了,你再對Promise物件新增回撥函式,也會立即得到這個結果。
優點: 將非同步操作以同步操作的流程表達出來,避免了層層巢狀的回撥函式。此外,Promise物件提供統一的介面,使得控制非同步操作更加容易。
不足:
1、無法取消Promise,一旦新建它就會立即執行,無法中途取消。
2、如果不設定回撥函式,Promise內部丟擲的錯誤,不會反應到外部。
3、當處於pending狀態時,無法得知目前進展到哪一個階段(剛剛開始還是即將完成)。
15.1、 基本用法
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 非同步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
複製程式碼
resolve函式的作用是,將Promise物件的狀態從“未完成”變為“成功”(即從 pending 變為 resolved),在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去;
reject函式的作用是,將Promise物件的狀態從“未完成”變為“失敗”(即從 pending 變為 rejected),在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。
Promise例項生成以後,可以用then方法分別指定resolved狀態和rejected狀態的回撥函式。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
複製程式碼
例項:
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms, 'done');
});
}
timeout(100).then((value) => {
console.log(value); //done
});
複製程式碼
Promise新建後就會立即執行:then方法指定的回撥函式,將在當前指令碼所有同步任務執行完才會執行。
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
順序:// Promise // Hi! // resolved
複製程式碼
15.2、 Promise.prototype.then()
它的作用是為 Promise 例項新增狀態改變時的回撥函式。 前面說過,then方法的第一個引數是resolved狀態的回撥函式,第二個引數(可選)是rejected狀態的回撥函式。
then方法返回的是一個新的Promise例項(注意,不是原來那個Promise例項)。因此可以採用鏈式寫法,即then方法後面再呼叫另一個then方法。
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {//引數post是上一個then返回的資料-json.post
// ...
});
複製程式碼
如果then()返回的是一個Promise,那麼接下來的程式碼可以這麼寫:
getJSON("/post/1.json").then(
post => getJSON(post.commentURL)
).then(
comments => console.log("resolved: ", comments),//成功
err => console.log("rejected: ", err)//失敗
);
複製程式碼
15.3、 Promise.prototype.catch
方法是.then(null, rejection)或.then(undefined, rejection)的別名,用於指定發生錯誤時的回撥函式。用於捕獲Promise物件和then()方法指定的回撥函式中的錯誤。
Promise 物件的錯誤具有“冒泡”性質,會一直向後傳遞,直到被捕獲為止。也就是說,錯誤總是會被下一個catch語句捕獲。 如果 Promise 狀態已經變成resolved,再丟擲錯誤是無效的。
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
複製程式碼
Promise 在resolve語句後面,再丟擲錯誤,不會被捕獲,等於沒有丟擲。因為 Promise 的狀態一旦改變,就永久保持該狀態,不會再變了。
15.4、 Promise.prototype.finally()
用於指定不管 Promise 物件最後狀態如何,都會執行的操作。不管前面的 Promise 是fulfilled還是rejected,都會執行回撥函式callback。
Promise.resolve(2).then(() => {}, () => {})
// resolve 的值是 undefined
Promise.resolve(2).finally(() => {})
// resolve 的值是 2
Promise.reject(3).then(() => {}, () => {})
// reject 的值是 undefined
Promise.reject(3).finally(() => {})
// reject 的值是 3
複製程式碼
15.5、 Promise.all()
const p = Promise.all([p1, p2, p3]);
複製程式碼
引數可以不是陣列,但必須具有 Iterator 介面,且返回的每個成員都是 Promise 例項。
(1)只有p1、p2、p3的狀態都變成fulfilled,p的狀態才會變成fulfilled,此時p1、p2、p3的返回值組成一個陣列,傳遞給p的回撥函式。
(2)只要p1、p2、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的例項的返回值,會傳遞給p的回撥函式。
15.6、 Promise.race()
const p = Promise.race([p1, p2, p3]);
複製程式碼
只要p1、p2、p3之中有一個例項率先改變狀態,p的狀態就跟著改變。那個率先改變的 Promise 例項的返回值,就傳遞給p的回撥函式。
15.7、 Promise.resolve()
將現有物件轉為 Promise 物件。
Promise.resolve('foo')
// 等價於
new Promise(resolve => resolve('foo'))
複製程式碼
Promise.resolve方法的引數分成四種情況。
(1)引數是一個 Promise 例項。那麼Promise.resolve將不做任何修改、原封不動地返回這個例項。
(2)引數是一個thenable物件。thenable物件指的是具有then方法的物件,Promise.resolve方法會將這個物件轉為 Promise 物件,然後就立即執行thenable物件的then方法。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
複製程式碼
(3)引數不是具有then方法的物件,或根本就不是物件,則Promise.resolve方法返回一個新的 Promise 物件,狀態為resolved。
const p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello
複製程式碼
(4)不帶有任何引數。直接返回一個resolved狀態的 Promise 物件。注意: 立即resolve的 Promise 物件,是在本輪“事件迴圈”(event loop)的結束時,而不是在下一輪“事件迴圈”的開始時。setTimeout(fn, 0)在下一輪“事件迴圈”開始時執行,
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one
// two
// three
複製程式碼
setTimeout(fn, 0)在下一輪“事件迴圈”開始時執行,Promise.resolve()在本輪“事件迴圈”結束時執行,console.log('one')則是立即執行,因此最先輸出。
15.8、 Promise.reject()
返回一個新的 Promise 例項,該例項的狀態為rejected。 const p = Promise.reject('出錯了');
// 等同於
const p = new Promise((resolve, reject) => reject('出錯了'))
p.then(null, function (s) {
console.log(s)
});
// 出錯了
複製程式碼
注意: Promise.reject()方法的引數,會原封不動地作為reject的理由,變成後續方法的引數。這一點與Promise.resolve方法不一致。
const thenable = {
then(resolve, reject) {
reject('出錯了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable) // true
})
複製程式碼
15.9、 Promise.try()
Promise.try為所有操作提供了統一的處理機制。事實上,Promise.try就是模擬try程式碼塊,就像promise.catch模擬的是catch程式碼塊。 Promise.try(() => database.users.get({id: userId})).then(...).catch(...)
複製程式碼
15.10 例項:載入圖片
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve(src,path);
image.onerror = reject(src);
image.src = path;
});
};
preloadImage(“http://www.tj.com/123.jpg”).then(function(img,path){
//此時的引數path為undefined,因為此時的path是preloadImage(path)中的形參,是區域性變數,在呼叫結束後他的記憶體就被釋放了。
},function(img){})
複製程式碼
16、Iterator和for…of迴圈
Iterator是一種介面,為各種不同的資料結構提供統一的訪問機制。任何資料結構只要部署 Iterator介面,就可以完成遍歷操作(即依次處理該資料結構的所有成員)。 Iterator 的作用有三個:
- 為各種資料結構,提供一個統一的、簡便的訪問介面;
- 使得資料結構的成員能夠按某種次序排列;
- ES6 創造了一種新的遍歷命令for...of迴圈,Iterator 介面主要供for...of消費。
ES6 規定,預設的 Iterator介面部署在資料結構的Symbol.iterator屬性上,或者說,一個資料結構只要具有Symbol.iterator屬性,就可以認為是“可遍歷”的。
const obj = {
[Symbol.iterator] : function () {
return {
next: function () {
return {
value: 1,
done: true
};
}
};
}
};
複製程式碼
原生具備 Iterator 介面的資料結構如下。
1、Array
2、Map
3、Set
4、String
5、TypedArray
6、函式的 arguments 物件
7、NodeList 物件
下面是兩個通過呼叫Symbol.iterator方法來Iterator介面的例子。
//陣列的遍歷 部署 Iterator 介面 let iter = arr[Symbol.iterator]();
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();
console.log(iter.next()) // { value: 'a', done: false }
console.log (iter.next()) // { value: 'b', done: false }
console.log (iter.next()) // { value: 'c', done: false }
console.log (iter.next()) // { value: undefined, done: true }
//字串的遍歷 部署 Iterator 介面 someString[Symbol.iterator]();
var someString = "hi";
typeof someString[Symbol.iterator]
// "function"
var iterator = someString[Symbol.iterator]();
iterator.next() // { value: "h", done: false }
iterator.next() // { value: "i", done: false }
iterator.next()
複製程式碼
對於類似陣列的物件(存在數值鍵名和length屬性),部署 Iterator 介面,有一個簡便方法,就是Symbol.iterator方法直接引用陣列的 Iterator 介面。
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
[...document.querySelectorAll('div')] // 可以執行了
複製程式碼
16.1、 呼叫iterator介面的場合
(1) 解構賦值
var set=[1,2,3]
let [first, ...rest] = set;
複製程式碼
(2) 擴充套件運算子
var str = 'hello';
console.log([...str]) // ['h','e','l','l','o']
複製程式碼
(3) yield*
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
var iterator = generator();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }
複製程式碼
(4) 其他一些場合(for…of、Array.form())
JavaScript 原有的for...in迴圈,只能獲得物件的鍵名,不能直接獲取鍵值。ES6 提供for...of迴圈,允許遍歷獲得鍵值。陣列的遍歷器介面只返回具有數字索引的屬性。
var arr = ['a', 'b', 'c', 'd'];
arr.foo = 'hello';
//獲取的是鍵名
for (let a in arr) {
console.log(a); // 0 1 2 3
}
//獲取的是鍵值
for (let a of arr) {
console.log(a); // a b c d
}
複製程式碼
Set 結構遍歷時,返回的是一個值,而 Map 結構遍歷時,返回的是一個陣列,該陣列的兩個成員分別為當前 Map 成員的鍵名和鍵值。
let map = new Map().set('a', 1).set('b', 2);
for (let pair of map) {
console.log(pair); // ['a', 1] // ['b', 2]
}
for (let [key, value] of map) {
console.log(key + ' : ' + value); // a : 1 // b : 2
}
複製程式碼
並不是所有類似陣列的物件都具有 Iterator 介面,一個簡便的解決方法,就是使用Array.from方法將其轉為陣列。
let arrayLike = { length: 2, 0: 'a', 1: 'b' };
// 報錯
for (let x of arrayLike) {
console.log(x);
}
// 正確
for (let x of Array.from(arrayLike)) {
console.log(x);
}
複製程式碼
for...in迴圈有幾個缺點。
1、陣列的鍵名是數字,但是for...in迴圈是以字串作為鍵名“0”、“1”、“2”等等。
2、for...in迴圈不僅遍歷數字鍵名,還會遍歷手動新增的其他鍵,甚至包括原型鏈上的鍵。
3、某些情況下,for...in迴圈會以任意順序遍歷鍵名。
總之,for...in迴圈主要是為遍歷物件而設計的,不適用於遍歷陣列。
for...of迴圈相比上面幾種做法,有一些顯著的優點:
1、有著同for...in一樣的簡潔語法,但是沒有for...in那些缺點。
2、不同於forEach方法,它可以與break、continue和return配合使用。
3、提供了遍歷所有資料結構的統一操作介面。