JavaScript 基礎卷(一):基礎語法

Ozzie發表於2019-12-26

使用與輸出

使用 JavaScript

  1. 內部使用:置於 <script></script>
    • HTML 中,JavaScript 指令碼需置於 <script></script> 標籤之間
    • 這部分內容可以放在 <head> 中,也可以放在 <body>
    • <head><body> 中的 JavaScript 有什麼區別呢
      • 簡單來說,區別就是執行順序不同:<head> 中的 JavaScript 會 HTML 標籤解析之前就執行,而 <body> 中的是在載入後才執行
      • 舉個例子,指令碼中的某個函式涉及到 DOM 操作,那麼前提當然是這個 DOM 節點已經生成了,若放在 <head> 中,那麼就會在 HTML 標籤解析之前執行,也就沒有對應的 DOM 物件可以操作了
    • 應該放在哪裡呢
      • 需呼叫才執行的指令碼或事件觸發執行的指令碼放在 HTML 的 <head> 部分中,這樣可以保證在任何呼叫之前被載入
      • 些當頁面被載入時才會執行的指令碼應該放在 HTML 的 <body> 部分,放在 <body> 部分的指令碼通常被用來生成頁面的內容
      • 一般是把函式放入 <head> 部分中,或者放在頁面底部,這樣可以把它們安置到同一位置,不會干擾頁面的內容
    • 參考連結(defer和async超出此卷的範圍,之後另做講解)
  2. 外部使用:置於外部 js 檔案中,通過 <script src="xxx.js"></script> 引入
    • 通常這個引入 src 的地方也就是你原本打算在 <script> 中寫入內容的地方

輸出 JavaScript

  1. JavaScript 沒有提供 任何列印或者顯示的函式,我們可以選擇以下幾種輔助方案輸出:
    • window.alert() 彈出警告框
    • document.write() 寫入 HTML 文件中
    • innerHTML 寫入 HTML 元素中
    • console.log() 寫入瀏覽器的控制檯中
  2. 下面我們詳解這幾個方法

    • window.alert()
    • document.write()

      • 基本用法請閱讀:
      • document.write(exp1, exp2, exp3, ...) 可以接受一個或者多個引數,該方法會按照順序寫入由 document.open() 開啟的一個文件流中(多個引數貌似不會換行顯示)
      • 這裡的引數可以是 變數、函式體、自執行函式、數字、字串、表示式 等等,引數也同樣支援 HTML 標籤,只是需要用字串形式
      • 例如:
        <script type="text/javascript">
        var a = 100;
        var b = function(){
            console.log('200')
        };
        //變數
        document.write(a,'<br/>');
        //函式體
        document.write(b,'<br/>');
        //自執行函式
        document.write(function(){
            return 300;
        }(),'<br/>');
        //數字
        document.write(400,'<br/>');
        //字串
        document.write('500','<br/>');
        //表示式
        document.write((300*2),'<br/>');
        //HTML 標籤
        document.write('<h1>700</h1>');
        </script>

        瀏覽器顯示如下:

      • 注意,因為 document.write() 寫入文件流,所以 在已載入或者已關閉的文件上呼叫 document.write() 會自動呼叫 document.open(),這將覆蓋該文件,比如:
        <p>ABCDE</p>
        <button onclick="displayDate()">點選這裡</button>
        <script type="text/javascript">
        function displayDate(){
            document.write('覆蓋頁面')
        }
        </script>

        此時,不管你的頁面上有什麼內容,點選按鈕,就會立刻覆蓋整個頁面
        這種情況一般伴隨著兩個條件:1、在函式內部呼叫 document.write();2、通過按鈕響應呼叫函式
        再次重複一遍!向一個已經載入,並且沒有呼叫過 document.open() 的文件寫入資料時,會自動完成呼叫 document.open() 的操作,一旦完成了資料寫入,系統要求呼叫 document.close() 以告訴瀏覽器頁面已經載入完畢,寫入的資料會被解析到文件結構模型裡,生成對應的 DOM 結點

      • 而直接嵌入 HTML 中,不會呼叫 document.open() 這也就是為什麼我們上上個例子中連續多個 document.write() 沒有被覆蓋
      • 試一下:手動使用 document.close()
        <h2>AB</h2>
        <h2>CD</h2>
        <h2>EF</h2>
        <script type="text/javascript">
        document.write('document.close()之前','<br />');
        document.close();
        document.write('document.close()之後')
        </script>
        <h2>AB</h2>
        <h2>CD</h2>
        <h2>EF</h2>
        <script type="text/javascript">
        window.onload = function(){
            document.write('document.close()之前','<br />');
            document.close();
            document.write('document.close()之後')
        }
        </script>

        這是因為,window.onload 表達的是

      An event handler for the load event of a window.

      換言之,在文件載入完畢的時候,document.write() 在頁面載入後呼叫,但在 W3C 規範中沒有定義時,會發生自動的 document.open() 呼叫,所以頁面才會被清除

    • innerHTML
      • innerHTML 有著雙向功能:①獲取 HTML 內容;②寫入 HTML 內容。案例如下:
        <div id="x">
        <p>此為用 innerHTML 獲取內容1<p>
        <p>此為用 innerHTML 獲取內容2<p>
        </div>
        <button type="button" onclick="Inner()">點選</button>
        <script type="text/javascript">
        function Inner(){
            alert(document.getElementById('x').innerHTML);
            document.getElementById('x').innerHTML = '此為用 innerHTML 寫入內容'
        }
        </script>
    • console.log()
      • alert() 不同的是,console.log() 並不會阻斷執行緒,用法比較簡單,在此不細說了,下面列舉一些控制檯常用方法
      • console.log() :輸出普通訊息
      • console.info() :輸出提示資訊
      • console.error() :輸出錯誤資訊
      • console.warn() :輸出警示資訊
      • console.debug() :輸出除錯資訊

語句與表示式

語句

語句(statement) 是為了 完成某種任務 而進行的 操作 。

  1. 你可以把一個語句理解成一個動作,一個行為,比如 if語句、迴圈語句,如下的賦值語句:
     var a = 1;
    • 上述語句先用 var 命令宣告瞭變數 a,再將 1+3 的運算結果賦值給變數 a
  2. 一個程式 是由 一系列語句 組成的
  3. JS 語句以分號結尾,JS是 弱型別 的語言,雖然分號有時可以省略,也請在省略時 注意是否會造成語法錯誤 ,但是一個分號就肯定表示著一個語句的結束,多個語句可以寫在一行內。如:
    var a = 1;  var b = 2;
  4. 分號前面可以沒有任何內容,JavaScript 引擎將其視為空語句。
    ;;;  //表示三條空語句
  5. 語句不存在返回值的說法
  6. 一般JavaScript的語句分為以下幾種:
    • 宣告語句:變數宣告和函式宣告
    • 賦值語句:在語句中比較重要的一類
    • 控制語句:能夠 改變語句的執行順序,包括條件語句和迴圈語句,還有比較特殊的標籤語句
    • 表示式語句:是JS中最簡單的語句,此類語句只由表示式組成,沒有其他語法元素
      • 賦值、delete、函式呼叫 這三類既是表示式,又是語句,所以叫做表示式語句
      • 注:JS中某些需要語句的地方,若可以使用一個表示式來代替,那麼此語句即是表示式語句。但反過來不可以,不能在一個需要表示式的地方放一個語句。比如,一個if語句不能作為一個函式的引數。

表示式

表示式 是由 運算子 和 運算元(可選) 構成的,併產生 運算結果 的語法結構 。

  1. 以下在 ES5 中,被稱為 基本表示式(Primary Expression)
    • this、null、arguments 等 內建關鍵字
    • 變數,即一個已宣告的識別符號
    • 字面量,僅包括數字字面量、布林值字面量、字串字面量、正則字面量
    • 分組表示式,即用來表示立刻進行計算的
  2. 上述表示式均為 原子表示式,即無法再分解的表示式
  3. 除了基本表示式外,還有 複雜表示式,這類表示式 需要其他表示式的參與,如下:
    • 物件、陣列初始化表示式:
      • 其實他們也算字面量的一種,但不把它們算作基本表示式,是因為物件字面量、陣列字面量所包含的成員也都是表示式
      • 陣列初始化表示式語法如下:
         [expression,expression,expression]
         //可以有 0個或多個子表示式

        初始化的結果是一個新建立的陣列,陣列的元素是逗號分隔的表示式的值。舉例:

         []   //一個空陣列;[]內留空即表示該陣列沒有任何元素
         [1+2,3+4]  //擁有兩個元素的陣列,第一個是3,第二個是7
      • 物件初始化表示式語法如下:
         {
            expression1: expression2,
            expression1: expression2,
            expression1: expression2,
         }
         //在ES5及其之前,expression1只能是字串字面量,但ES6開始支援以下寫法:
         {
         }
         //expression1可以是任何返回值為字串或Symbol型別的表示式

        初始化的結果是一個新建立的物件,舉例:

          {
            foo: bar(3,5)
          }
         //不過同時,它也是一個完全合法的語句,這個語句的組成部分有:
         // 1.一個程式碼塊:一個由大括號包圍的語句序列;
         // 2.一個標籤:你可以在任何語句前面放置一個標籤.這裡的foo就是一個標籤.
         // 3.一條語句:表示式語句bar(3, 5).
    • 函式定義表示式(注意,需與函式宣告語句區分開)
    • 屬性訪問表示式:
      • 屬性訪問表示式的兩種語法如下:
         expression.identifier
         //expression可以是任意的表示式,identifier是屬性名(必須合法)
         //注:跟在物件後面的句點'.'不是運算子,而是屬性訪問表示式的語法結構的一部分

        以及

         expression1[expression2]
         //其中,expression1與expression2都可以是任意表示式
         //而expression2的值會被轉化為字串(除非它是一個Symbol型別)
    • 呼叫表示式(分為 函式呼叫 和 方法呼叫):
      • 函式呼叫的語法如下:
         expression0([expression1[,expression2[,expression3]]])
         //expression0是一個返回值為函式物件的表示式
         //小括號內的是引數列表,其引數為0或多個,用逗號分隔
         //小括號並非操作符,而是呼叫語法的一部分
      • 方法呼叫的語法如下:
         expression0([[expression1[,expression2[,expression3]]])
         //其中,expression0 是一個返回值為函式物件的表示式
         //小括號提供一個逗號分隔的引數列表
    • 物件建立表示式:
      • 該表示式的語法如下:
         new expression0([expression1[,expression2[,expression3]]])
         //同呼叫表示式一樣,//expression0是一個返回值為函式物件的表示式
         //小括號並非操作符而是語法的一部分
  4. 若表示式中未使用運算子,則稱為 單值表示式,否則為 複合表示式
  5. JavaScript表示式必有返回值,單值表示式返回其值本身,複合表示式返回運算的結果值
  6. 總結出的表示式分類如下:
    • 單值表示式(不使用運算子)
      • 簡單表示式(不能再分解)
      • 複雜表示式(需要其他表示式的參與)
    • 符合表示式(由運算子將多個單值表示式結合而成的表示式)

變數

相關概念

  1. 變數是對 “值” 的具名引用
  2. 變數的型別是沒有限制的,所以你可以隨時修改變數的型別,這也表明了 JavaScript是一種動態型別語言

識別符號

  1. 識別符號(identifier) 指的是用來識別各種值的合法名稱,最常見的識別符號就是變數名以及函式名,識別符號對大小寫敏感
  2. 命名規則如下:
    • 字母 或者 $ 或者 _ 開頭,後面可包含 數字
    • 對大小寫敏感
    • 中文是合法識別符號,如:var 臨時變數 = 1;
    • 保留字不能作識別符號
  3. 保留字如下:

    arguments、break、case、catch、class、const、continue、debugger、default、delete、do、else、enum、eval、export、extends、false、finally、for、function、if、implements、import、in、instanceof、interface、let、new、null、package、private、protected、public、return、static、super、switch、this、throw、true、try、typeof、var、void、while、with、yield


變數宣告

  1. 變數的宣告與賦值是兩個過程,如果只是宣告變數而沒有賦值,則該變數的值是 undefined,undefined是一個特殊的值,表示未定義
  2. 重複地宣告變數是無效的
  3. 初識變數提升
    • 由於 JavaScript 引擎的工作方式是:先解析程式碼,獲取所有被宣告的變數再執行,所以,所有變數的宣告都會被提到 作用域的頂端
    • 看幾個例子:
      function test(){
          console.log(a);
          var a = 1;
      }
      test();  //undefined
      //上面的程式碼等效於:
      function test () {
          var a;
          console.log(a);
          a = 123;
      }
      test();  //undefined
      a = 1;
      var a;
      console.log(a);  //1

      看一道經典的面試題:

      console.log(v1);  //undefined
      var v1 = 100;
      function foo() {
          console.log(v1);  //undefined
          var v1 = 200;
          console.log(v1);  //200
      }
      foo();
      console.log(v1);  //100
    • 實際上,js 引擎並不會把 var a = 1; 當做一個過程,而是看做 var a;a = 1;,分別對應著宣告與賦值,分別對應著編譯階段和執行階段,而宣告會被提到作用域的頂端
    • 如果理解了上面三個例子,那麼已經成功認識了變數提升這個概念,徹底理解會在之後的作用域中詳解
    • 參考閱讀:
  4. 幾種變數的宣告:var、let、const
    • 先認識一個塊級作用域:在 ES5 中,作用域有全域性作用域和函式作用域,而在 ES6 中,增加了一個 塊級作用域,塊級作用域由 { } 包括(所以 if 語句等等也會形成塊級作用域),舉個栗子:
      //通過var定義的變數可以跨塊級作用域訪問到
      {
          var a = 1;
          console.log(a);  //1
      }
      console.log(a);  //1
      //通過var定義的變數不可以跨函式作用域訪問到
      (function A(){
          var b = 2;
          console.log(b);  //2
      })();
      console.log(b);  //報錯:b is not defined
      //if語句和for語句形成的是塊級作用域而非函式作用域,因為var定義的變數可以在外部訪問
      if(true){
          var c = 3;
      }
      console.log(c);  //3
      for(var i = 0; i < 4; i++){
          var d = 5;
      }
      console.log(i);  //4
      console.log(d);  //5
    • 承接之前的塊級作用域,說說 var、let、const 的區別:
      • var 定義的變數,沒有塊級作用域的概念,所以可以跨塊級作用域訪問, 但不能跨函式作用域訪問
      • let 定義的變數,只能在塊級作用域裡訪問,不能跨塊訪問,也不能跨函式訪問。
      • const 用來定義常量,使用時必須初始化(即必須賦值),只能在塊作用域裡訪問,而且不能修改
      • 舉例如下:
        //塊級作用域
        {
            //const 不允許改變
            var a = 1;
            let b = 2;
            const c = 3;
            c = 4;  //報錯:Assignment to constant variable
            //const 宣告變數時必須初始化
            var A;
            let B;
            const C;  //報錯:Missing initializer in const declaration
            //列印,觀察
            console.log(a);  //1
            console.log(b);  //2
            console.log(c);  //3
            console.log(A);  //undefined
            console.log(B);  //undefined
        }
        console.log(a);  //1
        console.log(b);  //報錯:b is not defined
        console.log(c):  //報錯:b is not defined
        //函式作用域
        (function Fun(){
            var i = 5;
            let j = 6;
            const k = 7;
        })();
        //console.log(i);  //報錯:i is not defined
        //console.log(j);  //報錯:j is not defined
        //console.log(k);  //報錯:k is not defined
      • 讀到這裡,已經初步認識了三者,接下來詳細說明一下各自的用法
    • let 宣告變數
      • 之前提到過,ES5 沒有塊級作用域,這導致很多場景只能通過閉包來解決不合理的衝突,ES6 出來之後,我們可以通過 let 即可實現
      • 注意,let 宣告的變數只在 let 命令所在的程式碼塊內有效,如上個例子中的 console.log(b);console.log(j); 均會報錯 “未定義”,所以,for 迴圈就很適合 let,另外,說到 for 迴圈順便提提它的特點:設定迴圈變數的那部分是一個 父作用域,而迴圈體內部是一個單獨的 子作用域
        for (let i = 0; i < 3; i++) {
          let i = 'abc';
          console.log(i);
        }
        // abc
        // abc
        // abc
      • let 不存在變數提升,之前在 var 的變數提升總讓人感覺邏輯怪怪的,現在在 let 中不存在變數提升了
      • 暫時性死區:只要塊級作用域記憶體在 let 命令,它所宣告的變數就 “繫結”(binding)這個區域,不再受外部的影響,例如:
        var a = 1;
        if(true){
            a = 'abc';
            let a;
        }

        上面程式碼中,var 先將 a 宣告為全域性變數,但是塊級作用域內 let 又宣告瞭一個區域性變數 a,導致後者繫結這個塊級作用域,所以在 let 宣告變數前,對 a 賦值會報錯
        因為 ES6 已經明確規定,如果區塊中存在 let 和 const 命令,這個區塊對這些命令宣告的變數,從一開始就形成了 封閉作用域,不可在宣告之前就使用這些變數
        也就是在用 let 和 const 宣告變數之前,不可以使用該變數,這種語法稱為 暫時性死區(temporal dead zone,簡稱 TDZ)

      • var 可以重複宣告變數的,只不過無效而已,但是 let 是不允許重複宣告同一變數的,例如:
        var a = 1;
        var a;
        console.log(a);
        let b = 1;
        let b;
        console.log(b);  //報錯:Identifier 'b' has already been declared
        function fun(){
            var a = 1;
            let a;  //報錯:Identifier 'a' has already been declared
        }
        //下面的不報錯
        function fun(x){
            {
                    let x;
            }
        }
        fun();  //不報錯
    • const 宣告變數
      • const 宣告一個 只讀的常量,一旦宣告,常量的值就不能改變,這也同時意味著,一旦用 const 宣告變數,就必須立刻賦值,不能留到之後再賦值,例如:
        //不能改變值
        const a = 1;
        a = 2;  //報錯:Assignment to constant variable.
        //必須立刻賦值
        const b;
        b = 3;  //報錯:Missing initializer in const declaration
      • const 的作用域與 let 相同:只在宣告所在的塊級作用域內有效,同樣的,const 宣告的常量也不會有提升現象,而且也同樣存在暫時性死區,只能在宣告的位置後面使用
      • 參考閱讀:
      • 擴充:const 真的不能修改麼?
        • 先看個例子:
          const person = {
              name: 'A',
              age: 15
          }
          person.name = 'B';
          console.log(person.name)  //B
        • 因為物件是引用型別的,person 儲存的僅僅是 物件的指標,這也就意味著,const 似乎只在乎指標有沒有發生改變,而修改物件的型別是不會改變物件指標的,所以是被允許的
        • 也就是說,const 定義的引用型別只要指標不發生改變,其他的不論如何改變都是允許的,用下面的程式碼測試,果然報錯:
          const person = {
              name: 'A',
              age: 15
          }
          person = {
              name: 'A',
              age: 16
          }
          console.log(person.name);  //報錯:Assignment to constant variable.