【JavaScript】《JavaScript高級程序設計 (第4版) 》筆記-Chapter3-語言基礎

三、語言基礎
  1. ECMAScript 的語法很大程度上借鑒了 C 語言和其他類 C 語言,如 Java 和 Perl。ECMAScript 中一切都區分大小寫。無論是變量、函數名還是操作符,都區分大小寫。

  2. 所謂標識符,就是變量、函數、屬性或函數參數的名稱。標識符可以由一或多個下列字符組成:

    • 第一個字符必須是一個字母、下劃線(_)或美元符號($);
    • 剩下的其他字符可以是字母、下劃線、美元符號或數字。
  3. 標識符中的字母可以是擴展 ASCII(Extended ASCII)中的字母,也可以是 Unicode 的字母字符,如 à 和 ?(但不推薦使用)。

  4. 按照慣例,ECMAScript 標識符使用駝峰大小寫形式,即第一個單詞的首字母小寫,后面每個單詞的首字母大寫。

  5. ECMAScript 采用 C 語言風格的注釋,包括單行注釋和塊注釋。

    • 單行注釋以兩個斜杠字符開頭,如:
      // 單行注釋
      
    • 塊注釋以一個斜杠和一個星號(/*)開頭,以它們的反向組合(*/)結尾,如:
      /* 這是多行
      注釋 */
      
  6. ECMAScript 5 增加了嚴格模式(strict mode)的概念。嚴格模式是一種不同的 JavaScript 解析和執行模型,ECMAScript 3 的一些不規范寫法在這種模式下會被處理,對于不安全的活動將拋出錯誤。

    • 要對整個腳本啟用嚴格模式,在腳本開頭加上這一行:
      "use strict";
      
    • 雖然看起來像個沒有賦值給任何變量的字符串,但它其實是一個預處理指令。任何支持的 JavaScript 引擎看到它都會切換到嚴格模式。選擇這種語法形式的目的是不破壞 ECMAScript 3 語法。
    • 也可以單獨指定一個函數在嚴格模式下執行,只要把這個預處理指令放到函數體開頭即可。
      function doSomething() { "use strict"; // 函數體 
      }
      
    • 嚴格模式會影響 JavaScript 執行的很多方面。
    • 所有現代瀏覽器都支持嚴格模式。
  7. ECMAScript 中的語句以分號結尾。省略分號意味著由解析器確定語句在哪里結尾。

    • 即使語句末尾的分號不是必需的,也應該加上。
    • 記著加分號有助于防止省略造成的問題,比如可以避免輸入內容不完整。
    • 加分號也便于開發者通過刪除空行來壓縮代碼(如果沒有結尾的分號,只刪除空行,則會導致語法錯誤)。
    • 加分號也有助于在某些情況下提升性能,因為解析器會嘗試在合適的位置補上分號以糾正語法錯誤。
  8. if 之類的控制語句只在執行多條語句時要求必須有代碼塊。不過,最佳實踐是始終在控制語句中使用代碼塊,即使要執行的只有一條語句。

    // 有效,但容易導致錯誤,應該避免
    if (test) console.log(test); // 推薦
    if (test) { console.log(test); 
    }
    
  9. ECMA-262 描述了一組保留的關鍵字,這些關鍵字有特殊用途。按照規定,保留的關鍵字不能用作標識符或屬性名。

    • ECMA-262 第 6 版規定的所有關鍵字如下:
      break do in typeof 
      case else instanceof var 
      catch export new void 
      class extends return while 
      const finally super with 
      continue for switch yield 
      debugger function this 
      default if throw 
      delete import try
      
    • 以下是 ECMA-262 第 6 版為將來保留的所有詞匯:
      始終保留: 
      enum 嚴格模式下保留: 
      implements package public 
      interface protected static 
      let private 模塊代碼中保留: 
      await
      
  10. ECMAScript 變量是松散類型的,意思是變量可以用于保存任何類型的數據。每個變量只不過是一個用于保存任意值的命名占位符。有 3 個關鍵字可以聲明變量:var、const 和 let。其中,var 在 ECMAScript 的所有版本中都可以使用,而 const 和 let 只能在 ECMAScript 6 及更晚的版本中使用。

  11. var 關鍵字

    • 要定義變量,可以使用 var 操作符(注意 var 是一個關鍵字),后跟變量名(即標識符):
      var message;var message = "hi";
      message = 100; // 合法,但不推薦
      
      • 第一行代碼定義了一個名為 message 的變量,可以用它保存任何類型的值。(不初始化的情況下,變量會保存一個特殊值 undefined)。
      • ECMAScript 實現變量初始化,因此可以同時定義變量并設置它的值。像這樣初始化變量不會將它標識為字符串類型,只是一個簡單的賦值而已。隨后,不僅可以改變保存的值,也可以改變值的類型。
    • 使用 var 操作符定義的變量會成為包含它的函數的局部變量。在函數內定義變量時省略 var 操作符,可以創建一個全局變量。
      function test() { var message = "hi"; // 局部變量
      } 
      test(); 
      console.log(message); // 出錯!function test() { message = "hi"; // 全局變量
      } 
      test(); 
      console.log(message); // "hi"
      
      • 去掉之前的 var 操作符之后,message 就變成了全局變量。只要調用一次函數 test(),就會定義這個變量,并且可以在函數外部訪問到。
      • 雖然可以通過省略 var 操作符定義全局變量,但不推薦這么做。在局部作用域中定義的全局變量很難維護,也會造成困惑。這是因為不能一下子斷定省略 var 是不是有意而為之。在嚴格模式下,如果像這樣給未聲明的變量賦值,則會導致拋出 ReferenceError。
    • 如果需要定義多個變量,可以在一條語句中用逗號分隔每個變量(及可選的初始化):
      var message = "hi", found = false, age = 29;
      
      • 因為 ECMAScript 是松散類型的,所以使用不同數據類型初始化的變量可以用一條語句來聲明。插入換行和空格縮進并不是必需的,但這樣有利于閱讀理解。
    • 在嚴格模式下,不能定義名為 eval 和 arguments 的變量,否則會導致語法錯誤。
    • 使用 var 時,下面的代碼不會報錯。這是因為使用這個關鍵字聲明的變量會自動提升到函數作用域頂部:
      function foo() { console.log(age); var age = 26; 
      } 
      foo(); // undefined// 之所以不會報錯,是因為 ECMAScript 運行時把它看成等價于如下代碼:
      function foo() { var age; console.log(age); age = 26; 
      } 
      foo(); // undefined
      
      • 這就是所謂的“提升”(hoist),也就是把所有變量聲明都拉到函數作用域的頂部。(讀者注:只是把聲明提前
    • 此外,反復多次使用 var 聲明同一個變量也沒有問題:
      function foo() { var age = 16; var age = 26; var age = 36; console.log(age); 
      } 
      foo(); // 36
      
      • 讀者注:(來自GPT3.5)當多次使用var聲明同一個變量時,JavaScript只會認為是同一個變量被聲明了多次,并不會創建多個不同的變量。這種行為在某些情況下可能會導致意外的結果,尤其是在使用循環或條件語句時。為了避免這種情況,推薦使用let或const關鍵字來聲明變量,因為它們具有塊級作用域,可以更好地控制變量的作用域范圍。
  12. let 聲明

    • let 跟 var 的作用差不多,但有著非常重要的區別。最明顯的區別是,let 聲明的范圍是塊作用域,而 var 聲明的范圍是函數作用域。
      if (true) { var name = 'Matt'; console.log(name); // Matt 
      } 
      console.log(name); // Mattif (true) { let age = 26; console.log(age); // 26 
      } 
      console.log(age); // ReferenceError: age 沒有定義
      
      • 讀者注:在JS中,函數作用域和塊作用域是分得很清的,var的作用域是函數作用域,var是能越過塊作用域的!
      • 塊作用域是函數作用域的子集,因此適用于 var 的作用域限制同樣也適用于 let。
    • let 也不允許同一個塊作用域中出現冗余聲明。這樣會導致報錯:
      var name; 
      var name; let age; 
      let age; // SyntaxError;標識符 age 已經聲明過了
      
    • 當然,JavaScript 引擎會記錄用于變量聲明的標識符及其所在的塊作用域,因此嵌套使用相同的標識符不會報錯,而這是因為同一個塊中沒有重復聲明。
      var name = 'Nicholas'; 
      console.log(name);      // 'Nicholas' 
      if (true) { var name = 'Matt'; console.log(name);  // 'Matt' 
      } let age = 30; 
      console.log(age);       // 30 
      if (true) { let age = 26; console.log(age);   // 26 
      }
      
    • 對聲明冗余報錯不會因混用 let 和 var 而受影響。這兩個關鍵字聲明的并不是不同類型的變量,它們只是指出變量在相關作用域如何存在。
      var name; 
      let name; // SyntaxError let age; 
      var age; // SyntaxError
      
    • let 與 var 的另一個重要的區別,就是 let 聲明的變量不會在作用域中被提升。
      // name 會被提升
      console.log(name); // undefined 
      var name = 'Matt'; // age 不會被提升
      console.log(age); // ReferenceError:age 沒有定義
      let age = 26;
      
      • 在解析代碼時,JavaScript 引擎也會注意出現在塊后面的 let 聲明,只不過在此之前不能以任何方式來引用未聲明的變量。
      • 在 let 聲明之前的執行瞬間被稱為“暫時性死區”(temporal dead zone),在此階段引用任何后面才聲明的變量都會拋出 ReferenceError。
    • 與 var 關鍵字不同,使用 let 在全局作用域中聲明的變量不會成為 window 對象的屬性(var 聲明的變量則會)
      var name = 'Matt'; 
      console.log(window.name); // 'Matt' let age = 26; 
      console.log(window.age); // undefined
      
      • 不過,let 聲明仍然是在全局作用域中發生的,相應變量會在頁面的生命周期內存續。因此,為了避免 SyntaxError,必須確保頁面不會重復聲明同一個變量。
    • 在使用 var 聲明變量時,由于聲明會被提升,JavaScript 引擎會自動將多余的聲明在作用域頂部合并為一個聲明。因為 let 的作用域是塊,所以不可能檢查前面是否已經使用 let 聲明過同名變量,同時也就不可能在沒有聲明的情況下聲明它。
      <script> var name = 'Nicholas'; let age = 26; 
      </script> 
      <script> // 假設腳本不確定頁面中是否已經聲明了同名變量// 那它可以假設還沒有聲明過var name = 'Matt'; // 這里沒問題,因為可以被作為一個提升聲明來處理// 不需要檢查之前是否聲明過同名變量let age = 36; // 如果 age 之前聲明過,這里會報錯
      </script>
      
      • 使用 try/catch 語句或 typeof 操作符也不能解決,因為條件塊中 let 聲明的作用域僅限于該塊。
        <script> let name = 'Nicholas'; let age = 36; 
        </script> <script> // 假設腳本不確定頁面中是否已經聲明了同名變量// 那它可以假設還沒有聲明過if (typeof name === 'undefined') { let name; } // name 被限制在 if {} 塊的作用域內// 因此這個賦值形同全局賦值name = 'Matt'; try { console.log(age); // 如果 age 沒有聲明過,則會報錯} catch(error) { let age;} // age 被限制在 catch {}塊的作用域內// 因此這個賦值形同全局賦值age = 26; 
        </script>
        
        • 為此,對于 let 這個新的 ES6 聲明關鍵字,不能依賴條件聲明模式。
        • 不能使用 let 進行條件式聲明是件好事,因為條件聲明是一種反模式,它讓程序變得更難理解。如果你發現自己在使用這個模式,那一定有更好的替代方式。
    • 在 let 出現之前,for 循環定義的迭代變量會滲透到循環體外部。改成使用 let 之后,這個問題就消失了,因為迭代變量的作用域僅限于 for 循環塊內部。
      for (var i = 0; i < 5; ++i) { // 循環邏輯 
      } 
      console.log(i); // 5for (let i = 0; i < 5; ++i) { // 循環邏輯
      } 
      console.log(i); // ReferenceError: i 沒有定義
      
    • 在使用 var 的時候,最常見的問題就是對迭代變量的奇特聲明和修改:
      for (var i = 0; i < 5; ++i) { setTimeout(() => console.log(i), 0) 
      } 
      // 你可能以為會輸出 0、1、2、3、4 
      // 實際上會輸出 5、5、5、5、5for (let i = 0; i < 5; ++i) { setTimeout(() => console.log(i), 0) 
      } 
      // 會輸出 0、1、2、3、4
      
      • 之所以會這樣,是因為在退出循環時,迭代變量保存的是導致循環退出的值:5。在之后執行超時邏輯時,所有的 i 都是同一個變量,因而輸出的都是同一個最終值。
      • 而在使用 let 聲明迭代變量時,JavaScript 引擎在后臺會為每個迭代循環聲明一個新的迭代變量。每個 setTimeout 引用的都是不同的變量實例,所以 console.log 輸出的是我們期望的值,也就是循環執行過程中每個迭代變量的值。
  13. const 聲明

    • const 的行為與 let 基本相同,唯一一個重要的區別是用它聲明變量時必須同時初始化變量,且嘗試修改 const 聲明的變量會導致運行時錯誤。
      const age = 26; 
      age = 36; // TypeError: 給常量賦值// const 也不允許重復聲明
      const name = 'Matt'; 
      const name = 'Nicholas'; // SyntaxError // const 聲明的作用域也是塊
      const name = 'Matt'; 
      if (true) { const name = 'Nicholas'; 
      } 
      console.log(name); // Matt
      
    • const 聲明的限制只適用于它指向的變量的引用。換句話說,如果 const 變量引用的是一個對象,那么修改這個對象內部的屬性并不違反 const 的限制
      const person = {}; 
      person.name = 'Matt'; // ok
      
    • JavaScript 引擎會為 for 循環中的 let 聲明分別創建獨立的變量實例,雖然 const 變量跟 let 變量很相似,但是不能用 const 來聲明迭代變量(因為迭代變量會自增):
      for (const i = 0; i < 10; ++i) {} // TypeError:給常量賦值
      
      • 如果你只想用 const 聲明一個不會被修改的 for 循環變量,那也是可以的。也就是說,每次迭代只是創建一個新變量。這對 for-of 和 for-in 循環特別有意義:
        let i = 0; 
        for (const j = 7; i < 5; ++i) { console.log(j); 
        } 
        // 7, 7, 7, 7, 7 for (const key in {a: 1, b: 2}) { console.log(key); 
        } 
        // a, b for (const value of [1,2,3,4,5]) { console.log(value); 
        } 
        // 1, 2, 3, 4, 5
        
  14. 聲明風格及最佳實踐

    • ECMAScript 6 增加 let 和 const 從客觀上為這門語言更精確地聲明作用域和語義提供了更好的支持。
    • 不使用 var。有了 let 和 const,大多數開發者會發現自己不再需要 var 了。限制自己只使用 let 和 const 有助于提升代碼質量,因為變量有了明確的作用域、聲明位置,以及不變的值。
    • const 優先,let 次之。使用 const 聲明可以讓瀏覽器運行時強制保持變量不變,也可以讓靜態代碼分析工具提前發現不合法的賦值操作。因此,很多開發者認為應該優先使用 const 來聲明變量,只在提前知道未來會有修改時,再使用 let。
  15. ECMAScript 有 6 種簡單數據類型(也稱為原始類型):Undefined、Null、Boolean、Number、String 和 Symbol。Symbol(符號)是 ECMAScript 6 新增的。還有一種復雜數據類型叫 Object(對象)。Object 是一種無序名值對的集合。因為在 ECMAScript 中不能定義自己的數據類型,所有值都可以用上述 7 種數據類型之一來表示。只有 7 種數據類型似乎不足以表示全部數據。但 ECMAScript 的數據類型很靈活,一種數據類型可以當作多種數據類型來使用。

  16. typeof 用來確定任意變量的數據類型。

    • 對一個值使用 typeof 操作符會返回下列字符串之一:
      • "undefined"表示值未定義;
      • "boolean"表示值為布爾值;
      • "string"表示值為字符串;
      • "number"表示值為數值;
      • "object"表示值為對象(而不是函數)或 null;
      • "function"表示值為函數;
      • "symbol"表示值為符號。
    • 使用 typeof 操作符的例子:
      let message = "some string"; 
      console.log(typeof message);    // "string" 
      console.log(typeof(message));   // "string" 
      console.log(typeof 95);         // "number"
      
    • 因為 typeof 是一個操作符而不是函數,所以不需要參數(但可以使用參數)。
    • 注意 typeof 在某些情況下返回的結果可能會讓人費解,但技術上講還是正確的。比如,調用 typeof null 返回的是"object"。這是因為特殊值 null 被認為是一個對空對象的引用。
    • 嚴格來講,函數在 ECMAScript 中被認為是對象,并不代表一種數據類型。可是,函數也有自己特殊的屬性。為此,就有必要通過 typeof 操作符來區分函數和其他對象。
  17. Undefined 類型只有一個值,就是特殊值 undefined。

    • 當使用 var 或 let 聲明了變量但沒有初始化時,就相當于給變量賦予了 undefined 值:
      let message; 
      console.log(message == undefined); // true
      
      • 變量 message 也可以顯式地以 undefined 來初始化。但這是不必要的,因為默認情況下,任何未經初始化的變量都會取得 undefined 值。
    • 一般來說,永遠不用顯式地給某個變量設置 undefined 值。字面值 undefined 主要用于比較,而且在 ECMA-262 第 3 版之前是不存在的。增加這個特殊值的目的就是為了正式明確空對象指針(null)和未初始化變量的區別。
    • 注意,包含 undefined 值的變量跟未定義變量是有區別的。
    • 對未聲明的變量,只能執行一個有用的操作,就是對它調用 typeof。(對未聲明的變量調用 delete 也不會報錯,但這個操作沒什么用,實際上在嚴格模式下會拋出錯誤。)
    • 在對未初始化的變量調用 typeof 時,返回的結果是"undefined",但對未聲明的變量調用它時,返回的結果還是"undefined"。
      let message;    // 這個變量被聲明了,只是值為 undefined 
      // 確保沒有聲明過這個變量
      // let age 
      console.log(typeof message); // "undefined" 
      console.log(typeof age);     // "undefined"console.log(message);   // "undefined" 
      console.log(age);       // 報錯
      
      • 無論是聲明還是未聲明,typeof 返回的都是字符串"undefined"。邏輯上講這是對的,因為雖然嚴格來講這兩個變量存在根本性差異,但它們都無法執行實際操作。
      • 即使未初始化的變量會被自動賦予 undefined 值,但我們仍然建議在聲明變量的同時進行初始化。這樣,當 typeof 返回"undefined"時,你就會知道那是因為給定的變量尚未聲明,而不是聲明了但未初始化。
    • undefined 是一個假值。因此,如果需要,可以用更簡潔的方式檢測它。不過要記住,也有很多其他可能的值同樣是假值。所以一定要明確自己想檢測的就是 undefined 這個字面值,而不僅僅是假值。
      let message; // 這個變量被聲明了,只是值為 undefined 
      // age 沒有聲明 if (message) { // 這個塊不會執行
      } if (!message) { // 這個塊會執行
      } if (age) { // 這里會報錯
      }
      
  18. Null 類型同樣只有一個值,即特殊值 null。

    • 邏輯上講,null 值表示一個空對象指針,這也是給 typeof 傳一個 null 會返回"object"的原因。
      let car = null; 
      console.log(typeof car); // "object"
      
    • 在定義將來要保存對象值的變量時,建議使用 null 來初始化,不要使用其他值。這樣,只要檢查這個變量的值是不是 null 就可以知道這個變量是否在后來被重新賦予了一個對象的引用。
      if (car != null) { // car 是一個對象的引用
      }
      
    • undefined 值是由 null 值派生而來的,因此 ECMA-262 將它們定義為表面上相等。用等于操作符(==)比較 null 和 undefined 始終返回 true。
      console.log(null == undefined);     // true
      console.log(null === undefined);    // false
      
    • 即使 null 和 undefined 有關系,它們的用途也是完全不一樣的。如前所述,永遠不必顯式地將變量值設置為 undefined。但 null 不是這樣的。任何時候,只要變量要保存對象,而當時又沒有那個對象可保存,就要用 null 來填充該變量。這樣就可以保持 null 是空對象指針的語義,并進一步將其與 undefined 區分開來。
    • null 是一個假值。因此,如果需要,可以用更簡潔的方式檢測它。不過要記住,也有很多其他可能的值同樣是假值。所以一定要明確自己想檢測的就是 null 這個字面值,而不僅僅是假值。
      let message = null; 
      let age; 
      if (message) { // 這個塊不會執行
      } 
      if (!message) { // 這個塊會執行
      }
      if (age) { // 這個塊不會執行
      } 
      if (!age) { // 這個塊會執行
      }
      
  19. Boolean(布爾值)類型有兩個字面值:true 和 false。這兩個布爾值不同于數值,因此 true 不等于 1,false 不等于 0。

    • 注意,布爾值字面量 true 和 false 是區分大小寫的,因此 True 和 False(及其他大小混寫形式)是有效的標識符,但不是布爾值。
    • 雖然布爾值只有兩個,但所有其他 ECMAScript 類型的值都有相應布爾值的等價形式。要將一個其他類型的值轉換為布爾值,可以調用特定的 Boolean()轉型函數:
      let message = "Hello world!"; 
      let messageAsBoolean = Boolean(message);
      
      • Boolean() 轉型函數可以在任意類型的數據上調用,而且始終返回一個布爾值。什么值能轉換為 true 或 false 的規則取決于數據類型和實際的值。
    • 下表總結了不同類型與布爾值之間的轉換規則。理解下表轉換非常重要,因為像 if 等流控制語句會自動執行其他類型值到布爾值的轉換:
      數據類型轉換為 true 的值轉換為 false 的值
      Booleantruefalse
      String非空字符串“”(空字符串)
      Number非零數值(包括無窮值)0、NaN(參見后面的相關內容)
      Object任意對象null
      UndefinedN/A(不存在)undefined
  20. Number 類型使用 IEEE 754 格式表示整數和浮點值(在某些語言中也叫雙精度值)。不同的數值類型相應地也有不同的數值字面量格式。

    • 最基本的數值字面量格式是十進制整數,直接寫出來即可。整數也可以用八進制(以 8 為基數)或十六進制(以 16 為基數)字面量表示。對于八進制字面量,第一個數字必須是零(0),然后是相應的八進制數字(數值 0~7)。如果字面量中包含的數字超出了應有的范圍,就會忽略前綴的零,后面的數字序列會被當成十進制數。
    • 要創建十六進制字面量,必須讓真正的數值前綴 0x(區分大小寫),然后是十六進制數字(0~9 以
      及 A~F)。十六進制數字中的字母大小寫均可。
      let intNum = 55; // 整數
      let octalNum1 = 070; // 八進制的 56 
      let octalNum2 = 079; // 無效的八進制值,當成 79 處理
      let octalNum3 = 08; // 無效的八進制值,當成 8 處理
      let hexNum1 = 0xA; // 十六進制 10 
      let hexNum2 = 0x1f; // 十六進制 31
      
    • 八進制字面量在嚴格模式下是無效的,會導致 JavaScript 引擎拋出語法錯誤。 ECMAScript 2015 或 ES6 中的八進制值通過前綴 0o 來表示;嚴格模式下,前綴 0 會被視為語法錯誤,如果要表示八進制值,應該使用前綴 0o
    • 使用八進制和十六進制格式創建的數值在所有數學操作中都被視為十進制數值。
    • 由于 JavaScript 保存數值的方式,實際中可能存在正零(+0)和負零(-0)。正零和負零在所有情況下都被認為是等同的。
  21. 浮點值

    • 要定義浮點值,數值中必須包含小數點,而且小數點后面必須至少有一個數字。雖然小數點前面不是必須有整數,但推薦加上。下面是幾個例子:
      let floatNum1 = 1.1; 
      let floatNum2 = 0.1; 
      let floatNum3 = .1;     // 有效,但不推薦
      let floatNum1 = 1.;     // 小數點后面沒有數字,當成整數 1 處理
      let floatNum2 = 10.0;   // 小數點后面是零,當成整數 10 處理
      let floatNum = 3.125e7; // 等于 31250000,“以 3.125 作為系數,乘以 10 的 7 次冪。”
      
    • 因為存儲浮點值使用的內存空間是存儲整數值的兩倍,所以 ECMAScript 總是想方設法把值轉換為整數。在小數點后面沒有數字的情況下,數值就會變成整數。類似地,如果數值本身就是整數,只是小數點后面跟著 0(如 1.0),那它也會被轉換為整數。
    • 對于非常大或非常小的數值,浮點值可以用科學記數法來表示。科學記數法用于表示一個應該乘以 10 的給定次冪的數值。ECMAScript 中科學記數法的格式要求是一個數值(整數或浮點數)后跟一個大寫或小寫的字母 e,再加上一個要乘的 10 的多少次冪。
    • 科學記數法也可以用于表示非常小的數值。默認情況下,ECMAScript 會將小數點后至少包含 6 個零的浮點值轉換為科學記數法(例如,0.000 000 3 會被轉換為 3e-7)。
    • 浮點值的精確度最高可達 17 位小數,但在算術計算中遠不如整數精確。例如,0.1 加 0.2 得到的不是 0.3,而是 0.300 000 000 000 000 04。
      if (a + b == 0.3) { // 別這么干! console.log("You got 0.3."); 
      }
      
      • 這里檢測兩個數值之和是否等于 0.3。如果兩個數值分別是 0.05 和 0.25,或者 0.15 和 0.15,那沒問題(讀者注:不知道為啥沒問題)。但如果是 0.1 和 0.2,如前所述,測試將失敗。因此永遠不要測試某個特定的浮點值。
      • 之所以存在這種舍入錯誤,是因為使用了 IEEE 754 數值,這種錯誤并非 ECMAScript 所獨有。其他使用相同格式的語言也有這個問題。
  22. 由于內存的限制,ECMAScript 并不支持表示這個世界上的所有數值。ECMAScript 可以表示的最小數值保存在 Number.MIN_VALUE 中,這個值在多數瀏覽器中是 5e-324;可以表示的最大數值保存在 Number.MAX_VALUE 中,這個值在多數瀏覽器中是 1.797 693 134 862 315 7e+308。

    • 如果某個計算得到的數值結果超出了 JavaScript 可以表示的范圍,那么這個數值會被自動轉換為一個特殊的 Infinity(無窮)值。任何無法表示的負數以-Infinity(負無窮大)表示,任何無法表示的正數以 Infinity(正無窮大)表示。
    • 如果計算返回正 Infinity 或負 Infinity,則該值將不能再進一步用于任何計算。這是因為 Infinity 沒有可用于計算的數值表示形式。要確定一個值是不是有限大(即介于 JavaScript 能表示的最小值和最大值之間),可以使用 isFinite() 函數。
      let result = Number.MAX_VALUE + Number.MAX_VALUE; 
      console.log(isFinite(result)); // false
      
    • 使用 Number.NEGATIVE_INFINITY 和 Number.POSITIVE_INFINITY 也可以獲取正、負 Infinity。沒錯,這兩個屬性包含的值分別就是 -Infinity 和 Infinity。
  23. NaN

    • 有一個特殊的數值叫 NaN,意思是“不是數值”(Not a Number),用于表示本來要返回數值的操作失敗了(而不是拋出錯誤)。比如,用 0 除任意數值在其他語言中通常都會導致錯誤,從而中止代碼執
      行。但在 ECMAScript 中,0、+0 或 -0 相除會返回 NaN。如果分子是非 0 值,分母是有符號 0 或無符號 0,則會返回 Infinity 或-Infinity。
      console.log(0/0);       // NaN 
      console.log(-0/+0);     // NaNconsole.log(5/0);       // Infinity 
      console.log(5/-0);      // -Infinity
      
    • NaN 有幾個獨特的屬性。首先,任何涉及 NaN 的操作始終返回 NaN(如 NaN/10),在連續多步計算時這可能是個問題。其次,NaN 不等于包括 NaN 在內的任何值。例如,下面的比較操作會返回 false:
      console.log(NaN == NaN); // false
      
    • ECMAScript 提供了 isNaN() 函數。該函數接收一個參數,可以是任意數據類型,然后判斷這個參數是否“不是數值”。把一個值傳給 isNaN()后,該函數會嘗試把它轉換為數值。某些非數值的值可以直接轉換成數值,如字符串"10"或布爾值。任何不能轉換為數值的值都會導致這個函數返回 true。
      console.log(isNaN(NaN));    // true 
      console.log(isNaN(10));     // false,10 是數值
      console.log(isNaN("10"));   // false,可以轉換為數值 10 
      console.log(isNaN("blue")); // true,不可以轉換為數值
      console.log(isNaN(true));   // false,可以轉換為數值 1
      
    • 雖然不常見,但 isNaN() 可以用于測試對象。此時,首先會調用對象的 valueOf() 方法,然后再確定返回的值是否可以轉換為數值。如果不能,再調用 toString()方法,并測試其返回值。這通常是 ECMAScript 內置函數和操作符的工作方式。
  24. 有 3 個函數可以將非數值轉換為數值:Number()、parseInt() 和 parseFloat()。Number() 是轉型函數,可用于任何數據類型。后兩個函數主要用于將字符串轉換為數值。對于同樣的參數,這 3 個函數執行的操作也不同。

  25. Number()函數基于如下規則執行轉換。

    • 布爾值,true 轉換為 1,false 轉換為 0。
    • 數值,直接返回。
    • null,返回 0。
    • undefined,返回 NaN。
    • 字符串,應用以下規則。
      • 如果字符串包含數值字符,包括數值字符前面帶加、減號的情況,則轉換為一個十進制數值。因此,Number(“1”)返回 1,Number(“123”)返回 123,Number(“011”)返回 11(忽略前面的零)。
      • 如果字符串包含有效的浮點值格式如"1.1",則會轉換為相應的浮點值(同樣,忽略前面的零)。
      • 如果字符串包含有效的十六進制格式如"0xf",則會轉換為與該十六進制值對應的十進制整數值。
      • 如果是空字符串(不包含字符),則返回 0。
      • 如果字符串包含除上述情況之外的其他字符,則返回 NaN。
    • 對象,調用 valueOf() 方法,并按照上述規則轉換返回的值。如果轉換結果是 NaN,則調用 toString()方法,再按照轉換字符串的規則轉換。
    • 具體例子:
      let num1 = Number("Hello world!");  // NaN 
      let num2 = Number("");              // 0 
      let num3 = Number("000011");        // 11 
      let num4 = Number(true);            // 1
      
  26. 考慮到用 Number() 函數轉換字符串時相對復雜且有點反常規,通常在需要得到整數時可以優先使用 parseInt() 函數。parseInt() 函數更專注于字符串是否包含數值模式。字符串最前面的空格會被忽略,從第一個非空格字符開始轉換。如果第一個字符不是數值字符、加號或減號,parseInt() 立即返回 NaN。這意味著空字符串也會返回 NaN(這一點跟 Number() 不一樣,它返回 0)。如果第一個字符是數值字符、加號或減號,則繼續依次檢測每個字符,直到字符串末尾,或碰到非數值字符。比如,"1234blue"會被轉換為 1234,因為"blue"會被完全忽略。類似地,"22.5"會被轉換為 22,因為小數點不是有效的整數字符。

    • parseInt()函數也能識別不同的整數格式(十進制、八進制、十六進制)。換句話說,如果字符串以"0x"開頭,就會被解釋為十六進制整數。如果字符串以"0"開頭,且緊跟著數值字符,在非嚴格模式下會被某些實現解釋為八進制整數。
    • 具體例子:
      let num1 = parseInt("1234blue");    // 1234 
      let num2 = parseInt("");            // NaN 
      let num3 = parseInt("0xA");         // 10,解釋為十六進制整數
      let num4 = parseInt(22.5);          // 22 
      let num5 = parseInt("70");          // 70,解釋為十進制值
      let num6 = parseInt("0xf");         // 15,解釋為十六進制整數
      
    • parseInt()也接收第二個參數,用于指定底數(進制數)。
      let num = parseInt("0xAF", 16); // 175let num1 = parseInt("AF", 16); // 175 
      let num2 = parseInt("AF"); // NaN,因為檢測到第一個字符就是非數值字符,隨即自動停止并返回 NaN。
      
      • 如果知道要解析的值是十六進制,那么可以傳入 16 作為第二個參數,以便正確解析。
      • 如果提供了十六進制參數,那么字符串前面的"0x"可以省掉。
      • 建議始終傳給它第二個參數。多數情況下解析的應該都是十進制數,此時第二個參數就要傳入 10。
  27. parseFloat() 函數的工作方式跟 parseInt() 函數類似,都是從位置 0 開始檢測每個字符。同樣,它也是解析到字符串末尾或者解析到一個無效的浮點數值字符為止。這意味著第一次出現的小數點是有效的,但第二次出現的小數點就無效了,此時字符串的剩余字符都會被忽略。因此,"22.34.5"將轉換成 22.34。

    • parseFloat()函數的另一個不同之處在于,它始終忽略字符串開頭的零。這個函數能識別前面討論的所有浮點格式,以及十進制格式(開頭的零始終被忽略)。十六進制數值始終會返回 0。因為 parseFloat()只解析十進制值,因此不能指定底數。最后,如果字符串表示整數(沒有小數點或者小數點后面只有一個零),則 parseFloat()返回整數。
    • 具體例子:
      let num1 = parseFloat("1234blue");  // 1234,按整數解析
      let num2 = parseFloat("0xA");       // 0 
      let num3 = parseFloat("22.5");      // 22.5 
      let num4 = parseFloat("22.34.5");   // 22.34 
      let num5 = parseFloat("0908.5");    // 908.5 
      let num6 = parseFloat("3.125e7");   // 31250000
      
  28. String(字符串)數據類型表示零或多個 16 位 Unicode 字符序列。字符串可以使用雙引號(")、單引號(')或反引號(`)標示。

    • 跟某些語言中使用不同的引號會改變對字符串的解釋方式不同,ECMAScript 語法中表示字符串的引號沒有區別。不過要注意的是,以某種引號作為字符串開頭,必須仍然以該種引號作為字符串結尾。
    • 字符串數據類型包含一些字符字面量,用于表示非打印字符或有其他用途的字符。
      字面量含義
      \n換行
      \t制表
      \b退格
      \r回車
      \f換頁
      \\反斜杠(\)
      單引號(‘),在字符串以單引號標示時使用,例如’He said, \‘hey.\’’
      "雙引號(“),在字符串以雙引號標示時使用,例如"He said, \“hey.\””
      `反引號(`),在字符串以反引號標示時使用,例如`He said, \`hey.\``
      \xnn以十六進制編碼 nn 表示的字符(其中 n 是十六進制數字 0~F),例如\x41 等于"A"
      \unnnn以十六進制編碼 nnnn 表示的 Unicode 字符(其中 n 是十六進制數字 0~F),例如\u03a3 等于希臘字符"Σ"
    • 這些字符字面量可以出現在字符串中的任意位置,且可以作為單個字符被解釋:
      let text = "This is the letter sigma: \u03a3.";console.log(text.length); // 28
      
      • 在這個例子中,即使包含 6 個字符長的轉義序列,變量 text 仍然是 28 個字符長。因為轉義序列表示一個字符,所以只算一個字符。
      • 字符串的長度可以通過其 length 屬性獲取。這個屬性返回字符串中 16 位字符的個數。
      • 注意,如果字符串中包含雙字節字符,那么 length 屬性返回的值可能不是準確的字符數。(讀者注:為啥不準確?)
  29. ECMAScript 中的字符串是不可變的(immutable),意思是一旦創建,它們的值就不能變了。要修改某個變量中的字符串值,必須先銷毀原始的字符串,然后將包含新值的另一個字符串保存到該變量。

    let lang = "Java"; 
    lang = lang + "Script";
    
    • 整個過程首先會分配一個足夠容納 10 個字符的空間,然后填充上"Java"和"Script"。最后銷毀原始的字符串"Java"和字符串"Script",因為這兩個字符串都沒有用了。所有處理都是在后臺發生的,而這也是一些早期的瀏覽器(如 Firefox 1.0 之前的版本和 IE6.0)在拼接字符串時非常慢的原因。這些瀏覽器在后來的版本中都有針對性地解決了這個問題。
  30. 有兩種方式把一個值轉換為字符串。

    • 首先是使用幾乎所有值都有的 toString()方法。這個方法唯一的用途就是返回當前值的字符串等價物。
      let age = 11; 
      let ageAsString = age.toString();       // 字符串"11" 
      let found = true; 
      let foundAsString = found.toString();   // 字符串"true"
      
    • toString()方法可見于數值、布爾值、對象和字符串值。(沒錯,字符串值也有 toString()方法,該方法只是簡單地返回自身的一個副本。)null 和 undefined 值沒有 toString() 方法。
    • 多數情況下,toString()不接收任何參數。不過,在對數值調用這個方法時,toString()可以接收一個底數參數,即以什么底數來輸出數值的字符串表示。默認情況下,toString()返回數值的十進制字符串表示。而通過傳入參數,可以得到數值的二進制、八進制、十六進制,或者其他任何有效基數的字符串表示。默認情況下(不傳參數)的輸出與傳入參數 10 得到的結果相同。
      let num = 10; 
      console.log(num.toString());    // "10" 
      console.log(num.toString(2));   // "1010" 
      console.log(num.toString(8));   // "12" 
      console.log(num.toString(10));  // "10" 
      console.log(num.toString(16));  // "a"
      
    • **如果你不確定一個值是不是 null 或 undefined,可以使用 String()轉型函數,它始終會返回表示相應類型值的字符串。**String()函數遵循如下規則。
      • 如果值有 toString()方法,則調用該方法(不傳參數)并返回結果。
      • 如果值是 null,返回"null"。
      • 如果值是 undefined,返回"undefined"。
      • 具體例子:
        let value1 = 10; 
        let value2 = true; 
        let value3 = null; 
        let value4; 
        console.log(String(value1)); // "10" 
        console.log(String(value2)); // "true" 
        console.log(String(value3)); // "null" 
        console.log(String(value4)); // "undefined"
        
        • 數值和布爾值的轉換結果與調用 toString()相同。因為 null 和 undefined 沒有 toString()方法,所以 String()方法就直接返回了這兩個值的字面量文本。
    • 注意,用加號操作符給一個值加上一個空字符串""也可以將其轉換為字符串。
  31. ECMAScript 6 新增了使用模板字面量定義字符串的能力。

    • 與使用單引號或雙引號不同,模板字面量保留換行字符,可以跨行定義字符串。
      let myMultiLineString = 'first line\nsecond line'; 
      let myMultiLineTemplateLiteral = `first line 
      second line`; 
      console.log(myMultiLineString); 
      // first line 
      // second line" 
      console.log(myMultiLineTemplateLiteral); 
      // first line
      // second line 
      console.log(myMultiLineString === myMultiLinetemplateLiteral); // true
      
    • 由于模板字面量會保持反引號內部的空格,因此在使用時要格外注意。格式正確的模板字符串看起來可能會縮進不當:
      // 這個模板字面量在換行符之后有 25 個空格符
      let myTemplateLiteral = `first line second line`; 
      console.log(myTemplateLiteral.length); // 47// 這個模板字面量以一個換行符開頭
      let secondTemplateLiteral = ` 
      first line 
      second line`; 
      console.log(secondTemplateLiteral[0] === '\n'); // true
      
    • 模板字面量在定義模板時特別有用,比如下面這個 HTML 模板:
      let pageHTML = ` 
      <div> <a href="#"> <span>Jake</span> </a> 
      </div>`;
      
  32. 模板字面量最常用的一個特性是支持字符串插值,也就是可以在一個連續定義中插入一個或多個值。技術上講,模板字面量不是字符串,而是一種特殊的 JavaScript 句法表達式,只不過求值后得到的是字符串。模板字面量在定義時立即求值并轉換為字符串實例,任何插入的變量也會從它們最接近的作用域中取值。

    • 字符串插值通過在${}中使用一個 JavaScript 表達式實現:
      let value = 5; 
      let exponent = 'second'; 
      // 以前,字符串插值是這樣實現的:
      let interpolatedString = value + ' to the ' + exponent + ' power is ' + (value * value); // 現在,可以用模板字面量這樣實現:
      let interpolatedTemplateLiteral = `${ value } to the ${ exponent } power is ${ value * value }`; 
      console.log(interpolatedString); // 5 to the second power is 25 
      console.log(interpolatedTemplateLiteral); // 5 to the second power is 25
      
    • 所有插入的值都會使用 toString()強制轉型為字符串,而且任何 JavaScript 表達式都可以用于插值。嵌套的模板字符串無須轉義:
      console.log(`Hello, ${ `World` }!`); // Hello, World!
      
    • 將表達式轉換為字符串時會調用 toString():
      let foo = { toString: () => 'World' }; 
      console.log(`Hello, ${ foo }!`); // Hello, World!
      
    • 在插值表達式中可以調用函數和方法:
      function capitalize(word) { return `${ word[0].toUpperCase() }${ word.slice(1) }`; 
      } 
      console.log(`${ capitalize('hello') }, ${ capitalize('world') }!`); // Hello, World!
      
    • 模板也可以插入自己之前的值:
      let value = ''; 
      function append() { value = `${value}abc` console.log(value); 
      } 
      append(); // abc 
      append(); // abcabc 
      append(); // abcabcabc
      
  33. 模板字面量也支持定義標簽函數(tag function),而通過標簽函數可以自定義插值行為。標簽函數會接收被插值記號分隔后的模板和對每個表達式求值的結果。標簽函數本身是一個常規函數,通過前綴到模板字面量來應用自定義行為。標簽函數接收到的參數依次是原始字符串數組和對每個表達式求值的結果。這個函數的返回值是對模板字面量求值得到的字符串。

    let a = 6; 
    let b = 9; 
    function simpleTag(strings, aValExpression, bValExpression, sumExpression) { console.log(strings); console.log(aValExpression); console.log(bValExpression); console.log(sumExpression); return 'foobar'; 
    } 
    let untaggedResult = `${ a } + ${ b } = ${ a + b }`; 
    let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`; 
    // ["", " + ", " = ", ""] 
    // 6 
    // 9 
    // 15 
    console.log(untaggedResult); // "6 + 9 = 15" 
    console.log(taggedResult); // "foobar"
    
    • 因為表達式參數的數量是可變的,所以通常應該使用剩余操作符(rest operator)將它們收集到一個數組中:
      let a = 6; 
      let b = 9; 
      function simpleTag(strings, ...expressions) { console.log(strings); for(const expression of expressions) { console.log(expression); } return 'foobar'; 
      } 
      let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`; 
      // ["", " + ", " = ", ""] 
      // 6 
      // 9 
      // 15 
      console.log(taggedResult); // "foobar"
      
    • 對于有 n 個插值的模板字面量,傳給標簽函數的表達式參數的個數始終是 n,而傳給標簽函數的第一個參數所包含的字符串個數則始終是 n+1。因此,如果你想把這些字符串和對表達式求值的結果拼接起來作為默認返回的字符串,可以這樣做:
      let a = 6; 
      let b = 9; 
      function zipTag(strings, ...expressions) { return strings[0] + expressions.map((e, i) => `${e}${strings[i + 1]}`) .join(''); 
      } 
      let untaggedResult = `${ a } + ${ b } = ${ a + b }`; 
      let taggedResult = zipTag`${ a } + ${ b } = ${ a + b }`; 
      console.log(untaggedResult); // "6 + 9 = 15" 
      console.log(taggedResult);   // "6 + 9 = 15"
      
  34. 使用模板字面量也可以直接獲取原始的模板字面量內容(如換行符或 Unicode 字符),而不是被轉換后的字符表示。

    • 為此,可以使用默認的 String.raw 標簽函數:
      // Unicode 示例
      // \u00A9 是版權符號
      console.log(`\u00A9`);              // ? 
      console.log(String.raw`\u00A9`);    // \u00A9 // 換行符示例
      console.log(`first line\nsecond line`); 
      // first line 
      // second line 
      console.log(String.raw`first line\nsecond line`); // "first line\nsecond line" // 對實際的換行符來說是不行的
      // 它們不會被轉換成轉義序列的形式
      console.log(`first line
      second line`); 
      // first line 
      // second line console.log(String.raw`first line 
      second line`); 
      // first line 
      // second line
      
    • 另外,也可以通過標簽函數的第一個參數,即字符串數組的.raw 屬性取得每個字符串的原始內容:
      function printRaw(strings) { console.log('Actual characters:'); for (const string of strings) { console.log(string); } console.log('Escaped characters;'); for (const rawString of strings.raw) { console.log(rawString); } 
      } printRaw`\u00A9${ 'and' }\n`; 
      // Actual characters: 
      // ? 
      //(換行符)
      // Escaped characters: 
      // \u00A9 
      // \n
      
  35. Symbol(符號)是 ECMAScript 6 新增的數據類型。符號是原始值,且符號實例是唯一、不可變的。符號的用途是確保對象屬性使用唯一標識符,不會發生屬性沖突的危險。

    • 盡管聽起來跟私有屬性有點類似,但符號并不是為了提供私有屬性的行為才增加的(尤其是因為 Object API 提供了方法,可以更方便地發現符號屬性)。相反,符號就是用來創建唯一記號,進而用作非字符串形式的對象屬性。
  36. 符號的基本用法

    • 符號需要使用 Symbol() 函數初始化。因為符號本身是原始類型,所以 typeof 操作符對符號返回 symbol。
      let sym = Symbol(); 
      console.log(typeof sym); // symbol
      
    • 調用 Symbol() 函數時,也可以傳入一個字符串參數作為對符號的描述(description),將來可以通過這個字符串來調試代碼。但是,這個字符串參數與符號定義或標識完全無關:
      let genericSymbol = Symbol(); 
      let otherGenericSymbol = Symbol(); 
      let fooSymbol = Symbol('foo'); 
      let otherFooSymbol = Symbol('foo'); 
      console.log(genericSymbol == otherGenericSymbol);   // false
      console.log(fooSymbol == otherFooSymbol);           // false
      
    • 符號沒有字面量語法,這也是它們發揮作用的關鍵。按照規范,你只要創建 Symbol() 實例并將其用作對象的新屬性,就可以保證它不會覆蓋已有的對象屬性,無論是符號屬性還是字符串屬性。
      let genericSymbol = Symbol(); 
      console.log(genericSymbol); // Symbol() 
      let fooSymbol = Symbol('foo'); 
      console.log(fooSymbol);     // Symbol(foo);
      
    • **Symbol()函數不能與 new 關鍵字一起作為構造函數使用。**這樣做是為了避免創建符號包裝對象,像使用 Boolean、String 或 Number 那樣,它們都支持構造函數且可用于初始化包含原始值的包裝對象:
      let myBoolean = new Boolean(); 
      console.log(typeof myBoolean);  // "object" 
      let myString = new String(); 
      console.log(typeof myString);   // "object" 
      let myNumber = new Number(); 
      console.log(typeof myNumber);   // "object" 
      let mySymbol = new Symbol();    // TypeError: Symbol is not a constructor
      
    • 如果你確實想使用符號包裝對象,可以借用 Object() 函數:
      let mySymbol = Symbol(); 
      let myWrappedSymbol = Object(mySymbol); 
      console.log(typeof myWrappedSymbol); // "object"
      
  37. 使用全局符號注冊表

    • 如果運行時的不同部分需要共享和重用符號實例,那么可以用一個字符串作為鍵,在全局符號注冊表中創建并重用符號。為此,需要使用 Symbol.for()方法:
      let fooGlobalSymbol = Symbol.for('foo'); 
      console.log(typeof fooGlobalSymbol);    // symbol
      
    • Symbol.for()對每個字符串鍵都執行冪等操作。第一次使用某個字符串調用時,它會檢查全局運行時注冊表,發現不存在對應的符號,于是就會生成一個新符號實例并添加到注冊表中。后續使用相同字符串的調用同樣會檢查注冊表,發現存在與該字符串對應的符號,然后就會返回該符號實例。
      let fooGlobalSymbol = Symbol.for('foo'); // 創建新符號
      let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符號
      console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true
      
    • 即使采用相同的符號描述,在全局注冊表中定義的符號跟使用 Symbol()定義的符號也并不等同:
      let localSymbol = Symbol('foo'); 
      let globalSymbol = Symbol.for('foo'); 
      console.log(localSymbol === globalSymbol); // false
      
    • 全局注冊表中的符號必須使用字符串鍵來創建,因此作為參數傳給 Symbol.for()的任何值都會被轉換為字符串。此外,注冊表中使用的鍵同時也會被用作符號描述。
      let emptyGlobalSymbol = Symbol.for(); 
      console.log(emptyGlobalSymbol);     // Symbol(undefined)
      
    • 還可以使用 Symbol.keyFor() 來查詢全局注冊表,這個方法接收符號,返回該全局符號對應的字符串鍵。如果查詢的不是全局符號,則返回 undefined。
      // 創建全局符號
      let s = Symbol.for('foo'); 
      console.log(Symbol.keyFor(s)); // foo 
      // 創建普通符號
      let s2 = Symbol('bar'); 
      console.log(Symbol.keyFor(s2)); // undefined
      
    • 如果傳給 Symbol.keyFor() 的不是符號,則該方法拋出 TypeError:
      Symbol.keyFor(123); // TypeError: 123 is not a symbol
      
  38. 凡是可以使用字符串或數值作為屬性的地方,都可以使用符號。這就包括了對象字面量屬性和 Object.defineProperty()/Object.defineProperties() 定義的屬性。

    • 對象字面量只能在計算屬性語法中使用符號作為屬性。
      let s1 = Symbol('foo'), s2 = Symbol('bar'), s3 = Symbol('baz'), s4 = Symbol('qux'); 
      let o = { [s1]: 'foo val' 
      }; 
      // 這樣也可以:o[s1] = 'foo val'; console.log(o); 
      // {Symbol(foo): foo val} Object.defineProperty(o, s2, {value: 'bar val'}); console.log(o); 
      // {Symbol(foo): foo val, Symbol(bar): bar val} Object.defineProperties(o, { [s3]: {value: 'baz val'}, [s4]: {value: 'qux val'} 
      }); console.log(o); 
      // {Symbol(foo): foo val, Symbol(bar): bar val, 
      // Symbol(baz): baz val, Symbol(qux): qux val}
      
    • 類似于 Object.getOwnPropertyNames()返回對象實例的常規屬性數組,Object.getOwnPropertySymbols()返回對象實例的符號屬性數組。這兩個方法的返回值彼此互斥。Object.getOwnPropertyDescriptors()會返回同時包含常規和符號屬性描述符的對象。Reflect.ownKeys()會返回兩種類型的鍵:
      let s1 = Symbol('foo'), s2 = Symbol('bar'); 
      let o = { [s1]: 'foo val', [s2]: 'bar val', baz: 'baz val', qux: 'qux val' 
      };console.log(Object.getOwnPropertySymbols(o)); 
      // [Symbol(foo), Symbol(bar)] console.log(Object.getOwnPropertyNames(o)); 
      // ["baz", "qux"] console.log(Object.getOwnPropertyDescriptors(o)); 
      // {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}} 
      console.log(Reflect.ownKeys(o)); 
      // ["baz", "qux", Symbol(foo), Symbol(bar)]
      
    • 因為符號屬性是對內存中符號的一個引用,所以直接創建并用作屬性的符號不會丟失。但是,如果沒有顯式地保存對這些屬性的引用,那么必須遍歷對象的所有符號屬性才能找到相應的屬性鍵:
      let o = { [Symbol('foo')]: 'foo val', [Symbol('bar')]: 'bar val' 
      }; console.log(o); 
      // {Symbol(foo): "foo val", Symbol(bar): "bar val"} let barSymbol = Object.getOwnPropertySymbols(o) .find((symbol) => symbol.toString().match(/bar/));console.log(barSymbol); 
      // Symbol(bar)
      
  39. 常用內置符號

    • ECMAScript 6 也引入了一批常用內置符號(well-known symbol),用于暴露語言內部行為,開發者可以直接訪問、重寫或模擬這些行為。這些內置符號都以 Symbol 工廠函數字符串屬性的形式存在。
    • 這些內置符號最重要的用途之一是重新定義它們,從而改變原生結構的行為。比如,我們知道 for-of 循環會在相關對象上使用 Symbol.iterator 屬性,那么就可以通過在自定義對象上重新定義 Symbol.iterator 的值,來改變 for-of 在迭代該對象時的行為。
    • 這些內置符號也沒有什么特別之處,它們就是全局函數 Symbol 的普通字符串屬性,指向一個符號的實例。所有內置符號屬性都是不可寫、不可枚舉、不可配置的。
    • 注意 在提到 ECMAScript 規范時,經常會引用符號在規范中的名稱,前綴為@@。比如,@@iterator 指的就是 Symbol.iterator。
  40. Symbol.asyncIterator

    • 根據 ECMAScript 規范,這個符號作為一個屬性表示“一個方法,該方法返回對象默認的 AsyncIterator。由 for-await-of 語句使用”。換句話說,這個符號表示實現異步迭代器 API 的函數。
    • for-await-of 循環會利用這個函數執行異步迭代操作。循環時,它們會調用以 Symbol.asyncIterator 為鍵的函數,并期望這個函數會返回一個實現迭代器 API 的對象。很多時候,返回的對象是實現該 API的 AsyncGenerator:
      class Foo { async *[Symbol.asyncIterator]() {} 
      } 
      let f = new Foo(); 
      console.log(f[Symbol.asyncIterator]()); 
      // AsyncGenerator {<suspended>} 
      
    • 技術上,這個由 Symbol.asyncIterator 函數生成的對象應該通過其 next()方法陸續返回 Promise 實例。可以通過顯式地調用 next()方法返回,也可以隱式地通過異步生成器函數返回:
      class Emitter { constructor(max) { this.max = max; this.asyncIdx = 0; } async *[Symbol.asyncIterator]() { while(this.asyncIdx < this.max) { yield new Promise((resolve) => resolve(this.asyncIdx++)); } } 
      }async function asyncCount() { let emitter = new Emitter(5); for await(const x of emitter) { console.log(x); } 
      } asyncCount(); 
      // 0 
      // 1 
      // 2 
      // 3 
      // 4 
      
    • Symbol.asyncIterator 是 ES2018 規范定義的,因此只有版本非常新的瀏覽器支持它。
    • 讀者備注:(來自GPT3.5)JavaScript的yield關鍵字用于定義一個generator函數,并指定生成器的返回值。yield關鍵字在生成器內部使用,用于暫停生成器的執行,并將一個值返回給生成器的調用者。
  41. Symbol.hasInstance

    • 根據 ECMAScript 規范,這個符號作為一個屬性表示“一個方法,該方法決定一個構造器對象是否認可一個對象是它的實例。instanceof 操作符使用”。instanceof 操作符可以用來確定一個對象實例的原型鏈上是否有原型。instanceof 的典型使用場景如下:
      function Foo() {} 
      let f = new Foo(); 
      console.log(f instanceof Foo); // true class Bar {} 
      let b = new Bar(); 
      console.log(b instanceof Bar); // true
      
    • 在 ES6 中,instanceof 操作符會使用 Symbol.hasInstance 函數來確定關系。以 Symbol.hasInstance 為鍵的函數會執行同樣的操作,只是操作數對調了一下:
      function Foo() {} 
      let f = new Foo(); 
      console.log(Foo[Symbol.hasInstance](f)); // true class Bar {} 
      let b = new Bar(); 
      console.log(Bar[Symbol.hasInstance](b)); // true
      
      • 這個屬性定義在 Function 的原型上,因此默認在所有函數和類上都可以調用。
    • 由于 instanceof 操作符會在原型鏈上尋找這個屬性定義,就跟在原型鏈上尋找其他屬性一樣,因此可以在繼承的類上通過靜態方法重新定義這個函數:
      class Bar {} 
      class Baz extends Bar { static [Symbol.hasInstance]() { return false; } 
      } 
      let b = new Baz(); 
      console.log(Bar[Symbol.hasInstance](b));    // true 
      console.log(b instanceof Bar);              // true 
      console.log(Baz[Symbol.hasInstance](b));    // false,因為Baz的方法返回了false
      console.log(b instanceof Baz);              // false,因為Baz的方法返回了false
      
  42. Symbol.isConcatSpreadable

    • 根據 ECMAScript 規范,這個符號作為一個屬性表示“一個布爾值,如果是 true,則意味著對象應該用 Array.prototype.concat() 打平其數組元素”。ES6 中的 Array.prototype.concat() 方法會根據接收到的對象類型選擇如何將一個類數組對象拼接成數組實例。覆蓋 Symbol.isConcatSpreadable 的值可以修改這個行為。
    • 數組對象默認情況下會被打平到已有的數組,false 或假值會導致整個對象被追加到數組末尾。類數組對象默認情況下會被追加到數組末尾,true 或真值會導致這個類數組對象被打平到數組實例。其他不是類數組對象的對象在 Symbol.isConcatSpreadable 被設置為 true 的情況下將被忽略。
      let initial = ['foo']; let array = ['bar']; 
      console.log(array[Symbol.isConcatSpreadable]);  // undefined 
      console.log(initial.concat(array));             // ['foo', 'bar'] 
      array[Symbol.isConcatSpreadable] = false; 
      console.log(initial.concat(array));             // ['foo', Array(1)]let arrayLikeObject = { length: 1, 0: 'baz' }; 
      console.log(arrayLikeObject[Symbol.isConcatSpreadable]);    // undefined 
      console.log(initial.concat(arrayLikeObject));               // ['foo', {...}] 
      arrayLikeObject[Symbol.isConcatSpreadable] = true; 
      console.log(initial.concat(arrayLikeObject));               // ['foo', 'baz'] let otherObject = new Set().add('qux'); 
      console.log(otherObject[Symbol.isConcatSpreadable]);    // undefined 
      console.log(initial.concat(otherObject));               // ['foo', Set(1)] 
      otherObject[Symbol.isConcatSpreadable] = true; 
      console.log(initial.concat(otherObject));               // ['foo']
      
  43. Symbol.iterator

    • 根據 ECMAScript 規范,這個符號作為一個屬性表示“一個方法,該方法返回對象默認的迭代器。由 for-of 語句使用”。換句話說,這個符號表示實現迭代器 API 的函數。
    • for-of 循環這樣的語言結構會利用這個函數執行迭代操作。循環時,它們會調用以 Symbol.iterator 為鍵的函數,并默認這個函數會返回一個實現迭代器 API 的對象。很多時候,返回的對象是實現該 API 的 Generator:
      class Foo { *[Symbol.iterator]() {} 
      } 
      let f = new Foo(); 
      console.log(f[Symbol.iterator]()); 
      // Generator {<suspended>}
      
    • 技術上,這個由 Symbol.iterator 函數生成的對象應該通過其 next()方法陸續返回值。可以通過顯式地調用 next()方法返回,也可以隱式地通過生成器函數返回:
      class Emitter { constructor(max) { this.max = max; this.idx = 0; } *[Symbol.iterator]() { while(this.idx < this.max) { yield this.idx++; }} 
      }function count() { let emitter = new Emitter(5); for (const x of emitter) { console.log(x); } 
      }
      count(); 
      // 0
      // 1 
      // 2 
      // 3 
      // 4
      
  44. Symbol.match

    • 根據 ECMAScript 規范,這個符號作為一個屬性表示“一個正則表達式方法,該方法用正則表達式去匹配字符串。由 String.prototype.match()方法使用”。String.prototype.match()方法會使用以 Symbol.match 為鍵的函數來對正則表達式求值。正則表達式的原型上默認有這個函數的定義,因此所有正則表達式實例默認是這個 String 方法的有效參數:
      console.log(RegExp.prototype[Symbol.match]); 
      // ? [Symbol.match]() { [native code] } 
      console.log('foobar'.match(/bar/)); 
      // ["bar", index: 3, input: "foobar", groups: undefined]
      
    • 給這個方法傳入非正則表達式值會導致該值被轉換為 RegExp 對象。如果想改變這種行為,讓方法直接使用參數,則可以重新定義 Symbol.match 函數以取代默認對正則表達式求值的行為,從而讓match()方法使用非正則表達式實例。Symbol.match 函數接收一個參數,就是調用 match()方法的字符串實例。返回的值沒有限制:
      class FooMatcher { static [Symbol.match](target) { return target.includes('foo'); } 
      }console.log('foobar'.match(FooMatcher)); // true 
      console.log('barbaz'.match(FooMatcher)); // false class StringMatcher { constructor(str) { this.str = str; } [Symbol.match](target) { return target.includes(this.str); } 
      }console.log('foobar'.match(new StringMatcher('foo')));  // true 
      console.log('barbaz'.match(new StringMatcher('qux')));  // false
      
  45. Symbol.replace

    • 根據 ECMAScript 規范,這個符號作為一個屬性表示“一個正則表達式方法,該方法替換一個字符串中匹配的子串。由 String.prototype.replace()方法使用”。String.prototype.replace()方法會使用以 Symbol.replace 為鍵的函數來對正則表達式求值。正則表達式的原型上默認有這個函數的定義,因此所有正則表達式實例默認是這個 String 方法的有效參數:
      console.log(RegExp.prototype[Symbol.replace]); 
      // ? [Symbol.replace]() { [native code] } 
      console.log('foobarbaz'.replace(/bar/, 'qux')); 
      // 'fooquxbaz'
      
    • 給這個方法傳入非正則表達式值會導致該值被轉換為 RegExp 對象。如果想改變這種行為,讓方法直接使用參數,可以重新定義 Symbol.replace 函數以取代默認對正則表達式求值的行為,從而讓replace()方法使用非正則表達式實例。Symbol.replace 函數接收兩個參數,即調用 replace()方法的字符串實例和替換字符串。返回的值沒有限制:
      class FooReplacer { static [Symbol.replace](target, replacement) { return target.split('foo').join(replacement); } 
      } console.log('barfoobaz'.replace(FooReplacer, 'qux')); 
      // "barquxbaz" class StringReplacer { constructor(str) { this.str = str; } [Symbol.replace](target, replacement) { return target.split(this.str).join(replacement); } 
      }console.log('barfoobaz'.replace(new StringReplacer('foo'), 'qux')); 
      // "barquxbaz" 
      
  46. Symbol.search

    • 根據 ECMAScript 規范,這個符號作為一個屬性表示“一個正則表達式方法,該方法返回字符串中匹配正則表達式的索引。由 String.prototype.search()方法使用”。String.prototype.search() 方法會使用以 Symbol.search 為鍵的函數來對正則表達式求值。正則表達式的原型上默認有這個函數的定義,因此所有正則表達式實例默認是這個 String 方法的有效參數:
      console.log(RegExp.prototype[Symbol.search]); 
      // ? [Symbol.search]() { [native code] } console.log('foobar'.search(/bar/)); 
      // 3
      
    • 給這個方法傳入非正則表達式值會導致該值被轉換為 RegExp 對象。如果想改變這種行為,讓方法直接使用參數,可以重新定義 Symbol.search 函數以取代默認對正則表達式求值的行為,從而讓search()方法使用非正則表達式實例。Symbol.search 函數接收一個參數,就是調用 match()方法的字符串實例。返回的值沒有限制:
      class FooSearcher { static [Symbol.search](target) { return target.indexOf('foo'); } 
      } console.log('foobar'.search(FooSearcher)); // 0 
      console.log('barfoo'.search(FooSearcher)); // 3 
      console.log('barbaz'.search(FooSearcher)); // -1 class StringSearcher { constructor(str) { this.str = str; } [Symbol.search](target) { return target.indexOf(this.str); } 
      } console.log('foobar'.search(new StringSearcher('foo'))); // 0 
      console.log('barfoo'.search(new StringSearcher('foo'))); // 3 
      console.log('barbaz'.search(new StringSearcher('qux'))); // -1 
      
  47. Symbol.species

    • 根據 ECMAScript 規范,這個符號作為一個屬性表示“一個函數值,該函數作為創建派生對象的構造函數”。這個屬性在內置類型中最常用,用于對內置類型實例方法的返回值暴露實例化派生對象的方法。用 Symbol.species 定義靜態的獲取器(getter)方法,可以覆蓋新創建實例的原型定義:
      class Bar extends Array {} 
      class Baz extends Array { static get [Symbol.species]() { return Array; } 
      } let bar = new Bar(); 
      console.log(bar instanceof Array);  // true 
      console.log(bar instanceof Bar);    // true 
      bar = bar.concat('bar'); 
      console.log(bar instanceof Array);  // true 
      console.log(bar instanceof Bar);    // true let baz = new Baz(); 
      console.log(baz instanceof Array);  // true 
      console.log(baz instanceof Baz);    // true 
      baz = baz.concat('baz');            // concat 之后返回了一個新的 Array 實例,而不是 Baz 實例。這是因為在派生類中覆蓋了 Symbol.species 屬性,指定了新實例的構造函數為 Array。 
      console.log(baz instanceof Array);  // true 
      console.log(baz instanceof Baz);    // false 
      
      • 讀者注:concat() 方法用于連接兩個或多個數組。
  48. Symbol.split

    • 根據 ECMAScript 規范,這個符號作為一個屬性表示“一個正則表達式方法,該方法在匹配正則表達式的索引位置拆分字符串。由 String.prototype.split()方法使用”。String.prototype.split()方法會使用以 Symbol.split 為鍵的函數來對正則表達式求值。正則表達式的原型上默認有這個函數的定義,因此所有正則表達式實例默認是這個 String 方法的有效參數:
      console.log(RegExp.prototype[Symbol.split]); 
      // ? [Symbol.split]() { [native code] } console.log('foobarbaz'.split(/bar/)); 
      // ['foo', 'baz'] 
      
    • 給這個方法傳入非正則表達式值會導致該值被轉換為 RegExp 對象。如果想改變這種行為,讓方法直接使用參數,可以重新定義 Symbol.split 函數以取代默認對正則表達式求值的行為,從而讓 split() 方法使用非正則表達式實例。Symbol.split 函數接收一個參數,就是調用 match()方法的字符串實
      例。返回的值沒有限制:
      class FooSplitter { static [Symbol.split](target) { return target.split('foo'); } 
      } console.log('barfoobaz'.split(FooSplitter)); 
      // ["bar", "baz"] class StringSplitter { constructor(str) { this.str = str; } [Symbol.split](target) { return target.split(this.str); } 
      } console.log('barfoobaz'.split(new StringSplitter('foo'))); 
      // ["bar", "baz"] 
      
  49. Symbol.toPrimitive

    • 根據 ECMAScript 規范,這個符號作為一個屬性表示“一個方法,該方法將對象轉換為相應的原始值。由 ToPrimitive 抽象操作使用”。很多內置操作都會嘗試強制將對象轉換為原始值,包括字符串、數值和未指定的原始類型。對于一個自定義對象實例,通過在這個實例的 Symbol.toPrimitive 屬性上定義一個函數可以改變默認行為。
    • 根據提供給這個函數的參數(string、number 或 default),可以控制返回的原始值:
      class Foo {} 
      let foo = new Foo(); console.log(3 + foo);       // "3[object Object]" 
      console.log(3 - foo);       // NaN 
      console.log(String(foo));   // "[object Object]" class Bar { constructor() { this[Symbol.toPrimitive] = function(hint) { switch (hint) { case 'number': return 3; case 'string': return 'string bar'; case 'default': default: return 'default bar'; } } } 
      } 
      let bar = new Bar(); 
      console.log(3 + bar);       // "3default bar" 
      console.log(3 - bar);       // 0 
      console.log(String(bar));   // "string bar"
      
  50. Symbol.toStringTag

    • 根據 ECMAScript 規范,這個符號作為一個屬性表示“一個字符串,該字符串用于創建對象的默認字符串描述。由內置方法 Object.prototype.toString()使用”。
    • 通過 toString()方法獲取對象標識時,會檢索由 Symbol.toStringTag 指定的實例標識符,默認為"Object"。內置類型已經指定了這個值,但自定義類實例還需要明確定義:
      let s = new Set(); console.log(s);                     // Set(0) {} 
      console.log(s.toString());          // [object Set] 
      console.log(s[Symbol.toStringTag]); // Set class Foo {} 
      let foo = new Foo(); console.log(foo);                       // Foo {} 
      console.log(foo.toString());            // [object Object] 
      console.log(foo[Symbol.toStringTag]);   // undefined class Bar { constructor() { this[Symbol.toStringTag] = 'Bar'; } 
      } 
      let bar = new Bar(); console.log(bar);                       // Bar {} 
      console.log(bar.toString());            // [object Bar] 
      console.log(bar[Symbol.toStringTag]);   // Bar 
      
  51. Symbol.unscopables

    • 根據 ECMAScript 規范,這個符號作為一個屬性表示“一個對象,該對象所有的以及繼承的屬性,都會從關聯對象的 with 環境綁定中排除”。設置這個符號并讓其映射對應屬性的鍵值為 true,就可以阻止該屬性出現在 with 環境綁定中,如下例所示:
      let o = { foo: 'bar' }; 
      with (o) { console.log(foo); // bar 
      }o[Symbol.unscopables] = { foo: true 
      }; with (o) { console.log(foo); // ReferenceError 
      }
      
    • 不推薦使用 with,因此也不推薦使用 Symbol.unscopables。
  52. Object 類型

    • ECMAScript 中的對象其實就是一組數據和功能的集合。對象通過 new 操作符后跟對象類型的名稱來創建。開發者可以通過創建 Object 類型的實例來創建自己的對象,然后再給對象添加屬性和方法。
    • ECMAScript 只要求在給構造函數提供參數時使用括號。如果沒有參數,那么完全可以省略括號(不推薦):
      let o = new Object();
      let o = new Object; // 合法,但不推薦
      
    • Object 的實例本身并不是很有用,但理解與它相關的概念非常重要。類似 Java 中的 java.lang.Object,ECMAScript 中的 Object 也是派生其他對象的基類。Object 類型的所有屬性和方法在派生的對象上同樣存在。
    • 每個 Object 實例都有如下屬性和方法:
      • constructor:用于創建當前對象的函數。在前面的例子中,這個屬性的值就是 Object() 函數。
      • hasOwnProperty(propertyName):用于判斷當前對象實例(不是原型)上是否存在給定的屬性。要檢查的屬性名必須是字符串(如 o.hasOwnProperty(“name”))或符號。
      • isPrototypeOf(object):用于判斷當前對象是否為另一個對象的原型。
      • propertyIsEnumerable(propertyName):用于判斷給定的屬性是否可以使用 for-in 語句枚舉。與 hasOwnProperty()一樣,屬性名必須是字符串。
      • toLocaleString():返回對象的字符串表示,該字符串反映對象所在的本地化執行環境。
      • toString():返回對象的字符串表示。
      • valueOf():返回對象對應的字符串、數值或布爾值表示。通常與 toString()的返回值相同。
    • 因為在 ECMAScript 中 Object 是所有對象的基類,所以任何對象都有這些屬性和方法。
    • 注意,嚴格來講,ECMA-262 中對象的行為不一定適合 JavaScript 中的其他對象。比如瀏覽器環境中的 BOM 和 DOM 對象,都是由宿主環境定義和提供的宿主對象。而宿主對象不受 ECMA-262 約束,所以它們可能會也可能不會繼承 Object。
  53. ECMAScript 中的操作符是獨特的,因為它們可用于各種值,包括字符串、數值、布爾值,甚至還有對象。在應用給對象時,操作符通常會調用 valueOf()和/或 toString()方法來取得可以計算的值。

  54. 只操作一個值的操作符叫一元操作符(unary operator)。

  55. 遞增/遞減操作符

    • 遞增和遞減操作符直接照搬自 C 語言,但有兩個版本:前綴版和后綴版。
    • 無論使用前綴遞增還是前綴遞減操作符,變量的值都會在語句被求值之前改變。(在計算機科學中,這通常被稱為具有副作用。)
      let age = 29; 
      let anotherAge = --age + 2; 
      console.log(age);           // 28 
      console.log(anotherAge);    // 30let num1 = 2; 
      let num2 = 20; 
      let num3 = num1-- + num2; 
      let num4 = num1 + num2;
      console.log(num3); // 22 
      console.log(num4); // 21
      
    • 前綴遞增和遞減在語句中的優先級是相等的,因此會從左到右依次求值。
    • 遞增和遞減的后綴版語法一樣(分別是++和–),只不過要放在變量后面。后綴版與前綴版的主要區別在于,后綴版遞增和遞減在語句被求值后才發生。
    • 這 4 個操作符可以作用于任何值,意思是不限于整數——字符串、布爾值、浮點值,甚至對象都可以。遞增和遞減操作符遵循如下規則:
      • 對于字符串,如果是有效的數值形式,則轉換為數值再應用改變。變量類型從字符串變成數值。
      • 對于字符串,如果不是有效的數值形式,則將變量的值設置為 NaN 。變量類型從字符串變成數值。
      • 對于布爾值,如果是 false,則轉換為 0 再應用改變。變量類型從布爾值變成數值。
      • 對于布爾值,如果是 true,則轉換為 1 再應用改變。變量類型從布爾值變成數值。
      • 對于浮點值,加 1 或減 1。
      • 如果是對象,則調用其 valueOf() 方法取得可以操作的值。對得到的值應用上述規則。如果是 NaN,則調用 toString()并再次應用其他規則。變量類型從對象變成數值。
    • 下面的例子演示了這些規則:
      let s1 = "2"; 
      let s2 = "z"; 
      let b = false; 
      let f = 1.1; 
      let o = { valueOf() { return -1; } 
      }; s1++;   // 值變成數值 3 
      s2++;   // 值變成 NaN 
      b++;    // 值變成數值 1 
      f--;    // 值變成 0.10000000000000009(因為浮點數不精確)
      o--;    // 值變成-2
      
  56. 一元加和減

    • 一元加和減操作符在 ECMAScript 中跟在高中數學中的用途一樣。一元加由一個加號(+)表示,放在變量前頭,對數值沒有任何影響:
      let num = 25; 
      num = +num; 
      console.log(num); // 25
      
    • 如果將一元加應用到非數值,則會執行與使用 Number()轉型函數一樣的類型轉換:布爾值 false和 true 轉換為 0 和 1,字符串根據特殊規則進行解析,對象會調用它們的 valueOf()和/或 toString()方法以得到可以轉換的值。
    • 下面的例子演示了一元加在應用到不同數據類型時的行為:
      let s1 = "01"; 
      let s2 = "1.1";
      let s3 = "z"; 
      let b = false; 
      let f = 1.1; 
      let o = { valueOf() { return -1; } 
      };s1 = +s1;   // 值變成數值 1 
      s2 = +s2;   // 值變成數值 1.1 
      s3 = +s3;   // 值變成 NaN 
      b = +b;     // 值變成數值 0 
      f = +f;     // 不變,還是 1.1 
      o = +o;     // 值變成數值-1
      
      • 一元加和減操作符主要用于基本的算術,但也可以像上面的例子那樣,用于數據類型轉換。
  57. 位操作符

    • ECMAScript 中的所有數值都以 IEEE 754 64 位格式存儲,但位操作并不直接應用到 64 位表示,而是先把值轉換為 32 位整數,再進行位操作,之后再把結果轉換為 64 位。
    • 有符號整數使用 32 位的前 31 位表示整數值。第 32 位表示數值的符號,如 0 表示正,1 表示負。這一位稱為符號位(sign bit),它的值決定了數值其余部分的格式。正值以真正的二進制格式存儲,即 31位中的每一位都代表 2 的冪。負值以一種稱為二補數(或補碼)的二進制編碼存儲。
    • 一個數值的二補數通過如下 3 個步驟計算得到:
      • (1) 確定絕對值的二進制表示(如,對于-18,先確定 18 的二進制表示);
      • (2) 找到數值的一補數(或反碼),換句話說,就是每個 0 都變成 1,每個 1 都變成 0;
      • (3) 給結果加 1。
    • 要注意的是,在處理有符號整數時,我們無法訪問第 31 位。
    • 在把負值輸出為一個二進制字符串時,我們會得到一個前面加了減號的絕對值,如下所示:
      let num = -18; 
      console.log(num.toString(2)); // "-10010"
      
    • 注意,默認情況下,ECMAScript 中的所有整數都表示為有符號數。不過,確實存在無符號整數。對無符號整數來說,第 32 位不表示符號,因為只有正值。無符號整數比有符號整數的范圍更大,因為符號位被用來表示數值了。
    • 在對 ECMAScript 中的數值應用位操作符時,后臺會發生轉換:64 位數值會轉換為 32 位數值,然后執行位操作,最后再把結果從 32 位轉換為 64 位存儲起來。整個過程就像處理 32 位數值一樣,這讓二進制操作變得與其他語言中類似。但這個轉換也導致了一個奇特的副作用,即特殊值NaN 和Infinity 在位操作中都會被當成 0 處理。
    • 如果將位操作符應用到非數值,那么首先會使用 Number()函數將該值轉換為數值(這個過程是自動的),然后再應用位操作。最終結果是數值。
    • 按位非操作符用波浪符(~)表示,它的作用是返回數值的一補數。按位非的最終效果是對數值取反并減 1。
    • 按位與操作符用和號(&)表示,有兩個操作數。本質上,按位與就是將兩個數的每一個位對齊,然后基于真值表中的規則,對每一位執行相應的與操作。
    • 按位或操作符用管道符(|)表示,同樣有兩個操作數。
    • 按位異或用脫字符(^)表示,同樣有兩個操作數。按位異或與按位或的區別是,它只在一位上是 1 的時候返回 1(兩位都是 1 或 0,則返回 0)
    • 左移操作符用兩個小于號(<<)表示,會按照指定的位數將數值的所有位向左移動。注意在移位后,數值右端會空出位。左移會以 0 填充這些空位,讓結果是完整的 32 位數值。注意,左移會保留它所操作數值的符號。
    • 有符號右移由兩個大于號(>>)表示,會將數值的所有 32 位都向右移,同時保留符號(正或負)。有符號右移實際上是左移的逆運算。同樣,移位后就會出現空位。不過,右移后空位會出現在左側,且在符號位之后。ECMAScript 會用符號位的值來填充這些空位,以得到完整的數值。
    • 無符號右移用 3 個大于號表示(>>>),會將數值的所有 32 位都向右移。對于正數,無符號右移與有符號右移結果相同。對于負數,有時候差異會非常大。與有符號右移不同,無符號右移會給空位補 0,而不管符號位是什么。無符號右移操作符將負數的二進制表示當成正數的二進制表示來處理。因為負數是其絕對值的二補數,所以右移之后結果變得非常之大。
  58. 布爾操作符一共有 3 個:邏輯非、邏輯與和邏輯或。

  59. 邏輯非操作符由一個嘆號(!)表示,可應用給 ECMAScript 中的任何值。這個操作符始終返回布爾值,無論應用到的是什么數據類型。邏輯非操作符首先將操作數轉換為布爾值,然后再對其取反。

    • 邏輯非操作符會遵循如下規則:
      • 如果操作數是對象,則返回 false。
      • 如果操作數是空字符串,則返回 true。
      • 如果操作數是非空字符串,則返回 false。
      • 如果操作數是數值 0,則返回 true。
      • 如果操作數是非 0 數值(包括 Infinity),則返回 false。
      • 如果操作數是 null,則返回 true。
      • 如果操作數是 NaN,則返回 true。
      • 如果操作數是 undefined,則返回 true。
    • 以下示例驗證了上述行為:
      console.log(!false);    // true 
      console.log(!"blue");   // false 
      console.log(!0);        // true 
      console.log(!NaN);      // true 
      console.log(!"");       // true 
      console.log(!12345);    // false
      
    • 邏輯非操作符也可以用于把任意值轉換為布爾值。同時使用兩個嘆號(!!),相當于調用了轉型函數 Boolean()。無論操作數是什么類型,第一個嘆號總會返回布爾值。第二個嘆號對該布爾值取反,從而給出變量真正對應的布爾值。結果與對同一個值使用 Boolean()函數是一樣的:
      console.log(!!"blue");  // true 
      console.log(!!0);       // false 
      console.log(!!NaN);     // false 
      console.log(!!"");      // false 
      console.log(!!12345);   // true
      

59.邏輯與操作符由兩個和號(&&)表示,應用到兩個值。
- 邏輯與操作符可用于任何類型的操作數,不限于布爾值。如果有操作數不是布爾值,則邏輯與并不一定會返回布爾值,而是遵循如下規則:
- 如果第一個操作數是對象,則返回第二個操作數。
- 如果第二個操作數是對象,則只有第一個操作數求值為 true 才會返回該對象。
- 如果兩個操作數都是對象,則返回第二個操作數。
- 如果有一個操作數是 null,則返回 null。
- 如果有一個操作數是 NaN,則返回 NaN。
- 如果有一個操作數是 undefined,則返回 undefined。
- 邏輯與操作符是一種短路操作符,意思就是如果第一個操作數決定了結果,那么永遠不會對第二個操作數求值。對邏輯與操作符來說,如果第一個操作數是 false,那么無論第二個操作數是什么值,結果也不可能等于 true。
```
let found = true;
let result = (found && someUndeclaredVariable); // 這里會出錯
console.log(result); // 不會執行這一行

    let found = false; let result = (found && someUndeclaredVariable); // 不會出錯console.log(result); // 會執行```- 例子1:變量 found 的值是 true,邏輯與操作符會繼續求值變量 someUndeclaredVariable。但是由于 someUndeclaredVariable 沒有定義,不能對它應用邏輯與操作符,因此就報錯了。- 例子2:即使變量 someUndeclaredVariable 沒有定義,由于第一個操作數是 false,邏輯與操作符也不會對它求值,因為此時對&&右邊的操作數求值是沒有意義的。
  1. 邏輯或操作符由兩個管道符(||)表示。

    • 與邏輯與類似,如果有一個操作數不是布爾值,那么邏輯或操作符也不一定返回布爾值。它遵循如下規則:
      • 如果第一個操作數是對象,則返回第一個操作數。
      • 如果第一個操作數求值為 false,則返回第二個操作數。
      • 如果兩個操作數都是對象,則返回第一個操作數。
      • 如果兩個操作數都是 null,則返回 null。
      • 如果兩個操作數都是 NaN,則返回 NaN。
      • 如果兩個操作數都是 undefined,則返回 undefined。
    • 邏輯或操作符也具有短路的特性。對邏輯或而言,第一個操作數求值為true,第二個操作數就不會再被求值了。**利用這個行為,可以避免給變量賦值 null 或 undefined。**比如:
      let myObject = preferredObject || backupObject;
      
      • 在這個例子中,變量 myObject 會被賦予兩個值中的一個。其中,preferredObject 變量包含首選的值,backupObject 變量包含備用的值。如果 preferredObject 不是 null,則它的值就會賦給myObject;如果 preferredObject 是 null,則 backupObject 的值就會賦給 myObject。這種模式在 ECMAScript 代碼中經常用于變量賦值。
  2. ECMAScript 定義了 3 個乘性操作符:乘法、除法和取模。這些操作符跟它們在 Java、C 語言及 Perl中對應的操作符作用一樣,但在處理非數值時,它們也會包含一些自動的類型轉換。如果乘性操作符有不是數值的操作數,則該操作數會在后臺被使用 Number()轉型函數轉換為數值。這意味著空字符串會被當成 0,而布爾值 true 會被當成 1。

  3. 乘法操作符由一個星號(*)表示,可以用于計算兩個數值的乘積。其語法類似于 C 語言。不過,乘法操作符在處理特殊值時也有一些特殊的行為:

    • 如果操作數都是數值,則執行常規的乘法運算,即兩個正值相乘是正值,兩個負值相乘也是正值,正負符號不同的值相乘得到負值。如果 ECMAScript 不能表示乘積,則返回 Infinity 或 -Infinity。
    • 如果有任一操作數是 NaN,則返回 NaN。
    • 如果是 Infinity 乘以 0,則返回 NaN。
    • 如果是 Infinity 乘以非 0的有限數值,則根據第二個操作數的符號返回 Infinity 或-Infinity。
    • 如果是 Infinity 乘以 Infinity,則返回 Infinity。
    • 如果有不是數值的操作數,則先在后臺用 Number()將其轉換為數值,然后再應用上述規則。
  4. 除法操作符由一個斜杠(/)表示,用于計算第一個操作數除以第二個操作數的商。跟乘法操作符一樣,除法操作符針對特殊值也有一些特殊的行為:

    • 如果操作數都是數值,則執行常規的除法運算,即兩個正值相除是正值,兩個負值相除也是正值,符號不同的值相除得到負值。如果ECMAScript不能表示商,則返回Infinity或-Infinity。
    • 如果有任一操作數是 NaN,則返回 NaN。
    • 如果是 Infinity 除以 Infinity,則返回 NaN。
    • 如果是 0 除以 0,則返回 NaN。
    • 如果是非 0 的有限值除以 0,則根據第一個操作數的符號返回 Infinity 或-Infinity。
    • 如果是 Infinity 除以任何數值,則根據第二個操作數的符號返回 Infinity 或-Infinity。
    • 如果有不是數值的操作數,則先在后臺用 Number()函數將其轉換為數值,然后再應用上述規則。
  5. 取模(余數)操作符由一個百分比符號(%)表示。與其他乘性操作符一樣,取模操作符對特殊值也有一些特殊的行為:

    • 如果操作數是數值,則執行常規除法運算,返回余數。
    • 如果被除數是無限值,除數是有限值,則返回 NaN。
    • 如果被除數是有限值,除數是 0,則返回 NaN。
    • 如果是 Infinity 除以 Infinity,則返回 NaN。
    • 如果被除數是有限值,除數是無限值,則返回被除數。
    • 如果被除數是 0,除數不是 0,則返回 0。
    • 如果有不是數值的操作數,則先在后臺用 Number()函數將其轉換為數值,然后再應用上述規則。
  6. 指數操作符

    • ECMAScript 7 新增了指數操作符,Math.pow()現在有了自己的操作符**,結果是一樣的:
      console.log(Math.pow(3, 2); // 9 
      console.log(3 ** 2);        // 9 console.log(Math.pow(16, 0.5);  // 4 
      console.log(16** 0.5);          // 4
      
    • 指數操作符也有自己的指數賦值操作符**=,該操作符執行指數運算和結果的賦值操作:
      let squared = 3; 
      squared **= 2; 
      console.log(squared);   // 9let sqrt = 16; 
      sqrt **= 0.5; 
      console.log(sqrt);      // 4
      
  7. 加性操作符,即加法和減法操作符。不過,在 ECMAScript 中,這兩個操作符擁有一些特殊的行為。與乘性操作符類似,加性操作符在后臺會發生不同數據類型的轉換。只不過對這兩個操作符來說,轉換規則不是那么直觀。

  8. 加法操作符(+)用于求兩個數的和。

    • 如果兩個操作數都是數值,加法操作符執行加法運算并根據如下規則返回結果:
      • 如果有任一操作數是 NaN,則返回 NaN;
      • 如果是 Infinity 加 Infinity,則返回 Infinity;
      • 如果是-Infinity 加-Infinity,則返回-Infinity;
      • 如果是 Infinity 加-Infinity,則返回 NaN;
      • 如果是+0 加+0,則返回+0;
      • 如果是-0 加+0,則返回+0;
      • 如果是-0 加-0,則返回-0。
    • 不過,如果有一個操作數是字符串,則要應用如下規則:
      • 如果兩個操作數都是字符串,則將第二個字符串拼接到第一個字符串后面;
      • 如果只有一個操作數是字符串,則將另一個操作數轉換為字符串,再將兩個字符串拼接在一起。
    • 如果有任一操作數是對象、數值或布爾值,則調用它們的 toString()方法以獲取字符串,然后再應用前面的關于字符串的規則。對于 undefined 和 null,則調用 String()函數,分別獲取"undefined"和"null"。
      let result1 = 5 + 5;        // 兩個數值
      console.log(result1);       // 10 let result2 = 5 + "5";      // 一個數值和一個字符串
      console.log(result2);       // "55",第一個操作數被轉換為字符串
      
    • **ECMAScript 中最常犯的一個錯誤,就是忽略加法操作中涉及的數據類型。**比如:
      let num1 = 5; 
      let num2 = 10; 
      let message = "The sum of 5 and 10 is " + num1 + num2; 
      console.log(message); // "The sum of 5 and 10 is 510"let message = "The sum of 5 and 10 is " + (num1 + num2); 
      console.log(message); // "The sum of 5 and 10 is 15"
      
  9. 減法操作符(-)

    • 與加法操作符一樣,減法操作符也有一組規則用于處理 ECMAScript 中不同類型之間的轉換。
      • 如果兩個操作數都是數值,則執行數學減法運算并返回結果。
      • 如果有任一操作數是 NaN,則返回 NaN。
      • 如果是 Infinity 減 Infinity,則返回 NaN。
      • 如果是-Infinity 減-Infinity,則返回 NaN。
      • 如果是 Infinity 減-Infinity,則返回 Infinity。
      • 如果是-Infinity 減 Infinity,則返回-Infinity。
      • 如果是+0 減+0,則返回+0。
      • 如果是+0 減-0,則返回-0。
      • 如果是-0 減-0,則返回+0。
      • **如果有任一操作數是字符串、布爾值、null 或 undefined,則先在后臺使用 Number()將其轉換為數值,然后再根據前面的規則執行數學運算。**如果轉換結果是 NaN,則減法計算的結果是NaN。
      • **如果有任一操作數是對象,則調用其 valueOf()方法取得表示它的數值。**如果該值是 NaN,則減法計算的結果是 NaN。如果對象沒有 valueOf()方法,則調用其 toString()方法,然后再將得到的字符串轉換為數值。
      • 讀者備注:注意和加法是不一樣的。
    • 以下示例演示了上面的規則:
      let result1 = 5 - true;     // true 被轉換為 1,所以結果是 4 
      let result2 = NaN - 1;      // NaN 
      let result3 = 5 - 3;        // 2 
      let result4 = 5 - "";       // ""被轉換為 0,所以結果是 5 
      let result5 = 5 - "2";      // "2"被轉換為 2,所以結果是 3 
      let result6 = 5 - null;     // null 被轉換為 0,所以結果是 5
      
  10. 關系操作符執行比較兩個值的操作,包括小于(<)、大于(>)、小于等于(<=)和大于等于(>=),用法跟數學課上學的一樣。這幾個操作符都返回布爾值。

    • 與 ECMAScript 中的其他操作符一樣,在將它們應用到不同數據類型時也會發生類型轉換和其他行為:
      • 如果操作數都是數值,則執行數值比較。
      • 如果操作數都是字符串,則逐個比較字符串中對應字符的編碼。
      • 如果有任一操作數是數值,則將另一個操作數轉換為數值,執行數值比較。
      • 如果有任一操作數是對象,則調用其 valueOf()方法,取得結果后再根據前面的規則執行比較。如果沒有 valueOf()操作符,則調用 toString()方法,取得結果后再根據前面的規則執行比較。
      • 如果有任一操作數是布爾值,則將其轉換為數值再執行比較。
    • 對字符串而言,關系操作符會比較字符串中對應字符的編碼,而這些編碼是數值。比較完之后,會返回布爾值。問題的關鍵在于,大寫字母的編碼都小于小寫字母的編碼。
      let result = "Brick" < "alphabet";  // true
      let result = "Brick".toLowerCase() < "alphabet".toLowerCase(); // falselet result = "23" < "3"; // true
      let result = "23" < 3;   // false(將字符串"23"轉換為數值 23,然后再跟 3 比較)
      
      • 字母 B 的編碼是 66,字母 a 的編碼是 97。
      • 要得到確實按字母順序比較的結果,就必須把兩者都轉換為相同的大小寫形式(全大寫或全小寫),然后再比較。
      • 只要是數值和字符串比較,字符串就會先被轉換為數值,然后進行數值比較。對于數值字符串而言,這樣能保證結果正確。
    • 任何關系操作符在涉及比較 NaN 時都返回 false。
      let result = "a" < 3;   // 因為"a"會轉換為 NaN,所以結果是 falselet result1 = NaN < 3;  // false 
      let result2 = NaN >= 3; // false
      
      • 因為字符"a"不能轉換成任何有意義的數值,所以只能轉換為 NaN。
      • 在大多數比較的場景中,如果一個值不小于另一個值,那就一定大于或等于它。但在比較 NaN 時,無論是小于還是大于等于,比較的結果都會返回 false。
  11. ECMAScript 提供了兩組操作符。第一組是等于和不等于,它們在比較之前執行轉換。第二組是全等和不全等,它們在比較之前不執行轉換。

  12. ECMAScript 中的等于操作符用兩個等于號(==)表示,如果操作數相等,則會返回 true。不等于操作符用嘆號和等于號(!=)表示,如果兩個操作數不相等,則會返回 true。這兩個操作符都會先進行類型轉換(通常稱為強制類型轉換)再確定操作數是否相等。

    • 在轉換操作數的類型時,相等和不相等操作符遵循如下規則:

      • 如果任一操作數是布爾值,則將其轉換為數值再比較是否相等。false 轉換為 0,true 轉換為 1。
      • 如果一個操作數是字符串,另一個操作數是數值,則嘗試將字符串轉換為數值,再比較是否相等。
      • 如果一個操作數是對象,另一個操作數不是,則調用對象的 valueOf()方法取得其原始值,再根據前面的規則進行比較。
    • 在進行比較時,這兩個操作符會遵循如下規則:

      • null 和 undefined 相等。
      • null 和 undefined 不能轉換為其他類型的值再進行比較。
      • 如果有任一操作數是 NaN,則相等操作符返回 false,不相等操作符返回 true。記住:即使兩個操作數都是 NaN,相等操作符也返回 false,因為按照規則,NaN 不等于 NaN。
      • 如果兩個操作數都是對象,則比較它們是不是同一個對象。如果兩個操作數都指向同一個對象,則相等操作符返回 true。否則,兩者不相等。
    • 下表總結了一些特殊情況及比較的結果:

      表達式結果
      null == undefinedtrue
      “NaN” == NaNfalse
      5 == NaNfalse
      NaN == NaNfalse
      NaN != NaNtrue
      false == 0true
      true == 1true
      true == 2false
      undefined == 0false
      null == 0false
      “5” == 5true
  13. 全等和不全等操作符與相等和不相等操作符類似,只不過它們在比較相等時不轉換操作數。全等操作符由 3 個等于號(===)表示,只有兩個操作數在不轉換的前提下相等才返回 true。不全等操作符用一個嘆號和兩個等于號(!==)表示,只有兩個操作數在不轉換的前提下不相等才返回 true。

    let result1 = ("55" == 55);     // true,轉換后相等
    let result2 = ("55" === 55);    // false,不相等,因為數據類型不同let result1 = ("55" != 55);     // false,轉換后相等
    let result2 = ("55" !== 55);    // true,不相等,因為數據類型不同
    
    • 雖然 null == undefined 是 true(因為這兩個值類似),但 null === undefined 是 false,因為它們不是相同的數據類型。
    • 注意,由于相等和不相等操作符存在類型轉換問題,因此推薦使用全等和不全等操作符。這樣有助于在代碼中保持數據類型的完整性。
  14. 條件操作符是 ECMAScript 中用途最為廣泛的操作符之一,語法跟 Java 中一樣:

    variable = boolean_expression ? true_value : false_value;let max = (num1 > num2) ? num1 : num2;
    
  15. 賦值操作符

    • 簡單賦值用等于號(=)表示,將右手邊的值賦給左手邊的變量。
      let num = 10;
      
    • 復合賦值使用乘性、加性或位操作符后跟等于號(=)表示。
      let num = 10; 
      num += 10;
      
    • 每個數學操作符以及其他一些操作符都有對應的復合賦值操作符:
      • 乘后賦值(*=)
      • 除后賦值(/=)
      • 取模后賦值(%=)
      • 加后賦值(+=)
      • 減后賦值(-=)
      • 左移后賦值(<<=)
      • 右移后賦值(>>=)
      • 無符號右移后賦值(>>>=)
    • 這些操作符僅僅是簡寫語法,使用它們不會提升性能。
  16. 逗號操作符

    • 逗號操作符可以用來在一條語句中執行多個操作:
      let num1 = 1, num2 = 2, num3 = 3;
      
      • 在一條語句中同時聲明多個變量是逗號操作符最常用的場景。
    • 也可以使用逗號操作符來輔助賦值。在賦值時使用逗號操作符分隔值,最終會返回表達式中最后一個值:
      let num = (5, 1, 4, 8, 0); // num 的值為 0
      
  17. if 語句

    • 語法如下:
      if (condition) statement1 else statement2
      
      • 這里的條件(condition)可以是任何表達式,并且求值結果不一定是布爾值。ECMAScript 會自動調用 Boolean()函數將這個表達式的值轉換為布爾值。
    • 可以像這樣連續使用多個 if 語句:
      if (condition1) statement1 else if (condition2) statement2 else statement3if (i > 25) { console.log("Greater than 25."); 
      } else if (i < 0) { console.log("Less than 0."); 
      } else { console.log("Between 0 and 25, inclusive."); 
      }
      
  18. do-while 語句

    • do-while 語句是一種后測試循環語句,即循環體中的代碼執行后才會對退出條件進行求值。換句話說,循環體內的代碼至少執行一次。
    • 語法如下:
      do { statement 
      } while (expression);
      
    • 注意,后測試循環經常用于這種情形:循環體內代碼在退出前至少要執行一次。
  19. while 語句

    • while 語句是一種先測試循環語句,即先檢測退出條件,再執行循環體內的代碼。因此,while 循環體內的代碼有可能不會執行。
    • 語法如下:
      while(expression) statement
      
  20. for 語句

    • for 語句也是先測試語句,只不過增加了進入循環之前的初始化代碼,以及循環執行后要執行的表達式,語法如下:
      for (initialization; expression; post-loop-expression) statement
      
    • 無法通過 while 循環實現的邏輯,同樣也無法使用 for 循環實現。
    • 初始化、條件表達式和循環后表達式都不是必需的。
  21. for-in 語句

    • for-in 語句是一種嚴格的迭代語句,**用于枚舉對象中的非符號鍵屬性,**語法如下:
      for (property in expression) statement
      
    • 例子:
      for (const propName in window) { document.write(propName); 
      }
      
      • 這個例子使用 for-in 循環顯示了 BOM 對象 window 的所有屬性。每次執行循環,都會給變量 propName 賦予一個 window 對象的屬性作為值,直到 window 的所有屬性都被枚舉一遍。
      • 與 for 循環一樣,這里控制語句中的 const 也不是必需的。但為了確保這個局部變量不被修改,推薦使用 const。
    • ECMAScript 中對象的屬性是無序的,因此 for-in 語句不能保證返回對象屬性的順序。換句話說,所有可枚舉的屬性都會返回一次,但返回的順序可能會因瀏覽器而異。
    • 如果 for-in 循環要迭代的變量是 null 或 undefined,則不執行循環體。
  22. for-of 語句

    • for-of 語句是一種嚴格的迭代語句,**用于遍歷可迭代對象的元素,**語法如下:
      for (property of expression) statement
      
    • 例子:
      for (const el of [2,4,6,8]) { document.write(el); 
      }
      
      • 在這個例子中,我們使用 for-of 語句顯示了一個包含 4 個元素的數組中的所有元素。循環會一直持續到將所有元素都迭代完。與 for 循環一樣,這里控制語句中的 const 也不是必需的。但為了確保這個局部變量不被修改,推薦使用 const。
    • for-of 循環會按照可迭代對象的 next()方法產生值的順序迭代元素。
    • 如果嘗試迭代的變量不支持迭代,則 for-of 語句會拋出錯誤。
    • 注意,ES2018 對 for-of 語句進行了擴展,增加了 for-await-of 循環,以支持生成期約(promise)的異步可迭代對象。
  23. 標簽語句

    • 標簽語句用于給語句加標簽,語法如下:
      label: statement
      
    • 例子:
      start: for (let i = 0; i < count; i++) { console.log(i); 
      }
      
      • 在這個例子中,start 是一個標簽,可以在后面通過 break 或 continue 語句引用。
    • 標簽語句的典型應用場景是嵌套循環。
  24. break 和 continue 語句

    • break 語句用于立即退出循環,強制執行循環后的下一條語句。而 continue 語句也用于立即退出循環,但會再次從循環頂部開始執行。
    • break 和 continue 都可以與標簽語句一起使用,返回代碼中特定的位置。這通常是在嵌套循環中,如下面的例子所示:
      let num = 0; 
      outermost: 
      for (let i = 0; i < 10; i++) { for (let j = 0; j < 10; j++) { if (i == 5 && j == 5) { break outermost; } num++; } 
      } 
      console.log(num); // 55
      
      • 在這個例子中,outermost 標簽標識的是第一個 for 語句。
      • break 語句帶來了一個變數,即要退出到的標簽。添加標簽不僅讓 break 退出(使用變量 j 的)內部循環,也會退出(使用變量 i 的)外部循環。
    • continue 語句也可以使用標簽,如下面的例子所示:
      let num = 0; 
      outermost: 
      for (let i = 0; i < 10; i++) { for (let j = 0; j < 10; j++) {if (i == 5 && j == 5) { continue outermost; } num++; } 
      } 
      console.log(num); // 95
      
      • 這一次,continue 語句會強制循環繼續執行,但不是繼續執行內部循環,而是繼續執行外部循環。當 i 和 j 都等于 5 時,會執行 continue,跳到外部循環繼續執行,從而導致內部循環少執行 5 次,結果 num 等于 95。
    • 組合使用標簽語句和 break、continue 能實現復雜的邏輯,但也容易出錯。注意標簽要使用描述性強的文本,而嵌套也不要太深。
  25. with 語句

    • with 語句的用途是將代碼作用域設置為特定的對象,其語法是:
      with (expression) statement;
      
    • 使用 with 語句的主要場景是針對一個對象反復操作,這時候將代碼作用域設置為該對象能提供便利,如下面的例子所示:
      let qs = location.search.substring(1); 
      let hostName = location.hostname; 
      let url = location.href;
      
    • 上面代碼中的每一行都用到了 location 對象。如果使用 with 語句,就可以少寫一些代碼:
      with(location) { let qs = search.substring(1); let hostName = hostname; let url = href; 
      }
      
      • 這里,with 語句用于連接 location 對象。這意味著在這個語句內部,每個變量首先會被認為是一個局部變量。如果沒有找到該局部變量,則會搜索 location 對象,看它是否有一個同名的屬性。如果有,則該變量會被求值為 location 對象的屬性。
    • 嚴格模式不允許使用 with 語句,否則會拋出錯誤。
    • 警告,由于 with 語句影響性能且難于調試其中的代碼,通常不推薦在產品代碼中使用 with語句。
  26. switch 語句

    • ECMAScript中 switch 語句跟 C 語言中 switch 語句的語法非常相似,如下所示:
      switch (expression) { case value1: statementbreak; case value2: statement break; case value3: statement break; case value4: statement break; default: statement 
      }
      
    • 為避免不必要的條件判斷,最好給每個條件后面都加上 break 語句。如果確實需要連續匹配幾個條件,那么推薦寫個注釋表明是故意忽略了 break,如下所示:
      switch (i) { case 25: /*跳過*/ case 35: console.log("25 or 35"); break; case 45: console.log("45"); break; default:console.log("Other"); 
      }
      
    • 雖然 switch 語句是從其他語言借鑒過來的,但 ECMAScript 為它賦予了一些獨有的特性。首先,switch 語句可以用于所有數據類型(在很多語言中,它只能用于數值),因此可以使用字符串甚至對象。其次,條件的值不需要是常量,也可以是變量或表達式。看下面的例子:
      switch ("hello world") { case "hello" + " world": console.log("Greeting was found."); break; case "goodbye": console.log("Closing was found."); break; default: console.log("Unexpected message was found."); 
      }
      
    • 能夠在條件判斷中使用表達式,就可以在判斷中加入更多邏輯:
      let num = 25; 
      switch (true) { case num < 0: console.log("Less than 0."); break; case num >= 0 && num <= 10: console.log("Between 0 and 10."); break; case num > 10 && num <= 20: console.log("Between 10 and 20."); break; default: console.log("More than 20."); 
      }
      
      • 上面的代碼首先在外部定義了變量 num,而傳給 switch 語句的參數之所以是 true,就是因為每個條件的表達式都會返回布爾值。條件的表達式分別被求值,直到有表達式返回 true;否則,就會一直跳到 default 語句(這個例子正是如此)。
    • 注意,switch 語句在比較每個條件的值時會使用全等操作符,因此不會強制轉換數據類型(比如,字符串"10"不等于數值 10)
  27. ECMAScript 中的函數使用 function 關鍵字聲明,后跟一組參數,然后是函數體。

    • 以下是函數的基本語法:
      function functionName(arg0, arg1,...,argN) { statements 
      }
      
    • 可以通過函數名來調用函數,要傳給函數的參數放在括號里(如果有多個參數,則用逗號隔開)。
    • ECMAScript 中的函數不需要指定是否返回值。任何函數在任何時間都可以使用 return 語句來返回函數的值,用法是后跟要返回的值。比如:
      function sum(num1, num2) { return num1 + num2; 
      }const result = sum(5, 10);
      
      • 注意,除了 return 語句之外沒有任何特殊聲明表明該函數有返回值。
    • 要注意的是,只要碰到 return 語句,函數就會立即停止執行并退出。因此,return 語句后面的代碼不會被執行。
    • 一個函數里也可以有多個 return 語句,例如if-else每個分支都有自己的return語句。
    • return 語句也可以不帶返回值。這時候,函數會立即停止執行并返回 undefined。這種用法最常用于提前終止函數執行,并不是為了返回值。
    • 注意,最佳實踐是函數要么返回值,要么不返回值。只在某個條件下返回值的函數會帶來麻煩,尤其是調試時。
    • 嚴格模式對函數也有一些限制:
      • 函數不能以 eval 或 arguments 作為名稱;
      • 函數的參數不能叫 eval 或 arguments;
      • 兩個命名參數不能擁有同一個名稱。
    • 如果違反上述規則,則會導致語法錯誤,代碼也不會執行。
  28. 本章總結

    • JavaScript 的核心語言特性在 ECMA-262 中以偽語言 ECMAScript 的形式來定義。ECMAScript 包含所有基本語法、操作符、數據類型和對象,能完成基本的計算任務,但沒有提供獲得輸入和產生輸出的機制。
    • 下面總結一下 ECMAScript 中的基本元素。
      • ECMAScript 中的基本數據類型包括 Undefined、Null、Boolean、Number、String 和 Symbol。
      • 與其他語言不同,ECMAScript 不區分整數和浮點值,只有 Number 一種數值數據類型。
      • Object 是一種復雜數據類型,它是這門語言中所有對象的基類。
      • 嚴格模式為這門語言中某些容易出錯的部分施加了限制。
      • ECMAScript 提供了 C 語言和類 C 語言中常見的很多基本操作符,包括數學操作符、布爾操作符、關系操作符、相等操作符和賦值操作符等。
      • 這門語言中的流控制語句大多是從其他語言中借鑒而來的,比如 if 語句、for 語句和 switch 語句等。
    • ECMAScript 中的函數與其他語言中的函數不一樣。
      • 不需要指定函數的返回值,因為任何函數可以在任何時候返回任何值。
      • 不指定返回值的函數實際上會返回特殊值 undefined

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/894696.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/894696.shtml
英文地址,請注明出處:http://en.pswp.cn/news/894696.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【前端】【Ts】【知識點總結】TypeScript知識總結

一、總體概述 TypeScript 是 JavaScript 的超集&#xff0c;主要通過靜態類型檢查和豐富的類型系統來提高代碼的健壯性和可維護性。它涵蓋了從基礎數據類型到高級類型、從函數與對象的類型定義到類、接口、泛型、模塊化及裝飾器等眾多知識點。掌握這些內容有助于編寫更清晰、結…

基于Springboot+vue的租車網站系統

基于SpringbootVue的租車網站系統是一個現代化的在線租車平臺&#xff0c;它結合了Springboot的后端開發能力和Vue的前端交互優勢&#xff0c;為用戶和汽車租賃公司提供了一個高效、便捷、易用的租車體驗和管理工具。以下是對該系統的詳細介紹&#xff1a; 一、系統架構 ?后…

藍橋杯之c++入門(二)【輸入輸出(上)】

目錄 前言1&#xff0e;getchar和 putchar1.1 getchar()1.2 putchar() 2&#xff0e;scanf和 printf2.1 printf2.1.1基本用法2.1.2占位符2.1.3格式化輸出2.1.3.1 限定寬度2.1.3.2 限定小數位數 2.2 scanf2.2.1基本用法2.2.2 占位符2.2.3 scanf的返回值 2.3練習練習1&#xff1a…

我的鴻蒙學習之旅:探索萬物互聯的新宇宙

在科技飛速發展的今天&#xff0c;操作系統領域的創新層出不窮。華為鴻蒙系統的出現&#xff0c;猶如一顆璀璨的新星&#xff0c;照亮了萬物互聯的未來之路。懷著對新技術的好奇與渴望&#xff0c;我踏上了學習鴻蒙的征程&#xff0c;這段經歷充滿了挑戰與驚喜&#xff0c;也讓…

Docker數據卷管理及優化

一、基礎概念 1.docker數據卷是一個可供容器使用的特殊目錄&#xff0c;它繞過了容器的文件系統&#xff0c;直接將數據存在宿主機上。 2.docker數據卷的作用&#xff1a; 數據持久化&#xff1a;即使容器被刪除或重建數據卷中的數據仍然存在 數據共享&#xff1a;多個容器可以…

java:mysql切換達夢數據庫(五分鐘適配完成)

背景 因為項目需要國產數據庫的支持&#xff0c;選擇了達夢數據庫&#xff0c;由于我們之前使用的是MySQL今天我們就來說一說&#xff0c;如何快速的切換到達夢數據庫&#xff0c;原本這一章我打算寫VIP章節的后續想想&#xff0c;就純分享。畢竟是國產數據庫遷移數據庫 這里…

在游戲本(6G顯存)上本地部署Deepseek,運行一個14B大語言模型,并使用API訪問

在游戲本6G顯存上本地部署Deepseek&#xff0c;運行一個14B大語言模型&#xff0c;并使用API訪問 環境說明環境準備下載lmstudio運行lmstudio 下載模型從huggingface.co下載模型 配置模型加載模型測試模型API啟動API服務代碼測試 deepseek在大語言模型上的進步確實不錯&#xf…

[leetcode]兩數之和等于target

源代碼 #include <iostream> #include <list> #include <iterator> // for std::prev using namespace std; int main() { int target 9; list<int> l{ 2, 3, 4, 6, 8 }; l.sort(); // 確保列表是排序的&#xff0c;因為雙指針法要求輸入是…

C# OpenCV機器視覺:學生注意力監測

小王是一位充滿活力的年輕教師&#xff0c;剛接手了一個新班級。他滿心歡喜地準備在課堂上大顯身手&#xff0c;把自己的知識毫無保留地傳授給學生。可沒上幾節課&#xff0c;他就發現了一個讓人頭疼的問題&#xff1a;課堂上總有那么幾個學生注意力不集中&#xff0c;要么偷偷…

DeepSeek R1技術報告關鍵解析(6/10):DeepSeek-R1 vs. OpenAI-o1-1217:性能對比分析

1. 為什么要對比 DeepSeek-R1 和 OpenAI-o1-1217&#xff1f; 在當前的大模型競爭中&#xff0c;OpenAI 的 o1-1217 被認為是推理能力較強的模型之一。 而 DeepSeek-R1 作為一個采用強化學習優化推理能力的開源模型&#xff0c;其性能是否能夠與 OpenAI-o1-1217 競爭&#xf…

PyQt6/PySide6 的 QTableView 類

QTableView 是 PyQt6 或 PySide6 庫中用于顯示二維表格數據的控件。它是一個非常強大且靈活的控件&#xff0c;適用于展示和編輯表格數據。QTableView 通常與 QAbstractItemModel 的子類&#xff08;如 QStandardItemModel 或自定義模型&#xff09;一起使用&#xff0c;以提供…

【嵌入式】C語言多文件編程與內聯函數

文章目錄 0 前言1 從C語言編譯說起2 重復定義錯誤&#xff08;ODR violation&#xff09;和條件編譯3 內聯函數inline和static inline4 總結 0 前言 最近在研究ARM內核代碼時&#xff0c;看到core_cm3.h中有大量的內聯函數&#xff0c;為此查閱了很多資料&#xff0c;也和朋友討…

10分鐘本地部署Deepseek-R1

10分鐘本地部署DeepSeek-R1 什么是DeepSeek-R1快速本地部署DeepSeek-R1Ollama下載Ollama安裝檢查是否安裝成功 安裝DeepSeek-R1模型模型使用測試 什么是DeepSeek-R1 DeepSeek-R1是中國的深度求索&#xff08;DeepSeek&#xff09;公司開發的智能助手。其具有極佳的語義理解和生…

Office / WPS 公式、Mathtype 公式輸入花體字、空心字

注&#xff1a;引文主要看注意事項。 1、Office / WPS 公式中字體轉換 花體字 字體選擇 “Eulid Math One” 空心字 字體選擇 “Eulid Math Two” 2、Mathtype 公式輸入花體字、空心字 2.1 直接輸入 花體字 在 mathtype 中直接輸入 \mathcal{L} L \Large \mathcal{L} L…

Python小游戲29乒乓球

import pygame import sys # 初始化pygame pygame.init() # 屏幕大小 screen_width 800 screen_height 600 screen pygame.display.set_mode((screen_width, screen_height)) pygame.display.set_caption("打乒乓球") # 顏色定義 WHITE (255, 255, 255) BLACK (…

【C++】STL——vector底層實現

目錄 &#x1f495; 1.vector三個核心 &#x1f495;2.begin函數&#xff0c;end函數的實現&#xff08;簡單略講&#xff09; &#x1f495;3.size函數&#xff0c;capacity函數的實現 &#xff08;簡單略講&#xff09; &#x1f495;4.reserve函數實現 &#xff08;細節…

7、怎么定義一個簡單的自動化測試框架?

定義一個簡單的自動化測試框架可以從需求理解、框架設計、核心模塊實現、測試用例編寫和集成執行等方面入手&#xff0c;以下為你詳細介紹&#xff1a; 1. 明確框架需求和范圍 確定測試類型&#xff1a;明確框架要支持的測試類型&#xff0c;如單元測試、接口測試、UI 測試等…

安卓(android)讀取手機通訊錄【Android移動開發基礎案例教程(第2版)黑馬程序員】

一、實驗目的&#xff08;如果代碼有錯漏&#xff0c;可在代碼地址查看&#xff09; 1.熟悉內容提供者(Content Provider)的概念和作用。 2.掌握內容提供者的創建和使用方法。 4.掌握內容URI的結構和用途。 二、實驗條件 1.熟悉內容提供者的工作原理。 2.掌握內容提供者訪問其…

AI取代人類?

每周跟蹤AI熱點新聞動向和震撼發展 想要探索生成式人工智能的前沿進展嗎&#xff1f;訂閱我們的簡報&#xff0c;深入解析最新的技術突破、實際應用案例和未來的趨勢。與全球數同行一同&#xff0c;從行業內部的深度分析和實用指南中受益。不要錯過這個機會&#xff0c;成為AI領…

C語言-----數據結構從門到精通

1.數據結構基本概念 數據結構是計算機中存儲、組織數據的方式&#xff0c;旨在提高數據的訪問和操作效率。它是實現高效算法和程序設計的基石。 目標:通過思維導圖了解數據結構的知識點,并掌握。 1.1邏輯結構 邏輯結構主要四種類型: 集合&#xff1a;結構中的數據元素之…