重學前端3 - JavaScript

JavaScript


05 | JavaScript類型:關於類型,有哪些你不知道的細節?

問題

  • 爲什麼有的編程規範要求用 void 0 代替 undefined?
  • 字符串有最大長度嗎?
  • 0.1 + 0.2 不是等於 0.3 麼?爲什麼 JavaScript 裏不是這樣的?
  • ES6 新加入的 Symbol 是個什麼東西?
  • 爲什麼給對象添加的方法能用在基本類型上?

7 種語言類型

Undefined; Null; Boolean; String; Number; Symbol; Object

爲什麼有的編程規範要求用 void 0 代替 undefined?

因爲 JavaScript 的代碼 undefined 是一個變量,而並非是一個關鍵字,這是 JavaScript 語言公認的設計失誤之一,所以,我們爲了避免無意中被篡改,我建議使用 void 0 來獲取 undefined 值。

字符串是否有最大長度

String 用於表示文本數據。String 有最大長度是 2^53 - 1,這在一般開發中都是夠用的,但是有趣的是,這個所謂最大長度,並不完全是你理解中的字符數。

因爲 String 的意義並非“字符串”,而是字符串的 UTF16 編碼,我們字符串的操作 charAt、charCodeAt、length 等方法針對的都是 UTF16 編碼。所以,字符串的最大長度,實際上是受字符串的編碼長度影響的。

爲什麼在 JavaScript 中,0.1+0.2 不能等於0.3

這裏輸出的結果是 false,說明兩邊不相等的,這是浮點運算的特點,也是很多同學疑惑的來源,浮點數運算的精度問題導致等式左右的結果並不是嚴格相等,而是相差了個微小的值。

console.log( 0.1 + 0.2 == 0.3);

所以實際上,這裏錯誤的不是結論,而是比較的方法,正確的比較方法是使用 JavaScript 提供的最小精度值:

console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);
// 檢查等式左右兩邊差的絕對值是否小於最小精度,纔是正確的比較浮點數的方法。

Symbol

Symbol 是 ES6 中引入的新類型,它是一切非字符串的對象 key 的集合,在 ES6 規範中,整個對象系統被用 Symbol 重塑。

Object

在 JavaScript 中,對象的定義是“屬性的集合”。屬性分爲數據屬性和訪問器屬性,二者都是 key-value 結構,key 可以是字符串或者 Symbol 類型。

JavaScript 中的幾個基本類型

  • Number
  • String
  • Boolean
  • Symbol

爲什麼給對象添加的方法能用在基本類型上?

.運算符提供了裝箱操作,它會根據基礎類型構造一個臨時對象,使得我們能在基礎類型上調用對應對象的方法。

StringToNumber

多數情況下,Number 是比 parseInt 和 parseFloat 更好的選擇。

NumberToString

數字到字符串的轉換是完全符合你直覺的十進制表示,當 Number 絕對值較大或者較小時,字符串表示則是使用科學計數法表示的

裝箱轉換

把基本類型轉換爲對應的對象

拆箱轉換

對象類型到基本類型的轉換

問題

最後我們留一個實踐問題,如果我們不用原生的 Number 和 parseInt,用 JS 代碼實現 String 到 Number 的轉換,該怎麼做呢?


06 | JavaScript對象:面向對象還是基於對象?

什麼是面向對象?

  • 一個可以觸摸或者可以看見的東西;
  • 人的智力可以理解的東西;
  • 可以指導思考或行動(進行想象或施加動作)的東西。

JavaScript 對象的特徵

  • 對象具有唯一標識性:即使完全相同的兩個對象,也並非同一個對象。
  • 對象有狀態:對象具有狀態,同一對象可能處於不同狀態之下。
  • 對象具有行爲:即對象的狀態,可能因爲它的行爲產生變遷。

唯一標識性

各種語言的對象唯一標識性都是用內存地址來體現的,對象具有唯一標識的內存地址,所以具有唯一的標識。

狀態和行爲

JavaScript 中的行爲和狀態都能用屬性來抽象

JavaScript 對象的兩類屬性

先來說第一類屬性,數據屬性。

  • value:就是屬性的值。
  • writable:決定屬性能否被賦值。
  • enumerable:決定 for in 能否枚舉該屬性。
  • configurable:決定該屬性能否被刪除或者改變特徵值。

第二類屬性是訪問器(getter/setter)屬性

  • getter:函數或 undefined,在取屬性值時被調用。
  • setter:函數或 undefined,在設置屬性值時被調用。
  • enumerable:決定 for in 能否枚舉該屬性。
  • configurable:決定該屬性能否被刪除或者改變特徵值。

這樣,我們就理解了,實際上 JavaScript 對象的運行時是一個“屬性的集合”,屬性以字符串或者 Symbol 爲 key,以數據屬性特徵值或者訪問器屬性特徵值爲 value。

爲什麼會有“JavaScript 不是面向對象”這樣的說法

這是由於 JavaScript 的對象設計跟目前主流基於類的面向對象差異非常大。

可事實上,這樣的對象系統設計雖然特別,但是 JavaScript 提供了完全運行時的對象系統,這使得它可以模仿多數面向對象編程範式(基於類和基於原型),所以它也是正統的面嚮對象語言。

小結

JavaScript 對象的具體設計:具有高度動態性的屬性集合。


07 | JavaScript對象:我們真的需要模擬類嗎?

  • 基於類的面向對象。C++、Java
  • 基於原型的面向對象。JavaScript

原型

  • 照貓畫虎
  • Object.create 來創建對象

ES6 中的類

  • class 關鍵字
  • get / set 關鍵字:創建 getter
  • 括號和大括號:創建方法
  • 數據型成員:構造器裏面
  • extends 關鍵字(繼承)

類與函數

  • class
  • function (=>)

08 | JavaScript對象:你知道全部的對象分類嗎?

JavaScript 中的對象分類

  • 宿主對象(host Objects):由 JavaScript 宿主環境提供的對象,它們的行爲完全由宿主環境決定。
  • 內置對象(Built-in Objects):由 JavaScript 語言提供的對象。
    • 固有對象(Intrinsic Objects ):由標準規定,隨着 JavaScript 運行時創建而自動創建的對象實例。
    • 原生對象(Native Objects):可以由用戶通過 Array、RegExp 等內置構造器或者特殊語法創建的對象。
    • 普通對象(Ordinary Objects):由{}語法、Object 構造器或者 class 關鍵字定義類創建的對象,它能夠被原型繼承。

用對象來模擬函數與構造器:函數對象與構造器對象

  • 函數對象的定義是:具有 [[call]] 私有字段的對象
  • 構造器對象的定義是:具有私有字段 [[construct]] 的對象

16 | JavaScript執行(一):Promise裏的代碼爲什麼比setTimeout先執行?

宏觀和微觀任務

這裏每次的執行過程,其實都是一個宏觀任務。我們可以大概理解:宏觀任務的隊列就相當於事件循環。

在宏觀任務中,JavaScript 的 Promise 還會產生異步代碼,JavaScript 必須保證這些異步代碼在一個宏觀任務中完成,因此,每個宏觀任務中又包含了一個微觀任務隊列:

有了宏觀任務和微觀任務機制,我們就可以實現 JS 引擎級和宿主級的任務了,例如:

  • Promise 永遠在隊列尾部添加微觀任務
  • setTimeout 等宿主 API,則會添加宏觀任務

Promise

Promise 是 JavaScript 語言提供的一種標準化的異步管理方式,它的總體思想是,需要進行 io、等待或者其它異步操作的函數不返回真實結果,而返回一個“承諾”,函數的調用方可以在合適的時機,選擇等待這個承諾兌現(通過 Promise 的 then 方法的回調)。

通過一系列的實驗,我們可以總結一下如何分析異步執行的順序:

  • 首先我們分析有多少個宏任務;
  • 在每個宏任務中,分析有多少個微任務;
  • 根據調用次序,確定宏任務中的微任務執行次序;
  • 根據宏任務的觸發規則和調用次序,確定宏任務的執行次序;
  • 確定整個順序。

新特性:async/await

async/await 是 ES2016 新加入的特性,它提供了用 for、if 等代碼結構來編寫異步的方式。它的運行時基礎是 Promise,面對這種比較新的特性,我們先來看一下基本用法。

async 函數必定返回 Promise,我們把所有返回 Promise 的函數都可以認爲是異步函數。

async 函數是一種特殊語法,特徵是在 function 關鍵字之前加上 async 關鍵字,這樣,就定義了一個 async 函數,我們可以在其中使用 await 來等待一個 Promise。

async 函數強大之處在於,它是可以嵌套的。我們在定義了一批原子操作的情況下,可以利用 async 函數組合出新的 async 函數。

結語

把宿主發起的任務稱爲宏觀任務,把 JavaScript 引擎發起的任務稱爲微觀任務。許多的微觀任務的隊列組成了宏觀任務。

除此之外,我們還展開介紹了用 Promise 來添加微觀任務的方式,並且介紹了 async/await 這個語法的改進。


17 | JavaScript執行(二):閉包和執行上下文到底是怎麼回事?

  • 閉包
  • 執行上下文:執行的基礎設施
  • let
  • Realm

18 | JavaScript執行(三):你知道現在有多少種函數嗎?

函數

  • 第一種,普通函數:用 function 關鍵字定義的函數。
  • 第二種,箭頭函數:用 => 運算符定義的函數。
  • 第三種,方法:在 class 中定義的函數。
  • 第四種,生成器函數:用 function * 定義的函數。
  • 第五種,類:用 class 定義的類,實際上也是函數。
  • 第六種,異步函數:普通函數加上 async 關鍵字。
  • 第七種,異步函數:箭頭函數加上 async 關鍵字。
  • 第八種,異步函數:生成器函數加上 async 關鍵字。

this 關鍵字的行爲

  • this 是執行上下文中很重要的一個組成部分。同一個函數調用方式不同,得到的 this 值也不同。
  • 調用函數時使用的引用,決定了函數執行時刻的 this 值。
  • 我們看到,改爲箭頭函數後,不論用什麼引用來調用它,都不影響它的 this 值。
  • 異步普通函數跟普通函數行爲是一致的
  • 生成器函數、異步生成器函數跟普通函數行爲是一致的
  • 異步箭頭函數與箭頭函數行爲是一致的

this 關鍵字的機制

JavaScript 用一個棧來管理執行上下文,這個棧中的每一項又包含一個鏈表。如下圖所示:

當函數調用時,會入棧一個新的執行上下文,函數調用結束時,執行上下文被出棧。

而 this 則是一個更爲複雜的機制,JavaScript 標準定義了 [[thisMode]] 私有屬性。

[[thisMode]] 私有屬性有三個取值。

  • lexical:表示從上下文中找 this,這對應了箭頭函數。
  • global:表示當 this 爲 undefined 時,取全局對象,對應了普通函數。
  • strict:當嚴格模式時使用,this 嚴格按照調用時傳入的值,可能爲 null 或者 undefined。

操作 this 的內置函數

Function.prototype.call 和 Function.prototype.apply 可以指定函數調用時傳入的 this 值。

此外,還有 Function.prototype.bind 它可以生成一個綁定過的函數,這個函數的 this 值固定了參數。

new 與 this

我們在之前的對象部分已經講過 new 的執行過程,我們再來看一下:

  • 以構造器的 prototype 屬性(注意與私有字段 [[prototype]] 的區分)爲原型,創建新對象;
  • 將 this 和調用參數傳給構造器,執行;
  • 如果構造器返回的是對象,則返回,否則返回第一步創建的對象。

顯然,通過 new 調用函數,跟直接調用的 this 取值有明顯區別。那麼我們今天講的這些函數跟 new 搭配又會產生什麼效果呢?

我們可以看到,僅普通函數和類能夠跟 new 搭配使用。


19 | JavaScript執行(四):try裏面放return,finally還會執行嗎?

Completion 類型

Completion Record 表示一個語句執行完之後的結果,它有三個字段:

  • [[type]] 表示完成的類型,有 break continue return throw 和 normal 幾種類型;
  • [[value]] 表示語句的返回值,如果語句沒有,則是 empty;
  • [[target]] 表示語句的目標,通常是一個 JavaScript 標籤(標籤在後文會有介紹)。

語句

語句塊

語句塊就是拿大括號括起來的一組語句,它是一種語句的複合結構,可以嵌套。

控制型語句

控制型語句帶有 if、switch 關鍵字,它們會對不同類型的 Completion Record 產生反應。

控制類語句分成兩部分,一類是對其內部造成影響,如 if、switch、while/for、try。另一類是對外部造成影響如 break、continue、return、throw,這兩類語句的配合,會產生控制代碼執行順序和執行邏輯的效果,這也是我們編程的主要工作。

一般來說, for/while - break/continue 和 try - throw 這樣比較符合邏輯的組合,是大家比較熟悉的,但是,實際上,我們需要控制語句跟 break 、continue 、return 、throw 四種類型與控制語句兩兩組合產生的效果。

帶標籤的語句

前文我重點講了 type 在語句控制中的作用,接下來我們重點來講一下最後一個字段:target,這涉及了 JavaScript 中的一個語法,帶標籤的語句。

實際上,任何 JavaScript 語句是可以加標籤的,在語句前加冒號即可:


    firstStatement: var i = 1;

大部分時候,這個東西類似於註釋,沒有任何用處。唯一有作用的時候是:與完成記錄類型中的 target 相配合,用於跳出多層循環。


    outer: while(true) {
      inner: while(true) {
          break outer;
      }
    }
    console.log("finished")

break/continue 語句如果後跟了關鍵字,會產生帶 target 的完成記錄。一旦完成記錄帶了 target,那麼只有擁有對應 label 的循環語句會消費它。


26 | JavaScript詞法:爲什麼12.toString會報錯?

文法是編譯原理中對語言的寫法的一種規定,一般來說,文法分成詞法和語法兩種。

詞法規定了語言的最小語義單元:token,可以翻譯成“標記”或者“詞”,在我的專欄文章中,我統一把 token 翻譯成詞。

  • WhiteSpace 空白字符
  • LineTerminator 換行符
  • Comment 註釋
  • Token 詞
    • IdentifierName 標識符名稱,典型案例是我們使用的變量名,注意這裏關鍵字也包含在內了。
    • Punctuator 符號,我們使用的運算符和大括號等符號。
    • NumericLiteral 數字直接量,就是我們寫的數字。
    • StringLiteral 字符串直接量,就是我們用單引號或者雙引號引起來的直接量。
    • Template 字符串模板,用反引號 ` 括起來的直接量。

28 | JavaScript語法(預備篇):到底要不要寫分號呢?

自動插入分號規則

自動插入分號規則其實獨立於所有的語法產生式定義,它的規則說起來非常簡單,只有三條。

  • 要有換行符,且下一個符號是不符合語法的,那麼就嘗試插入分號。
  • 有換行符,且語法中規定此處不能有換行符,那麼就自動插入分號。
  • 源代碼結束處,不能形成完整的腳本或者模塊結構,那麼就自動插入分號。

no LineTerminator here 規則

no LineTerminator here 規則表示它所在的結構中的這一位置不能插入換行符。

自動插入分號規則的第二條:有換行符,且語法中規定此處不能有換行符,那麼就自動插入分號。跟 no LineTerminator here 規則強相關,那麼我們就找出 JavaScript 語法定義中的這些規則。

不寫分號需要注意的情況

  • 以括號開頭的語句
  • 以數組開頭的語句
  • 以正則表達式開頭的語句
  • 以 Template 開頭的語句

29 | JavaScript語法(一):在script標籤寫export爲什麼會拋錯?

腳本和模塊

模塊和腳本之間的區別僅僅在於是否包含 import 和 export。

函數體

函數體實際上有四種

  • 普通函數體,例如:

function foo(){
    //Function body
}

  • 異步函數體,例如:

async function foo(){
    //Function body
}

  • 生成器函數體,例如:

function *foo(){
    //Function body
}

  • 異步生成器函數體,例如:

async function *foo(){
    //Function body
}

上面四種函數體的區別在於:能否使用 await 或者 yield 語句。

關於函數體、模塊和腳本能使用的語句,我整理了一個表格,你可以參考一下:

預處理

JavaScript 執行前,會對腳本、模塊和函數體中的語句進行預處理。預處理過程將會提前處理 var、函數聲明、class、const 和 let 這些語句,以確定其中變量的意義。

  • var 聲明

立即執行的函數表達式(IIFE)

  • function 聲明
  • class 聲明

指令序言機制

腳本和模塊都支持一種特別的語法,叫做指令序言(Directive Prologs)。

這裏的指令序言最早是爲了 use strict 設計的,它規定了一種給 JavaScript 代碼添加元信息的方式。


30 | JavaScript語法(二):你知道哪些JavaScript語句?

普通語句:

聲明型語句:


31 | JavaScript語法(三):什麼是表達式語句?

什麼是表達式語句

表達式語句實際上就是一個表達式,它是由運算符連接變量或者直接量構成的。

一般來說,我們的表達式語句要麼是函數調用,要麼是賦值,要麼是自增、自減,否則表達式計算的結果沒有任何意義。

PrimaryExpression 主要表達式

首先我們來給你講解一下表達式的原子項:Primary Expression。它是表達式的最小單位,它所涉及的語法結構也是優先級最高的。

Primary Expression 包含了各種“直接量”,直接量就是直接用某種語法寫出來的具有特定類型的值。我們已經知道,在運行時有各種值,比如數字 123,字符串 Hello world,所以通俗地講,直接量就是在代碼中把它們寫出來的語法。

我們在類型部分,已經介紹過一些基本類型的直接量。比如,我們當時用 null 關鍵字獲取 null 值,這個用法就是 null 直接量,這就是這裏我們僅僅把它們簡單回顧一下:

"abc";
123;
null;
true;
false;

除這些之外,JavaScript 還能夠直接量的形式定義對象,針對函數、類、數組、正則表達式等特殊對象類型,JavaScript 提供了語法層面的支持。

({});
(function(){});
(class{ });
[];
/abc/g;

需要注意,在語法層面,function、{ 和 class 開頭的表達式語句與聲明語句有語法衝突,所以,我們要想使用這樣的表達式,必須加上括號來回避語法衝突。

Primary Expression 還可以是 this 或者變量,在語法上,把變量稱作“標識符引用”。

this;
myVar;

任何表達式加上圓括號,都被認爲是 Primary Expression,這個機制使得圓括號成爲改變運算優先順序的手段。

(a + b);

MemberExpression 成員表達式

Member Expression 通常是用於訪問對象成員的。它有幾種形式:

a.b;  // 用標識符的屬性訪問
a["b"];  // 用字符串的屬性訪問
new.target;  // 用於判斷函數是否是被 new 調用
super.b;  // 用於訪問父類的屬性的語法

從名字就可以看出,Member Expression 最初設計是爲了屬性訪問的,不過從語法結構需要,以下兩種在 JavaScript 標準中當做 Member Expression:

f`a${b}c`;  // 這是一個是帶函數的模板,這個帶函數名的模板表示把模板的各個部分算好後傳遞給一個函數。

new Cls();  // 另一個是帶參數列表的 new 運算,注意,不帶參數列表的 new 運算優先級更低,不屬於 Member Expression。

NewExpression NEW 表達式

這種非常簡單,Member Expression 加上 new 就是 New Expression(當然,不加 new 也可以構成 New Expression,JavaScript 中默認獨立的高優先級表達式都可以構成低優先級表達式)。

CallExpression 函數調用表達式

除了 New Expression,Member Expression 還能構成 Call Expression。它的基本形式是 Member Expression 後加一個括號裏的參數列表,或者我們可以用上 super 關鍵字代替 Member Expression。

LeftHandSideExpression 左值表達式

接下來,我們需要理解一個概念:New Expression 和 Call Expression 統稱 LeftHandSideExpression,左值表達式。

我們直觀地講,左值表達式就是可以放到等號左邊的表達式。JavaScript 語法則是下面這樣。

AssignmentExpression 賦值表達式

AssignmentExpression 賦值表達式也有多種形態,最基本的當然是使用等號賦值。

Expression 表達式

在 JavaScript 中,比賦值運算優先級更低的就是逗號運算符了。我們可以把逗號可以理解爲一種小型的分號。

a = b, b = 1, null;

逗號分隔的表達式會順次執行,就像不同的表達式語句一樣。“整個表達式的結果”就是“最後一個逗號後的表達式結果”。


32 | JavaScript語法(四):新加入的**運算符,哪裏有些不一樣呢?

更新表達式 UpdateExpression

左值表達式搭配 ++、-- 運算符,可以形成更新表達式。

-- a;
++ a;
a --
a ++

更新表達式會改變一個左值表達式的值。分爲前後自增,前後自減一共四種。

一元運算表達式 UnaryExpression

delete a.b;
void a;
typeof a;
- a;
~ a;
! a;
await a;

乘方表達式 ExponentiationExpression

乘方表達式也是由更新表達式構成的。它使用 ** 號。

++i ** 30
2 ** 30 // 正確
-2 ** 30 // 報錯

乘法表達式 MultiplicativeExpression

乘法表達式有三種運算符:

*
/
%

加法表達式 AdditiveExpression

加法表達式 AdditiveExpression

+ 
-

移位表達式 ShiftExpression

移位表達式由加法表達式構成,移位是一種位運算,分成三種:

<< 向左移位
>> 向右移位
>>> 無符號向右移位

移位運算把操作數看做二進制表示的整數,然後移動特定位數。所以左移 n 位相當於乘以 2 的 n 次方,右移 n 位相當於除以 2 取整 n 次。

普通移位會保持正負數。無符號移位會把減號視爲符號位 1,同時參與移位:

關係表達式 RelationalExpression

移位表達式可以構成關係表達式,這裏的關係表達式就是大於、小於、大於等於、小於等於等運算符號連接,統稱爲關係運算。

  • <=
  • =

  • <
  • instanceof
  • in

需要注意,這裏的 <= 和 >= 關係運算,完全是針對數字的,所以 <= 並不等價於 < 或 ==。例如

null <= undefined
//false
null == undefined
//true

相等表達式 EqualityExpression

相等表達式由四種運算符和關係表達式構成,我們來看一下運算符:

  • ==
  • !=
  • ===
  • !==

雖然標準中寫的 == 十分複雜,但是歸根結底,類型不同的變量比較時

  • undefined 與 null 相等;
  • 字符串和 bool 都轉爲數字再比較;
  • 對象轉換成 primitive 類型再比較。

這樣我們就可以理解一些不太符合直覺的例子了,比如:

false == '0'  // true
true == 'true' // false
[] == 0 //  true
[] == false //  true
new Boolean('false') == false  //  false

這裏不太符合直覺的有兩點:

  • 一個是即使字符串與 boolean 比較,也都要轉換成數字;
  • 二是對象如果轉換成了 primitive 類型跟等號另一邊類型恰好相同,則不需要轉換成數字。

此外,== 的行爲也經常跟 if 的行爲(轉換爲 boolean)混淆。總之,我建議,僅在確認 == 發生在 Number 和 String 類型之間時使用,比如:

document.getElementsByTagName('input')[0].value == 100

在這個例子中,等號左邊必然是 string,右邊的直接量必然是 number,這樣使用 == 就沒有問題了。

位運算表達式

位運算表達式含有三種:

  • 按位與表達式 BitwiseANDExpression (&)
  • 按位異或表達式 BitwiseANDExpression (^)
  • 按位或表達式 BitwiseORExpression (|)

異或運算來交換兩個整數的值


let a = 102, b = 324;

a = a ^ b;
b = a ^ b;
a = a ^ b;

console.log(a, b);

按位或運算常常被用在一種叫做 Bitmask 的技術上。Bitmask 相當於使用一個整數來當做多個布爾型變量,現在已經不太提倡了。不過一些比較老的 API 還是會這樣設計,比如我們在 DOM 課程中,提到過的 Iterator API,我們看下例子:

var iterator = document.createNodeIterator(document.body, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT, null, false);
var node;
while(node = iterator.nextNode())
{
    console.log(node);
}

這裏的第二個參數就是使用了 Bitmask 技術,所以必須配合位運算表達式才能方便地傳參。

邏輯與表達式和邏輯或表達式

這裏需要注意的是,這兩種表達式都不會做類型轉換,所以儘管是邏輯運算,但是最終的結果可能是其它類型。

false || 1;  // 1

false && undefined;  // undefined

true || foo();  // 這裏的 foo 將不會被執行,這種中斷後面表達式執行的特性就叫做短路。

條件表達式 ConditionalExpression

三目運算符

condition ? branch1 : branch2
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章