ES6常用知識點總結(上)

慕斯不想說話發表於2019-04-07

  這片文章主要是基於阮一峰老師ECMAScript 6 入門。在看了阮一峰老師的這ES6入門之後,自己做了一下總結,將一些覺得對自己目前有用的東西整理出來方便日後再來鞏固複習。總覺得看別人的東西當時懂了過了一段時間就忘記了,所以我總是會將別人的東西驗證一遍,這樣對知識的理解是能提升一個層次的。

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 的作用有三個:
  1. 為各種資料結構,提供一個統一的、簡便的訪問介面;
  2. 使得資料結構的成員能夠按某種次序排列;
  3. 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、提供了遍歷所有資料結構的統一操作介面。

相關文章