JavaScript高階程式設計讀後感(一)之零碎知識點查漏補缺

Echoyya、 發表於 2021-11-25
JavaScript

1-script延遲指令碼defer及非同步指令碼async,區別及應用場景

  1. defer和async在讀取下載時是一樣的,相對於html解析來說都是非同步的。
  2. 區別是下載完後執行時間
  3. defer:立即下載,延遲執行。是最接近我們對指令碼載入和執行要求的,要在所有元素解析完成之後,DOMContentLoaded 事件觸發之前完成。但最好只包含一個。
  4. async:立即下載,立即執行。不管宣告順序如何,只要載入完就會立即執行。用處不大,因為它完全不考慮依賴,不過它對於那些可以不依賴任何指令碼或不被任何指令碼依賴的指令碼來說卻是非常合適的

defer:指令碼程式碼依賴於頁面中的DOM元素(文件是否解析完畢),或者被其他指令碼檔案依賴。

async:指令碼並不關心頁面中的DOM元素(文件是否解析完畢),並且也不會被其他指令碼檔案依賴。

2-未宣告的變數,未初始化變數

  1. 未宣告的變數,只能進行typeof操作符檢測其資料型別,而delete操作本身不會導致錯誤,但並沒有意義,且嚴格模式下會拋異常
  2. 未宣告的變數賦值,在非嚴格模式下為全域性變數,而嚴格模式下拋異常
  3. 未初始化變數執行typeof操作會返回undefined,而未宣告的變數執行typeof操作同樣會返回undefined

3-Number parseInt 字串轉數值 ,進位制轉換

Number:可以看出在字串轉數字時,能夠識別十六進位制和十進位制,而在進位制轉換時,並不能很準確的識別是二進位制還是八進位制,因此提供了Number.toString(radix)方法。

// 轉數值
Number('0x34')      // 52(十六進位制數)
Number('0111')      // 111(十進位制數)
Number('00001111')  // 1111(十進位制數)
// 進位制轉換
Number(0x34)      // 52(十六進位制數)
Number(0111)      // 73(八進位制數)
Number(111)       // 111(十進位制數)
Number(00001111)  // 585(八進位制數)

22.toString(16)    // '16'(十六進位制數)
22..toString(8)    // '26'(八進位制數)
22..toString(2)    // '10110'(二進位制數)

parseInt: 使用該方法解析二進位制及八進位制字串時,ECMAScript3和5 存在分歧,

ECMAScript3中,'070'唄認為是八進位制,因此轉換後為十進位制的56,

ECMAScript5中,該方法已經不具備解析八進位制的能力,會忽略字串的前導0,被解析為70,

為消除以上問題,該方法提供了第二個引數,即轉化時使用的進位制:

// 轉數值 & 進位制轉換
parseInt('0x34')      // 52(十六進位制數)
parseInt('00001111')  // 1111(十進位制數)
parseInt('070')       // 70(十進位制數)
parseInt(070)         // 56(八進位制數)

parseInt('70',16)     // 112(十六進位制數)
parseInt('70',10)     // 70(十進位制數)
parseInt('70',8)      // 56(八進位制數)
parseInt('70',2)       // NaN
parseInt('070',2)      // 0(二進位制數)

4-undefined && null 區別

undefined null
轉數值 NaN 0
typeof 'undefined' 'object'
if語句 被自動轉為false 被自動轉為false
應用場景 變數宣告未賦值時,就等於undefined 一個表示"無"的物件
函式定義形參未傳實參,該引數等於undefined 作為物件原型鏈的終點
物件還未賦值的屬性,該屬性的值為undefined
函式沒有返回值時,預設返回undefined。

5-操作符

一元操作符: ++ / --

  • 前置遞增遞減,變數的值都是在包含它們的語句被求值之執行
  • 後置遞增遞減,變數的值都是在包含它們的語句被求值之執行

一元加和減操作符: + / -

  • 一元操作符以一個加號(+)表示,放在數值前面,對數值不會產生任何影響。放在非數值前,會像Number()轉型函式一樣進行轉化。
  • 一元操作符以一個減號(-)表示,放在數值前面,該值會變成負數。放在非數值前,會像Number()轉型函式一樣進行轉化。得到的數值轉為負數。
let num1 = 2;
let num2 = 20;
let num3 = --num1 + num2;   // 21
//let num3 = num1-- + num2;   // 22
let num4 = num1 + num2;     // 21

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

6-Label語句

在 JavaScript 中,使用 label 語句可以為一行語句新增標籤,以便在複雜結構中,設定跳轉目標。語法格式:label : states

label 為任意合法的識別符號,但不能使用保留字。然後使用冒號分隔簽名與標籤語句。

多與 break 語句配合使用,主要應用在迴圈結構、多分支結構中,或者巢狀的 switch 結構。且需要退出非當前層結構,以便跳出內層巢狀體。break label;,break 與label之間不能包含換行符,否則會解析為兩個句子。如果沒有設定標籤名,則表示跳出當前最內層結構。

var num = 0;
outer: for (var i = 0; i < 10; i++) {
  for (var j = 0; j < 10; j++) {
    if (i == 5 && j == 5) {
      break outer;
      // break ;
      // continue outer;    //  跳出本次迴圈,開始下一次迴圈,效果同break
    }
    num++;
  }
}
console.log('break label:' + num);    // 55
console.log('break:' + num);          // 95
console.log('continue label:' + num); // 95

7-with語句

用於設定程式碼在特定物件中的作用域,也就是將 statement 中的變數作用域新增到 expression 中。語法:with (expression) { statement }

with語句中查詢變數順序: 是否是 with語句中的區域性變數 -> 是否是 expression中的變數 ->查詢更高作用域範圍

// eg1:
var sMessage = "hello";
function toUpperCase(){
  console.log('word');
}
with(sMessage) {
  console.log(toUpperCase());	 //輸出 "HELLO"
}

// eg2:
 var obj = {
   a: 'aa',
   b: 'bb',
   c: 'cc',
 }
 with(obj) {
   var a = a
   var b = b
   var c = c
 }
 /**
 * 等同於
  var a = obj.a
  var b = obj.b
  var c = obj.c
 */

呼叫 toUpperCase() 時,解釋程式將檢查該方法是否是當前作用域函式。如果不是,將檢查偽物件 sMessage,看它是否為該物件的方法。 輸出 "HELLO",因為解釋程式找到了字串 "hello" 的 toUpperCase() 方法。

提示:with 語句是執行緩慢的程式碼塊,會導致效能下降,同時也會給除錯程式碼造成困難,因此在開發大型應用程式時,不建議使用with語句 。

with語句會建立新的變數物件,起到延長作用域鏈的作用,另外try catch中的錯誤物件,也會被新增到變數物件中,起到延長作用域鏈的作用。

8-垃圾回收機制簡述

垃圾回收:JS程式碼執行,需要作業系統或者執行時環境分配記憶體空間,來儲存變數及它的值。當某些變數不在參與執行時,就需要系統回收被佔用的記憶體空間,稱為垃圾回收,而Javascript具有自動垃圾回收機制(GC:Garbage Collecation)

記憶體洩漏:不再用到的變數所佔記憶體沒有及時釋放,導致程式執行中,記憶體越佔越大,極端情況下可導致系統崩潰、伺服器當機。

目前各大瀏覽器通常用採用的垃圾回收有兩種方法:標記清除引用計數

  1. 標記清除

    • JS中最常用的垃圾回收方式。當變數進入執行環境時,就標記這個變數為“進入環境”。從邏輯上講,永遠不能釋放進入環境的變數所佔用的記憶體,因為只要執行流進入相應的環境,就可能會用到他們。當變數離開環境時,則將其標記為“離開環境”
    • 垃圾收集器在執行時會給儲存在記憶體中的所有變數都加上標記。然後去掉環境中的變數以及被環境中的變數引用的標記。而在此之後再被加上標記的變數將被視為準備刪除的變數,垃圾收集器完成記憶體清除工作,銷燬那些帶標記的值,並回收他們所佔用的記憶體空間。
  2. 引用計數

    另一種不太常見的垃圾回收策略是引用計數,跟蹤記錄每個值被引用的次數。當宣告瞭一個變數並將一個引用型別賦值給該變數時,則這個值的引用次數就+1。相反,如果該引用的變數又取得了另外一個值,則這個值的引用次數就-1。當引用次數為0時,則說明沒有辦法再訪問到這個值,因而就可以將其所佔的記憶體空間給收回來。這樣,垃圾收集器下次再執行時,它就會釋放那些引用次數為0的值所佔的記憶體。

    而引用計數有個最大的問題: 迴圈引用,如物件A有一個屬性指向物件B,而物件B也有一個屬性指向物件A。obj1和obj2通過各自的屬性相互引用;也就是說這兩個物件的引用次數都為2。

    在採用標記清除的策略時,由於函式執行之後,這兩個物件都離開了作用域,因此這種相互引用不是個問題。

    在採用引用計數的策略時,函式執行完成之後,obj1和obj2還將會繼續存在,因為他們的引用次數永遠不會是0。如果該函式被多次呼叫,就會導致大量的記憶體得不到回收。

function func() {
   let obj1 = {};
   let obj2 = {};
   
   obj1.a = obj2; // obj1 引用 obj2
    obj2.a = obj1; // obj2 引用 obj1
}

因此大部分瀏覽器都是以標記清除來實現垃圾回收機制,而IE老版本中有一部分物件並不是原生JavaScript物件。例如,其BOM和DOM中的物件就是使用C++以COM(Component Object Model,元件物件)物件的形式實現的,而COM物件的垃圾回收器就是採用的引用計數的策略。因此,即使IE的JS引擎使用標記清除的策略來實現的,但JS訪問的COM物件依然是基於引用計數的策略的。換句話說,只要IE中涉及COM物件,就會存在迴圈引用的問題。

解決: 未避免, 最好是在不使用時,手動斷開二者的引用關聯,垃圾收集器下次執行時,就可以刪除這些值並回收對應的記憶體

   // 迴圈引用
   var ele = document.getElementById('app');
   var myObj = {};
   myObj.node = ele;
   ele.style = myObj;
   // 手動斷開二者的引用關聯 
   myObj.node = null;
   ele.style = null;

IE9把DOM和BOM物件都轉換成真正的JS物件,這樣就避免了兩種垃圾收集演算法並存導致的問題,也消除了常見的記憶體洩漏現象。

避免記憶體洩漏

  1. 清空陣列優化:arr = []雖然可以清空陣列,同時又建立了一個新陣列 ,而arr.length = 0也能達到清空陣列的目的,同時能實現陣列重用,減少記憶體垃圾的產生。
  2. 迴圈優化:在迴圈中的函式表示式,能複用最好放到迴圈外面。
  3. 意外的全域性變數:區域性變數未宣告,會變成一個全域性變數,在頁面關閉之前不會被釋放,若非必要儘量宣告使用區域性變數。
  4. 被遺忘的定時器和回撥函式:定時器任務執行完畢後,需要清除定時器。
  5. 閉包:閉包可以維持函式內區域性變數,使其得不到釋放。

9-函式內部屬性 arguments

雖然arguments的主要用途是儲存函式的引數,但這個物件還有一個callee屬性,該屬性是一個指標,指向擁有這個arguments物件的函式,表示對函式物件本身的引用。

// 階乘函式遞迴,使用arguments.callee 與函式名解耦
function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}
console.log(factorial(5));   // 120

// 使用新的引用替換階乘方法, 仍可以使用原函式體,實現解耦
let trueFactorial = factorial;
                   
factorial = function() {
  return 0;
};
                   
console.log(trueFactorial(5));  // 120
console.log(factorial(5));      // 0

ECMAScript5規範化了另一個函式物件的屬性,caller,該屬性儲存著呼叫當前函式的函式引用,如果是在全域性作用域下呼叫當前函式,則它的值為null。

為了實現更鬆散的耦合,也可以通過arguments.callee.caller來訪問相同的資訊。

function outer() {
  inner();
}
function inner() {
  console.log(inner.caller);              // [Function: outer]
  console.log(arguments.callee.caller);   // [Function: outer]
}
outer();    

10-嚴格模式

通過嚴格模式,可以在函式內部選擇進行較為嚴格的全域性或區域性的錯誤條件檢測,好處就是可以提早知道程式碼中存在的錯誤,及時捕獲一些可能導致程式設計錯誤的ECMAScript行為

選擇進入嚴格模式,可以使用嚴格模式的編譯指示,實際就是一個不會賦給任何變數的字串

“use strict”,如果你沒有控制頁面中所有指令碼的權力,建議只在需要測試的特定函式中開啟嚴格模式。

  • 未宣告的變數賦值,str = "hello world",非嚴格模式會意外建立全域性變數,嚴格模式丟擲ReferenceError;

  • 刪除變數,var str = 'hello';delete str;非嚴格模式會預設返回false,嚴格模式丟擲ReferenceError;

  • 為物件的只讀屬性賦值會丟擲TypeError;

  • 為物件不可配置的屬性使用delete會丟擲TypeError;

  • 為不可擴充套件的物件新增屬性會丟擲TypeError;

  • 函式的引數重名,非嚴格模式通過引數名只能訪問第二個引數,要訪問第一個引數需要使用arguments,嚴格模式丟擲SyntaxError;

  • 修改命名引數的值,非嚴格模式下修改會對映到arguments中,嚴格模式下修改不會對映到arguments中

  function get(foo){ 
      "use strict";
      foo = 'bar'; 
      console.log(foo);   // bar
      console.log(arguments[0]);  // 非嚴格 bar  嚴格 Hi
  } 
  get('Hi')
  • 不支援八進位制字面量,報錯

  • 不允許使用with語句,視為語法錯誤

  • 不能訪問arguments.callee,arguments.caller,不能為函式的caller屬性賦值。

  • 在eval()中建立變數,在外部訪問不到任何eval()中建立的任何變數或函式。

  function doSomething(){
  	eval("var x = 10");
  	console.log(x);  // 非嚴格 列印10 ,嚴格模式丟擲 ReferenceError
  }
  • this指向問題,非嚴格模式下,使用函式的apply()或者call()方法時,null或undefined值會被轉換為全域性物件,而在嚴格模式下,函式的this值始終是指定的值,無論指定的是什麼值
var color = 'red';
function displayColor(){
	console.log(this.color);
}
// 非嚴格訪問全域性列印 red, 
// 嚴格模式TypeError: Cannot read properties of null (reading 'color')
displayColor.call(null);