JavaScript 教程

入門篇

導論

什麼是JavaScript語言

JavaScript 是一種輕量級的腳本語言。所謂“腳本語言”(script language),指的是它不具備開發操作系統的能力,而是隻用來編寫控制其他大型應用程序(比如瀏覽器)的“腳本”

JavaScript 也是一種嵌入式(embedded)語言。它本身提供的核心語法不算很多,只能用來做一些數學和邏輯運算。JavaScript 本身不提供任何與 I/O(輸入/輸出)相關的 API,都要靠宿主環境(host)提供,所以 JavaScript 只合適嵌入更大型的應用程序環境,去調用宿主環境提供的底層 API

目前,已經嵌入 JavaScript 的宿主環境有多種,最常見的環境就是瀏覽器,另外還有服務器環境,也就是 Node 項目

從語法角度看,JavaScript 語言是一種“對象模型”語言。各種宿主環境通過這個模型,描述自己的功能和操作接口,從而通過 JavaScript 控制這些功能。但是,JavaScript 並不是純粹的“面嚮對象語言”,還支持其他編程範式(比如函數式編程)。這導致幾乎任何一個問題,JavaScript 都有多種解決方法

JavaScript 的核心語法部分相當精簡,只包括兩個部分:基本的語法構造(比如操作符、控制結構、語句)和標準庫(就是一系列具有各種功能的對象比如Array、Date、Math等)。除此之外,各種宿主環境提供額外的 API(即只能在該環境使用的接口),以便 JavaScript 調用。以瀏覽器爲例,它提供的額外 API 可以分成三大類。

1.瀏覽器控制類:操作瀏覽器

2.DOM 類:操作網頁的各種元素

3.Web 類:實現互聯網的各種功能

如果宿主環境是服務器,則會提供各種操作系統的 API,比如文件操作 API、網絡通信 API等等。這些都可以在 Node 環境中找到。

本書主要介紹 JavaScript 核心語法和瀏覽器網頁開發的基本知識,不涉及 Node。全書可以分成以下四大部分。

爲什麼學習 JavaScript

JavaScript既適合作爲學習編程的入門語言,也適合當作日常開發的工作語言。它是目前最有希望、前途最光明的計算機語言之一

操控瀏覽器的能力

JavaScript 的發明目的,就是作爲瀏覽器的內置腳本語言,爲網頁開發者提供操控瀏覽器的能力。它是目前唯一一種通用的瀏覽器腳本語言,所有瀏覽器都支持。它可以讓網頁呈現各種特殊效果,爲用戶提供良好的互動體驗。

目前,全世界幾乎所有網頁都使用 JavaScript。如果不用,網站的易用性和使用效率將大打折扣,無法成爲操作便利、對用戶友好的網站。

對於一個互聯網開發者來說,如果你想提供漂亮的網頁、令用戶滿意的上網體驗、各種基於瀏覽器的便捷功能、前後端之間緊密高效的聯繫,JavaScript 是必不可少的工具

廣泛的使用領域

近年來,JavaScript 的使用範圍,慢慢超越了瀏覽器,正在向通用的系統語言發展

瀏覽器的平臺化

隨着 HTML5 的出現,瀏覽器本身的功能越來越強,不再僅僅能瀏覽網頁,而是越來越像一個平臺,JavaScript 因此得以調用許多系統功能,比如操作本地文件、操作圖片、調用攝像頭和麥克風等等。這使得 JavaScript 可以完成許多以前無法想象的事情

Node

Node 項目使得 JavaScript 可以用於開發服務器端的大型項目,網站的前後端都用 JavaScript 開發已經成爲了現實。有些嵌入式平臺(Raspberry Pi)能夠安裝 Node,於是 JavaScript 就能爲這些平臺開發應用程序

數據庫操作

JavaScript 甚至也可以用來操作數據庫。NoSQL 數據庫這個概念,本身就是在 JSON(JavaScript Object Notation)格式的基礎上誕生的,大部分 NoSQL 數據庫允許 JavaScript 直接操作。基於 SQL 語言的開源數據庫 PostgreSQL 支持 JavaScript 作爲操作語言,可以部分取代 SQL 查詢語言

移動平臺開發

JavaScript 也正在成爲手機應用的開發語言。一般來說,安卓平臺使用 Java 語言開發,iOS 平臺使用 Objective-C 或 Swift 語言開發。許多人正在努力,讓 JavaScript 成爲各個平臺的通用開發語言。

PhoneGap 項目就是將 JavaScript 和 HTML5 打包在一個容器之中,使得它能同時在 iOS 和安卓上運行。Facebook 公司的 React Native 項目則是將 JavaScript 寫的組件,編譯成原生組件,從而使它們具備優秀的性能。

Mozilla 基金會的手機操作系統 Firefox OS,更是直接將 JavaScript 作爲操作系統的平臺語言,但是很可惜這個項目沒有成功

內嵌腳本語言

越來越多的應用程序,將 JavaScript 作爲內嵌的腳本語言,比如 Adobe 公司的著名 PDF 閱讀器 Acrobat、Linux 桌面環境 GNOME 3

跨平臺的桌面應用程序

Chromium OS、Windows 8 等操作系統直接支持 JavaScript 編寫應用程序。Mozilla 的 Open Web Apps 項目、Google 的 Chrome App 項目、GitHub 的 Electron 項目、以及 TideSDK 項目,都可以用來編寫運行於 Windows、Mac OS 和 Android 等多個桌面平臺的程序,不依賴瀏覽器

小結

可以預期,JavaScript 最終將能讓你只用一種語言,就開發出適應不同平臺(包括桌面端、服務器端、手機端)的程序。早在2013年9月的統計之中,JavaScript 就是當年 GitHub 上使用量排名第一的語言。

易學性

相比學習其他語言,學習 JavaScript 有一些有利條件

學習環境無處不在

只要有瀏覽器,就能運行 JavaScript 程序;只要有文本編輯器,就能編寫 JavaScript 程序。這意味着,幾乎所有電腦都原生提供 JavaScript 學習環境,不用另行安裝複雜的 IDE(集成開發環境)和編譯器

簡單性

相比其他腳本語言(比如 Python 或 Ruby),JavaScript 的語法相對簡單一些,本身的語法特性並不是特別多。而且,那些語法中的複雜部分,也不是必需要學會。你完全可以只用簡單命令,完成大部分的操作

與主流語言的相似性

JavaScript 的語法很類似 C/C++ 和 Java,如果學過這些語言(事實上大多數學校都教),JavaScript 的入門會非常容易。必須說明的是,雖然核心語法不難,但是 JavaScript 的複雜性體現在另外兩個方面。

1.它涉及大量的外部 API。JavaScript 要發揮作用,必須與其他組件配合,這些外部組件五花八門,數量極其龐大,幾乎涉及網絡應用的各個方面,掌握它們絕非易事。

2.JavaScript 語言有一些設計缺陷。某些地方相當不合理,另一些地方則會出現怪異的運行結果。學習 JavaScript,很大一部分時間是用來搞清楚哪些地方有陷阱

儘管如此,目前看來,JavaScript 的地位還是無法動搖。加之,語言標準的快速進化,使得 JavaScript 功能日益增強,而語法缺陷和怪異之處得到了彌補。所以,JavaScript 還是值得學習,況且它的入門真的不難

強大的性能

JavaScript 的性能優勢體現在以下方面

靈活的語法,表達力強。

JavaScript 既支持類似 C 語言清晰的過程式編程,也支持靈活的函數式編程,可以用來寫併發處理(concurrent)。這些語法特性已經被證明非常強大,可以用於許多場合,尤其適用異步編程。

JavaScript 的所有值都是對象,這爲程序員提供了靈活性和便利性。因爲你可以很方便地、按照需要隨時創造數據結構,不用進行麻煩的預定義。

JavaScript 的標準還在快速進化中,並不斷合理化,添加更適用的語法特性

支持編譯運行。

JavaScript 語言本身雖然是一種解釋型語言,但是在現代瀏覽器中,JavaScript 都是編譯後運行。程序會被高度優化,運行效率接近二進制程序。而且,JavaScript 引擎正在快速發展,性能將越來越好。

此外,還有一種 WebAssembly 格式,它是 JavaScript 引擎的中間碼格式,全部都是二進制代碼。由於跳過了編譯步驟,可以達到接近原生二進制代碼的運行速度。各種語言(主要是 C 和 C++)通過編譯成 WebAssembly,就可以在瀏覽器裏面運行

事件驅動和非阻塞式設計。

JavaScript 程序可以採用事件驅動(event-driven)和非阻塞式(non-blocking)設計,在服務器端適合高併發環境,普通的硬件就可以承受很大的訪問量

開放性

JavaScript 是一種開放的語言。它的標準 ECMA-262 是 ISO 國際標準,寫得非常詳盡明確;該標準的主要實現(比如 V8 和 SpiderMonkey 引擎)都是開放的,而且質量很高。這保證了這門語言不屬於任何公司或個人,不存在版權和專利的問題。

語言標準由 TC39 委員會負責制定,該委員會的運作是透明的,所有討論都是開放的,會議記錄都會對外公佈。

不同公司的 JavaScript 運行環境,兼容性很好,程序不做調整或只做很小的調整,就能在所有瀏覽器上運行

社區支持和就業機會

全世界程序員都在使用 JavaScript,它有着極大的社區、廣泛的文獻和圖書、豐富的代碼資源。絕大部分你需要用到的功能,都有多個開源函數庫可供選用

實驗環境

推薦安裝 Chrome 瀏覽器,進入 Chrome 瀏覽器的“控制檯”

1.直接進入:按下Option + Command + J(Mac)或者Ctrl + Shift + J(Windows / Linux)

2.開發者工具進入:開發者工具的快捷鍵是 F12,或者Option + Command + I(Mac)以及Ctrl + Shift + I(Windows / Linux),然後選擇 Console 面板

JavaScript 語言的歷史

誕生

JavaScript 因爲互聯網而生,緊跟着瀏覽器的出現而問世。回顧它的歷史,就要從瀏覽器的歷史講起。

1990年底,歐洲核能研究組織(CERN)科學家 Tim Berners-Lee,在全世界最大的電腦網絡——互聯網的基礎上,發明了萬維網(World Wide Web),從此可以在網上瀏覽網頁文件。最早的網頁只能在操作系統的終端裏瀏覽,也就是說只能使用命令行操作,網頁都是在字符窗口中顯示,這當然非常不方便。

1992年底,美國國家超級電腦應用中心(NCSA)開始開發一個獨立的瀏覽器,叫做 Mosaic。這是人類歷史上第一個瀏覽器,從此網頁可以在圖形界面的窗口瀏覽。

1994年10月,NCSA 的一個主要程序員 Marc Andreessen 聯合風險投資家 Jim Clark,成立了 Mosaic 通信公司(Mosaic Communications),不久後改名爲 Netscape。這家公司的方向,就是在 Mosaic 的基礎上,開發面向普通用戶的新一代的瀏覽器 Netscape Navigator。

1994年12月,Navigator 發佈了1.0版,市場份額一舉超過90%。

Netscape 公司很快發現,Navigator 瀏覽器需要一種可以嵌入網頁的腳本語言,用來控制瀏覽器行爲。當時,網速很慢而且上網費很貴,有些操作不宜在服務器端完成。比如,如果用戶忘記填寫“用戶名”,就點了“發送”按鈕,到服務器再發現這一點就有點太晚了,最好能在用戶發出數據之前,就告訴用戶“請填寫用戶名”。這就需要在網頁中嵌入小程序,讓瀏覽器檢查每一欄是否都填寫了。

管理層對這種瀏覽器腳本語言的設想是:功能不需要太強,語法較爲簡單,容易學習和部署。那一年,正逢 Sun 公司的 Java 語言問世,市場推廣活動非常成功。Netscape 公司決定與 Sun 公司合作,瀏覽器支持嵌入 Java 小程序(後來稱爲 Java applet)。但是,瀏覽器腳本語言是否就選用 Java,則存在爭論。後來,還是決定不使用 Java,因爲網頁小程序不需要 Java 這麼“重”的語法。但是,同時也決定腳本語言的語法要接近 Java,並且可以支持 Java 程序。這些設想直接排除了使用現存語言,比如 Perl、Python 和 TCL。

1995年,Netscape 公司僱傭了程序員 Brendan Eich 開發這種網頁腳本語言。Brendan Eich 有很強的函數式編程背景,希望以 Scheme 語言(函數式語言鼻祖 LISP 語言的一種方言)爲藍本,實現這種新語言。

1995年5月,Brendan Eich 只用了10天,就設計完成了這種語言的第一版。它是一個大雜燴,語法有多個來源。

1.基本語法:借鑑 C 語言和 Java 語言。

2.數據結構:借鑑 Java 語言,包括將值分成原始值和對象兩大類。

3.函數的用法:借鑑 Scheme 語言和 Awk 語言,將函數當作第一等公民,並引入閉包。

4.原型繼承模型:借鑑 Self 語言(Smalltalk 的一種變種)。

5.正則表達式:借鑑 Perl 語言。

6.字符串和數組處理:借鑑 Python 語言。

爲了保持簡單,這種腳本語言缺少一些關鍵的功能,比如塊級作用域、模塊、子類型(subtyping)等等,但是可以利用現有功能找出解決辦法。這種功能的不足,直接導致了後來 JavaScript 的一個顯著特點:對於其他語言,你需要學習語言的各種功能,而對於 JavaScript,你常常需要學習各種解決問題的模式。而且由於來源多樣,從一開始就註定JavaScript 的編程風格是函數式編程和麪向對象編程的一種混合體。

Netscape 公司的這種瀏覽器腳本語言,最初名字叫做 Mocha,1995年9月改爲 LiveScript。12月,Netscape 公司與 Sun 公司(Java 語言的發明者和所有者)達成協議,後者允許將這種語言叫做 JavaScript。這樣一來,Netscape 公司可以藉助 Java 語言的聲勢,而 Sun 公司則將自己的影響力擴展到了瀏覽器。

之所以起這個名字,並不是因爲 JavaScript 本身與 Java 語言有多麼深的關係(事實上,兩者關係並不深),而是因爲 Netscape 公司已經決定,使用 Java 語言開發網絡應用程序,JavaScript 可以像膠水一樣,將各個部分連接起來。當然,後來的歷史是 Java 語言的瀏覽器插件失敗了,JavaScript 反而發揚光大。

1995年12月4日,Netscape 公司與 Sun 公司聯合發佈了 JavaScript 語言,對外宣傳 JavaScript 是 Java 的補充,屬於輕量級的 Java,專門用來操作網頁。

1996年3月,Navigator 2.0 瀏覽器正式內置了 JavaScript 腳本語言

JavaScript 與 Java 的關係

JavaScript 和 Java是兩種不一樣的語言,但是彼此存在聯繫。

JavaScript 的基本語法和對象體系,是模仿 Java 而設計的。但是,JavaScript 沒有采用 Java 的靜態類型。正是因爲 JavaScript 與 Java 有很大的相似性,所以這門語言才從一開始的 LiveScript 改名爲 JavaScript。基本上,JavaScript 這個名字的原意是“很像Java的腳本語言”。

JavaScript 語言的函數是一種獨立的數據類型,以及採用基於原型對象(prototype)的繼承鏈。這是它與 Java 語法最大的兩點區別。JavaScript 語法要比 Java 自由得多。另外,Java 語言需要編譯,而 JavaScript 語言則是運行時由解釋器直接執行。

總之,JavaScript 的原始設計目標是一種小型的、簡單的動態語言,與 Java 有足夠的相似性,使得使用者(尤其是 Java 程序員)可以快速上手

JavaScript 的基本語法

語句

JavaScript 程序的執行單位爲行(line),一般情況下每一行就是一個語句。語句(statement)是爲了完成某種任務而進行的操作,比如下面就是一行賦值語句

var a = 1 + 3;

1 + 3叫做表達式(expression),指一個爲了得到返回值的計算式。語句和表達式的區別在於,前者主要爲了進行某種操作,一般不需要返回值;後者則是爲了得到返回值。凡是 JavaScript 語言中預期爲值的地方,都可以使用表達式。比如,賦值語句的等號右邊,預期是一個值,因此可以放置各種表達式。

語句以分號結尾,一個分號就表示一個語句結束。多個語句可以寫在一行內。

var a = 1 + 3 ; var b = 'abc';

分號前面可以沒有任何內容,JavaScript 引擎將其視爲空語句

;;;

上面的代碼就表示3個空語句。

表達式不需要分號結尾。一旦在表達式後面添加分號,則 JavaScript 引擎就將表達式視爲語句,這樣會產生一些沒有任何意義的語句

1 + 3;
'abc';

上面兩行語句只是單純地產生一個值,並沒有任何實際的意義

變量

概念

變量是對“值”的具名引用。變量就是爲“值”起名,然後引用這個名字,就等同於引用這個值。變量的名字就是變量名

注意,JavaScript 的變量名區分大小寫,A和a是兩個不同的變量。變量的聲明和賦值,是分開的兩個步驟,也可以將它們合在一起

如果只是聲明變量而沒有賦值,則該變量的值是undefined。undefined是一個特殊的值,表示“無定義”

如果變量賦值的時候,忘了寫var命令,這條語句也是有效的。但是不利於表達意圖,而且容易不知不覺地創建全局變量

如果一個變量沒有聲明就直接使用,JavaScript 會報錯,告訴你變量未定義

x  // ReferenceError: x is not defined

上面代碼直接使用變量x,系統就報錯,告訴你變量x沒有聲明

可以在同一條var命令中聲明多個變量

JavaScript 是一種動態類型語言,也就是說,變量的類型沒有限制,變量可以隨時更改類型

如果使用var重新聲明一個已經存在的變量,是無效的

var x = 1;
var x;
x // 1

上面代碼中,變量x聲明瞭兩次,第二次聲明是無效的。但是,如果第二次聲明的時候還進行了賦值,則會覆蓋掉前面的值

變量提升

JavaScript 引擎的工作方式是,先解析代碼,獲取所有被聲明的變量,然後再一行一行地運行。這造成的結果,就是所有的變量的聲明語句,都會被提升到代碼的頭部,這就叫做變量提升(hoisting)

console.log(a);
var a = 1;

上面代碼首先使用console.log方法,在控制檯(console)顯示變量a的值。這時變量a還沒有聲明和賦值,所以這是一種錯誤的做法,但是實際上不會報錯,因爲存在變量提升。最後的結果是顯示undefined,表示變量a已聲明,但還未賦值

標識符

標識符(identifier)指的是用來識別各種值的合法名稱。最常見的標識符就是變量名,以及後面要提到的函數名。JavaScript 語言的標識符對大小寫敏感,所以a和A是兩個不同的標識符。

標識符有一套命名規則,不符合規則的就是非法標識符。JavaScript 引擎遇到非法標識符,就會報錯。

簡單說,標識符命名規則如下:

第一個字符,可以是任意 Unicode 字母(包括英文字母和其他語言的字母),以及美元符號($)和下劃線(_)。

第二個字符及後面的字符,除了 Unicode 字母、美元符號和下劃線,還可以用數字0-9

中文是合法的標識符,可以用作變量名

var 臨時變量 = 1;

JavaScript 有一些保留字,不能用作標識符:

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

註釋

源碼中被 JavaScript 引擎忽略的部分就叫做註釋,它的作用是對代碼進行解釋。JavaScript 提供兩種註釋的寫法:一種是單行註釋,用//起頭;另一種是多行註釋,放在//之間。此外,由於歷史上 JavaScript 可以兼容 HTML 代碼的註釋,所以也被視爲合法的單行註釋

x = 1; <!-- x = 2;
--> x = 3;

上面代碼中,只有x = 1會執行,其他的部分都被註釋掉了。需要注意的是,-->只有在行首,纔會被當成單行註釋,否則會當作正常的運算

function countdown(n) {
  while (n --> 0) console.log(n);
}
countdown(3)
// 2
// 1
// 0

上面代碼中,n --> 0實際上會當作n-- > 0,因此輸出2、1、0

區塊

JavaScript 使用大括號,將多個相關的語句組合在一起,稱爲“區塊”(block)。對於var命令來說,JavaScript 的區塊不構成單獨的作用域(scope)

{  var a = 1; }
a // 1

上面代碼在區塊內部,使用var命令聲明並賦值了變量a,然後在區塊外部,變量a依然有效,區塊對於var命令不構成單獨的作用域,與不使用區塊的情況沒有任何區別。在 JavaScript 語言中,單獨使用區塊並不常見,區塊往往用來構成其他更復雜的語法結構,比如for、if、while、function等

條件語句

JavaScript 提供if結構和switch結構,完成條件判斷,即只有滿足預設的條件,纔會執行相應的語句

if 結構

if結構先判斷一個表達式的布爾值,然後根據布爾值的真僞,執行不同的語句。所謂布爾值,指的是 JavaScript 的兩個特殊值,true表示真,false表示僞

if...else 結構

if代碼塊後面,還可以跟一個else代碼塊,表示不滿足條件時,所要執行的代碼。對同一個變量進行多次判斷時,多個if...else語句可以連寫在一起

switch 結構

多個if...else連在一起使用的時候,可以轉爲使用更方便的switch結構

switch (fruit) {
  case "banana":
    // ...
    break;
  case "apple":
    // ...
    break;
  default:
    // ...
}

上面代碼根據變量fruit的值,選擇執行相應的case。如果所有case都不符合,則執行最後的default部分。需要注意的是,每個case代碼塊內部的break語句不能少,否則會接下去執行下一個case代碼塊,而不是跳出switch結構

switch語句部分和case語句部分,都可以使用表達式

switch (1 + 3) {
  case 2 + 2:
    // ...
    break;
  default:
    // ...
}

需要注意的是,switch語句後面的表達式,與case語句後面的表示式比較運行結果時,採用的是嚴格相等運算符(===),而不是相等運算符(==),這意味着比較時不會發生類型轉換

var x = 1;
switch (x) {
  case true:
    console.log('x 發生類型轉換');
    break;
  default:
    console.log('x 沒有發生類型轉換');
} // x 沒有發生類型轉換

上面代碼中,由於變量x沒有發生類型轉換,所以不會執行case true的情況。這表明,switch語句內部採用的是“嚴格相等運算符”

三元運算符 ?:

JavaScript 還有一個三元運算符(即該運算符需要三個運算子)?:,也可以用於邏輯判斷。這個三元運算符可以被視爲if...else...的簡寫形式

循環語句

while 循環

While語句包括一個循環條件和一段代碼塊,只要條件爲真,就不斷循環執行代碼塊

for 循環

for語句是循環命令的另一種形式,可以指定循環的起點、終點和終止條件。所有for循環,都可以改寫成while循環

do...while 循環

do...while循環與while循環類似,唯一的區別就是先運行一次循環體,然後判斷循環條件

do {
  語句
} while (條件);

不管條件是否爲真,do...while循環至少運行一次,這是這種結構最大的特點。另外,while語句後面的分號注意不要省略

break 語句和 continue 語句

break語句和continue語句都具有跳轉作用,可以讓代碼不按既有的順序執行。

break語句用於跳出代碼塊或循環

var i = 0;
while(i < 100) {
  console.log('i 當前爲:' + i);
  i++;
  if (i === 10) break;
}

上面代碼只會執行10次循環,一旦i等於10,就會跳出循環。for循環也可以使用break語句跳出循環

for (var i = 0; i < 5; i++) {
  console.log(i);
  if (i === 3)
    break;
}

上面代碼執行到i等於3,就會跳出循環

continue語句用於立即終止本輪循環,返回循環結構的頭部,開始下一輪循環

var i = 0;
while (i < 100){
  i++;
  if (i % 2 === 0) continue;
  console.log('i 當前爲:' + i);
}

上面代碼只有在i爲奇數時,纔會輸出i的值。如果i爲偶數,則直接進入下一輪循環。如果存在多重循環,不帶參數的break語句和continue語句都只針對最內層循環

標籤(label)

JavaScript 語言允許語句的前面有標籤(label),相當於定位符,用於跳轉到程序的任意位置,標籤的格式如下

label:
  語句

標籤可以是任意的標識符,但不能是保留字,語句部分可以是任意語句。標籤通常與break語句和continue語句配合使用,跳出特定的循環

top:
  for (var i = 0; i < 3; i++){
    for (var j = 0; j < 3; j++){
      if (i === 1 && j === 1) break top;
      console.log('i=' + i + ', j=' + j);
    }
  }
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0

上面代碼爲一個雙重循環區塊,break命令後面加上了top標籤(注意,top不用加引號),滿足條件時,直接跳出雙層循環。如果break語句後面不使用標籤,則只能跳出內層循環,進入下一次的外層循環。標籤也可以用於跳出代碼塊

foo: {
  console.log(1);
  break foo;
  console.log('本行不會輸出');
}
console.log(2);
// 1
// 2

上面代碼執行到break foo,就會跳出區塊。continue語句也可以與標籤配合使用。

top:
  for (var i = 0; i < 3; i++){
    for (var j = 0; j < 3; j++){
      if (i === 1 && j === 1) continue top;
      console.log('i=' + i + ', j=' + j);
    }
  }
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0
// i=2, j=0
// i=2, j=1
// i=2, j=2

上面代碼中,continue命令後面有一個標籤名,滿足條件時,會跳過當前循環,直接進入下一輪外層循環。如果continue語句後面不使用標籤,則只能進入下一輪的內層循環

數據類型

數據類型概述

簡介

JavaScript 語言的每一個值,都屬於某一種數據類型。JavaScript 的數據類型,共有六種。(ES6 又新增了第七種 Symbol 類型的值,這裏不涉及)

1.數值(number):整數和小數(比如1和3.14)

2.字符串(string):文本(比如Hello World)。

3.布爾值(boolean):表示真僞的兩個特殊值,即true(真)和false(假)

4.undefined:表示“未定義”或不存在,即由於目前沒有定義,所以此處暫時沒有任何值

5.null:表示空值,即此處的值爲空。

6.對象(object):各種值組成的集合。

通常,數值、字符串、布爾值這三種類型,合稱爲原始類型(primitive type)的值,即它們是最基本的數據類型,不能再細分了。對象則稱爲合成類型(complex type)的值,因爲一個對象往往是多個原始類型的值的合成,可以看作是一個存放各種值的容器。至於undefined和null,一般將它們看成兩個特殊值。

對象是最複雜的數據類型,又可以分成三個子類型。

1.狹義的對象(object)

2.數組(array)

3.函數(function)

狹義的對象和數組是兩種不同的數據組合方式,除非特別聲明,本文的“對象”都特指狹義的對象。函數其實是處理數據的方法,JavaScript 把它當成一種數據類型,可以賦值給變量,這爲編程帶來了很大的靈活性,也爲 JavaScript 的“函數式編程”奠定了基礎

typeof 運算符

JavaScript 有三種方法,可以確定一個值到底是什麼類型

1.typeof運算符

2.instanceof運算符

3.Object.prototype.toString方法

typeof運算符可以返回一個值的數據類型

typeof 123 // "number"
typeof '123' // "string"
typeof false // "boolean"
function f() {}
typeof f // "function"
typeof undefined // "undefined"

利用這一點,typeof可以用來檢查一個沒有聲明的變量,而不報錯

v  // ReferenceError: v is not defined
typeof v // "undefined"

上面代碼中,變量v沒有用var命令聲明,直接使用就會報錯。但是,放在typeof後面,就不報錯了,而是返回undefined。實際編程中,這個特點通常用在判斷語句

typeof window // "object"
typeof {} // "object"
typeof [] // "object"
typeof null // "object"

上面代碼中,空數組([])的類型也是object,這表示在 JavaScript 內部,數組本質上只是一種特殊的對象。這裏順便提一下,instanceof運算符可以區分數組和對象,下節 會說到

null的類型是object,這是由於歷史原因造成的。1995年的 JavaScript 語言第一版,只設計了五種數據類型(對象、整數、浮點數、字符串和布爾值),沒考慮null,只把它當作object的一種特殊值。後來null獨立出來,作爲一種單獨的數據類型,爲了兼容以前的代碼,typeof null返回object就沒法改變了

null, undefined 和布爾值

null 和 undefined

概述

null與undefined都可以表示“沒有”,含義非常相似。將一個變量賦值爲undefined或null,老實說,語法效果幾乎沒區別。在if語句中,null和undefined都會被自動轉爲false,相等運算符(==)甚至直接報告兩者相等

既然含義與用法都差不多,爲什麼要同時設置兩個這樣的值,這不是無端增加複雜度,令初學者困擾嗎?這與歷史原因有關。

1995年 JavaScript 誕生時,最初像 Java 一樣,只設置了null表示"無"。根據 C 語言的傳統,null可以自動轉爲0。但是,JavaScript 的設計者 Brendan Eich,覺得這樣做還不夠。首先,第一版的 JavaScript 裏面,null就像在 Java 裏一樣,被當成一個對象,Brendan Eich 覺得表示“無”的值最好不是對象。其次,那時的 JavaScript 不包括錯誤處理機制,Brendan Eich 覺得,如果null自動轉爲0,很不容易發現錯誤。因此,他又設計了一個undefined。區別是這樣的:null是一個表示“空”的對象,轉爲數值時爲0;undefined是一個表示"此處無定義"的原始值,轉爲數值時爲NaN

Number(null) // 0
5 + null // 5

Number(undefined) // NaN
5 + undefined // NaN

用法和含義

對於null和undefined,大致可以像這樣理解:null表示空值,即該處的值現在爲空;調用函數時,某個參數未設置任何值,這時就可以傳入null,表示該參數爲空。比如,某個函數接受引擎拋出的錯誤作爲參數,如果運行過程中未出錯,那麼這個參數就會傳入null,表示未發生錯誤。undefined表示“未定義”,下面是返回undefined的典型場景

var i; // 變量聲明瞭,但沒有賦值
i // undefined

function f(x) { // 調用函數時,應該提供的參數沒有提供,該參數等於 undefined
  return x;
}
f() // undefined

var  o = new Object(); // 對象沒有賦值的屬性
o.p // undefined

function f() {} // 函數沒有返回值時,默認返回 undefined
f() // undefined

布爾值

布爾值代表“真”和“假”兩個狀態。“真”用關鍵字true表示,“假”用關鍵字false表示。布爾值只有這兩個值。

下列運算符會返回布爾值:

1.前置邏輯運算符: ! (Not)

2.相等運算符:===,!==,==,!=

3.比較運算符:>,>=,<,<=

如果JavaScript預期某個位置應該是布爾值,會將該位置上現有的值自動轉爲布爾值。轉換規則是除了下面六個值被轉爲false,其他值都視爲true

undefined / null / false / 0 / NaN / "" 或 '' (空字符串)

注意,空數組([])和空對象({})對應的布爾值,都是true

數值

概述

整數和浮點數

JavaScript 內部,所有數字都是以64位浮點數形式儲存,即使整數也是如此。所以,1與1.0是相同的,是同一個數。這就是說,JavaScript 語言的底層根本沒有整數,所有數字都是小數(64位浮點數)。容易造成混淆的是,某些運算只有整數才能完成,此時 JavaScript 會自動把64位浮點數,轉成32位整數,然後再進行運算。由於浮點數不是精確的值,所以涉及小數的比較和運算要特別小心

數值精度

根據國際標準 IEEE 754,JavaScript 浮點數的64個二進制位,從最左邊開始,是這樣組成的。

第1位:符號位,0表示正數,1表示負數

第2位到第12位(共11位):指數部分

第13位到第64位(共52位):小數部分(即有效數字)

符號位決定了一個數的正負,指數部分決定了數值的大小,小數部分決定了數值的精度。

指數部分一共有11個二進制位,因此大小範圍就是0到2047。IEEE 754 規定,如果指數部分的值在0到2047之間(不含兩個端點),那麼有效數字的第一位默認總是1,不保存在64位浮點數之中。也就是說,有效數字這時總是1.xx...xx的形式,其中xx..xx的部分保存在64位浮點數之中,最長可能爲52位。因此,JavaScript 提供的有效數字最長爲53個二進制位。

(-1)^符號位 * 1.xx...xx * 2^指數部分

上面公式是正常情況下(指數部分在0到2047之間),一個數在 JavaScript 內部實際的表示形式。精度最多隻能到53個二進制位,這意味着,絕對值小於2的53次方的整數,即-253到253,都可以精確表示

Math.pow(2, 53) // 9007199254740992
Math.pow(2, 53) + 1 // 9007199254740992
Math.pow(2, 53) + 2 // 9007199254740994
Math.pow(2, 53) + 3 // 9007199254740996
Math.pow(2, 53) + 4 // 9007199254740996

上面代碼中,大於2的53次方後整數運算的結果開始出現錯誤。所以,大於2的53次方的數值都無法保持精度。由於2的53次方是一個16位的十進制數值,所以簡單的法則就是:JavaScript 對15位的十進制數都可以精確處理

數值範圍

根據標準,64位浮點數的指數部分長度是11個二進制位,意味着指數部分的最大值是2047(2的11次方減1)。也就是說,64位浮點數的指數部分的值最大爲2047,分出一半表示負數,則 JavaScript 能夠表示的數值範圍爲21024到2-1023(開區間),超出這個範圍的數無法表示。

如果一個數大於等於2的1024次方,那麼就會發生“正向溢出”,即 JavaScript 無法表示這麼大的數,這時就會返回Infinity

Math.pow(2, 1024) // Infinity

如果一個數小於等於2的-1075次方(指數部分最小值-1023,再加上小數部分的52位),那麼就會發生爲“負向溢出”,即 JavaScript 無法表示這麼小的數,這時會直接返回0

JavaScript 提供Number對象的MAX_VALUE和MIN_VALUE屬性,返回可以表示的具體的最大值和最小值

Number.MAX_VALUE // 1.7976931348623157e+308
Number.MIN_VALUE // 5e-324

數值的表示法

JavaScript 的數值有多種表示方法,可以用字面形式直接表示,比如35(十進制)和0xFF(十六進制)。數值也可以採用科學計數法表示,下面是幾個科學計數法的例子

123e3 // 123000
123e-3 // 0.123
-3.1E+12
.1e-23

科學計數法允許字母e或E的後面,跟着一個整數,表示這個數值的指數部分。以下兩種情況,JavaScript 會自動將數值轉爲科學計數法表示,其他情況都採用字面形式直接表示

1.小數點前的數字多於21位

1234567890123456789012 // 1.2345678901234568e+21
123456789012345678901 // 123456789012345680000

2.小數點後的零多於5個

// 小數點後緊跟5個以上的零,就自動轉爲科學計數法
0.0000003 // 3e-7
// 否則,就保持原來的字面形式
0.000003 // 0.000003

數值的進制

使用字面量(literal)直接表示一個數值時,JavaScript 對整數提供四種進制的表示方法:十進制、十六進制、八進制、二進制

十進制:沒有前導0的數值

八進制:有前綴0o或0O的數值,或者有前導0、且只用到0-7的八個阿拉伯數字的數值

十六進制:有前綴0x或0X的數值

二進制:有前綴0b或0B的數值

默認情況下,JavaScript 內部會自動將八進制、十六進制、二進制轉爲十進制

通常來說,有前導0的數值會被視爲八進制,但是如果前導0後面有數字8和9,則該數值被視爲十進制,處理時很容易造成混亂。ES5 的嚴格模式和 ES6,已經廢除了這種表示法,但是瀏覽器爲了兼容以前的代碼,目前還繼續支持這種表示法

特殊數值

JavaScript 提供了幾個特殊的數值

正零和負零

前面說過,JavaScript 的64位浮點數之中,有一個二進制位是符號位。這意味着,任何一個數都有一個對應的負值,就連0也不例外。JavaScript 內部實際上存在2個0:一個是+0,一個是-0,區別就是64位浮點數表示法的符號位不同。它們是等價的

幾乎所有場合,正零和負零都會被當作正常的0。唯一有區別的場合是,+0或-0當作分母,返回的值是不相等的

(1 / +0) === (1 / -0) // false

上面的代碼之所以出現這樣結果,是因爲除以正零得到+Infinity,除以負零得到-Infinity,這兩者是不相等的

NaN

含義

NaN是 JavaScript 的特殊值,表示“非數字”(Not a Number),主要出現在將字符串解析成數字出錯的場合

0 / 0 // NaN

需要注意的是,NaN不是獨立的數據類型,而是一個特殊數值,它的數據類型依然屬於Number,使用typeof運算符可以看得很清楚

運算規則

NaN不等於任何值,包括它本身

NaN === NaN // false

數組的indexOf方法內部使用的是嚴格相等運算符,所以該方法對NaN不成立

[NaN].indexOf(NaN) // -1

NaN在布爾運算時被當作false

Boolean(NaN) // false

NaN與任何數(包括它自己)的運算,得到的都是NaN

Infinity

含義

Infinity表示“無窮”,用來表示兩種場景。一種是一個正的數值太大,或一個負的數值太小,無法表示;另一種是非0數值除以0,得到Infinity。由於數值正向溢出(overflow)、負向溢出(underflow)和被0除,JavaScript 都不報錯,所以單純的數學運算幾乎沒有可能拋出錯誤。Infinity大於一切數值(除了NaN),-Infinity小於一切數值(除了NaN)。Infinity與NaN比較,總是返回false

Infinity > NaN // false
-Infinity > NaN // false
Infinity < NaN // false
-Infinity < NaN // false
運算規則

Infinity的四則運算,符合無窮的數學計算規則。0乘以Infinity,返回NaN;0除以Infinity,返回0;Infinity除以0,返回Infinity

0 * Infinity // NaN
0 / Infinity // 0
Infinity / 0 // Infinity

Infinity加上或乘以Infinity,返回的還是Infinity。Infinity減去或除以Infinity,得到NaN。Infinity與null計算時,null會轉成0,等同於與0的計算。Infinity與undefined計算,返回的都是NaN

與數值相關的全局方法

parseInt()

基本用法

parseInt方法用於將字符串轉爲整數;如果字符串頭部有空格,空格會被自動去除。如果parseInt的參數不是字符串,則會先轉爲字符串再轉換。字符串轉爲整數的時候,是一個個字符依次轉換,如果遇到不能轉爲數字的字符,就不再進行下去,返回已經轉好的部分

parseInt('8a') // 8
parseInt('12**') // 12
parseInt('12.34') // 12
parseInt('15e2') // 15
parseInt('15px') // 15

如果字符串的第一個字符不能轉化爲數字(後面跟着數字的正負號除外),返回NaN。所以,parseInt的返回值只有兩種可能,要麼是一個十進制整數,要麼是NaN。

如果字符串以0x或0X開頭,parseInt會將其按照十六進制數解析;如果字符串以0開頭,將其按照10進制解析;對於那些會自動轉爲科學計數法的數字,parseInt會將科學計數法的表示方法視爲字符串,因此導致一些奇怪的結果

parseInt(1000000000000000000000.5) // 1
// 等同於
parseInt('1e+21') // 1

parseInt(0.0000008) // 8
// 等同於
parseInt('8e-7') // 8
進制轉換

parseInt方法還可以接受第二個參數(2到36之間),表示被解析的值的進制,返回該值對應的十進制數。默認情況下,parseInt的第二個參數爲10,即默認是十進制轉十進制

parseInt('1000') // 1000
// 等同於
parseInt('1000', 10) // 1000

parseInt('1000', 2) // 8
parseInt('1000', 6) // 216
parseInt('1000', 8) // 512

這意味着,可以用parseInt方法進行進制的轉換

如果第二個參數不是數值,會被自動轉爲一個整數。這個整數只有在2到36之間,才能得到有意義的結果,超出這個範圍,則返回NaN。如果第二個參數是0、undefined和null,則直接忽略

parseInt('10', 37) // NaN
parseInt('10', 1) // NaN
parseInt('10', 0) // 10
parseInt('10', null) // 10
parseInt('10', undefined) // 10

如果字符串包含對於指定進制無意義的字符,則從最高位開始,只返回可以轉換的數值。如果最高位無法轉換,則直接返回NaN

前面說過,如果parseInt的第一個參數不是字符串,會被先轉爲字符串。這會導致一些令人意外的結果

parseInt(0x11, 36) // 43
parseInt(0x11, 2) // 1
// 等同於
parseInt(String(0x11), 36)
parseInt(String(0x11), 2)
// 等同於
parseInt('17', 36)
parseInt('17', 2)

上面代碼中,十六進制的0x11會被先轉爲十進制的17,再轉爲字符串。然後,再用36進制或二進制解讀字符串17,最後返回結果43和1。這種處理方式,對於八進制的前綴0,尤其需要注意

parseInt(011, 2) // NaN
// 等同於
parseInt(String(011), 2)
// 等同於
parseInt(String(9), 2)

上面代碼中,第一行的011會被先轉爲字符串9,因爲9不是二進制的有效字符,所以返回NaN。如果直接計算parseInt('011', 2),011則是會被當作二進制處理,返回3。JavaScript 不再允許將帶有前綴0的數字視爲八進制數,而是要求忽略這個0。但是,爲了保證兼容性,大部分瀏覽器並沒有部署這一條規定

parseFloat()

parseFloat方法用於將一個字符串轉爲浮點數。如果字符串符合科學計數法,則會進行相應的轉換。如果字符串包含不能轉爲浮點數的字符,則不再進行往後轉換,返回已經轉好的部分

parseFloat('314e-2') // 3.14
parseFloat('0.0314E+2') // 3.14

parseFloat方法會自動過濾字符串前導的空格

parseFloat('\t\v\r12.34\n ') // 12.34

如果參數不是字符串,或者字符串的第一個字符不能轉化爲浮點數,則返回NaN

parseFloat([]) // NaN
parseFloat('FF2') // NaN
parseFloat('') // NaN

尤其值得注意,parseFloat會將空字符串轉爲NaN。這些特點使得parseFloat的轉換結果不同於Number函數

parseFloat(true)  // NaN
Number(true) // 1
parseFloat(null) // NaN
Number(null) // 0
parseFloat('') // NaN
Number('') // 0
parseFloat('123.45#') // 123.45
Number('123.45#') // NaN

isNaN()

isNaN方法可以用來判斷一個值是否爲NaN。但是isNaN只對數值有效,如果傳入其他值,會被先轉成數值。比如,傳入字符串的時候,字符串會被先轉成NaN,所以最後返回true,這一點要特別引起注意。也就是說,isNaN爲true的值,有可能不是NaN,而是一個字符串。出於同樣的原因,對於對象和數組,isNaN也返回true。但是,對於空數組和只有一個數值成員的數組,isNaN返回false

isNaN([]) // false
isNaN([123]) // false
isNaN(['123']) // false

上面代碼之所以返回false,原因是這些數組能被Number函數轉成數值。因此,使用isNaN之前,最好判斷一下數據類型

function myIsNaN(value) {
  return typeof value === 'number' && isNaN(value);
}

判斷NaN更可靠的方法是,利用NaN爲唯一不等於自身的值的這個特點,進行判斷

function myIsNaN(value) {
  return value !== value;
}

isFinite()

isFinite方法返回一個布爾值,表示某個值是否爲正常的數值

isFinite(Infinity) // false
isFinite(-Infinity) // false
isFinite(NaN) // false
isFinite(undefined) // false
isFinite(null) // true
isFinite(-1) // true

除了Infinity、-Infinity、NaN和undefined這幾個值會返回false,isFinite對於其他的數值都會返回true

字符串

概述

定義

字符串就是零個或多個排在一起的字符,放在單引號或雙引號之中。單引號字符串的內部,可以使用雙引號。雙引號字符串的內部,可以使用單引號。如果要在單引號字符串的內部,使用單引號,就必須在內部的單引號前面加上反斜槓,用來轉義。雙引號字符串內部使用雙引號,也是如此。

由於 HTML 語言的屬性值使用雙引號,所以很多項目約定 JavaScript 語言的字符串只使用單引號。當然,只使用雙引號也完全可以,重要的是堅持使用一種風格,不要一會使用單引號表示字符串,一會又使用雙引號表示。字符串默認只能寫在一行內,分成多行將會報錯;如果長字符串必須分成多行,可以在每一行的尾部使用反斜槓

var longString = 'Long \
long \
long \
string';
longString  // "Long long long string"

注意,反斜槓的後面必須是換行符,而不能有其他字符(比如空格),否則會報錯。連接運算符(+)可以連接多個單行字符串,將長字符串拆成多行書寫,輸出的時候也是單行。

var longString = 'Long '
  + 'long '
  + 'long '
  + 'string';

如果想輸出多行字符串,有一種利用多行註釋的變通方法

(function () { /*
line 1
line 2
line 3
*/}).toString().split('\n').slice(1, -1).join('\n')
// "line 1
// line 2
// line 3"

轉義

反斜槓()在字符串內有特殊含義,用來表示一些特殊字符,所以又稱爲轉義符。需要用反斜槓轉義的特殊字符,主要有下面這些。

\0 :null(\u0000)
\b :後退鍵(\u0008)
\f :換頁符(\u000C)
\n :換行符(\u000A)
\r :回車鍵(\u000D)
\t :製表符(\u0009)
\v :垂直製表符(\u000B)
\' :單引號(\u0027)
\" :雙引號(\u0022)
\\ :反斜槓(\u005C)

反斜槓還有三種特殊用法

HHH

反斜槓後面緊跟三個八進制數(000到377),代表一個字符。HHH對應該字符的 Unicode 碼點,比如251表示版權符號。顯然,這種方法只能輸出256種字符

xHH

x後面緊跟兩個十六進制數(00到FF),代表一個字符。HH對應該字符的 Unicode 碼點,比如xA9表示版權符號。這種方法也只能輸出256種字符

uXXXX

u後面緊跟四個十六進制數(0000到FFFF),代表一個字符。XXXX對應該字符的 Unicode 碼點,比如u00A9表示版權符號

'\251' // "©"
'\xA9' // "©"
'\u00A9' // "©"
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true

如果在非特殊字符前面使用反斜槓,則反斜槓會被省略。如果字符串的正常內容之中,需要包含反斜槓,則反斜槓前面需要再加一個反斜槓,用來對自身轉義

字符串與數組

字符串可以被視爲字符數組,因此可以使用數組的方括號運算符,用來返回某個位置的字符(位置編號從0開始)。如果方括號中的數字超過字符串的長度,或者方括號中根本不是數字,則返回undefined

'abc'[3] // undefined
'abc'[-1] // undefined
'abc'['x'] // undefined

但是,字符串與數組的相似性僅此而已。實際上,無法改變字符串之中的單個字符

length 屬性

length屬性返回字符串的長度,該屬性也是無法改變的

字符集

JavaScript 使用 Unicode 字符集。JavaScript 引擎內部,所有字符都用 Unicode 表示。JavaScript 不僅以 Unicode 儲存字符,還允許直接在程序中使用 Unicode 碼點表示字符,即將字符寫成uxxxx的形式,其中xxxx代表該字符的 Unicode 碼點。比如,u00A9代表版權符號。解析代碼的時候,JavaScript 會自動識別一個字符是字面形式表示,還是 Unicode 形式表示。輸出給用戶的時候,所有字符都會轉成字面形式

var f\u006F\u006F = 'abc';
f // "abc"

我們還需要知道,每個字符在 JavaScript 內部都是以16位(即2個字節)的 UTF-16 格式儲存。也就是說,JavaScript 的單位字符長度固定爲16位長度,即2個字節。但是,UTF-16 有兩種長度:對於碼點在U+0000到U+FFFF之間的字符,長度爲16位(即2個字節);對於碼點在U+10000到U+10FFFF之間的字符,長度爲32位(即4個字節),而且前兩個字節在0xD800到0xDBFF之間,後兩個字節在0xDC00到0xDFFF之間。舉例來說,碼點U+1D306對應的字符爲𝌆,它寫成 UTF-16 就是0xD834 0xDF06。

JavaScript 對 UTF-16 的支持是不完整的,由於歷史原因,只支持兩字節的字符,不支持四字節的字符。這是因爲 JavaScript 第一版發佈的時候,Unicode 的碼點只編到U+FFFF,因此兩字節足夠表示了。後來,Unicode 納入的字符越來越多,出現了四字節的編碼。但是,JavaScript 的標準此時已經定型了,統一將字符長度限制在兩字節,導致無法識別四字節的字符。例如四字節字符𝌆,瀏覽器會正確識別這是一個字符,但是 JavaScript 無法識別,會認爲這是兩個字符

'𝌆'.length // 2

總結一下,對於碼點在U+10000到U+10FFFF之間的字符,JavaScript 總是認爲它們是兩個字符(length屬性爲2)。所以處理的時候,必須把這一點考慮在內,也就是說,JavaScript 返回的字符串長度可能是不正確的

Base64 轉碼

有時,文本里麪包含一些不可打印的符號,比如 ASCII 碼0到31的符號都無法打印出來,這時可以使用 Base64 編碼,將它們轉成可以打印的字符。另外,有時需要以文本格式傳遞二進制數據,那麼也可以使用 Base64 編碼。所謂 Base64 就是一種編碼方法,可以將任意值轉成 0~9、A~Z、a-z、+和/這64個字符組成的可打印字符。使用它的主要目的,不是爲了加密,而是爲了不出現特殊字符,簡化程序的處理。

JavaScript 原生提供兩個 Base64 相關的方法:

btoa():任意值轉爲 Base64 編碼

atob():Base64 編碼轉爲原來的值

var string = 'Hello World!';
btoa(string) // "SGVsbG8gV29ybGQh"
atob('SGVsbG8gV29ybGQh') // "Hello World!"

注意,這兩個方法不適合非 ASCII 碼的字符,會報錯。要將非 ASCII 碼字符轉爲 Base64 編碼,必須中間插入一個轉碼環節,再使用這兩個方法

function b64Encode(str) {
  return btoa(encodeURIComponent(str));
}
function b64Decode(str) {
  return decodeURIComponent(atob(str));
}
b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE"
b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好"

對象

概述

生成方法

對象(object)是 JavaScript 語言的核心概念,也是最重要的數據類型。什麼是對象?簡單說,對象就是一組“鍵值對”(key-value)的集合,是一種無序的複合數據集合

鍵名

對象的所有鍵名都是字符串(ES6 又引入了 Symbol 值也可以作爲鍵名),所以加不加引號都可以

var obj = {
  foo: 'Hello',
  bar: 'World'
};
等同於
var obj = {
  'foo': 'Hello',
  'bar': 'World'
};

如果鍵名是數值,會被自動轉爲字符串。如果鍵名不符合標識名的條件(比如第一個字符爲數字,或者含有空格或運算符),且也不是數字,則必須加上引號,否則會報錯

var obj = { // 報錯
  1p: 'Hello World'
};
var obj = { // 不報錯
  '1p': 'Hello World',
  'h w': 'Hello World',
  'p+q': 'Hello World'
};

上面對象的三個鍵名,都不符合標識名的條件,所以必須加上引號。對象的每一個鍵名又稱爲“屬性”(property),它的“鍵值”可以是任何數據類型。如果一個屬性的值爲函數,通常把這個屬性稱爲“方法”,它可以像函數那樣調用

var obj = {
  p: function (x) {
    return 2 * x;
  }
};
obj.p(1) // 2

如果屬性的值還是一個對象,就形成了鏈式引用

var o1 = {};
var o2 = { bar: 'hello' };
o1.foo = o2;
o1.foo.bar // "hello"

屬性可以動態創建,不必在對象聲明時就指定

對象的引用

如果不同的變量名指向同一個對象,那麼它們都是這個對象的引用,也就是說指向同一個內存地址。修改其中一個變量,會影響到其他所有變量;如果取消某一個變量對於原對象的引用,不會影響到另一個變量

var x = 1;
var y = x;
x = 2;
y // 1

上面的代碼中,當x的值發生變化後,y的值並不變,這就表示y和x並不是指向同一個內存地址

表達式還是語句

對象採用大括號表示,這導致了一個問題:如果行首是一個大括號,它到底是表達式還是語句?

{ foo: 123 }

JavaScript 引擎讀到上面這行代碼,會發現可能有兩種含義。第一種:這是一個表達式,表示一個包含foo屬性的對象;第二種:這是一個語句,表示一個代碼區塊,裏面有一個標籤foo,指向表達式123。爲了避免這種歧義,JavaScript 引擎的做法是,如果遇到這種情況,無法確定是對象還是代碼塊,一律解釋爲代碼塊

如果要解釋爲對象,最好在大括號前加上圓括號。因爲圓括號的裏面,只能是表達式,所以確保大括號只能解釋爲對象

({ foo: 123 }) // 正確
({ console.log(123) }) // 報錯

這種差異在eval語句(作用是對字符串求值)中反映得最明顯

eval('{foo: 123}') // 123
eval('({foo: 123})') // {foo: 123}

上面代碼中,如果沒有圓括號,eval將其理解爲一個代碼塊;加上圓括號以後,就理解成一個對象

屬性的操作

屬性的讀取

讀取對象的屬性,有兩種方法,一種是使用點運算符,還有一種是使用方括號運算符

var obj = {
  p: 'Hello World'
};
obj.p // "Hello World"
obj['p'] // "Hello World"

請注意,如果使用方括號運算符,鍵名必須放在引號裏面,否則會被當作變量處理

var foo = 'bar';
var obj = {
  foo: 1,
  bar: 2
};
obj.foo  // 1
obj[foo]  // 2

上面代碼中,引用對象obj的foo屬性時,如果使用點運算符,foo就是字符串;如果使用方括號運算符但不使用引號,foo就是一個變量,指向字符串bar

方括號運算符內部還可以使用表達式

obj['hello' + ' world']
obj[3 + 3]

數字鍵可以不加引號,因爲會自動轉成字符串

var obj = {
  0.7: 'Hello World'
};
obj['0.7'] // "Hello World"
obj[0.7] // "Hello World"

注意,數值鍵名不能使用點運算符(因爲會被當成小數點),只能使用方括號運算符

屬性的賦值

點運算符和方括號運算符,不僅可以用來讀取值,還可以用來賦值。JavaScript 允許屬性的“後綁定”,也就是說,你可以在任意時刻新增屬性,沒必要在定義對象的時候,就定義好屬性

var obj = { p: 1 };
// 等價於
var obj = {};
obj.p = 1;

屬性的查看

查看一個對象本身的所有屬性,可以使用Object.keys方法

var obj = {
  key1: 1,
  key2: 2
};
Object.keys(obj); // ['key1', 'key2']

屬性的刪除:delete 命令

delete命令用於刪除對象的屬性,刪除成功後返回true

var obj = { p: 1 };
Object.keys(obj) // ["p"]
delete obj.p // true
obj.p // undefined
Object.keys(obj) // []

注意,刪除一個不存在的屬性,delete不報錯,而且返回true。因此不能根據delete命令的結果,認定某個屬性是存在的。只有一種情況,delete命令會返回false,那就是該屬性存在,且不得刪除

var obj = Object.defineProperty({}, 'p', { //Object.defineProperty()方法會直接在一個對象上定義一個新屬性,或修改一個對象的現有屬性,並返回這個對象。默認使用 Object.defineProperty() 添加的屬性值是不可修改的
  value: 123,
  configurable: false //表示能否通過delete刪除屬性從而重新定義屬性
});
obj.p // 123
delete obj.p // false

注意:delete命令只能刪除對象本身的屬性,無法刪除繼承的屬性

var obj = {};
delete obj.toString // true
obj.toString // function toString() { [native code] }

上面代碼中,toString是對象obj繼承的屬性,雖然delete命令返回true,但該屬性並沒有被刪除,依然存在。這個例子還說明,即使delete返回true,該屬性依然可能讀取到值

屬性是否存在:in 運算符

in運算符用於檢查對象是否包含某個屬性(注意:檢查的是鍵名,不是鍵值),如果包含就返回true,否則返回false。它的左邊是一個字符串,表示屬性名,右邊是一個對象

var obj = { p: 1 };
'p' in obj // true
'toString' in obj // true

in運算符的一個問題是,它不能識別哪些屬性是對象自身的,哪些屬性是繼承的。就像上面代碼中,對象obj本身並沒有toString屬性,但是in運算符會返回true,因爲這個屬性是繼承的。

這時,可以使用對象的hasOwnProperty方法判斷一下,是否爲對象自身的屬性

var obj = {};
if ('toString' in obj) {
  console.log(obj.hasOwnProperty('toString')) // false
}

屬性的遍歷:for...in 循環

for...in循環用來遍歷一個對象的全部屬性

var obj = {a: 1, b: 2, c: 3};
for (var i in obj) {
  console.log('鍵名:', i);
  console.log('鍵值:', obj[i]);
}
// 鍵名: a
// 鍵值: 1
// 鍵名: b
// 鍵值: 2
// 鍵名: c
// 鍵值: 3

for...in循環有兩個使用注意點:

1.它遍歷的是對象所有可遍歷(enumerable)的屬性,會跳過不可遍歷的屬性

2.它僅遍歷對象自身的屬性,會忽略掉那些從原型鏈上繼承到的屬性

舉例來說,對象都繼承了toString屬性,但是for...in循環不會遍歷到這個屬性

var obj = {};
// toString 屬性是存在的
obj.toString // toString() { [native code] }
for (var p in obj) {
  console.log(p);
} // 沒有任何輸出

如果繼承的屬性是可遍歷的,那麼就會被for...in循環遍歷到。但一般情況下都是隻想遍歷對象自身的屬性,所以使用for...in的時候,應該結合使用hasOwnProperty方法,在循環內部判斷一下,某個屬性是否爲對象自身的屬性

var person = { name: '老張' };
for (var key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key);
  }
}
// name

with 語句

with語句的格式如下:

with (對象) {
  語句;
}

它的作用是操作同一個對象的多個屬性時,提供一些書寫的方便

// 例一
var obj = {
  p1: 1,
  p2: 2,
};
with (obj) {
  p1 = 4;
  p2 = 5;
}
// 等同於
obj.p1 = 4;
obj.p2 = 5;

// 例二
with (document.links[0]){
  console.log(href);
  console.log(title);
  console.log(style);
}
// 等同於
console.log(document.links[0].href);
console.log(document.links[0].title);
console.log(document.links[0].style);

注意:如果with區塊內部有變量的賦值操作,必須是當前對象已經存在的屬性,否則會創造一個當前作用域的全局變量

var obj = {};
with (obj) {
  p1 = 4;
  p2 = 5;
}
obj.p1 // undefined
p1 // 4

上面代碼中,對象obj並沒有p1屬性,對p1賦值等於創造了一個全局變量p1。正確的寫法應該是,先定義對象obj的屬性p1,然後在with區塊內操作它

with區塊沒有改變作用域,它的內部依然是當前作用域。這造成了with語句的一個很大的弊病,就是綁定對象不明確。可以考慮用一個臨時變量代替with

with(obj1.obj2.obj3) {
  console.log(p1 + p2);
}
// 可以寫成
var temp = obj1.obj2.obj3;
console.log(temp.p1 + temp.p2);

函數

函數是一段可以反覆調用的代碼塊。函數還能接受輸入的參數,不同的參數會返回不同的值

概述

函數的聲明

JavaScript 有三種聲明函數的方法

function 命令

function命令聲明的代碼區塊,就是一個函數。function命令後面是函數名,函數名後面是一對圓括號,裏面是傳入函數的參數。函數體放在大括號裏面

function print(s) {
  console.log(s);
}

上面的代碼命名了一個print函數,以後使用print()這種形式,就可以調用相應的代碼。這叫做函數的聲明(Function Declaration)

函數表達式

除了用function命令聲明函數,還可以採用變量賦值的寫法

var print = function(s) {
  console.log(s);
};

這種寫法將一個匿名函數賦值給變量。這時,這個匿名函數又稱函數表達式(Function Expression),因爲賦值語句的等號右側只能放表達式。採用函數表達式聲明函數時,function命令後面不帶有函數名。如果加上函數名,該函數名只在函數體內部有效,在函數體外部無效

var print = function x(){
  console.log(typeof x);
};
x // ReferenceError: x is not defined
print() // function

上面代碼在函數表達式中加入了函數名x。這個x只在函數體內部可用,指代函數表達式本身,其他地方都不可用。這種寫法的用處有兩個,一是可以在函數體內部調用自身,二是方便除錯(除錯工具顯示函數調用棧時,將顯示函數名,而不再顯示這裏是一個匿名函數)

注意:函數的表達式需要在語句結尾加上分號表示語句結束。而函數的聲明在結尾的大括號後面不用加分號。總的來說,這兩種聲明函數的方式,差別很細微,可以近似認爲是等價的

Function 構造函數

第三種聲明函數的方式是Function構造函數

var add = new Function(
  'x',
  'y',
  'return x + y'
);
// 等同於
function add(x, y) {
  return x + y;
}

總的來說,這種聲明函數的方式非常不直觀,幾乎無人使用

函數的重複聲明

如果同一個函數被多次聲明,後面的聲明就會覆蓋前面的聲明

function f() {
  console.log(1);
}
f() // 2
function f() {
  console.log(2);
}
f() // 2

上面代碼中,後一次的函數聲明覆蓋了前面一次。而且,由於函數名的提升,前一次聲明在任何時候都是無效的

圓括號運算符,return 語句和遞歸

調用函數時,要使用圓括號運算符。圓括號之中,可以加入函數的參數

function add(x, y) {
  return x + y;
}
add(1, 1) // 2

函數體內部的return語句表示返回。JavaScript 引擎遇到return語句就直接返回return後面的那個表達式的值,不再執行後面語句。return語句不是必需的,如果沒有的話,該函數就不返回任何值,或者說返回undefined。

函數可以調用自身,這就是遞歸(recursion)。下面就是通過遞歸,計算斐波那契數列的代碼

function fib(num) {
  if (num === 0) return 0;
  if (num === 1) return 1;
  return fib(num - 2) + fib(num - 1);
}

fib(6) // 8

第一等公民

JavaScript 語言將函數看作一種值,與其它值(數值、字符串、布爾值等等)地位相同。凡是可以使用值的地方,就能使用函數。比如,可以把函數賦值給變量和對象的屬性,也可以當作參數傳入其他函數,或者作爲函數的結果返回。函數只是一個可以執行的值,此外並無特殊之處。由於函數與其他數據類型地位平等,所以在 JavaScript 語言中又稱函數爲第一等公民

function add(x, y) {
  return x + y;
}
var operator = add; // 將函數賦值給一個變量
function a(op){ // 將函數作爲參數和返回值
  return op;
}
a(add)(1, 1) // 2

函數名的提升

JavaScript 引擎將函數名視同變量名,所以採用function命令聲明函數時,整個函數會像變量聲明一樣,被提升到代碼頭部

f();
function f() {}

表面上,上面代碼好像在聲明之前就調用了函數f。但是實際上,由於“變量提升”,函數f被提升到了代碼頭部,也就是在調用之前已經聲明瞭。但是,如果採用賦值語句定義函數,JavaScript 就會報錯

f();
var f = function (){}; // TypeError: undefined is not a function
等同於
var f;
f();
f = function () {};  

上面代碼調用f的時候,f只是被聲明瞭,還沒有被賦值,等於undefined,所以會報錯。因此,如果同時採用function命令和賦值語句聲明同一個函數,最後總是採用賦值語句的定義

var f = function () {
  console.log('1');
}
function f() {
  console.log('2');
}
f() // 1

函數的屬性和方法

name 屬性

函數的name屬性返回函數的名字

function f1() {}
f1.name // "f1"

如果是通過變量賦值定義的函數,那麼name屬性返回變量名

var f2 = function () {};
f2.name // "f2"

但是,上面這種情況,只有在變量的值是一個匿名函數時纔是如此。如果變量的值是一個具名函數,那麼name屬性返回function關鍵字之後的那個函數名

var f3 = function myName() {};
f3.name // 'myName'

上面代碼中,f3.name返回函數表達式的名字。注意,真正的函數名還是f3,而myName這個名字只在函數體內部可用。name屬性的一個用處,就是獲取參數函數的名字

var myFunc = function () {};
function test(f) {
  console.log(f.name);
}
test(myFunc) // myFunc

length 屬性

函數的length屬性返回函數預期傳入的參數個數,即函數定義之中的參數個數

function f(a, b) {}
f.length // 2

上面代碼定義了空函數f,它的length屬性就是定義時的參數個數。不管調用時輸入了多少個參數,length屬性始終等於2。length屬性提供了一種機制,判斷定義時和調用時參數的差異,以便實現面向對象編程的“方法重載”(overload)

toString()

函數的toString方法返回一個字符串,內容是函數的源碼

function f() {
  /*
  這是一個
  多行註釋
*/
  a();
  b();;
}

f.toString()
// function f() {
//  /*
//  這是一個
//  多行註釋
// */
//  a();
//  b();
// }

函數內部的註釋也可以返回。利用這點可以變相實現多行字符串

var multiline = function (fn) {
  var arr = fn.toString().split('\n');
  return arr.slice(1, arr.length - 1).join('\n');
};
function f() {/*
  這是一個
  多行註釋
*/}
multiline(f);
// " 這是一個
//   多行註釋"

函數作用域

定義

作用域(scope)指變量存在的範圍。在 ES5 的規範中,JavaScript 只有兩種作用域:一種是全局作用域,變量在整個程序中一直存在,所有地方都可以讀取;另一種是函數作用域,變量只在函數內部存在。ES6 又新增了塊級作用域,這裏不涉及。對於頂層函數來說,函數外部聲明的變量就是全局變量(global variable),它可以在函數內部讀取。函數內部定義的變量外部無法讀取,稱爲“局部變量”(local variable)。函數內部定義的變量,會在該作用域內覆蓋同名全局變量

var v = 1;
function f(){
  var v = 2;
  console.log(v);
}
f() // 2
v // 1

函數內部的變量提升

與全局作用域一樣,函數作用域內部也會產生“變量提升”現象。var命令聲明的變量,不管在什麼位置,變量聲明都會被提升到函數體的頭部

函數本身的作用域

函數本身也是一個值,也有自己的作用域。它的作用域與變量一樣,就是其聲明時所在的作用域,與其運行時所在的作用域無關

var a = 1;
var x = function () {
  console.log(a);
};
function f() {
  var a = 2;
  x();
}
f() // 1

上面代碼中,函數x是在函數f的外部聲明的,所以它的作用域綁定外層,內部變量a不會到函數f體內取值,所以輸出1,而不是2。總之,函數執行時所在的作用域,是定義時的作用域,而不是調用時所在的作用域。很容易犯錯的一點是,如果函數A調用函數B,卻沒考慮到函數B不會引用函數A的內部變量

同樣的,函數體內部聲明的函數,作用域綁定函數體內部。正是這種機制構成了“閉包”現象

function foo() {
  var x = 1;
  function bar() {
    console.log(x);
  }
  return bar;
}
var x = 2;
var f = foo();
f() // 1

參數

概述

函數運行的時候,有時需要提供外部數據,不同的外部數據會得到不同的結果,這種外部數據就叫參數

參數的省略

函數參數不是必需的,JavaScript 允許省略參數

function f(a, b) {
  return a;
}
f(1, 2, 3) // 1
f(1) // 1
f() // undefined
f.length // 2

JavaScript中省略的參數值就變爲undefined。但是,沒辦法只省略靠前的參數保留靠後的參數。如果一定要省略靠前的參數,只有顯式傳入undefined

function f(a, b) {
  return a;
}
//如果省略第一個參數,就會報錯
f( , 1) // SyntaxError: Unexpected token ,(…)
f(undefined, 1) // undefined

傳遞方式

函數參數如果是原始類型的值(數值、字符串、布爾值),傳遞方式是傳值傳遞(passes by value)。這意味着,在函數體內修改參數值,不會影響到函數外部。但是,如果函數參數是複合類型的值(數組、對象、其他函數),傳遞方式是傳址傳遞(pass by reference)。也就是說,傳入函數的原始值的地址,因此在函數內部修改參數,將會影響到原始值

var obj = { p: 1 };
function f(o) {
  o.p = 2;
}
f(obj);
obj.p // 2

上面代碼中,傳入函數f的是參數對象obj的地址。因此,在函數內部修改obj的屬性p,會影響到原始值。但是,如果函數內部修改的,不是參數對象的某個屬性,而是替換掉整個參數,這時不會影響到原始值

var obj = [1, 2, 3];
function f(o) {
  o = [2, 3, 4];
}
f(obj);
obj // [1, 2, 3]

同名參數

如果有同名的參數,則取最後出現的那個值

function f(a, a) {
  console.log(a);
}
f(1) // undefined

上面代碼中,函數f有兩個參數,且參數名都是a。取值的時候,以後面的a爲準,即使後面的a沒有值或被省略,也是以其爲準。這時,如果要獲得第一個a的值,可以使用arguments對象

function f(a, a) {
  console.log(arguments[0]);
}
f(1) // 1

arguments 對象

定義

由於 JavaScript 允許函數有不定數目的參數,所以需要一種機制,可以在函數體內部讀取所有參數。這就是arguments對象的由來。arguments對象包含了函數運行時的所有參數,arguments[0]就是第一個參數,arguments[1]就是第二個參數,以此類推。這個對象只有在函數體內部,纔可以使用

var f = function (one) {
  console.log(arguments[0]);
  console.log(arguments[1]);
  console.log(arguments[2]);
}
f(1, 2, 3)
// 1
// 2
// 3

正常模式下,arguments對象可以在運行時修改

var f = function(a, b) {
  arguments[0] = 3;
  arguments[1] = 2;
  return a + b;
}
f(1, 1) // 5

上面代碼中,函數f調用時傳入的參數,在函數內部被修改成3和2。嚴格模式下,arguments對象與函數參數不具有聯動關係。也就是說:嚴格模式下修改arguments對象不會影響到實際的函數參數

通過arguments對象的length屬性,可以判斷函數調用時到底帶幾個參數

function f() {
  return arguments.length;
}
f(1, 2, 3) // 3
f(1) // 1
f() // 0
與數組的關係

雖然arguments很像數組,但它是一個對象。數組專有的方法(比如slice和forEach),不能在arguments對象上直接使用。如果要讓arguments對象使用數組方法,真正的解決方法是將arguments轉爲真正的數組。下面是兩種常用的轉換方法:slice方法和逐一填入新數組

var args = Array.prototype.slice.call(arguments);
// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {
  args.push(arguments[i]);
}
callee 屬性

arguments對象帶有一個callee屬性,返回它所對應的原函數

var f = function () {
  console.log(arguments.callee === f);
}
f() // true

可以通過arguments.callee,達到調用函數自身的目的。這個屬性在嚴格模式裏面是禁用的

函數的其他知識點

閉包

閉包(closure)是 JavaScript 語言的一個難點,也是它的特色,很多高級應用都要依靠閉包實現。理解閉包,首先必須理解變量作用域。前面提到,JavaScript 有兩種作用域:全局作用域和函數作用域。函數內部可以直接讀取全局變量。但是函數外部無法讀取函數內部聲明的變量。如果出於種種原因,需要得到函數內的局部變量。正常情況下,這是辦不到的,只有通過變通方法才能實現。那就是在函數的內部,再定義一個函數

function f1() {
  var n = 999;
  function f2() {
    console.log(n);
  }
  return f2;
}
var result = f1();
result(); // 999

上面代碼中,函數f1的返回值就是函數f2,由於f2可以讀取f1的內部變量,所以就可以在外部獲得f1的內部變量了。

閉包就是函數f2,即能夠讀取其他函數內部變量的函數。由於在 JavaScript 語言中,只有函數內部的子函數才能讀取內部變量,因此可以把閉包簡單理解成“定義在一個函數內部的函數”。閉包最大的特點,就是它可以“記住”誕生的環境,比如f2記住了它誕生的環境f1,所以從f2可以得到f1的內部變量。在本質上,閉包就是將函數內部和函數外部連接起來的一座橋樑。閉包的最大用處有兩個,一個是可以讀取函數內部的變量,另一個就是讓這些變量始終保持在內存中,即閉包可以使得它誕生環境一直存在

閉包的另一個用處,是封裝對象的私有屬性和私有方法

function Person(name) {
  var _age;
  function setAge(n) {
    _age = n;
  }
  function getAge() {
    return _age;
  }
  return {
    name: name,
    getAge: getAge,
    setAge: setAge
  };
}
var p1 = Person('張三');
p1.setAge(25);
p1.getAge() // 25

上面代碼中,函數Person的內部變量_age,通過閉包getAge和setAge,變成了返回對象p1的私有變量。

注意:外層函數每次運行,都會生成一個新的閉包,而這個閉包又會保留外層函數的內部變量,所以內存消耗很大。因此不能濫用閉包,否則會造成網頁的性能問題

立即調用的函數表達式(IIFE)

在 JavaScript 中,圓括號()是一種運算符,跟在函數名之後,表示調用該函數。比如,print()就表示調用print函數

function這個關鍵字即可以當作語句,也可以當作表達式。爲了避免解析上的歧義,JavaScript 引擎規定,如果function關鍵字出現在行首,一律解釋成語句

// 語句
function f() {}
// 表達式
var f = function f() {}

當我們需要在定義函數之後立即調用該函數時,不能只是在函數的定義之後加圓括號。解決方法就是不要讓function出現在行首,讓引擎將其理解成一個表達式。最簡單的處理,就是將其放在一個圓括號裏面

(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();

上面兩種寫法都是以圓括號開頭,引擎就會認爲後面跟的是一個表示式,而不是函數定義語句,所以就避免了錯誤。這就叫做“立即調用的函數表達式”(Immediately-Invoked Function Expression),簡稱 IIFE。注意,上面兩種寫法最後的分號都是必須的。如果省略分號,遇到連着兩個 IIFE,JavaScript 會將它們連在一起解釋,將第二行解釋爲第一行的參數,從而報錯

推而廣之,任何讓解釋器以表達式來處理函數定義的方法,都能產生同樣的效果,比如下面寫法

var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();

通常情況下,只對匿名函數使用這種“立即執行的函數表達式”。它的目的有兩個:一是不必爲函數命名,避免了污染全局變量;二是 IIFE 內部形成了一個單獨的作用域,可以封裝一些外部無法讀取的私有變量

// 寫法一
var tmp = newData;
processData(tmp);
storeData(tmp);

// 寫法二
(function () {
  var tmp = newData;
  processData(tmp);
  storeData(tmp);
}());

上面代碼中,寫法二比寫法一更好,因爲完全避免了污染全局變量

eval 命令

基本用法

eval命令接受一個字符串作爲參數,並將這個字符串當作語句執行

eval('var a = 1;');
a // 1

上面代碼將字符串當作語句運行,生成了變量a。如果參數字符串無法當作語句運行,那麼就會報錯

eval('3x') // Uncaught SyntaxError: Invalid or unexpected token

放在eval中的字符串,應該有獨自存在的意義,不能用來與eval以外的命令配合使用。舉例來說,下面的代碼將會報錯

eval('return;'); // Uncaught SyntaxError: Illegal return statement

上面代碼會報錯,因爲return不能單獨使用,必須在函數中使用。如果eval的參數不是字符串,那麼會原樣返回

eval(123) // 123

eval沒有自己的作用域,都在當前作用域內執行,因此可能會修改當前作用域的變量的值,造成安全問題

var a = 1;
eval('a = 2');
a // 2

上面代碼中,eval命令修改了外部變量a的值,因此eval有安全風險。爲了防止這種風險,JavaScript 規定,如果使用嚴格模式,eval內部聲明的變量,不會影響到外部作用域

(function f() {
  'use strict';
  eval('var foo = 123');
  console.log(foo);  // ReferenceError: foo is not defined
})()

上面代碼中,函數f內部是嚴格模式,這時eval內部聲明的foo變量,就不會影響到外部。不過,即使在嚴格模式下,eval依然可以讀寫當前作用域的變量

(function f() {
  'use strict';
  var foo = 1;
  eval('foo = 2');
  console.log(foo);  // 2
})()

上面代碼中,嚴格模式下,eval內部還是改寫了外部變量,可見安全風險依然存在。總之,eval的本質是在當前作用域中注入代碼。由於安全風險和不利於 JavaScript 引擎優化執行速度,所以一般不推薦使用。通常情況下,eval最常見的場合是解析 JSON 數據的字符串,不過正確的做法應該是使用原生的JSON.parse方法

eval 的別名調用

前面說過eval不利於引擎優化執行速度。更麻煩的是,還有下面這種情況,引擎在靜態代碼分析的階段,根本無法分辨執行的是eval

var m = eval;
m('var x = 1');
x // 1

上面代碼中,變量m是eval的別名。靜態代碼分析階段,引擎分辨不出m('var x = 1')執行的是eval命令。爲了保證eval的別名不影響代碼優化,JavaScript 的標準規定,凡是使用別名執行eval,eval內部一律是全局作用域

var a = 1;
function f() {
  var a = 2;
  var e = eval;
  e('console.log(a)');
}
f() // 1

上面代碼中,eval是別名調用,所以即使它是在函數中,它的作用域還是全局作用域,因此輸出的a爲全局變量。這樣的話,引擎就能確認e()不會對當前的函數作用域產生影響,優化的時候就可以把這一行排除掉。eval的別名調用的形式五花八門,只要不是直接調用,都屬於別名調用,因爲引擎只能分辨eval()這一種形式是直接調用

eval.call(null, '...')
window.eval('...')
(1, eval)('...')
(eval, eval)('...')

上面這些形式都是eval的別名調用,作用域都是全局作用域

數組

定義

數組(array)是按次序排列的一組值。每個值的位置都有編號(從0開始),整個數組用方括號表示

var arr = ['a', 'b', 'c'];

除了在定義時賦值,數組也可以先定義後賦值;任何類型的數據都可以放入數組;如果數組的元素還是數組,就形成了多維數組

數組的本質

本質上,數組屬於一種特殊的對象。typeof運算符會返回數組的類型是object;數組的特殊性體現在,它的鍵名是按次序排列的一組整數(0,1,2...)

var arr = ['a', 'b', 'c'];
Object.keys(arr) // ["0", "1", "2"]

上面代碼中,Object.keys方法返回數組的所有鍵名。由於數組成員的鍵名是固定的(默認總是0、1、2...),因此數組不用爲每個元素指定鍵名,而對象的每個成員都必須指定鍵名。JavaScript 語言規定,對象的鍵名一律爲字符串,所以,數組的鍵名其實也是字符串。之所以可以用數值讀取,是因爲非字符串的鍵名會被轉爲字符串。這點在賦值時也成立,一個值總是先轉成字符串再進行賦值

var arr = ['a', 'b', 'c'];
arr['0'] // 'a'
arr[0] // 'a'

對象有兩種讀取成員的方法:點結構(object.key)和方括號結構(object[key])。但是,對於數值的鍵名,不能使用點結構

length 屬性

數組的length屬性,返回數組的成員數量。JavaScript 使用一個32位整數保存數組的元素個數,這意味着數組成員最多隻有4294967295個(232 - 1)個,也就是說length屬性的最大值就是4294967295。只要是數組,就一定有length屬性。該屬性是一個動態值,等於鍵名中的最大整數加上1

var arr = ['a', 'b'];
arr.length // 2
arr[2] = 'c';
arr.length // 3
arr[9] = 'd';
arr.length // 10
arr[1000] = 'e';
arr.length // 1001

上面代碼表示:數組的數字鍵不需要連續,length屬性的值總是比最大的那個整數鍵大1。另外,這也表明數組是一種動態的數據結構,可以隨時增減數組的成員,length屬性是可寫的。如果人爲設置一個小於當前成員個數的值,該數組的成員會自動減少到length設置的值

var arr = [ 'a', 'b', 'c' ];
arr.length // 3
arr.length = 2;
arr // ["a", "b"]

清空數組的一個有效方法,就是將length屬性設爲0。如果人爲設置length大於當前元素個數,則數組的成員數量會增加到這個值,新增的位置都是空位,,讀取新增的位置都會返回undefined。如果人爲設置length爲不合法的值JavaScript 會報錯

// 設置負值
[].length = -1 // RangeError: Invalid array length
// 數組元素個數大於等於2的32次方
[].length = Math.pow(2, 32) // RangeError: Invalid array length
// 設置字符串
[].length = 'abc' // RangeError: Invalid array length

值得注意的是,由於數組本質上是一種對象,所以可以爲數組添加屬性,但是這不影響length屬性的值

var a = [];
a['p'] = 'abc';
a.length // 0
a[2.1] = 'abc';
a.length // 0

上面代碼將數組的鍵分別設爲字符串和小數,結果都不影響length屬性。因爲,length屬性的值就是等於最大的數字鍵加1,而這個數組沒有整數鍵,所以length屬性保持爲0。如果數組的鍵名是添加超出範圍的數值,該鍵名會自動轉爲字符串

var arr = [];
arr[-1] = 'a';
arr[Math.pow(2, 32)] = 'b';
arr.length // 0
arr[-1] // "a"
arr[4294967296] // "b"

上面代碼中爲數組arr添加了兩個不合法的數字鍵,結果length屬性沒有發生變化,這些數字鍵都變成了字符串鍵名。最後兩行之所以會取到值,是因爲取鍵值時,數字鍵名會默認轉爲字符串

in 運算符

檢查某個鍵名是否存在的運算符in,適用於對象,也適用於數組

var arr = [ 'a', 'b', 'c' ];
2 in arr  // true
'2' in arr // true
4 in arr // false

上面代碼表明,數組存在鍵名爲2的鍵;由於鍵名都是字符串,所以數值2會自動轉成字符串。注意:如果數組的某個位置是空位,in運算符返回false

var arr = [];
arr[100] = 'a';
100 in arr // true
1 in arr // false

for...in 循環和數組的遍歷

for...in循環不僅可以遍歷對象,也可以遍歷數組,畢竟數組只是一種特殊對象;for...in不僅會遍歷數組所有的數字鍵,還會遍歷非數字鍵;所以不推薦使用for...in遍歷數組

var a = [1, 2, 3];
a.foo = true;
for (var key in a) {
  console.log(key);
}
// 0
// 1
// 2
// foo

數組的遍歷可以考慮使用for循環或while循環

var a = [1, 2, 3];
// for循環
for(var i = 0; i < a.length; i++) {
  console.log(a[i]);
}
// while循環
var i = 0;
while (i < a.length) {
  console.log(a[i]);
  i++;
}
var l = a.length;
while (l--) { //逆向遍歷
  console.log(a[l]);
}

數組的forEach方法,也可以用來遍歷數組

var colors = ['red', 'green', 'blue'];
colors.forEach(function (color) {
  console.log(color);
});
// red
// green
// blue

數組的空位

當數組的某個位置是空元素,即兩個逗號之間沒有任何值,我們稱該數組存在空位(hole)。數組的空位不影響length屬性。需要注意的是,如果最後一個元素後面有逗號,並不會產生空位。也就是說,有沒有這個逗號,結果都是一樣的

var a = [1, 2, 3,];
a.length // 3
a // [1, 2, 3]

數組的空位是可以讀取的,返回undefined;使用delete命令刪除一個數組成員會形成空位,且不會影響length屬性,也就是說length屬性不過濾空位

數組的某個位置是空位,與某個位置是undefined,是不一樣的。如果是空位,使用數組的forEach方法、for...in結構、以及Object.keys方法進行遍歷,空位都會被跳過。如果某個位置是undefined,遍歷的時候就不會被跳過

var a = [, , ,];
a.forEach(function (x, i) {
  console.log(i + '. ' + x);
}) // 0. undefined
for (var i in a) {
  console.log(i);
} // 0
Object.keys(a) // ["0"]

這就是說,空位就是數組沒有這個元素,所以不會被遍歷到,而undefined則表示數組有這個元素,值是undefined,所以遍歷不會跳過

類似數組的對象

如果一個對象的所有鍵名都是正整數或零,並且有length屬性,那麼這個對象就很像數組,語法上稱爲“類似數組的對象”(array-like object)

var obj = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
};
obj[0] // 'a'
obj[1] // 'b'
obj.length // 3
obj.push('d') // TypeError: obj.push is not a function

對象obj就是一個類似數組的對象。但是,“類似數組的對象”並不是數組,因爲它們不具備數組特有的方法。對象obj沒有數組的push方法,使用該方法就會報錯。“類似數組的對象”的根本特徵,就是具有length屬性。只要有length屬性,就可以認爲這個對象類似於數組。但是有一個問題,這種length屬性不是動態值,不會隨着成員的變化而變化

典型的“類似數組的對象”是函數的arguments對象,以及大多數 DOM 元素集,還有字符串

// arguments對象
function args() { return arguments }
var arrayLike = args('a', 'b');
arrayLike[0] // 'a'
arrayLike.length // 2
arrayLike instanceof Array // false

// DOM元素集
var elts = document.getElementsByTagName('h3');
elts.length // 3
elts instanceof Array // false

// 字符串
'abc'[1] // 'b'
'abc'.length // 3
'abc' instanceof Array // false

數組的slice方法可以將“類似數組的對象”變成真正的數組

var arr = Array.prototype.slice.call(arrayLike);

除了轉爲真正的數組,“類似數組的對象”還有一個辦法可以使用數組的方法,就是通過call()把數組的方法放到對象上面

function print(value, index) {
  console.log(index + ' : ' + value);
}
Array.prototype.forEach.call(arrayLike, print);

上面代碼中,arrayLike代表一個類似數組的對象,本來是不可以使用數組的forEach()方法的,但是通過call(),可以把forEach()嫁接到arrayLike上面調用。下面的例子就是通過這種方法,在arguments對象上面調用forEach方法

// forEach 方法
function logArgs() {
  Array.prototype.forEach.call(arguments, function (elem, i) {
    console.log(i + '. ' + elem);
  });
}
// 等同於 for 循環
function logArgs() {
  for (var i = 0; i < arguments.length; i++) {
    console.log(i + '. ' + arguments[i]);
  }
}

字符串也是類似數組的對象,所以也可以用Array.prototype.forEach.call遍歷

注意,這種方法比直接使用數組原生的forEach要慢,所以最好還是先將“類似數組的對象”轉爲真正的數組,然後再直接調用數組的forEach方法

運算符

算術運算符

運算符是處理數據的基本方法,用來從現有的值得到新的值。JavaScript 提供了多種運算符,覆蓋了所有主要的運算

概述

JavaScript 共提供10個算術運算符,用來完成基本的算術運算。

加法運算符:x + y
減法運算符: x - y
乘法運算符: x * y
除法運算符:x / y
指數運算符:x ** y
餘數運算符:x % y
自增運算符:++x 或者 x++
自減運算符:--x 或者 x--
數值運算符: +x
負數值運算符:-x

減法、乘法、除法運算法比較單純,就是執行相應的數學運算。下面介紹其他幾個算術運算符,重點是加法運算符

加法運算符

基本規則

加法運算符(+)是最常見的運算符,用來求兩個數值的和。JavaScript 允許非數值的相加

true + true // 2
1 + true // 2

上面代碼中,第一行是兩個布爾值相加,第二行是數值與布爾值相加。這兩種情況,布爾值都會自動轉成數值,然後再相加。比較特殊的是,如果是兩個字符串相加,這時加法運算符會變成連接運算符,返回一個新的字符串,將兩個原字符串連接在一起。如果一個運算子是字符串,另一個運算子是非字符串,這時非字符串會轉成字符串,再連接在一起

加法運算符是在運行時決定,到底是執行相加還是執行連接。也就是說,運算值的不同導致了不同的語法行爲,這種現象稱爲“重載”(overload)

'3' + 4 + 5 // "345"
3 + 4 + '5' // "75"

上面代碼中,由於從左到右的運算次序,字符串的位置不同會導致不同的結果

除了加法運算符,其他算術運算符(比如減法、除法和乘法)都不會發生重載。它們的規則是:所有運算值一律轉爲數值,再進行相應的數學運算

對象的相加

如果運算值是對象,必須先轉成原始類型的值,然後再相加

var obj = { p: 1 };
obj + 2 // "[object Object]2"

上面代碼中,對象obj轉成原始類型的值是[object Object],再加2就得到了上面的結果

對象轉成原始類型的值,規則如下

首先,自動調用對象的valueOf方法

var obj = { p: 1 };
obj.valueOf() // { p: 1 }

一般來說,對象的valueOf方法總是返回對象自身,這時再自動調用對象的toString方法,將其轉爲字符串

var obj = { p: 1 };
obj.valueOf().toString() // "[object Object]"

對象的toString方法默認返回[object Object],所以就得到了最前面那個例子的結果。知道了這個規則以後,就可以自己定義valueOf方法或toString方法,得到想要的結果

var obj = {
  valueOf: function () {
    return 1;
  }
};
obj + 2 // 3

由於valueOf方法直接返回一個原始類型的值,所以不再調用toString方法。下面是自定義toString方法的例子

var obj = {
  toString: function () {
    return 'hello';
  }
};
obj + 2 // "hello2"

如果運算值是一個Date對象的實例,那麼會優先執行toString方法

var obj = new Date();
obj.valueOf = function () { return 1 };
obj.toString = function () { return 'hello' };
obj + 2 // "hello2"

上面代碼中,對象obj是一個Date對象的實例,並且自定義了valueOf方法和toString方法,結果toString方法優先執行

餘數運算符

餘數運算符(%)返回前一個運算值被後一個運算值除,所得的餘數。需要注意的是,運算結果的正負號由第一個運算子的正負號決定

-1 % 2 // -1
12 % 5 // 2

所以,爲了得到負數的正確餘數值,可以先使用絕對值函數

// 錯誤的寫法
function isOdd(n) {
  return n % 2 === 1;
}
isOdd(-5) // false
isOdd(-4) // false
// 正確的寫法
function isOdd(n) {
  return Math.abs(n % 2) === 1;
}
isOdd(-5) // true
isOdd(-4) // false

餘數運算符還可以用於浮點數的運算。但是,由於浮點數不是精確的值,無法得到完全準確的結果

自增和自減運算符

自增和自減運算符,是一元運算符,只需要一個運算值。它們的作用是將運算值首先轉爲數值,然後加上1或者減去1。它們會修改原始變量。運算之後,變量的值發生變化,這種效應叫做運算的副作用(side effect)。自增和自減運算符是僅有的兩個具有副作用的運算符,其他運算符都不會改變變量的值

自增和自減運算符有一個需要注意的地方,就是放在變量之後,會先返回變量操作前的值,再進行自增/自減操作;放在變量之前,會先進行自增/自減操作,再返回變量操作後的值

var x = 1;
var y = 1;
x++ // 1
++y // 2

數值運算符,負數值運算符

數值運算符(+)同樣使用加號,但它是一元運算符(只需要一個操作數),而加法運算符是二元運算符(需要兩個操作數)。數值運算符的作用在於可以將任何值轉爲數值(與Number函數的作用相同)

+true // 1
+[] // 0
+{} // NaN

負數值運算符(-),也同樣具有將一個值轉爲數值的功能,只不過得到的值正負相反。連用兩個負數值運算符,等同於數值運算符

var x = 1;
-x // -1
-(-x) // 1

上面代碼最後一行的圓括號不可少,否則會變成自減運算符。數值運算符號和負數值運算符,都會返回一個新的值,而不會改變原始變量的值

指數運算符

指數運算符(**)完成指數運算,前一個運算子是底數,後一個運算子是指數

注意,指數運算符是右結合,而不是左結合。即多個指數運算符連用時,先進行最右邊的計算

// 相當於 2 ** (3 ** 2)
2 ** 3 ** 2
// 512

賦值運算符

賦值運算符(Assignment Operators)用於給變量賦值。最常見的賦值運算符,當然就是等號(=)。賦值運算符還可以與其他運算符結合,形成變體。下面是與算術運算符的結合

x += y // 等同於 x = x + y
x -= y // 等同於 x = x - y
x *= y // 等同於 x = x * y
x /= y // 等同於 x = x / y
x %= y // 等同於 x = x % y
x **= y // 等同於 x = x ** y

下面是與位運算符的結合

x >>= y // 等同於 x = x >> y
x <<= y // 等同於 x = x << y
x >>>= y // 等同於 x = x >>> y
x &= y // 等同於 x = x & y
x |= y // 等同於 x = x | y
x ^= y // 等同於 x = x ^ y

這些複合的賦值運算符,都是先進行指定運算,然後將得到值返回給左邊的變量

比較運算符

概述

比較運算符用於比較兩個值的大小,然後返回一個布爾值,表示是否滿足指定的條件。

注意:比較運算符可以比較各種類型的值,不僅僅是數值

JavaScript 一共提供了8個比較運算符

> 大於運算符
< 小於運算符
<= 小於或等於運算符
>= 大於或等於運算符
== 相等運算符
=== 嚴格相等運算符
!= 不相等運算符
!== 嚴格不相等運算符

這八個比較運算符分成兩類:相等比較和非相等比較。兩者的規則是不一樣的,對於非相等的比較,算法是先看兩個運算值是否都是字符串,如果是的,就按照字典順序比較(實際上是比較 Unicode 碼點);否則,將兩個運算值都轉成數值,再比較數值的大小

非相等運算符:字符串的比較

字符串按照字典順序進行比較

'cat' > 'dog' // false
'cat' > 'catalog' // false
'cat' > 'Cat' // true'
'大' > '小' // false

JavaScript 引擎內部首先比較首字符的 Unicode 碼點。如果相等,再比較第二個字符的 Unicode 碼點,以此類推。上面代碼中,小寫的c的 Unicode 碼點(99)大於大寫的C的 Unicode 碼點(67),所以返回true。由於所有字符都有 Unicode 碼點,因此漢字也可以比較

非相等運算符:非字符串的比較

如果兩個運算值之中,至少有一個不是字符串,需要分成以下兩種情況

原始類型值

如果兩個運算值都是原始類型的值,則是先轉成數值再比較

5 > '4' // true
// 等同於 5 > Number('4')
// 即 5 > 4
true > false // true
// 等同於 Number(true) > Number(false)
// 即 1 > 0
2 > true // true
// 等同於 2 > Number(true)
// 即 2 > 1

這裏注意,任何值(包括NaN本身)與NaN比較,返回的都是false

對象

如果運算值是對象,會轉爲原始類型的值,再進行比較。對象轉換成原始類型的值,算法是先調用valueOf方法;如果返回的還是對象,再接着調用toString方法(前面有講到)

var x = [2];
x > '11' // true
// 等同於 [2].valueOf().toString() > '11'
// 即 '2' > '11'
x.valueOf = function () { return '1' };
x > '11' // false
// 等同於 [2].valueOf() > '11'
// 即 '1' > '11'
[2] > [1] // true
// 等同於 [2].valueOf().toString() > [1].valueOf().toString()
// 即 '2' > '1'
[2] > [11] // true
// 等同於 [2].valueOf().toString() > [11].valueOf().toString()
// 即 '2' > '11'
{ x: 2 } >= { x: 1 } // true
// 等同於 { x: 2 }.valueOf().toString() >= { x: 1 }.valueOf().toString()
// 即 '[object Object]' >= '[object Object]'

嚴格相等運算符

JavaScript 提供兩種相等運算符:==和===。簡單說,它們的區別是相等運算符(==)比較兩個值是否相等,嚴格相等運算符(===)比較它們是否爲“同一個值”。如果兩個值不是同一類型,嚴格相等運算符(===)直接返回false,而相等運算符(==)會將它們轉換成同一個類型,再用嚴格相等運算符進行比較。

不同類型的值

如果兩個值的類型不同,直接返回false

同一類的原始類型值

同一類型的原始類型的值(數值、字符串、布爾值)比較時,值相同就返回true,值不同就返回false

注意:NaN與任何值都不相等(包括自身);另外,正0等於負0

複合類型值

兩個複合類型(對象、數組、函數)的數據比較時,不是比較它們的值是否相等,而是比較它們是否指向同一個地址

{} === {} // false
[] === [] // false
(function () {} === function () {}) // false

上面代碼分別比較兩個空對象、兩個空數組、兩個空函數,結果都是不相等。原因是對於複合類型的值,嚴格相等運算比較的是,它們是否引用同一個內存地址,而運算符兩邊的空對象、空數組、空函數的值,都存放在不同的內存地址,結果當然是false。如果兩個變量引用同一個對象,則它們相等

var v1 = {};
var v2 = v1;
v1 === v2 // true

注意,對於兩個對象的比較,嚴格相等運算符比較的是地址,而大於或小於運算符比較的是值

var obj1 = {};
var obj2 = {};
obj1 > obj2 // false
obj1 < obj2 // false
obj1 === obj2 // false

undefined 和 null

undefined和null與自身嚴格相等。由於變量聲明後默認值是undefined,因此兩個只聲明未賦值的變量是相等的

undefined === undefined // true
null === null // true
var v1;
var v2;
v1 === v2 // true

嚴格不相等運算符

嚴格相等運算符有一個對應的“嚴格不相等運算符”(!==),它的算法就是先求嚴格相等運算符的結果,然後返回相反值

1 !== '1' // true
// 等同於
!(1 === '1')

相等運算符

相等運算符用來比較相同類型的數據時,與嚴格相等運算符完全一樣

1 == 1.0
// 等同於
1 === 1.0

比較不同類型的數據時,相等運算符會先將數據進行類型轉換,然後再用嚴格相等運算符比較。下面分成四種情況,討論不同類型的值互相比較的規則

原始類型值

原始類型的值會轉換成數值再進行比較

1 == true // true
// 等同於 1 === Number(true)
0 == false // true
// 等同於 0 === Number(false)
2 == true // false
// 等同於 2 === Number(true)
2 == false // false
// 等同於 2 === Number(false)
'true' == true // false
// 等同於 Number('true') === Number(true)
// 等同於 NaN === 1
'' == 0 // true
// 等同於 Number('') === 0
// 等同於 0 === 0
'' == false  // true
// 等同於 Number('') === Number(false)
// 等同於 0 === 0
'1' == true  // true
// 等同於 Number('1') === Number(true)
// 等同於 1 === 1
'\n  123  \t' == 123 // true
// 因爲字符串轉爲數字時,省略前置和後置的空格

對象與原始類型值比較

對象(這裏指廣義的對象,包括數組和函數)與原始類型的值比較時,對象轉換成原始類型的值,再進行比較

// 對象與數值比較時,對象轉爲數值
[1] == 1 // true
// 等同於 Number([1]) == 1

// 對象與字符串比較時,對象轉爲字符串
[1] == '1' // true
// 等同於 String([1]) == '1'
[1, 2] == '1,2' // true
// 等同於 String([1, 2]) == '1,2'

// 對象與布爾值比較時,兩邊都轉爲數值
[1] == true // true
// 等同於 Number([1]) == Number(true)
[2] == true // false
// 等同於 Number([2]) == Number(true)

undefined 和 null

undefined和null與其他類型的值比較時,結果都爲false,它們互相比較時結果爲true

false == null // false
false == undefined // false
0 == null // false
0 == undefined // false
undefined == null // true

相等運算符的缺點

相等運算符隱藏的類型轉換,會帶來一些違反直覺的結果

0 == ''             // true
0 == '0'            // true
2 == true           // false
2 == false          // false
false == 'false'    // false
false == '0'        // true
false == undefined  // false
false == null       // false
null == undefined   // true
' \t\r\n ' == 0     // true

不相等運算符

相等運算符有一個對應的“不相等運算符”(!=),它的算法就是先求相等運算符的結果,然後返回相反值

布爾運算符

概述

布爾運算符用於將表達式轉爲布爾值,一共包含四個運算符。

取反運算符:!
且運算符:&&
或運算符:||
三元運算符:?:

取反運算符(!)

取反運算符是一個感嘆號,用於將布爾值變爲相反值,即true變成false,false變成true

對於非布爾值,取反運算符會將其轉爲布爾值。可以這樣記憶,以下六個值取反後爲true,其他值都爲false。

undefined
null
false
0
NaN
空字符串('')

如果對一個值連續做兩次取反運算,等於將其轉爲對應的布爾值,與Boolean函數的作用相同。這是一種常用的類型轉換的寫法

!!x
// 等同於
Boolean(x)

兩次取反就是將一個值轉爲布爾值的簡便寫法

且運算符(&&)

且運算符(&&)往往用於多個表達式的求值。它的運算規則是:如果第一個運算值爲true,則返回第二個運算值(注意是值,不是布爾值);如果第一個運算值爲false,則直接返回第一個運算值,且不再對第二個運算值求值

't' && ''  // ""
'' && 'f'  // ""
'' && ''  // ""

var x = 1;
(1 - 1) && ( x += 1) // 0
x  // 1

上面代碼的最後一個例子,由於且運算符的第一個運算布爾值爲false,則直接返回它的值0,而不再對第二個運算值求值,所以變量x的值沒變。這種跳過第二個運算值的機制,被稱爲“短路”。有些程序員喜歡用它取代if結構,比如下面是一段if結構的代碼,就可以用且運算符改寫

if (i) {
  doSomething();
}
// 等價於
i && doSomething();

上面代碼的兩種寫法是等價的,但是後一種不容易看出目的,也不容易除錯,建議謹慎使用。且運算符可以多個連用,這時返回第一個布爾值爲false的表達式的值。如果所有表達式的布爾值都爲true,則返回最後一個表達式的值

或運算符(||)

或運算符(||)也用於多個表達式的求值。它的運算規則是:如果第一個運算值的布爾值爲true,則返回第一個運算值,且不再對第二個運算值求值;如果第一個運算值的布爾值爲false,則返回第二個運算值

't' || '' // "t"
't' || 'f' // "t"
'' || 'f' // "f"
'' || '' // ""

短路規則對這個運算符也適用

var x = 1;
true || (x = 2) // true
x // 1

上面代碼中,且運算符的第一個運算值爲true,所以直接返回true,不再運行第二個運算值。所以,x的值沒有改變。這種只通過第一個表達式的值,控制是否運行第二個表達式的機制,就稱爲“短路”(short-cut)。或運算符可以多個連用,這時返回第一個布爾值爲true的表達式的值。如果所有表達式都爲false,則返回最後一個表達式的值

三元條件運算符(?:)

三元條件運算符由問號(?)和冒號(:)組成,分隔三個表達式。它是 JavaScript 語言唯一一個需要三個運算值的運算符。如果第一個表達式的布爾值爲true,則返回第二個表達式的值,否則返回第三個表達式的值

通常來說,三元條件表達式與if...else語句具有同樣表達效果,前者可以表達的,後者也能表達。但是兩者具有一個重大差別,if...else是語句,沒有返回值;三元條件表達式是表達式,具有返回值。所以,在需要返回值的場合,只能使用三元條件表達式,而不能使用if..else

console.log(true ? 'T' : 'F');

上面代碼中,console.log方法的參數必須是一個表達式,這時就只能使用三元條件表達式。如果要用if...else語句,就必須改變整個代碼寫法了

二進制位運算符

概述

二進制位運算符用於直接對二進制位進行計算,一共有7個

二進制或運算符(or):符號爲|,表示若兩個二進制位都爲0,則結果爲0,否則爲1
二進制與運算符(and):符號爲&,表示若兩個二進制位都爲1,則結果爲1,否則爲0
二進制否運算符(not):符號爲~,表示對一個二進制位取反
異或運算符(xor):符號爲^,表示若兩個二進制位不相同,則結果爲1,否則爲0
左移運算符(left shift):符號爲<<
右移運算符(right shift):符號爲>>
帶符號位的右移運算符(zero filled right shift):符號爲>>>

這些位運算符直接處理每一個比特位(bit),所以是非常底層的運算,好處是速度極快,缺點是很不直觀,許多場合不能使用它們,否則會使代碼難以理解和查錯。

有一點需要特別注意,位運算符只對整數起作用,如果一個運算值不是整數,會自動轉爲整數後再執行。另外,雖然在 JavaScript 內部,數值都是以64位浮點數的形式儲存,但是做位運算的時候,是以32位帶符號的整數進行運算的,並且返回值也是一個32位帶符號的整數

i = i | 0;

上面這行代碼的意思,就是將i(不管是整數或小數)轉爲32位整數。利用這個特性,可以寫出一個函數,將任意數值轉爲32位整數

function toInt32(x) {
  return x | 0;
}

上面代碼中,toInt32可以將小數轉爲整數。對於一般的整數,返回值不會有任何變化。對於大於或等於2的32次方的整數,大於32位的數位都會被捨去

二進制或運算符

二進制或運算符(|)逐位比較兩個運算子,兩個二進制位之中只要有一個爲1,就返回1,否則返回0

0 | 3 // 3

上面代碼中,0和3的二進制形式分別是00和11,所以進行二進制或運算會得到11(即3)。位運算只對整數有效,遇到小數時,會將小數部分捨去,只保留整數部分。所以,將一個小數與0進行二進制或運算,等同於對該數去除小數部分,即取整數位

2.9 | 0 // 2
-2.9 | 0 // -2

需要注意的是,這種取整方法不適用超過32位整數最大值2147483647的數

2147483649.4 | 0; // -2147483647

二進制與運算符

二進制與運算符(&)的規則是逐位比較兩個運算子,兩個二進制位之中只要有一個位爲0,就返回0,否則返回1

二進制否運算符

二進制否運算符(~)將每個二進制位都變爲相反值(0變爲1,1變爲0)。它的返回結果有時比較難理解,因爲涉及到計算機內部的數值表示機制

~ 3 // -4

上面表達式對3進行二進制否運算,得到-4。之所以會有這樣的結果,是因爲位運算時JavaScript 內部將所有的運算值都轉爲32位的二進制整數再進行運算。3的32位整數形式是00000000000000000000000000000011,二進制否運算以後得到11111111111111111111111111111100。由於第一位(符號位)是1,所以這個數是一個負數。JavaScript 內部採用補碼形式表示負數,即需要將這個數減去1,再取一次反,然後加上負號,才能得到這個負數對應的10進制值。這個數減去1等於11111111111111111111111111111011,再取一次反得到00000000000000000000000000000100,再加上負號就是-4。考慮到這樣的過程比較麻煩,可以簡單記憶成,一個數與自身的取反值相加,等於-1

對一個整數連續兩次二進制否運算,得到它自身。所有的位運算都只對整數有效。二進制否運算遇到小數時,也會將小數部分捨去,只保留整數部分。所以,對一個小數連續進行兩次二進制否運算,能達到取整效果

~~2.9 // 2
~~47.11 // 47
~~3 // 3

使用二進制否運算取整,是所有取整方法中最快的一種。對字符串進行二進制否運算,JavaScript 引擎會先調用Number函數,將字符串轉爲數值

對於其他類型的值,二進制否運算也是先用Number轉爲數值,然後再進行處理

// 相當於~Number('011')
~'011'  // -12
// 相當於~Number('42 cats')
~'42 cats' // -1
// 相當於~Number('0xcafebabe')
~'0xcafebabe' // 889275713
// 相當於~Number('deadbeef')
~'deadbeef' // -1
// 相當於 ~Number([])
~[] // -1
// 相當於 ~Number(NaN)
~NaN // -1
// 相當於 ~Number(null)
~null // -1

異或運算符

異或運算(^)在兩個二進制位不同時返回1,相同時返回0

0 ^ 3  // 3

上面表達式中,0(二進制00)與3(二進制11)進行異或運算,它們每一個二進制位都不同,所以得到11(即3)。“異或運算”有一個特殊運用,連續對兩個數a和b進行三次異或運算,a^=b; b^=a; a^=b;,可以互換它們的值。這意味着,使用“異或運算”可以在不引入臨時變量的前提下,互換兩個變量的值

var a = 10;
var b = 99;
a ^= b, b ^= a, a ^= b;
a // 99
b // 10

這是互換兩個變量的值的最快方法。異或運算也可以用來取整

12.9 ^ 0 // 12

左移運算符

左移運算符(<<)表示將一個數的二進制值向左移動指定的位數,尾部補0,即乘以2的指定次方

// 4 的二進制形式爲100,左移一位爲1000(即十進制的8),相當於乘以2的1次方
4 << 1 // 8
-4 << 1 // -8

如果左移0位,就相當於將該數值轉爲32位整數,等同於取整,對於正數和負數都有效

13.5 << 0  // 13
-13.5 << 0  // -13

左移運算符用於二進制數值非常方便

var color = {r: 186, g: 218, b: 85};
// RGB to HEX  (1 << 24)的作用爲保證結果是6位數
var rgb2hex = function(r, g, b) {
  return '#' + ((1 << 24) + (r << 16) + (g << 8) + b)
    .toString(16) // 先轉成十六進制,然後返回字符串
    .substr(1);   // 去除字符串的最高位,返回後面六個字符串
}
rgb2hex(color.r, color.g, color.b)  // "#bada55"

上面代碼使用左移運算符,將顏色的 RGB 值轉爲 HEX 值

右移運算符

右移運算符(>>)表示將一個數的二進制值向右移動指定的位數,頭部補0,即除以2的指定次方(最高位即符號位不參與移動)

4 >> 1 // 2
// 因爲4的二進制形式爲 00000000000000000000000000000100,右移一位得到 00000000000000000000000000000010,即爲十進制的2
-4 >> 1 // -2
// 因爲-4的二進制形式爲 11111111111111111111111111111100,右移一位,頭部補1,得到 11111111111111111111111111111110,即爲十進制的-2

右移運算可以模擬 2 的整除運算

5 >> 1 // 2
// 相當於 5 / 2 = 2
21 >> 2 // 5
// 相當於 21 / 4 = 5
21 >> 3 // 2
// 相當於 21 / 8 = 2
21 >> 4 // 1
// 相當於 21 / 16 = 1

帶符號位的右移運算符

帶符號位的右移運算符(>>>)表示將一個數的二進制形式向右移動,包括符號位也參與移動,頭部補0。所以,該運算總是得到正值。對於正數,該運算的結果與右移運算符(>>)完全一致,區別主要在於負數

4 >>> 1  // 2
-4 >>> 1  // 2147483646
// 因爲-4的二進制形式爲11111111111111111111111111111100,帶符號位的右移一位,得到01111111111111111111111111111110, 即爲十進制的2147483646。

這個運算實際上將一個值轉爲32位無符號整數。查看一個負整數在計算機內部的儲存形式,最快的方法就是使用這個運算符

-1 >>> 0 // 4294967295

上面代碼表示,-1作爲32位整數時,內部的儲存形式使用無符號整數格式解讀,值爲 4294967295(即(2^32)-1,等於11111111111111111111111111111111)

開關作用

位運算符可以用作設置對象屬性的開關。假定某個對象有四個開關,每個開關都是一個變量。那麼,可以設置一個四位的二進制數,它的每個位對應一個開關

var FLAG_A = 1; // 0001
var FLAG_B = 2; // 0010
var FLAG_C = 4; // 0100
var FLAG_D = 8; // 1000

上面代碼設置 A、B、C、D 四個開關,每個開關分別佔有一個二進制位。然後,就可以用二進制與運算檢驗,當前設置是否打開了指定開關

var flags = 5; // 二進制的0101
if (flags & FLAG_C) {
  // ...
}
// 0101 & 0100 => 0100 => true

上面代碼檢驗是否打開了開關C,如果打開會返回true,否則返回false。現在假設需要打開A、B、D三個開關,我們可以構造一個掩碼變量

var mask = FLAG_A | FLAG_B | FLAG_D; // 0001 | 0010 | 1000 => 1011

上面代碼對A、B、D三個變量進行二進制或運算,得到掩碼值爲二進制的1011。有了掩碼,二進制或運算可以確保打開指定的開關

flags = flags | mask;

二進制與運算可以將當前設置中凡是與開關設置不一樣的項,全部關閉

flags = flags & mask;

異或運算可以切換(toggle)當前設置,即第一次執行可以得到當前設置的相反值,再執行一次又得到原來的值

flags = flags ^ mask;

二進制否運算可以翻轉當前設置,即原設置爲0,運算後變爲1;原設置爲1,運算後變爲0

flags = ~flags;

其他運算符,運算順序

void 運算符

void運算符的作用是執行一個表達式,然後不返回任何值,或者說返回undefined

void 0 // undefined
void(0) // undefined

上面是void運算符的兩種寫法,都正確。建議採用後一種形式,即總是使用圓括號。因爲void運算符的優先性很高,如果不使用括號,容易造成錯誤的結果。比如,void 4 + 7實際上等同於(void 4) + 7。

這個運算符的主要用途是瀏覽器的書籤工具(Bookmarklet),以及在超級鏈接中插入代碼防止網頁跳轉

<script>
function f() {
  console.log('Hello World');
}
</script>
<a href="http://example.com" onclick="f(); return false;">點擊</a>

上面代碼中,點擊鏈接後,會先執行onclick的代碼,由於onclick返回false,所以瀏覽器不會跳轉到 example.com。void運算符可以取代上面的寫法

<a href="javascript: void(f())">文字</a>

下面是一個更實際的例子,用戶點擊鏈接提交表單,但是不產生頁面跳轉

<a href="javascript: void(document.form.submit())">
  提交
</a>

逗號運算符

逗號運算符用於對兩個表達式求值,並返回後一個表達式的值

'a', 'b' // "b"
var x = 0;
var y = (x++, 10);
x // 1
y // 10

逗號運算符的一個用途是,在返回一個值之前,進行一些輔助操作

var value = (console.log('Hi!'), true); // Hi!
value // true

上面代碼中,先執行逗號之前的操作,然後返回逗號後面的值

運算順序

優先級

JavaScript 各種運算符的優先級別(Operator Precedence)是不一樣的。優先級高的運算符先執行,優先級低的運算符後執行

var x = 1;
var arr = [];
var y = arr.length <= 0 || arr[0] === undefined ? x : arr[0];

上面代碼中,變量y的值就很難看出來,因爲這個表達式涉及5個運算符,到底誰的優先級最高,實在不容易記住。根據語言規格,這五個運算符的優先級從高到低依次爲:小於等於(<=)、嚴格相等(===)、或(||)、三元(?:)、等號(=)。因此上面的表達式,實際的運算順序如下

var y = ((arr.length <= 0) || (arr[0] === undefined)) ? x : arr[0];

圓括號的作用

圓括號(())可以用來提高運算的優先級,因爲它的優先級是最高的,即圓括號中的表達式會第一個運算

注意,因爲圓括號不是運算符,而是一種語法結構,所以不具有求值作用,只改變運算的優先級

函數放在圓括號中,會返回函數本身。如果圓括號緊跟在函數的後面,就表示調用函數

function f() {
  return 1;
}
(f) // function f(){return 1;}
f() // 1

圓括號之中,只能放置表達式,如果將語句放在圓括號之中,就會報錯

(var a = 1) // SyntaxError: Unexpected token var

左結合與右結合

對於優先級別相同的運算符,大多數情況,計算順序總是從左到右,這叫做運算符的“左結合”(left-to-right associativity),即從左邊開始計算;但是少數運算符的計算順序是從右到左,即從右邊開始計算,這叫做運算符的“右結合”(right-to-left associativity)。其中,最主要的是賦值運算符(=)和三元條件運算符(?:);指數運算符(**)也是右結合的

語法專題

數據類型的轉換

概述

JavaScript 是一種動態類型語言,變量沒有類型限制,可以隨時賦予任意值

var x = y ? 1 : 'a';

上面代碼中,變量x到底是數值還是字符串,取決於另一個變量y的值。y爲true時,x是一個數值;y爲false時,x是一個字符串。這意味着,x的類型沒法在編譯階段就知道,必須等到運行時才能知道。雖然變量的數據類型是不確定的,但是各種運算符對數據類型是有要求的。如果運算符發現,運算值的類型與預期不符,就會自動轉換類型。比如,減法運算符預期左右兩側的運算值應該是數值,如果不是,就會自動將它們轉爲數值

'4' - '3' // 1

強制轉換

強制轉換主要指使用Number()、String()和Boolean()三個函數,手動將各種類型的值,分別轉換成數字、字符串或者布爾值

Number()

使用Number函數,可以將任意類型的值轉化成數值。下面分成兩種情況討論,一種是參數是原始類型的值,另一種是參數是對象

原始類型值
// 數值:轉換後還是原來的值
Number(324) // 324
// 字符串:如果可以被解析爲數值,則轉換爲相應的數值
Number('324') // 324
// 字符串:如果不可以被解析爲數值,返回 NaN
Number('324abc') // NaN
// 空字符串轉爲0
Number('') // 0
// 布爾值:true 轉成 1,false 轉成 0
Number(true) // 1
Number(false) // 0
// undefined:轉成 NaN
Number(undefined) // NaN
// null:轉成0
Number(null) // 0

Number函數將字符串轉爲數值,要比parseInt函數嚴格很多。基本上,只要有一個字符無法轉成數值,整個字符串就會被轉爲NaN。另外,parseInt和Number函數都會自動過濾一個字符串前導和後綴的空格

parseInt('\t\v\r12.34\n') // 12
Number('\t\v\r12.34\n') // 12.34
對象

簡單的規則是,Number方法的參數是對象時,將返回NaN,除非是包含單個數值的數組

Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([5]) // 5

之所以會這樣,是因爲Number背後的轉換規則比較複雜。

第一步,調用對象自身的valueOf方法。如果返回原始類型的值,則直接對該值使用Number函數,不再進行後續步驟

第二步,如果valueOf方法返回的還是對象,則改爲調用對象自身的toString方法。如果toString方法返回原始類型的值,則對該值使用Number函數,不再進行後續步驟

第三步,如果toString方法返回的是對象,就報錯

var obj = {x: 1};
Number(obj) // NaN
// 等同於
if (typeof obj.valueOf() === 'object') {
  Number(obj.toString());
} else {
  Number(obj.valueOf());
}

上面代碼中,Number函數將obj對象轉爲數值。背後發生了一連串的操作,首先調用obj.valueOf方法, 結果返回對象本身;於是,繼續調用obj.toString方法,這時返回字符串[object Object],對這個字符串使用Number函數,得到NaN。默認情況下,對象的valueOf方法返回對象本身,所以一般總是會調用toString方法,而toString方法返回對象的類型字符串(比如[object Object])。所以,會有下面的結果

Number({}) // NaN

如果toString方法返回的不是原始類型的值,結果就會報錯。valueOf和toString方法,都是可以自定義的

Number({
  valueOf: function () {
    return 2;
  }
}) // 2
Number({
  toString: function () {
    return 3;
  }
}) // 3
Number({
  valueOf: function () {
    return 2;
  },
  toString: function () {
    return 3;
  }
}) // 2

valueOf方法先於toString方法執行

String()

String函數可以將任意類型的值轉化成字符串,轉換規則如下

原始類型值
數值:轉爲相應的字符串
字符串:轉換後還是原來的值
布爾值:true轉爲字符串"true",false轉爲字符串"false"
undefined:轉爲字符串"undefined"
null:轉爲字符串"null"
對象

String方法的參數如果是對象,返回一個類型字符串;如果是數組,返回該數組的字符串形式

String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"

String方法背後的轉換規則,與Number方法基本相同,只是互換了valueOf方法和toString方法的執行順序

1.先調用對象自身的toString方法。如果返回原始類型的值,則對該值使用String函數,不再進行以下步驟。

2.如果toString方法返回的是對象,再調用原對象的valueOf方法。如果valueOf方法返回原始類型的值,則對該值使用String函數,不再進行以下步驟。

3.如果valueOf方法返回的是對象,就報錯

String({a: 1}) // "[object Object]"
// 等同於
String({a: 1}.toString()) // "[object Object]"

如果toString法和valueOf方法,返回的都是對象,就會報錯

var obj = {
  valueOf: function () {
    return {};
  },
  toString: function () {
    return {};
  }
};
String(obj) // TypeError: Cannot convert object to primitive value

toString方法先於valueOf方法執行

String({
  toString: function () {
    return 3;
  }
}) // "3"
String({
  valueOf: function () {
    return 2;
  }
}) // "[object Object]"
String({
  valueOf: function () {
    return 2;
  },
  toString: function () {
    return 3;
  }
}) // "3"

Boolean()

Boolean函數可以將任意類型的值轉爲布爾值。它的轉換規則相對簡單:除了以下五個值的轉換結果爲false,其他的值全部爲true

Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false

注意:所有對象(包括空對象)的轉換結果都是true,甚至連false對應的布爾對象new Boolean(false)也是true

Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true

自動轉換

自動轉換是以強制轉換爲基礎的。遇到以下三種情況時,JavaScript 會自動轉換數據類型,即轉換是自動完成的,用戶不可見

1.不同類型的數據互相運算

123 + 'abc' // "123abc"

2.對非布爾值類型的數據求布爾值

if ('abc') {
  console.log('hello')
}  // "hello"

3.對非數值類型的值使用一元運算符(即+和-)

+ {foo: 'bar'} // NaN
- [1, 2, 3] // NaN

自動轉換的規則是這樣的:預期什麼類型的值,就調用該類型的轉換函數。比如,某個位置預期爲字符串,就調用String函數進行轉換。如果該位置即可以是字符串,也可能是數值,那麼默認轉爲數值。由於自動轉換具有不確定性,而且不易除錯,建議在預期爲布爾值、數值、字符串的地方,全部使用Boolean、Number和String函數進行顯式轉換

自動轉換爲布爾值

JavaScript 遇到預期爲布爾值的地方(比如if語句的條件部分),就會將非布爾值的參數自動轉換爲布爾值。系統內部會自動調用Boolean函數。因此除了以下五個值,其他都是自動轉爲true

undefined
null
+0或-0
NaN
''(空字符串)

下面兩種寫法,有時也用於將一個表達式轉爲布爾值。它們內部調用的也是Boolean函數

// 寫法一
expression ? true : false
// 寫法二
!! expression

自動轉換爲字符串

JavaScript 遇到預期爲字符串的地方,就會將非字符串的值自動轉爲字符串。具體規則是,先將複合類型的值轉爲原始類型的值,再將原始類型的值轉爲字符串。字符串的自動轉換,主要發生在字符串的加法運算時。當一個值爲字符串,另一個值爲非字符串,則後者轉爲字符串

'5' + 1 // '51'
'5' + true // "5true"
'5' + false // "5false"
'5' + {} // "5[object Object]"
'5' + [] // "5"
'5' + function (){} // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"

這種自動轉換很容易出錯

自動轉換爲數值

JavaScript 遇到預期爲數值的地方,就會將參數值自動轉換爲數值。系統內部會自動調用Number函數。除了加法運算符(+)有可能把運算值轉爲字符串,其他運算符都會把運算值自動轉成數值

'5' - '2' // 3
'5' * '2' // 10
true - 1  // 0
false - 1 // -1
'1' - 1   // 0
'5' * []    // 0
false / '5' // 0
'abc' - 1   // NaN
null + 1 // 1
undefined + 1 // NaN

注意:null轉爲數值時爲0,而undefined轉爲數值時爲NaN

一元運算符也會把運算子轉成數值

+'abc' // NaN
-'abc' // NaN
+true // 1
-false // 0

錯誤處理機制

Error 實例對象

JavaScript 解析或運行時,一旦發生錯誤,引擎就會拋出一個錯誤對象。JavaScript 原生提供Error構造函數,所有拋出的錯誤都是這個構造函數的實例

var err = new Error('出錯了');
err.message // "出錯了"

上面代碼中,我們調用Error構造函數,生成一個實例對象err。Error構造函數接受一個參數,表示錯誤提示,可以從實例的message屬性讀到這個參數。拋出Error實例對象以後,整個程序就中斷在發生錯誤的地方,不再往下執行。JavaScript 語言標準只提到,Error實例對象必須有message屬性,表示出錯時的提示信息,沒有提到其他屬性。大多數 JavaScript 引擎,對Error實例還提供name和stack屬性,分別表示錯誤的名稱和錯誤的堆棧,但它們是非標準的,不是每種實現都有

message:錯誤提示信息
name:錯誤名稱(非標準屬性)
stack:錯誤的堆棧(非標準屬性)

使用name和message這兩個屬性,可以對發生什麼錯誤有一個大概的瞭解

if (error.name) {
  console.log(error.name + ': ' + error.message);
}

stack屬性用來查看錯誤發生時的堆棧

function throwit() {
  throw new Error('');
}
function catchit() {
  try {
    throwit();
  } catch(e) {
    console.log(e.stack); // print stack trace
  }
}
catchit()
// Error
//    at throwit (~/examples/throwcatch.js:9:11)
//    at catchit (~/examples/throwcatch.js:3:9)
//    at repl:1:5

上面代碼中,錯誤堆棧的最內層是throwit函數,然後是catchit函數,最後是函數的運行環境

原生錯誤類型

Error實例對象是最一般的錯誤類型,在它的基礎上,JavaScript 還定義了其他6種錯誤對象。也就是說,存在Error的6個派生對象

SyntaxError 對象

SyntaxError對象是解析代碼時發生的語法錯誤

// 變量名錯誤
var 1a; // Uncaught SyntaxError: Invalid or unexpected token
// 缺少括號
console.log 'hello'); // Uncaught SyntaxError: Unexpected string

上面代碼的錯誤,都是在語法解析階段就可以發現,所以會拋出SyntaxError。第一個錯誤提示是“token 非法”,第二個錯誤提示是“字符串不符合要求”

ReferenceError 對象

ReferenceError對象是引用一個不存在的變量時發生的錯誤

// 使用一個不存在的變量
unknownVariable // Uncaught ReferenceError: unknownVariable is not defined

另一種觸發場景是,將一個值分配給無法分配的對象,比如對函數的運行結果或者this賦值

// 等號左側不是變量
console.log() = 1 // Uncaught ReferenceError: Invalid left-hand side in assignment
// this 對象不能手動賦值
this = 1 // ReferenceError: Invalid left-hand side in assignment

RangeError 對象

RangeError對象是一個值超出有效範圍時發生的錯誤。主要有幾種情況,一是數組長度爲負數,二是Number對象的方法參數超出範圍,以及函數堆棧超過最大值

// 數組長度不得爲負數
new Array(-1) // Uncaught RangeError: Invalid array length

TypeError 對象

TypeError對象是變量或參數不是預期類型時發生的錯誤。比如,對字符串、布爾值、數值等原始類型的值使用new命令,就會拋出這種錯誤,因爲new命令的參數應該是一個構造函數

new 123 // Uncaught TypeError: number is not a func
var obj = {};
obj.unknownMethod() // Uncaught TypeError: obj.unknownMethod is not a function

上面代碼的第二種情況,調用對象不存在的方法,也會拋出TypeError錯誤,因爲obj.unknownMethod的值是undefined,而不是一個函數

URIError 對象

URIError對象是 URI 相關函數的參數不正確時拋出的錯誤,主要涉及encodeURI()、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和unescape()這六個函數

decodeURI('%2') // URIError: URI malformed

EvalError 對象

eval函數沒有被正確執行時,會拋出EvalError錯誤。該錯誤類型已經不再使用了,只是爲了保證與以前代碼兼容,才繼續保留

總結

以上這6種派生錯誤,連同原始的Error對象,都是構造函數。開發者可以使用它們,手動生成錯誤對象的實例。這些構造函數都接受一個參數,代表錯誤提示信息(message)

var err1 = new Error('出錯了!');
var err2 = new RangeError('出錯了,變量超出有效範圍!');
var err3 = new TypeError('出錯了,變量類型無效!');
err1.message // "出錯了!"
err2.message // "出錯了,變量超出有效範圍!"
err3.message // "出錯了,變量類型無效!"

自定義錯誤

除了 JavaScript 原生提供的七種錯誤對象,還可以定義自己的錯誤對象

function UserError(message) {
  this.message = message || '默認信息';
  this.name = 'UserError';
}
UserError.prototype = new Error();
UserError.prototype.constructor = UserError;

上面代碼自定義一個錯誤對象UserError,讓它繼承Error對象。然後,就可以生成這種自定義類型的錯誤了

new UserError('這是自定義的錯誤!');

throw 語句

throw語句的作用是手動中斷程序執行,拋出一個錯誤

if (x <= 0) {
  throw new Error('x 必須爲正數');
} // Uncaught ReferenceError: x is not defined

上面代碼中,如果變量x小於等於0,就手動拋出一個錯誤,告訴用戶x的值不正確,整個程序就會在這裏中斷執行。可以看到,throw拋出的錯誤就是它的參數,這裏是一個Error實例。throw也可以拋出自定義錯誤

function UserError(message) {
  this.message = message || '默認信息';
  this.name = 'UserError';
}
throw new UserError('出錯了!'); // Uncaught UserError {message: "出錯了!", name: "UserError"}

實際上,throw可以拋出任何類型的值。也就是說,它的參數可以是任何值

// 拋出一個字符串
throw 'Error!'; // Uncaught Error!
// 拋出一個數值
throw 42; // Uncaught 42
// 拋出一個布爾值
throw true; // Uncaught true
// 拋出一個對象
throw {
  toString: function () {
    return 'Error!';
  }
}; // Uncaught {toString: ƒ}

對於 JavaScript 引擎來說,遇到throw語句,程序就中止了。引擎會接收到throw拋出的信息,可能是一個錯誤實例,也可能是其他類型的值

try...catch 結構

一旦發生錯誤,程序就中止執行了。JavaScript 提供了try...catch結構,允許對錯誤進行處理,選擇是否往下執行

try {
  throw new Error('出錯了!');
} catch (e) {
  console.log(e.name + ": " + e.message);
  console.log(e.stack);
}
// Error: 出錯了!
//   at <anonymous>:3:9

上面代碼中,try代碼塊拋出錯誤(上例用的是throw語句),JavaScript 引擎就立即把代碼的執行,轉到catch代碼塊,或者說錯誤被catch代碼塊捕獲了。catch接受一個參數,表示try代碼塊拋出的值。如果你不確定某些代碼是否會報錯,就可以把它們放在try...catch代碼塊之中,便於進一步對錯誤進行處理

try {
  f();
} catch(e) {
  // 處理錯誤
}

上面代碼中,如果函數f執行報錯,就會進行catch代碼塊,接着對錯誤進行處理。catch代碼塊捕獲錯誤之後,程序不會中斷,會按照正常流程繼續執行下去

try {
  throw "出錯了";
} catch (e) {
  console.log(111);
}
console.log(222);
// 111
// 222

上面代碼中,try代碼塊拋出的錯誤,被catch代碼塊捕獲後,程序會繼續向下執行。catch代碼塊之中,還可以再拋出錯誤,甚至使用嵌套的try...catch結構

try {
  foo.bar();
} catch (e) {
  if (e instanceof EvalError) {
    console.log(e.name + ": " + e.message);
  } else if (e instanceof RangeError) {
    console.log(e.name + ": " + e.message);
  }
  // ...
}

finally 代碼塊

try...catch結構允許在最後添加一個finally代碼塊,表示不管是否出現錯誤,都必需在最後運行的語句

function cleansUp() {
  try {
    throw new Error('出錯了……');
    console.log('此行不會執行');
  } finally {
    console.log('完成清理工作');
  }
}
cleansUp()
// 完成清理工作
// Uncaught Error: 出錯了……
//    at cleansUp (<anonymous>:3:11)
//    at <anonymous>:10:1

上面代碼中,由於沒有catch語句塊,一旦發生錯誤,代碼就會中斷執行。中斷執行前,會先執行finally代碼塊,然後再向用戶提示報錯信息

function idle(x) {
  try {
    console.log(x);
    return 'result';
  } finally {
    console.log('FINALLY');
  }
}
idle('hello')
// hello
// FINALLY

上面代碼中,try代碼塊沒有發生錯誤,而且裏面還包括return語句,但是finally代碼塊依然會執行。而且,這個函數的返回值還是result

var count = 0;
function countUp() {
  try {
    return count;
  } finally {
    count++;
  }
}
countUp() // 0
count // 1

上面代碼說明,return語句裏面的count的值,是在finally代碼塊運行之前就獲取了

下面是finally代碼塊用法的典型場景

openFile();
try {
  writeFile(Data);
} catch(e) {
  handleError(e);
} finally {
  closeFile();
}

上面代碼首先打開一個文件,然後在try代碼塊中寫入文件,如果沒有發生錯誤,則運行finally代碼塊關閉文件;一旦發生錯誤,則先使用catch代碼塊處理錯誤,再使用finally代碼塊關閉文件。下面的例子充分反映了try...catch...finally這三者之間的執行順序

function f() {
  try {
    console.log(0);
    throw 'bug';
  } catch(e) {
    console.log(1);
    return true; // 這句原本會延遲到 finally 代碼塊結束再執行
    console.log(2); // 不會運行
  } finally {
    console.log(3);
    return false; // 這句會覆蓋掉前面那句 return
    console.log(4); // 不會運行
  }
  console.log(5); // 不會運行
}
var result = f();
// 0
// 1
// 3
result
// false

上面代碼中,catch代碼塊結束執行之前,會先執行finally代碼塊。catch代碼塊之中,觸發轉入finally代碼快的標誌,不僅有return語句,還有throw語句

function f() {
  try {
    throw '出錯了!';
  } catch(e) {
    console.log('捕捉到內部錯誤');
    throw e; // 這句原本會等到finally結束再執行
  } finally {
    return false; // 直接返回
  }
}
try {
  f();
} catch(e) {
  // 此處不會執行
  console.log('caught outer "bogus"');
} //  捕捉到內部錯誤

上面代碼中,進入catch代碼塊之後,一遇到throw語句,就會去執行finally代碼塊,其中有return false語句,因此就直接返回了,不再會回去執行catch代碼塊剩下的部分了

try {
  try {
    console.log('Hello world!'); // 報錯
  }
  finally {
    console.log('Finally');
  }
  console.log('Will I run?');
} catch(error) {
  console.error(error.message);
}
// Finally
// console is not defined

上面代碼中,try裏面還有一個try。內層的try報錯,這時會執行內層的finally代碼塊,然後拋出錯誤,被外層的catch捕獲

編程風格

概述

“編程風格”(programming style)指的是編寫代碼的樣式規則。不同的程序員,往往有不同的編程風格。

有人說,編譯器的規範叫做“語法規則”(grammar),這是程序員必須遵守的;而編譯器忽略的部分,就叫“編程風格”(programming style),這是程序員可以自由選擇的。這種說法不完全正確,程序員固然可以自由選擇編程風格,但是好的編程風格有助於寫出質量更高、錯誤更少、更易於維護的程序。所以,編程風格的選擇不應該基於個人愛好、熟悉程度、打字量等因素,而要考慮如何儘量使代碼清晰易讀、減少出錯。你選擇的,不是你喜歡的風格,而是一種能夠清晰表達你的意圖的風格。這一點,對於 JavaScript 這種語法自由度很高的語言尤其重要。必須牢記的一點是,如果你選定了一種“編程風格”,就應該堅持遵守,切忌多種風格混用。如果你加入他人的項目,就應該遵守現有的風格

縮進

行首的空格和 Tab 鍵,都可以產生代碼縮進效果(indent)。Tab 鍵可以節省擊鍵次數,但不同的文本編輯器對 Tab 的顯示不盡相同,有的顯示四個空格,有的顯示兩個空格,所以有人覺得,空格鍵可以使得顯示效果更統一。無論你選擇哪一種方法,都是可以接受的,要做的就是始終堅持這一種選擇。不要一會使用 Tab 鍵,一會使用空格鍵

區塊

如果循環和判斷的代碼體只有一行,JavaScript 允許該區塊(block)省略大括號;但是,建議總是使用大括號表示區塊

JavaScript 要起首的大括號要跟在關鍵字的後面,因爲 JavaScript 會自動添加句末的分號,導致一些難以察覺的錯誤

return
{
  key: value
};
// 相當於
return;
{
  key: value
};

圓括號

圓括號(parentheses)在 JavaScript 中有兩種作用,一種表示函數的調用,另一種表示表達式的組合(grouping)

console.log('abc'); // 圓括號表示函數的調用
(1 + 2) * 3 // 圓括號表示表達式的組合

建議可以用空格,區分這兩種不同的括號

1.表示函數調用時,函數名與左括號之間沒有空格
2.表示函數定義時,函數名與左括號之間沒有空格
3.其他情況時,前面位置的語法元素與左括號之間,都有一個空格

行尾的分號

分號表示一條語句的結束,JavaScript 允許省略行尾的分號。事實上,確實有一些開發者行尾從來不寫分號。但是,由於下面要討論的原因,建議還是不要省略這個分號

不使用分號的情況

首先,以下三種情況,語法規定本來就不需要在結尾添加分號

1.for 和 while 循環

for ( ; ; ) {
} // 沒有分號
while (true) {
} // 沒有分號

注意,do...while循環是有分號的

2.分支語句:if,switch,try

if (true) {
} // 沒有分號
switch () {
} // 沒有分號
try {
} catch {
} // 沒有分號

3.函數的聲明語句

function f() {
} // 沒有分號

注意,函數表達式仍然要使用分號

var f = function f() {
};

以上三種情況,如果使用了分號,並不會出錯。因爲,解釋引擎會把這個分號解釋爲空語句

分號的自動添加

除了上面三種情況,所有語句都應該使用分號。但是,如果沒有使用分號,大多數情況下,JavaScript 會自動添加

var a = 1
// 等同於
var a = 1;

這種語法特性被稱爲“分號的自動添加”(Automatic Semicolon Insertion,簡稱 ASI)。因此,有人提倡省略句尾的分號。麻煩的是,如果下一行的開始可以與本行的結尾連在一起解釋,JavaScript 就不會自動添加分號;另外,如果一行的起首是“自增”(++)或“自減”(--)運算符,則它們的前面會自動添加分號。如果continue、break、return和throw這四個語句後面直接跟換行符,則會自動添加分號。這意味着,如果return語句返回的是一個對象的字面量,起首的大括號一定要寫在同一行,否則得不到預期結果

a = b = c = 1
a
++
b
--
c
// 等同於
a = b = c = 1;
a;
++b;
--c;

return
{ first: 'Jane' };
// 等同於
return;
{ first: 'Jane' };

由於解釋引擎自動添加分號的行爲難以預測,因此編寫代碼的時候不應該省略行尾的分號。有些 JavaScript 代碼壓縮器(uglifier)不會自動添加分號,因此遇到沒有分號的結尾,就會讓代碼保持原狀,而不是壓縮成一行,使得壓縮無法得到最優的結果。另外,不寫結尾的分號,可能會導致腳本合併出錯。所以,有的代碼庫在第一行語句開始前,會加上一個分號

全局變量

JavaScript 最大的語法缺點,可能就是全局變量對於任何一個代碼塊,都是可讀可寫。這對代碼的模塊化和重複使用非常不利,因此建議避免使用全局變量。如果不得不使用,可以考慮用大寫字母表示變量名,這樣更容易看出這是全局變量,比如UPPER_CASE

變量聲明

JavaScript 會自動將變量聲明“提升”(hoist)到代碼塊(block)的頭部

if (!x) {
  var x = {};
}
// 等同於
var x;
if (!x) {
  x = {};
}

另外,所有函數都應該在使用之前定義。函數內部的變量聲明,都應該放在函數的頭部

with 語句

with可以減少代碼的書寫,但是會造成混淆

with (o) {
 foo = bar;
}

上面的代碼,可以有四種運行結果

o.foo = bar;
o.foo = o.bar;
foo = bar;
foo = o.bar;

這四種結果都可能發生,取決於不同的變量是否有定義。因此,不要使用with語句

相等和嚴格相等

JavaScript 有兩個表示相等的運算符:“相等”(==)和“嚴格相等”(===)。相等運算符會自動轉換變量類型,造成很多意想不到的情況;因此,建議不要使用相等運算符(==),只使用嚴格相等運算符(===)

語句的合併

有些程序員追求簡潔,喜歡合併不同目的的語句。雖然語句少了,但是可讀性大打折扣,而且會造成誤讀,讓別人誤解代碼的意思。建議不要將不同目的的語句,合併成一行

自增和自減運算符

自增(++)和自減(--)運算符,放在變量的前面或後面,返回的值不一樣,很容易發生錯誤。事實上,所有的++運算符都可以用+= 1代替;改用+= 1,代碼變得更清晰了

switch...case 結構

switch...case結構要求,在每一個case的最後一行必須是break語句,否則會接着運行下一個case。這樣不僅容易忘記,還會造成代碼的冗長。而且,switch...case不使用大括號,不利於代碼形式的統一。此外,這種結構類似於goto語句,容易造成程序流程的混亂,使得代碼結構混亂不堪,不符合面向對象編程的原則

function doAction(action) {
  switch (action) {
    case 'hack':
      return 'hack';
    case 'slash':
      return 'slash';
    case 'run':
      return 'run';
    default:
      throw new Error('Invalid action.');
  }
}

上面的代碼建議改寫成對象結構

function doAction(action) {
  var actions = {
    'hack': function () {
      return 'hack';
    },
    'slash': function () {
      return 'slash';
    },
    'run': function () {
      return 'run';
    }
  };
  if (typeof actions[action] !== 'function') {
    throw new Error('Invalid action.');
  }
  return actions[action]();
}

因此,建議switch...case結構可以用對象結構代替

console 對象與控制檯

console 對象

console對象是 JavaScript 的原生對象,它有點像 Unix 系統的標準輸出stdout和標準錯誤stderr,可以輸出各種信息到控制檯,並且還提供了很多有用的輔助方法

console的常見用途有兩個:

1.調試程序,顯示網頁代碼運行時的錯誤信息

2.提供了一個命令行接口,用來與網頁代碼互動

console對象的瀏覽器實現,包含在瀏覽器自帶的開發工具之中。以 Chrome 瀏覽器的“開發者工具”(Developer Tools)爲例,可以使用下面三種方法的打開它

1.按 F12 或者Control + Shift + i(PC)/ Command + Option + i(Mac)。

2.瀏覽器菜單選擇“工具/開發者工具”。

3.在一個頁面元素上,打開右鍵菜單,選擇其中的“Inspect Element”

打開開發者工具以後,頂端有多個面板,包括:

Elements:查看網頁的 HTML 源碼和 CSS 代碼

Resources:查看網頁加載的各種資源文件(比如代碼文件、字體文件 CSS 文件等),以及在硬盤上創建的各種內容(比如本地緩存、Cookie、Local Storage等)

Network:查看網頁的 HTTP 通信情況

Sources:查看網頁加載的腳本源碼

Timeline:查看各種網頁行爲隨時間變化的情況

Performance:查看網頁的性能情況,比如 CPU 和內存消耗

Console:用來運行 JavaScript 命令

console 對象的靜態方法

console對象提供的各種靜態方法,用來與控制檯窗口互動

console.log(),console.info(),console.debug()

console.log方法用於在控制檯輸出信息。它可以接受一個或多個參數,將它們連接起來輸出;console.log方法會自動在每次輸出的結尾,添加換行符

console.log('Hello World') // Hello World
console.log('a', 'b', 'c') // a b c

如果第一個參數是格式字符串(使用了格式佔位符),console.log方法將依次用後面的參數替換佔位符,然後再進行輸出

console.log(' %s + %s = %s', 1, 1, 2) //  1 + 1 = 2

上面代碼中,console.log方法的第一個參數有三個佔位符(%s),第二、三、四個參數會在顯示時,依次替換掉這個三個佔位符

console.log方法支持以下佔位符,不同類型的數據必須使用對應的佔位符。

%s 字符串
%d 整數
%i 整數
%f 浮點數
%o 對象的鏈接
%c CSS格式字符串

使用%c佔位符時,對應的參數必須是 CSS 代碼,用來對輸出內容進行 CSS 渲染

var number = 11 * 9;
var color = 'red';
console.log('%d %s balloons', number, color); // 99 red balloons

console.log(
  '%cThis text is styled!',
  'color: red; background: yellow; font-size: 24px;'
)

上面代碼運行後,輸出的內容將顯示爲黃底紅字;如果參數是一個對象,console.log會顯示該對象的值

console.log({foo: 'bar'}) // Object {foo: "bar"}
console.log(Date) // function Date() { [native code] }

上面代碼輸出Date對象的值,結果爲一個構造函數

console.info是console.log方法的別名,用法完全一樣。只不過console.info方法會在輸出信息的前面,加上一個藍色圖標。console.debug方法與console.log方法類似,會在控制檯輸出調試信息。但是,默認情況下,console.debug輸出的信息不會顯示,只有在打開顯示級別在verbose的情況下才會顯示

console對象的所有方法,都可以被覆蓋。因此,可以按照自己的需要,定義console.log方法

['log', 'info', 'warn', 'error'].forEach(function(method) {
  console[method] = console[method].bind(
    console,
    new Date().toISOString()
  );
});
console.log("出錯了!"); // 2014-05-18T09:00.000Z 出錯了!

上面代碼表示,使用自定義的console.log方法,可以在顯示結果添加當前時間

console.warn(),console.error()

warn方法和error方法也是在控制檯輸出信息,它們與log方法的不同之處在於,warn方法輸出信息時,在最前面加一個黃色三角,表示警告;error方法輸出信息時,在最前面加一個紅色的叉,表示出錯。同時,還會高亮顯示輸出文字和錯誤發生的堆棧。其他方面都一樣

console.error('Error: %s (%i)', 'Server is not responding', 500) // Error: Server is not responding (500)
console.warn('Warning! Too few nodes (%d)', document.childNodes.length) // Warning! Too few nodes (1)

console.table()

對於某些複合類型的數據,console.table方法可以將其轉爲表格顯示

var languages = [
  { name: "JavaScript", fileExtension: ".js" },
  { name: "TypeScript", fileExtension: ".ts" },
  { name: "CoffeeScript", fileExtension: ".coffee" }
];
console.table(languages);

上面代碼的language變量,轉爲表格顯示如下

(index) name fileExtension
0 "JavaScript" ".js"
1 "TypeScript" ".ts"
2 "CoffeeScript" ".coffee"

console.count()

count方法用於計數,輸出它被調用了多少次

function greet(user) {
  console.count(user);
  return "hi " + user;
}
greet('bob')
// bob: 1
// "hi bob"
greet('alice')
// alice: 1
// "hi alice"
greet('bob')
// bob: 2
// "hi bob"

上面代碼根據參數的不同,顯示bob執行了兩次,alice執行了一次

console.dir(),console.dirxml()

dir方法用來對一個對象進行檢查(inspect),並以易於閱讀和打印的格式顯示

console.log({f1: 'foo', f2: 'bar'}) // Object {f1: "foo", f2: "bar"}
console.dir({f1: 'foo', f2: 'bar'})
// Object
//   f1: "foo"
//   f2: "bar"
//   __proto__: Object

上面代碼顯示dir方法的輸出結果,比log方法更易讀,信息也更豐富。該方法對於輸出 DOM 對象非常有用,因爲會顯示 DOM 對象的所有屬性

Node 環境之中,還可以指定以代碼高亮的形式輸出

console.dir(obj, {colors: true})

dirxml方法主要用於以目錄樹的形式,顯示 DOM 節點

console.dirxml(document.body)

如果參數不是 DOM 節點,而是普通的 JavaScript 對象,console.dirxml等同於console.dir

console.assert()

console.assert方法主要用於程序運行過程中進行條件判斷,如果不滿足條件,就顯示一個錯誤,但不會中斷程序執行。這樣就相當於提示用戶,內部狀態不正確。它接受兩個參數,第一個參數是表達式,第二個參數是字符串。只有當第一個參數爲false,纔會提示有錯誤,在控制檯輸出第二個參數,否則不會有任何結果

console.assert(false, '判斷條件不成立') // Assertion failed: 判斷條件不成立
// 相當於
try {
  if (!false) {
    throw new Error('判斷條件不成立');
  }
} catch(e) {
  console.error(e);
}

console.time(),console.timeEnd()

這兩個方法用於計時,可以算出一個操作所花費的準確時間

console.time('Array initialize');
var array= new Array(1000000);
for (var i = array.length - 1; i >= 0; i--) {
  array[i] = new Object();
};
console.timeEnd('Array initialize');  // Array initialize: 1914.481ms

time方法表示計時開始,timeEnd方法表示計時結束。它們的參數是計時器的名稱。調用timeEnd方法之後,控制檯會顯示“計時器名稱: 所耗費的時間”

console.group(),console.groupEnd(),console.groupCollapsed()

console.group和console.groupEnd這兩個方法用於將顯示的信息分組。它只在輸出大量信息時有用,分在一組的信息,可以用鼠標摺疊/展開

console.group('一級分組');
console.log('一級分組的內容');
console.group('二級分組');
console.log('二級分組的內容');
console.groupEnd(); // 二級分組結束
console.groupEnd(); // 一級分組結束

上面代碼會將“二級分組”顯示在“一級分組”內部,並且“一級分組”和“二級分組”前面都有一個摺疊符號,可以用來摺疊本級的內容。console.groupCollapsed方法與console.group方法很類似,唯一的區別是該組的內容,在第一次顯示時是收起的(collapsed),而不是展開的

console.groupCollapsed('Fetching Data');
console.log('Request Sent');
console.error('Error: Server not responding (500)');
console.groupEnd();

上面代碼只顯示一行”Fetching Data“,點擊後纔會展開,顯示其中包含的兩行

console.trace(),console.clear()

console.trace方法顯示當前執行的代碼在堆棧中的調用路徑

console.trace()
// console.trace()
//   (anonymous function)
//   InjectedScript._evaluateOn
//   InjectedScript._evaluateAndWrap
//   InjectedScript.evaluate

console.clear方法用於清除當前控制檯的所有輸出,將光標回置到第一行。如果用戶選中了控制檯的“Preserve log”選項,console.clear方法將不起作用

控制檯命令行 API

瀏覽器控制檯中,除了使用console對象,還可以使用一些控制檯自帶的命令行方法

$_

$_屬性返回上一個表達式的值

2 + 2 // 4
$_ // 4

$0 - $4

控制檯保存了最近5個在 Elements 面板選中的 DOM 元素,$0代表倒數第一個(最近一個),$1代表倒數第二個,以此類推直到$4

$(selector)

$(selector)返回第一個匹配的元素,等同於document.querySelector()。注意,如果頁面腳本對$有定義,則會覆蓋原始的定義。比如,頁面裏面有 jQuery,控制檯執行$(selector)就會採用 jQuery 的實現,返回一個數組

$$(selector)

$$(selector)返回選中的 DOM 對象,等同於document.querySelectorAll #### $x(path) $x(path)方法返回一個數組,包含匹配特定 XPath 表達式的所有 DOM 元素

$x("//p[a]")

上面代碼返回所有包含a元素的p元素

inspect(object)

inspect(object)方法打開相關面板,並選中相應的元素,顯示它的細節。DOM 元素在Elements面板中顯示,比如inspect(document)會在 Elements 面板顯示document元素。JavaScript 對象在控制檯面板Profiles面板中顯示,比如inspect(window)

getEventListeners(object)

getEventListeners(object)方法返回一個對象,該對象的成員爲object登記了回調函數的各種事件(比如click或keydown),每個事件對應一個數組,數組的成員爲該事件的回調函數

keys(object),values(object)

keys(object)方法返回一個數組,包含object的所有鍵名。values(object)方法返回一個數組,包含object的所有鍵值

var o = {'p1': 'a', 'p2': 'b'};
keys(o) // ["p1", "p2"]
values(o) // ["a", "b"]

monitorEvents(object[, events]) ,unmonitorEvents(object[, events])

monitorEvents(object[, events])方法監聽特定對象上發生的特定事件。事件發生時,會返回一個Event對象,包含該事件的相關信息。unmonitorEvents方法用於停止監聽

monitorEvents(window, "resize");
monitorEvents(window, ["resize", "scroll"])

上面代碼分別表示單個事件和多個事件的監聽方法

monitorEvents($0, 'mouse');
unmonitorEvents($0, 'mousemove');

上面代碼表示如何停止監聽。monitorEvents允許監聽同一大類的事件。所有事件可以分成四個大類

1.mouse:"mousedown", "mouseup", "click", "dblclick", "mousemove", "mouseover", "mouseout", "mousewheel"

2.key:"keydown", "keyup", "keypress", "textInput"

3.touch:"touchstart", "touchmove", "touchend", "touchcancel"

4.control:"resize", "scroll", "zoom", "focus", "blur", "select", "change", "submit", "reset"

monitorEvents($("#msg"), "key");

上面代碼表示監聽所有key大類的事件

其他方法

命令行 API 還提供以下方法。

1.clear():清除控制檯的歷史

2.copy(object):複製特定 DOM 元素到剪貼板

3.dir(object):顯示特定對象的所有屬性,是console.dir方法的別名

4.dirxml(object):顯示特定對象的 XML 形式,是console.dirxml方法的別名

debugger 語句

debugger語句主要用於除錯,作用是設置斷點。如果有正在運行的除錯工具,程序運行到debugger語句時會自動停下。如果沒有除錯工具,debugger語句不會產生任何結果,JavaScript 引擎自動跳過這一句。Chrome 瀏覽器中,當代碼運行到debugger語句時,就會暫停運行,自動打開腳本源碼界面

for(var i = 0; i < 5; i++){
  console.log(i);
  if (i === 2) debugger;
}

上面代碼打印出0,1,2以後,就會暫停,自動打開源碼界面,等待進一步處理

標準庫

Object 對象

概述

JavaScript 原生提供Object對象(注意起首的O是大寫),現在介紹該對象原生的各種方法。JavaScript 的所有其他對象都繼承自Object對象,即那些對象都是Object的實例。

Object對象的原生方法分成兩類:Object本身的方法與Object的實例方法

Object對象本身的方法

所謂“本身的方法”就是直接定義在Object對象的方法

Object.print = function (o) { console.log(o) };

上面代碼中,print方法就是直接定義在Object對象上

Object的實例方法

所謂實例方法就是定義在Object原型對象Object.prototype上的方法。它可以被Object實例直接使用

Object.prototype.print = function () {
  console.log(this);
};
var obj = new Object();
obj.print() // Object

上面代碼中,Object.prototype定義了一個print方法,然後生成一個Object的實例obj。obj直接繼承了Object.prototype的屬性和方法,可以直接使用obj.print調用print方法。也就是說,obj對象的print方法實質上就是調用Object.prototype.print方法。凡是定義在Object.prototype對象上面的屬性和方法,將被所有實例對象共享

以下先介紹Object作爲函數的用法,然後再介紹Object對象的原生方法,分成對象自身的方法(又稱爲“靜態方法”)和實例方法兩部分

Object()

Object本身是一個函數,可以當作工具方法使用,將任意值轉爲對象。這個方法常用於保證某個值一定是對象。如果參數爲空(或者爲undefined和null),Object()返回一個空對象

var obj = Object();
// 等同於
var obj = Object(undefined);
var obj = Object(null);

obj instanceof Object // true

上面代碼的含義,是將undefined和null轉爲對象,結果得到了一個空對象obj。instanceof運算符用來驗證,一個對象是否爲指定的構造函數的實例。obj instanceof Object返回true,就表示obj對象是Object的實例。如果參數是原始類型的值,Object方法將其轉爲對應的包裝對象的實例

var obj = Object(1);
obj instanceof Object // true
obj instanceof Number // true
var obj = Object('foo');
obj instanceof Object // true
obj instanceof String // true
var obj = Object(true);
obj instanceof Object // true
obj instanceof Boolean // true

上面代碼中,Object函數的參數是各種原始類型的值,轉換成對象就是原始類型值對應的包裝對象。如果Object方法的參數是一個對象,它總是返回該對象,即不用轉換

var arr = [];
var obj = Object(arr); // 返回原數組
obj === arr // true
var value = {};
var obj = Object(value) // 返回原對象
obj === value // true
var fn = function () {};
var obj = Object(fn); // 返回原函數
obj === fn // true

利用這一點,可以寫一個判斷變量是否爲對象的函數

function isObject(value) {
  return value === Object(value);
}
isObject([]) // true
isObject(true) // false

Object 構造函數

Object不僅可以當作工具函數使用,還可以當作構造函數使用,即前面可以使用new命令。Object構造函數的首要用途,是直接通過它來生成新對象

var obj = new Object();

注意:通過var obj = new Object()的寫法生成新對象,與字面量的寫法var obj = {}是等價的。或者說,後者只是前者的一種簡便寫法

Object構造函數的用法與工具方法很相似,使用時可以接受一個參數;如果該參數是一個對象,則直接返回這個對象;如果是一個原始類型的值,則返回該值對應的包裝對象

var o1 = {a: 1};
var o2 = new Object(o1);
o1 === o2 // true
var obj = new Object(123);
obj instanceof Number // true

雖然用法相似,但是Object(value)與new Object(value)兩者的語義是不同的,Object(value)表示將value轉成一個對象,new Object(value)則表示新生成一個對象,它的值是value

Object 的靜態方法

所謂“靜態方法”,是指部署在Object對象自身的方法

Object.keys(),Object.getOwnPropertyNames()

Object.keys方法和Object.getOwnPropertyNames方法都用來遍歷對象的屬性。Object.keys方法的參數是一個對象,返回一個數組。該數組的成員都是該對象自身的(而不是繼承的)所有屬性名

Object.getOwnPropertyNames方法與Object.keys類似,也是接受一個對象作爲參數,返回一個數組,包含了該對象自身的所有屬性名

var obj = {
  p1: 123,
  p2: 456
};
Object.keys(obj) // ["p1", "p2"]
Object.getOwnPropertyNames(obj) // ["p1", "p2"]

對於一般的對象來說,Object.keys()和Object.getOwnPropertyNames()返回的結果是一樣的。只有涉及不可枚舉屬性時,纔會有不一樣的結果。Object.keys方法只返回可枚舉的屬性,Object.getOwnPropertyNames方法還返回不可枚舉的屬性名

var a = ['Hello', 'World'];
Object.keys(a) // ["0", "1"]
Object.getOwnPropertyNames(a) // ["0", "1", "length"]

上面代碼中,數組的length屬性是不可枚舉的屬性,所以只出現在Object.getOwnPropertyNames方法的返回結果中。由於 JavaScript 沒有提供計算對象屬性個數的方法,所以可以用這兩個方法代替

var obj = {
  p1: 123,
  p2: 456
};
Object.keys(obj).length // 2
Object.getOwnPropertyNames(obj).length // 2

一般情況下,幾乎總是使用Object.keys方法,遍歷對象的屬性

其他方法

除了上面提到的兩個方法,Object還有不少其他靜態方法

對象屬性模型的相關方法

Object.getOwnPropertyDescriptor():獲取某個屬性的描述對象

Object.defineProperty():通過描述對象,定義某個屬性

Object.defineProperties():通過描述對象,定義多個屬性

控制對象狀態的方法

Object.preventExtensions():防止對象擴展

Object.isExtensible():判斷對象是否可擴展

Object.seal():禁止對象配置

Object.isSealed():判斷一個對象是否可配置

Object.freeze():凍結一個對象

Object.isFrozen():判斷一個對象是否被凍結

原型鏈相關方法

Object.create():該方法可以指定原型對象和屬性,返回一個新的對象

Object.getPrototypeOf():獲取對象的Prototype對象

Object 的實例方法

除了靜態方法,還有不少方法定義在Object.prototype對象。它們稱爲實例方法,所有Object的實例對象都繼承了這些方法。Object實例對象的方法,主要有以下六個:

Object.prototype.valueOf():返回當前對象對應的值

Object.prototype.toString():返回當前對象對應的字符串形式

Object.prototype.toLocaleString():返回當前對象對應的本地字符串形式

Object.prototype.hasOwnProperty():判斷某個屬性是否爲當前對象自身的屬性,還是繼承自原型對象的屬性

Object.prototype.isPrototypeOf():判斷當前對象是否爲另一個對象的原型

Object.prototype.propertyIsEnumerable():判斷某個屬性是否可枚舉

Object.prototype.valueOf()

valueOf方法的作用是返回一個對象的“值”,默認情況下返回對象本身

var obj = new Object();
obj.valueOf() === obj // true

valueOf方法的主要用途是,JavaScript 自動類型轉換時會默認調用這個方法

var obj = new Object();
1 + obj // "1[object Object]"

上面代碼將對象obj與數字1相加,這時 JavaScript 就會默認調用valueOf()方法,求出obj的值再與1相加。所以,如果自定義valueOf方法,就可以得到想要的結果

var obj = new Object();
obj.valueOf = function () {
  return 2;
};
1 + obj // 3

上面代碼自定義了obj對象的valueOf方法,於是1 + obj就得到3。這種方法就相當於用自定義的obj.valueOf,覆蓋Object.prototype.valueOf

Object.prototype.toString()

toString方法的作用是返回一個對象的字符串形式,默認情況下返回類型字符串

var o1 = new Object();
o1.toString() // "[object Object]"
var o2 = {a:1};
o2.toString() // "[object Object]"

上面代碼表示,對於一個對象調用toString方法,會返回字符串[object Object],該字符串說明對象的類型。字符串[object Object]本身沒有太大的用處,但是通過自定義toString方法,可以讓對象在自動類型轉換時,得到想要的字符串形式

var obj = new Object();
obj.toString = function () {
  return 'hello';
};
obj + ' ' + 'world' // "hello world"

上面代碼表示,當對象用於字符串加法時,會自動調用toString方法。由於自定義了toString方法,所以返回字符串hello world。數組、字符串、函數、Date 對象都分別部署了自定義的toString方法,覆蓋了Object.prototype.toString方法

[1, 2, 3].toString() // "1,2,3"
'123'.toString() // "123"
(function () {
  return 123;
}).toString()
// "function () {
//   return 123;
// }"
(new Date()).toString() // "Tue May 10 2016 09:11:31 GMT+0800 (CST)"

上面代碼中,數組、字符串、函數、Date 對象調用toString方法,並不會返回[object Object],因爲它們都自定義了toString方法,覆蓋原始方法

toString() 的應用:判斷數據類型

Object.prototype.toString方法返回對象的類型字符串,因此可以用來判斷一個值的類型

var obj = {};
obj.toString() // "[object Object]"

上面代碼調用空對象的toString方法,結果返回一個字符串object Object,其中第二個Object表示該值的構造函數。這是一個十分有用的判斷數據類型的方法。由於實例對象可能會自定義toString方法,覆蓋掉Object.prototype.toString方法,所以爲了得到類型字符串,最好直接使用Object.prototype.toString方法。通過函數的call方法,可以在任意值上調用這個方法,幫助我們判斷這個值的類型

Object.prototype.toString.call(value)

不同數據類型的Object.prototype.toString方法返回值如下;這就是說,Object.prototype.toString可以看出一個值到底是什麼類型

Object.prototype.toString.call(2) // "[object Number]"
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(Math) // "[object Math]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call([]) // "[object Array]"

利用這個特性,可以寫出一個比typeof運算符更準確的類型判斷函數

var type = function (o){
  var s = Object.prototype.toString.call(o);
  return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};
type({}); // "object"
type([]); // "array"
type(5); // "number"
type(null); // "null"
type(); // "undefined"
type(/abcd/); // "regex"
type(new Date()); // "date"

在上面這個type函數的基礎上,還可以加上專門判斷某種類型數據的方法

var type = function (o){
  var s = Object.prototype.toString.call(o);
  return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};
['Null', 'Undefined', 'Object', 'Array', 'String', 'Number', 'Boolean', 'Function', 'RegExp'].forEach(function (t) {
  type['is' + t] = function (o) {
    return type(o) === t.toLowerCase();
  };
});
type.isObject({}) // true
type.isNumber(NaN) // true
type.isRegExp(/abc/) // true

Object.prototype.toLocaleString()

Object.prototype.toLocaleString方法與toString的返回結果相同,也是返回一個值的字符串形式。這個方法的主要作用是留出一個接口,讓各種不同的對象實現自己版本的toLocaleString,用來返回針對某些地域的特定的值

var person = {
  toString: function () {
    return 'Henry Norman Bethune';
  },
  toLocaleString: function () {
    return '白求恩';
  }
};
person.toString() // Henry Norman Bethune
person.toLocaleString() // 白求恩

目前,主要有三個對象自定義了toLocaleString方法。

1.Array.prototype.toLocaleString()

2.Number.prototype.toLocaleString()

3.Date.prototype.toLocaleString()

舉例來說,日期的實例對象的toString和toLocaleString返回值就不一樣,而且toLocaleString的返回值跟用戶設定的所在地域相關

var date = new Date();
date.toString() // "Tue Jan 01 2018 12:01:33 GMT+0800 (CST)"
date.toLocaleString() // "1/01/2018, 12:01:33 PM"

Object.prototype.hasOwnProperty()

Object.prototype.hasOwnProperty方法接受一個字符串作爲參數,返回一個布爾值,表示該實例對象自身是否具有該屬性

var obj = { p: 123 };
obj.hasOwnProperty('p') // true
obj.hasOwnProperty('toString') // false

上面代碼中,對象obj自身具有p屬性,所以返回true。toString屬性是繼承的,所以返回false

屬性描述對象

概述

JavaScript 提供了一個內部數據結構,用來描述對象的屬性,控制它的行爲,比如該屬性是否可寫、可遍歷等等。這個內部數據結構稱爲“屬性描述對象”(attributes object)。每個屬性都有自己對應的屬性描述對象,保存該屬性的一些元信息。

{
  value: 123,
  writable: false,
  enumerable: true,
  configurable: false,
  get: undefined,
  set: undefined
}

屬性描述對象提供6個元屬性

1.value:value是該屬性的屬性值,默認爲undefined

2.writable:writable是一個布爾值,表示屬性值(value)是否可改變(即是否可寫),默認爲true

3.enumerable:enumerable是一個布爾值,表示該屬性是否可遍歷,默認爲true。如果設爲false,會使得某些操作(比如for...in循環、Object.keys())跳過該屬性

4.configurable:configurable是一個布爾值,表示可配置性,默認爲true。如果設爲false,將阻止某些操作改寫該屬性,比如無法刪除該屬性,也不得改變該屬性的屬性描述對象(value屬性除外)。也就是說,configurable屬性控制了屬性描述對象的可寫性

5.get:get是一個函數,表示該屬性的取值函數(getter),默認爲undefined

6.set:set是一個函數,表示該屬性的存值函數(setter),默認爲undefined

Object.getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor()方法可以獲取屬性描述對象。它的第一個參數是目標對象,第二個參數是一個字符串,對應目標對象的某個屬性名

var obj = { p: 'a' };
Object.getOwnPropertyDescriptor(obj, 'p') // Object { value: "a", writable: true, enumerable: true, configurable: true }

上面代碼中,Object.getOwnPropertyDescriptor()方法獲取obj.p的屬性描述對象。

注意,Object.getOwnPropertyDescriptor()方法只能用於對象自身的屬性,不能用於繼承的屬性

var obj = { p: 'a' };
Object.getOwnPropertyDescriptor(obj, 'toString') // undefined

上面代碼中,toString是obj對象繼承的屬性,Object.getOwnPropertyDescriptor()無法獲取

Object.getOwnPropertyNames()

Object.getOwnPropertyNames方法返回一個數組,成員是參數對象自身的全部屬性的屬性名,不管該屬性是否可遍歷

var obj = Object.defineProperties({}, {
  p1: { value: 1, enumerable: true },
  p2: { value: 2, enumerable: false }
});
Object.getOwnPropertyNames(obj) // ["p1", "p2"]

上面代碼中,obj.p1是可遍歷的,obj.p2是不可遍歷的。Object.getOwnPropertyNames會將它們都返回。這跟Object.keys的行爲不同,Object.keys只返回對象自身的可遍歷屬性的全部屬性名

Object.keys([]) // []
Object.getOwnPropertyNames([]) // [ 'length' ]
Object.keys(Object.prototype) // []
Object.getOwnPropertyNames(Object.prototype) // ['hasOwnProperty', 'valueOf', 'constructor', 'toLocaleString', 'isPrototypeOf', 'propertyIsEnumerable', 'toString']

上面代碼中,數組自身的length屬性是不可遍歷的,Object.keys不會返回該屬性。第二個例子的Object.prototype也是一個對象,所有實例對象都會繼承它,它自身的屬性都是不可遍歷的

Object.defineProperty(),Object.defineProperties()

Object.defineProperty()方法允許通過屬性描述對象,定義或修改一個屬性,然後返回修改後的對象,它的用法如下

Object.defineProperty(object, propertyName, attributesObject)

Object.defineProperty方法接受三個參數:

1.object:屬性所在的對象

2.propertyName:字符串,表示屬性名

3.attributesObject:屬性描述對象

舉例來說,定義obj.p可以寫成下面這樣

var obj = Object.defineProperty({}, 'p', {
  value: 123,
  writable: false,
  enumerable: true,
  configurable: false
});
obj.p // 123
obj.p = 246;
obj.p // 123

上面代碼中,Object.defineProperty()方法定義了obj.p屬性。由於屬性描述對象的writable屬性爲false,所以obj.p屬性不可寫。注意,這裏的Object.defineProperty方法的第一個參數是{}(一個新建的空對象),p屬性直接定義在這個空對象上面,然後返回這個對象,這是Object.defineProperty()的常見用法。如果屬性已經存在,Object.defineProperty()方法相當於更新該屬性的屬性描述對象。如果一次性定義或修改多個屬性,可以使用Object.defineProperties()方法

var obj = Object.defineProperties({}, {
  p1: { value: 123, enumerable: true },
  p2: { value: 'abc', enumerable: true },
  p3: { get: function () { return this.p1 + this.p2 },
    enumerable:true,
    configurable:true
  }
});  
obj.p1 // 123
obj.p2 // "abc"
obj.p3 // "123abc"

上面代碼中,Object.defineProperties()同時定義了obj對象的三個屬性。其中,p3屬性定義了取值函數get,即每次讀取該屬性,都會調用這個取值函數。注意,一旦定義了取值函數get(或存值函數set),就不能將writable屬性設爲true,或者同時定義value屬性,否則會報錯

Object.defineProperty()和Object.defineProperties()參數裏面的屬性描述對象,writable、configurable、enumerable這三個屬性的默認值都爲false

var obj = {};
Object.defineProperty(obj, 'foo', {});
Object.getOwnPropertyDescriptor(obj, 'foo') // { value: undefined, writable: false, enumerable: false, configurable: false }

上面代碼中,定義obj.foo時用了一個空的屬性描述對象,就可以看到各個元屬性的默認值

Object.prototype.propertyIsEnumerable()

實例對象的propertyIsEnumerable()方法返回一個布爾值,用來判斷某個屬性是否可遍歷。注意,這個方法只能用於判斷對象自身的屬性,對於繼承的屬性一律返回false

var obj = {};
obj.p = 123;
obj.propertyIsEnumerable('p') // true
obj.propertyIsEnumerable('toString') // false

元屬性

屬性描述對象的各個屬性稱爲“元屬性”,因爲它們可以看作是控制屬性的屬性

value

value屬性是目標屬性的值

var obj = {};
obj.p = 123;
Object.getOwnPropertyDescriptor(obj, 'p').value // 123
Object.defineProperty(obj, 'p', { value: 246 }); obj.p // 246

上面代碼是通過value屬性,讀取或改寫obj.p的例子

writable

writable屬性是一個布爾值,決定了目標屬性的值(value)是否可以被改變

var obj = {};
Object.defineProperty(obj, 'a', {  value: 37,  writable: false});
obj.a // 37
obj.a = 25;
obj.a // 37

上面代碼中,obj.a的writable屬性是false。然後,改變obj.a的值,不會有任何效果。

注意,正常模式下,對writable爲false的屬性賦值不會報錯,只會默默失敗。但是,嚴格模式下會報錯,即使對a屬性重新賦予一個同樣的值

如果原型對象的某個屬性的writable爲false,那麼子對象將無法自定義這個屬性

var proto = Object.defineProperty({}, 'foo', { value: 'a',  writable: false });
var obj = Object.create(proto);
obj.foo = 'b';
obj.foo // 'a'

上面代碼中,proto是原型對象,它的foo屬性不可寫。obj對象繼承proto,也不可以再自定義這個屬性了。如果是嚴格模式,這樣做還會拋出一個錯誤。

但是,有一個規避方法,就是通過覆蓋屬性描述對象,繞過這個限制。原因是這種情況下,原型鏈會被完全忽視

var proto = Object.defineProperty({}, 'foo', { value: 'a',  writable: false});
var obj = Object.create(proto);
Object.defineProperty(obj, 'foo', { value: 'b'});
obj.foo // "b"

enumerable

enumerable(可遍歷性)返回一個布爾值,表示目標屬性是否可遍歷。

JavaScript 的早期版本,for...in循環是基於in運算符的。我們知道,in運算符不管某個屬性是對象自身的還是繼承的,都會返回true

var obj = {};
'toString' in obj // true

toString不是obj對象自身的屬性,但是in運算符也返回true,這導致了toString屬性也會被for...in循環遍歷,這顯然不太合理,後來就引入了“可遍歷性”這個概念。只有可遍歷的屬性,纔會被for...in循環遍歷,同時還規定toString這一類實例對象繼承的原生屬性,都是不可遍歷的,這樣就保證了for...in循環的可用性。具體來說,如果一個屬性的enumerable爲false,下面三個操作不會取到該屬性

1.for..in循環

2.Object.keys方法

3.JSON.stringify方法

因此,enumerable可以用來設置“祕密”屬性

var obj = {};
Object.defineProperty(obj, 'x', { value: 123,  enumerable: false});
obj.x // 123
for (var key in obj) {
  console.log(key);
} // undefined
Object.keys(obj)  // []
JSON.stringify(obj) // "{}"

上面代碼中,obj.x屬性的enumerable爲false,所以一般的遍歷操作都無法獲取該屬性,使得它有點像“祕密”屬性,但不是真正的私有屬性,還是可以直接獲取它的值。

注意,for...in循環包括繼承的屬性,Object.keys方法不包括繼承的屬性。如果需要獲取對象自身的所有屬性,不管是否可遍歷,可以使用Object.getOwnPropertyNames方法。另外,JSON.stringify方法會排除enumerable爲false的屬性,有時可以利用這一點。如果對象的 JSON 格式輸出要排除某些屬性,就可以把這些屬性的enumerable設爲false

configurable

configurable(可配置性)返回一個布爾值,決定了是否可以修改屬性描述對象。也就是說,configurable爲false時,value、writable、enumerable和configurable都不能被修改了

var obj = Object.defineProperty({}, 'p', { writable: true,  configurable: false });
Object.defineProperty(obj, 'p', {writable: false}) // 修改成功

注意,writable只有在false改爲true會報錯,true改爲false是允許的。至於value,只要writable和configurable有一個爲true,就允許改動。另外,configurable爲false時,直接目標屬性賦值,不報錯,但不會成功;如果是嚴格模式,還會報錯

var obj = Object.defineProperty({}, 'p', {
  value: 1,
  configurable: false
});
obj.p = 2;
obj.p // 1

可配置性決定了目標屬性是否可以被刪除(delete)

var obj = Object.defineProperties({}, {
  p1: { value: 1, configurable: true },
  p2: { value: 2, configurable: false }
});
delete obj.p1 // true
delete obj.p2 // false
obj.p1 // undefined
obj.p2 // 2

上面代碼中,obj.p1的configurable是true,所以可以被刪除,obj.p2就無法刪除

存取器

除了直接定義以外,屬性還可以用存取器(accessor)定義。其中,存值函數稱爲setter,使用屬性描述對象的set屬性;取值函數稱爲getter,使用屬性描述對象的get屬性。一旦對目標屬性定義了存取器,那麼存取的時候,都將執行對應的函數。利用這個功能,可以實現許多高級特性,比如某個屬性禁止賦值

var obj = Object.defineProperty({}, 'p', {
  get: function () {
    return 'getter';
  },
  set: function (value) {
    console.log('setter: ' + value);
  }
});
obj.p // "getter"
obj.p = 123 // "setter: 123"

上面代碼中,obj.p定義了get和set屬性。obj.p取值時,就會調用get;賦值時,就會調用set。JavaScript 還提供了存取器的另一種寫法

var obj = {
  get p() {
    return 'getter';
  },
  set p(value) {
    console.log('setter: ' + value);
  }
}

上面的寫法與定義屬性描述對象是等價的,而且使用更廣泛。

注意,取值函數get不能接受參數,存值函數set只能接受一個參數(即屬性的值)。存取器往往用於屬性的值依賴對象內部數據的場合

var obj ={
  $n : 5,
  get next() { return this.$n++ },
  set next(n) {
    if (n >= this.$n) this.$n = n;
    else throw new Error('新的值必須大於當前值');
  }
};
obj.next // 5
obj.next = 10;
obj.next // 10
obj.next = 5; // Uncaught Error: 新的值必須大於當前值

上面代碼中,next屬性的存值函數和取值函數,都依賴於內部屬性$n

對象的拷貝

有時,我們需要將一個對象的所有屬性,拷貝到另一個對象,可以用下面的方法實現

var extend = function (to, from) {
  for (var property in from) {
    to[property] = from[property];
  }
  return to;
}
extend({}, {
  a: 1
}) // {a: 1}

上面這個方法的問題在於,如果遇到存取器定義的屬性,會只拷貝值

extend({}, {
  get a() { return 1 }
}) // {a: 1}

爲了解決這個問題,我們可以通過Object.defineProperty方法來拷貝屬性

var extend = function (to, from) {
  for (var property in from) {
    if (!from.hasOwnProperty(property)) continue;
    Object.defineProperty(
      to,
      property,
      Object.getOwnPropertyDescriptor(from, property)
    );
  }
  return to;
}
extend({}, { get a(){ return 1 } }) // { get a(){ return 1 } })

上面代碼中,hasOwnProperty那一行用來過濾掉繼承的屬性,否則可能會報錯,因爲Object.getOwnPropertyDescriptor讀不到繼承屬性的屬性描述對象

控制對象狀態

有時需要凍結對象的讀寫狀態,防止對象被改變。JavaScript 提供了三種凍結方法,最弱的一種是Object.preventExtensions,其次是Object.seal,最強的是Object.freeze

Object.preventExtensions()

Object.preventExtensions方法可以使得一個對象無法再添加新的屬性

var obj = new Object();
Object.preventExtensions(obj);
Object.defineProperty(obj, 'p', {
  value: 'hello'
}); // TypeError: Cannot define property:p, object is not extensible.
obj.p = 1;
obj.p // undefined

Object.isExtensible()

Object.isExtensible方法用於檢查一個對象是否使用了Object.preventExtensions方法。也就是說,檢查是否可以爲一個對象添加屬性

var obj = new Object();
Object.isExtensible(obj) // true
Object.preventExtensions(obj);
Object.isExtensible(obj) // false

上面代碼中,對obj對象使用Object.preventExtensions方法以後,再使用Object.isExtensible方法,返回false,表示已經不能添加新屬性了

Object.seal()

Object.seal方法使得一個對象既無法添加新屬性,也無法刪除舊屬性

var obj = { p: 'hello' };
Object.seal(obj);
delete obj.p;
obj.p // "hello"
obj.x = 'world';
obj.x // undefined

Object.seal實質是把屬性描述對象的configurable屬性設爲false,因此屬性描述對象不再能改變了

var obj = { p: 'a' };
// seal方法之前
Object.getOwnPropertyDescriptor(obj, 'p') // Object { value: "a",  writable: true, enumerable: true, configurable: true }
Object.seal(obj);
// seal方法之後
Object.getOwnPropertyDescriptor(obj, 'p') // Object { value: "a", writable: true, enumerable: true, configurable: false }
Object.defineProperty(o, 'p', { enumerable: false }) // TypeError: Cannot redefine property: p

Object.seal只是禁止新增或刪除屬性,並不影響修改某個屬性的值

var obj = { p: 'a' };
Object.seal(obj);
obj.p = 'b';
obj.p // 'b'

Object.isSealed()

Object.isSealed方法用於檢查一個對象是否使用了Object.seal方法

var obj = { p: 'a' };
Object.seal(obj);
Object.isSealed(obj) // true

這時,Object.isExtensible方法也返回false

var obj = { p: 'a' };
Object.seal(obj);
Object.isExtensible(obj) // false

Object.freeze()

Object.freeze方法可以使得一個對象無法添加新屬性、無法刪除舊屬性、也無法改變屬性的值,使得這個對象實際上變成了常量

var obj = { p: 'hello' };
Object.freeze(obj);
obj.p = 'world';
obj.p // "hello"
obj.t = 'hello';
obj.t // undefined
delete obj.p // false
obj.p // "hello"

上面代碼中,對obj對象進行Object.freeze()以後,修改屬性、新增屬性、刪除屬性都無效了。這些操作並不報錯,只是默默地失敗。如果在嚴格模式下,則會報錯

Object.isFrozen()

Object.isFrozen方法用於檢查一個對象是否使用了Object.freeze方法

var obj = { p: 'hello'};
Object.freeze(obj);
Object.isFrozen(obj) // true

使用Object.freeze方法以後,Object.isSealed將會返回true,Object.isExtensible返回false

var obj = { p: 'hello' };
Object.freeze(obj);
Object.isSealed(obj) // true
Object.isExtensible(obj) // false

Object.isFrozen的一個用途是,確認某個對象沒有被凍結後,再對它的屬性賦值

var obj = { p: 'hello' };
Object.freeze(obj);
if (!Object.isFrozen(obj)) {
  obj.p = 'world';
}

上面代碼中,確認obj沒有被凍結後,再對它的屬性賦值,就不會報錯了

侷限性

上面的三個方法鎖定對象的可寫性有一個漏洞:可以通過改變原型對象,來爲對象增加屬性

var obj = new Object();
Object.preventExtensions(obj);
var proto = Object.getPrototypeOf(obj);
proto.t = 'hello';
obj.t // hello

上面代碼中,對象obj本身不能新增屬性,但是可以在它的原型對象上新增屬性,就依然能夠在obj上讀到。一種解決方案是,把obj的原型也凍結住

var obj = new Object();
Object.preventExtensions(obj);
var proto = Object.getPrototypeOf(obj);
Object.preventExtensions(proto);
proto.t = 'hello';
obj.t // undefined

另外一個侷限是,如果屬性值是對象,上面這些方法只能凍結屬性指向的對象,而不能凍結對象本身的內容

var obj = { foo: 1,  bar: ['a', 'b'] };
Object.freeze(obj);
obj.bar.push('c');
obj.bar // ["a", "b", "c"]

上面代碼中,obj.bar屬性指向一個數組,obj對象被凍結以後,這個指向無法改變,即無法指向其他值,但是所指向的數組是可以改變的

Array 對象

構造函數

Array是 JavaScript 的原生對象,同時也是一個構造函數,可以用它生成新的數組

var arr = new Array(2);
arr.length // 2
arr // [ empty x 2 ]

上面代碼中,Array構造函數的參數2,表示生成一個兩個成員的數組,每個位置都是空值。如果沒有使用new,運行結果也是一樣的

var arr = new Array(2);
// 等同於
var arr = Array(2);

Array構造函數有一個很大的缺陷,就是不同的參數,會導致它的行爲不一致

// 無參數時,返回一個空數組
new Array() // []
// 單個正整數參數,表示返回的新數組的長度
new Array(1) // [ empty ]
new Array(2) // [ empty x 2 ]
// 非正整數的數值作爲參數,會報錯
new Array(3.2) // RangeError: Invalid array length
new Array(-3) // RangeError: Invalid array length
// 單個非數值(比如字符串、布爾值、對象等)作爲參數,則該參數是返回的新數組的成員
new Array('abc') // ['abc']
new Array([1]) // [Array[1]]
// 多參數時,所有參數都是返回的新數組的成員
new Array(1, 2) // [1, 2]
new Array('a', 'b', 'c') // ['a', 'b', 'c']

可以看到,Array作爲構造函數,行爲很不一致。因此,不建議使用它生成新數組,直接使用數組字面量是更好的做法

// bad
var arr = new Array(1, 2);
// good
var arr = [1, 2];

注意,如果參數是一個正整數,返回數組的成員都是空位。雖然讀取的時候返回undefined,但實際上該位置沒有任何值。雖然可以取到length屬性,但是取不到鍵名

var a = new Array(3);
var b = [undefined, undefined, undefined];
a.length // 3
b.length // 3
a[0] // undefined
b[0] // undefined
0 in a // false
0 in b // true

上面代碼中,a是一個長度爲3的空數組,b是一個三個成員都是undefined的數組。讀取鍵值的時候,a和b都返回undefined,但是a的鍵位都是空的,b的鍵位是有值的

靜態方法

Array.isArray()

Array.isArray方法返回一個布爾值,表示參數是否爲數組。它可以彌補typeof運算符的不足

var arr = [1, 2, 3];
typeof arr // "object"
Array.isArray(arr) // true

實例方法

valueOf(),toString()

valueOf方法是一個所有對象都擁有的方法,表示對該對象求值。不同對象的valueOf方法不盡一致,數組的valueOf方法返回數組本身。toString方法也是對象的通用方法,數組的toString方法返回數組的字符串形式

var arr = [1, 2, 3, [4, 5, 6]];
arr.valueOf() // [1, 2, 3, [4, 5, 6]]
arr.toString() // "1,2,3,4,5,6"

push(),pop()

push方法用於在數組的末端添加一個或多個元素,並返回添加新元素後的數組長度。pop方法用於刪除數組的最後一個元素,並返回該元素。注意,這兩個方法都會改變原數組

var arr = [];
arr.push(1) // 1
arr.push('a') // 2
arr.push(true, {},'c') // 4
arr // [1, 'a', true, {}]  
arr.pop() // 'c'
arr // [1, 'a', true, {}]

對空數組使用pop方法,不會報錯,而是返回undefined

[].pop() // undefined

push和pop結合使用,就構成了“後進先出”的棧結構(stack)

shift(),unshift()

shift()方法用於刪除數組的第一個元素,並返回該元素。unshift()方法用於在數組的第一個位置添加元素,並返回添加新元素後的數組長度。注意,這兩個方法都會改變原數組

var a = ['a', 'b', 'c'];
a.shift() // 'a'
a // ['b', 'c']
a.unshift('x','y'); // 3
a // ['x','y', 'b', 'c']

unshift()方法可以接受多個參數,這些參數都會添加到目標數組頭部。shift()方法可以遍歷並清空一個數組

var list = [1, 2, 3, 4];
var item;
while (item = list.shift()) {
  console.log(list);
}
list // []

上面代碼通過list.shift()方法每次取出一個元素,從而遍歷數組。它的前提是數組元素不能是0或任何布爾值等於false的元素,因此這樣的遍歷不是很可靠。push()和shift()結合使用,就構成了“先進先出”的隊列結構(queue)

join()

join()方法以指定參數作爲分隔符,將所有數組成員連接爲一個字符串返回。如果不提供參數,默認用逗號分隔;如果數組成員是undefined或null或空位,會被轉成空字符串

var a = [1,,2, 3, undefined, null, 4];
a.join(' ') // '1  2 3   4'
a.join(' | ') // '1 |  | 2 | 3 |  |  | 4'
a.join() // '1,,2,3,,,4'

通過call方法,這個方法也可以用於字符串或類似數組的對象

Array.prototype.join.call('hello', '-') // "h-e-l-l-o"
var obj = { 0: 'a', 1: 'b', length: 2 };
Array.prototype.join.call(obj, '-') // 'a-b'

concat()

concat方法用於多個數組的合併。它將新數組的成員,添加到原數組成員的後部,然後返回一個新數組,原數組不變

['hello'].concat(['world'], ['!']) // ["hello", "world", "!"]
[].concat({a: 1}, {b: 2}) // [{ a: 1 }, { b: 2 }]
[2].concat({a: 1}) // [2, {a: 1}]

除了數組作爲參數,concat也接受其他類型的值作爲參數,添加到目標數組尾部。如果數組成員包括對象,concat方法返回當前數組的一個淺拷貝。所謂“淺拷貝”,指的是新數組拷貝的是對象的引用

var obj = { a: 1 };
var oldArray = [obj];
var newArray = oldArray.concat();
obj.a = 2;
newArray[0].a // 2

上面代碼中,原數組包含一個對象,concat方法生成的新數組包含這個對象的引用。所以,改變原對象以後,新數組跟着改變

reverse()

reverse方法用於顛倒排列數組元素,返回改變後的數組。注意,該方法將改變原數組

var a = ['a', 'b', 'c'];
a.reverse() // ["c", "b", "a"]
a // ["c", "b", "a"]

slice()

slice方法用於提取目標數組的一部分,返回一個新數組,原數組不變

arr.slice(start, end);

它的第一個參數爲起始位置(從0開始),第二個參數爲終止位置(但該位置的元素本身不包括在內)。如果省略第二個參數,則一直返回到原數組的最後一個成員

var a = ['a', 'b', 'c'];
a.slice(1) // ["b", "c"]
a.slice(2, 6) // ["c"]
a.slice() // ["a", "b", "c"]
a.slice(-2, -1) // ["b"]

slice沒有參數則等於返回一個原數組的拷貝;如果slice方法的參數是負數,則表示倒數計算的位置;如果第一個參數大於等於數組長度或者第二個參數小於第一個參數,則返回空數組;slice方法的一個重要應用,是將類似數組的對象轉爲真正的數組

Array.prototype.slice.call({ 0: 'a', 1: 'b', length: 2 }) // ['a', 'b']
Array.prototype.slice.call(document.querySelectorAll("div"));
Array.prototype.slice.call(arguments);

上面代碼的參數都不是數組,但是通過call方法,在它們上面調用slice方法,就可以把它們轉爲真正的數組

splice()

splice方法用於刪除原數組的一部分成員,並可以在刪除的位置添加新的數組成員,返回值是被刪除的元素。注意,該方法會改變原數組

arr.splice(start, count, addElement1, addElement2, ...);

splice的第一個參數是刪除的起始位置(從0開始),第二個參數是被刪除的元素個數。如果後面還有更多參數,則表示這些就是要被插入數組的新元素;起始位置如果是負數,就表示從倒數位置開始刪除

var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(4, 2, 1, 2) // ["e", "f"]
a // ["a", "b", "c", "d", 1, 2]
a.splice(-4, 2) // ["c", "d"]
a // ["a", "b", 1, 2]

如果只是單純地插入元素,splice方法的第二個參數可以設爲0;如果只提供第一個參數,等同於將原數組在指定位置拆分成兩個數組

var a = [1, 2, 3, 4];
a.splice(2) // [3, 4]
a // [1, 2]

sort()

sort方法對數組成員進行排序,默認是按照字典順序排序。排序後,原數組將被改變

['d', 'c', 'b', 'a'].sort() // ['a', 'b', 'c', 'd']
[4, 3, 2, 1].sort() // [1, 2, 3, 4]
[11, 101].sort() // [101, 11]
[10111, 1101, 111].sort() // [10111, 1101, 111]

需要特殊注意:sort方法不是按照大小排序,而是按照字典順序;也就是說,數值會被先轉成字符串,再按照字典順序進行比較,所以101排在11的前面。如果想讓sort方法按照自定義方式排序,可以傳入一個函數作爲參數。sort的參數函數本身接受兩個參數,表示進行比較的兩個數組成員。如果該函數的返回值大於0,表示第一個成員排在第二個成員後面;其他情況下,都是第一個元素排在第二個元素前面

[
  { name: "張三", age: 30 },
  { name: "李四", age: 24 },
  { name: "王五", age: 28  }
].sort(function (o1, o2) {
  return o1.age - o2.age;
})
// [
//   { name: "李四", age: 24 },
//   { name: "王五", age: 28  },
//   { name: "張三", age: 30 }
// ]

map()

map方法將數組的所有成員依次傳入參數函數,然後把每一次的執行結果組成一個新數組返回,原數組沒有變化

var numbers = [1, 2, 3];
numbers.map(function (n) {
  return n + 1;
}); // [2, 3, 4]
numbers // [1, 2, 3]

map方法接受一個函數作爲參數。該函數調用時,map方法向它傳入三個參數:當前成員、當前位置和數組本身

[1, 2, 3].map(function(elem, index, arr) {
  return elem * index;
}); // [0, 2, 6]

map方法還可以接受第二個參數,用來綁定回調函數內部的this變量

var arr = ['a', 'b', 'c'];
[1, 2].map(function (e) {
  return this[e];
}, arr) // ['b', 'c']

上面代碼通過map方法的第二個參數,將回調函數內部的this對象,指向arr數組。如果數組有空位,map方法的回調函數在這個位置不會執行,會跳過數組的空位

var f = function (n) { return 'a' };
[1, undefined, 2].map(f) // ["a", "a", "a"]
[1, null, 2].map(f) // ["a", "a", "a"]
[1, , 2].map(f) // ["a", , "a"]

上面代碼中,map方法不會跳過undefined和null,但是會跳過空位

forEach()

forEach方法與map方法很相似,也是對數組的所有成員依次執行參數函數。但是,forEach方法不返回值,只用來操作數據。這就是說,如果數組遍歷的目的是爲了得到返回值,那麼使用map方法,否則使用forEach方法。forEach的用法與map方法一致,參數是一個函數,該函數同樣接受三個參數:當前值、當前位置、整個數組

function log(element, index, array) {
  console.log('[' + index + '] = ' + element);
}
[2, 5, 9].forEach(log);
// [0] = 2
// [1] = 5
// [2] = 9

上面代碼中,forEach遍歷數組不是爲了得到返回值,而是爲了在屏幕輸出內容,所以不必使用map方法。forEach方法也可以接受第二個參數,綁定參數函數的this變量

var out = [];
[1, 2, 3].forEach(function(elem) {
  this.push(elem * elem);
}, out);
out // [1, 4, 9]

上面代碼中,空數組out是forEach方法的第二個參數,結果,回調函數內部的this關鍵字就指向out。注意,forEach方法無法中斷執行,總是會將所有成員遍歷完。如果希望符合某種條件時就中斷遍歷要使用for循環

var arr = [1, 2, 3];
for (var i = 0; i < arr.length; i++) {
  if (arr[i] === 2) break;
  console.log(arr[i]);
} // 1

上面代碼中,執行到數組的第二個成員時,就會中斷執行,forEach方法做不到這一點。forEach方法不會跳過undefined和null,但會跳過空位

var log = function (n) {
  console.log(n + 1);
};
[1, undefined, 2].forEach(log) // 2 NaN 3
[1, null, 2].forEach(log) // 2 1 3
[1, , 2].forEach(log)// 2 3

filter()

filter方法用於過濾數組成員,滿足條件的成員組成一個新數組返回。它的參數是一個函數,所有數組成員依次執行該函數,返回結果爲true的成員組成一個新數組返回。該方法不會改變原數組

[1, 2, 3, 4, 5].filter(function (elem) {
  return (elem > 3);
}) // [4, 5]

filter方法的參數函數可以接受三個參數:當前成員,當前位置和整個數組

[1, 2, 3, 4, 5].filter(function (elem, index, arr) {
  return index % 2 === 0;
}); // [1, 3, 5]

filter方法還可以接受第二個參數,用來綁定參數函數內部的this變量

var obj = { MAX: 3 };
var myFilter = function (item) {
  if (item > this.MAX) return true;
};
var arr = [2, 8, 3, 4, 1, 3, 2, 9];
arr.filter(myFilter, obj) // [8, 4, 9]

上面代碼中,過濾器myFilter內部有this變量,它可以被filter方法的第二個參數obj綁定,返回大於3的成員

some(),every()

這兩個方法類似“斷言”(assert),返回一個布爾值,表示判斷數組成員是否符合某種條件。它們接受一個函數作爲參數,所有數組成員依次執行該函數。該函數接受三個參數:當前成員、當前位置和整個數組,然後返回一個布爾值。

some方法是隻要一個成員的返回值是true,則整個some方法的返回值就是true,否則返回false。every方法是所有成員的返回值都是true,整個every方法才返回true,否則返回false

var arr = [1, 2, 3, 4, 5];
arr.some(function (elem, index, arr) {
  return elem >= 3;
}); // true
arr.every(function (elem, index, arr) {
  return elem >= 3;
}); // false

some和every方法還可以接受第二個參數,用來綁定參數函數內部的this變量

function isEven(x) { return x % 2 === 0 }
[].some(isEven) // false
[].every(isEven) // true

reduce(),reduceRight()

reduce方法和reduceRight方法依次處理數組的每個成員,最終累計爲一個值。它們的差別是,reduce是從左到右處理(從第一個成員到最後一個成員),reduceRight則是從右到左(從最後一個成員到第一個成員),其他完全一樣

[1, 2, 3, 4, 5].reduce(function (a, b) {
  console.log(a, b);
  return a + b;
})
// 1 2
// 3 3
// 6 4
// 10 5
//最後結果:15

reduce方法和reduceRight方法的第一個參數都是一個函數。該函數接受以下四個參數。

1.累積變量,默認爲數組的第一個成員

2.當前變量,默認爲數組的第二個成員

3.當前位置(從0開始)

4.原數組

這四個參數之中,只有前兩個是必須的,後兩個則是可選的。如果要對累積變量指定初值,可以把它放在reduce方法和reduceRight方法的第二個參數

[1, 2, 3, 4, 5].reduce(function (a, b) {
  return a + b;
}, 10); // 25

上面的第二個參數相當於設定了默認值,處理空數組時尤其有用

function add(prev, cur) {
  return prev + cur;
}
[].reduce(add) // TypeError: Reduce of empty array with no initial value
[].reduce(add, 1) // 1

上面代碼中,由於空數組取不到初始值,reduce方法會報錯。這時,加上第二個參數,就能保證總是會返回一個值

function subtract(prev, cur) {
  return prev - cur;
}
[3, 2, 1].reduce(subtract) // 0
[3, 2, 1].reduceRight(subtract) // -4

由於這兩個方法會遍歷數組,所以實際上還可以用來做一些遍歷相關的操作。比如,找出字符長度最長的數組成員

function findLongest(entries) {
  return entries.reduce(function (longest, entry) {
    return entry.length > longest.length ? entry : longest;
  }, '');
}
findLongest(['aaa', 'bb', 'c']) // "aaa"

上面代碼中,reduce的參數函數會將字符長度較長的那個數組成員,作爲累積值。這導致遍歷所有成員之後,累積值就是字符長度最長的那個成員

indexOf(),lastIndexOf()

indexOf方法返回給定元素在數組中第一次出現的位置,如果沒有出現則返回-1

var a = ['a', 'b', 'c'];
a.indexOf('b') // 1
a.indexOf('y') // -1

indexOf方法還可以接受第二個參數,表示搜索的開始位置

['a', 'b', 'c'].indexOf('a', 1) // -1

上面代碼從1號位置開始搜索字符a,結果爲-1,表示沒有搜索到

lastIndexOf方法返回給定元素在數組中最後一次出現的位置,如果沒有出現則返回-1

var a = [2, 5, 9, 2];
a.lastIndexOf(2) // 3
a.lastIndexOf(7) // -1

注意,這兩個方法不能用來搜索NaN的位置,即它們無法確定數組成員是否包含NaN

[NaN].indexOf(NaN) // -1
[NaN].lastIndexOf(NaN) // -1

這是因爲這兩個方法內部,使用嚴格相等運算符(===)進行比較,而NaN是唯一一個不等於自身的值

鏈式使用

上面這些數組方法之中,有不少返回的還是數組,所以可以鏈式使用

var users = [
  {name: 'tom', email: '[email protected]'},
  {name: 'peter', email: '[email protected]'}
];
users.map(function (user) {
  return user.email;
}).filter(function (email) {
  return /^t/.test(email);
})
.forEach(function (email) {
  console.log(email);
}); // "[email protected]"

上面代碼中,先產生一個所有 Email 地址組成的數組,然後再過濾出以t開頭的 Email 地址,最後將它打印出來

包裝對象

定義

對象是 JavaScript 語言最主要的數據類型,三種原始類型的值——數值、字符串、布爾值——在一定條件下,也會自動轉爲對象,也就是原始類型的“包裝對象”。所謂“包裝對象”,就是分別與數值、字符串、布爾值相對應的Number、String、Boolean三個原生對象。這三個原生對象可以把原始類型的值變成(包裝成)對象

var v1 = new Number(123);
var v2 = new String('abc');
var v3 = new Boolean(true);
typeof v1 // "object"
typeof v2 // "object"
typeof v3 // "object"
v1 === 123 // false
v2 === 'abc' // false
v3 === true // false

包裝對象的最大目的,首先是使得 JavaScript 的對象涵蓋所有的值,其次使得原始類型的值可以方便地調用某些方法。

Number、String和Boolean如果不作爲構造函數調用(即調用時不加new),常常用於將任意類型的值轉爲數值、字符串和布爾值。即這三個對象作爲構造函數使用(帶有new)時,可以將原始類型的值轉爲對象;作爲普通函數使用時(不帶有new),可以將任意類型的值,轉爲原始類型的值

實例方法

三種包裝對象各自提供了許多實例方法,這裏介紹兩種它們共同具有、從Object對象繼承的方法:valueOf和toString

valueOf()

valueOf方法返回包裝對象實例對應的原始類型的值

new Number(123).valueOf()  // 123
new String('abc').valueOf() // "abc"
new Boolean(true).valueOf() // true

toString()

toString方法返回對應的字符串形式

new Number(123).toString() // "123"
new String('abc').toString() // "abc"
new Boolean(true).toString() // "true"

原始類型與實例對象的自動轉換

原始類型的值,可以自動當作包裝對象調用,即調用包裝對象的屬性和方法。這時,JavaScript 引擎會自動將原始類型的值轉爲包裝對象實例,在使用後立刻銷燬實例。比如,字符串可以調用length屬性,返回字符串的長度

var str = 'abc';
str.length // 3
// 等同於
var strObj = new String(str)
// String { 0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc" }
strObj.length // 3

上面代碼中,abc是一個字符串,本身不是對象,不能調用length屬性。JavaScript 引擎自動將其轉爲包裝對象,在這個對象上調用length屬性。調用結束後,這個臨時對象就會被銷燬。這就叫原始類型與實例對象的自動轉換

自動轉換生成的包裝對象是隻讀的,無法修改。所以,字符串無法添加新屬性

var s = 'Hello World';
s.x = 123;
s.x // undefined

另一方面,調用結束後,包裝對象實例會自動銷燬。這意味着,下一次調用字符串的屬性時,實際是調用一個新生成的對象,而不是上一次調用時生成的那個對象,所以取不到賦值在上一個對象的屬性。如果要爲字符串添加屬性,只有在它的原型對象String.prototype上定義

自定義方法

除了原生的實例方法,包裝對象還可以自定義方法和屬性供原始類型的值直接調用。比如,我們可以新增一個double方法,使得字符串和數字翻倍

String.prototype.double = function () {
  return this.valueOf() + this.valueOf();
};
'abc'.double() // abcabc
Number.prototype.double = function () {
  return this.valueOf() + this.valueOf();
};
(123).double() // 246

上面代碼在123外面必須要加上圓括號,否則後面的點運算符(.)會被解釋成小數點。但是,這種自定義方法和屬性的機制,只能定義在包裝對象的原型上,如果直接對原始類型的變量添加屬性,則無效

Boolean 對象

概述

Boolean對象是 JavaScript 的三個包裝對象之一。作爲構造函數,它主要用於生成布爾值的包裝對象實例

注意,false對應的包裝對象實例,布爾運算結果也是true

if (new Boolean(false)) {
  console.log('true');
} // true
if (new Boolean(false).valueOf()) {
  console.log('true');
} // 無輸出

上面代碼的第一個例子之所以得到true,是因爲false對應的包裝對象實例是一個對象,進行邏輯運算時,被自動轉化成布爾值true(因爲所有對象對應的布爾值都是true)。而實例的valueOf方法,則返回實例對應的原始值,本例爲false

Boolean 函數的類型轉換作用

Boolean對象除了可以作爲構造函數,還可以單獨使用,將任意值轉爲布爾值。這時Boolean就是一個單純的工具方法

Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean('') // false
Boolean(NaN) // false
Boolean(1) // true
Boolean('false') // true
Boolean([]) // true
Boolean({}) // true
Boolean(function () {}) // true
Boolean(/foo/) // true

使用雙重的否運算符(!)也可以將任意值轉爲對應的布爾值

!!undefined // false
!!null // false
!!0 // false
!!'' // false
!!NaN // false
!!1 // true
!!'false' // true
!![] // true
!!{} // true
!!function(){} // true
!!/foo/ // true

對於一些特殊值,Boolean對象前面加不加new,會得到完全相反的結果,必須小心

if (Boolean(false)) {
  console.log('true');
} // 無輸出
if (new Boolean(false)) {
  console.log('true');
} // true
if (Boolean(null)) {
  console.log('true');
} // 無輸出
if (new Boolean(null)) {
  console.log('true');
} // true

Number 對象

概述

Number對象是數值對應的包裝對象,可以作爲構造函數使用,也可以作爲工具函數使用。作爲構造函數時,它用於生成值爲數值的對象

var n = new Number(1);
typeof n // "object"

作爲工具函數時,它可以將任何類型的值轉爲數值

Number(true) // 1

靜態屬性

Number對象擁有以下一些靜態屬性(即直接定義在Number對象上的屬性,而不是定義在實例上的屬性)。

//Number.POSITIVE_INFINITY:正的無限,指向Infinity
Number.POSITIVE_INFINITY // Infinity
//Number.NEGATIVE_INFINITY:負的無限,指向-Infinity
Number.NEGATIVE_INFINITY // -Infinity
//Number.NaN:表示非數值,指向NaN
Number.NaN // NaN
Number.MAX_VALUE // 1.7976931348623157e+308
Number.MAX_VALUE < Infinity // true
//Number.MIN_VALUE:表示最小的正數(即最接近0的正數,在64位浮點數體系中爲5e-324),相應的,最接近0的負數爲-Number.MIN_VALUE
Number.MIN_VALUE // 5e-324
Number.MIN_VALUE > 0 // true
//Number.MAX_SAFE_INTEGER:表示能夠精確表示的最大整數,即9007199254740991
Number.MAX_SAFE_INTEGER // 9007199254740991
//Number.MIN_SAFE_INTEGER:表示能夠精確表示的最小整數,即-9007199254740991
Number.MIN_SAFE_INTEGER // -9007199254740991

實例方法

Number對象有4個實例方法,都跟將數值轉換成指定格式有關

Number.prototype.toString()

Number對象部署了自己的toString方法,用來將一個數值轉爲字符串形式。toString方法可以接受一個參數,表示輸出的進制。如果省略這個參數,默認將數值先轉爲十進制,再輸出字符串;否則,就根據參數指定的進制,將一個數字轉化成某個進制的字符串

(10).toString() // "10"
(10).toString(2) // "1010"
(10).toString(8) // "12"
(10).toString(16) // "a"

只要能夠讓 JavaScript 引擎不混淆小數點和對象的點運算符,各種寫法都能用。除了爲10加上括號,還可以在10後面加兩個點,JavaScript 會把第一個點理解成小數點(即10.0),把第二個點理解成調用對象屬性,從而得到正確結果

10..toString(2) // "1010"
// 其他方法還包括
10 .toString(2) // "1010"
10.0.toString(2) // "1010"

這實際上意味着,可以直接對一個小數使用toString方法。通過方括號運算符也可以調用toString方法

10['toString'](2) // "1010"

toString方法只能將十進制的數,轉爲其他進制的字符串。如果要將其他進制的數,轉回十進制,需要使用parseInt方法

Number.prototype.toFixed()

toFixed方法先將一個數轉爲指定位數的小數,然後返回這個小數對應的字符串

(10).toFixed(2) // "10.00"
10.005.toFixed(2) // "10.01"

toFixed方法的參數爲小數位數,有效範圍爲0到20,超出這個範圍將拋出 RangeError 錯誤

Number.prototype.toExponential()

toExponential方法用於將一個數轉爲科學計數法形式

(10).toExponential()  // "1e+1"
(10).toExponential(1) // "1.0e+1"
(10).toExponential(2) // "1.00e+1"
(1234).toExponential()  // "1.234e+3"
(1234).toExponential(1) // "1.2e+3"
(1234).toExponential(2) // "1.23e+3"

toExponential方法的參數是小數點後有效數字的位數,範圍爲0到20,超出這個範圍,會拋出一個 RangeError 錯誤

Number.prototype.toPrecision()

toPrecision方法用於將一個數轉爲指定位數的有效數字

(12.35).toPrecision(1) // "1e+1"
(12.35).toPrecision(2) // "12"
(12.35).toPrecision(3) // "12.3"
(12.25).toPrecision(3) // "12.3"
(12.35).toPrecision(4) // "12.35"
(12.35).toPrecision(5) // "12.350"

toPrecision方法的參數爲有效數字的位數,範圍是1到21,超出這個範圍會拋出 RangeError 錯誤。toPrecision方法用於四捨五入時不太可靠,跟浮點數不是精確儲存有關

自定義方法

與其他對象一樣,Number.prototype對象上面可以自定義方法,被Number的實例繼承。由於add方法返回的還是數值,所以可以鏈式運算

Number.prototype.add = function (x) {
  return this + x;
};
Number.prototype.subtract = function (x) {
  return this - x;
};
(8).add(2).subtract(4) // 6

我們還可以部署更復雜的方法

Number.prototype.iterate = function () {
  var result = [];
  for (var i = 0; i <= this; i++) {
    result.push(i);
  }
  return result;
};
(8).iterate() // [0, 1, 2, 3, 4, 5, 6, 7, 8]

注意,數值的自定義方法,只能定義在它的原型對象Number.prototype上面,數值本身是無法自定義屬性的

var n = 1;
n.x = 1;
n.x // undefined

上面代碼中,n是一個原始類型的數值。直接在它上面新增一個屬性x,不會報錯,但毫無作用,總是返回undefined。這是因爲一旦被調用屬性,n就自動轉爲Number的實例對象,調用結束後,該對象自動銷燬。所以,下一次調用n的屬性時,實際取到的是另一個對象,屬性x當然就讀不出來

String 對象

概述

String對象是 JavaScript 原生提供的三個包裝對象之一,用來生成字符串對象

var s1 = 'abc';
var s2 = new String('abc');
typeof s1 // "string"
typeof s2 // "object"
s2.valueOf() // "abc"

字符串對象是一個類似數組的對象(很像數組,但不是數組)

new String('abc') // String {0: "a", 1: "b", 2: "c", length: 3}
(new String('abc'))[1] // "b"

除了用作構造函數,String對象還可以當作工具方法使用,將任意類型的值轉爲字符串

靜態方法

String.fromCharCode()

String對象提供的靜態方法(即定義在對象本身,而不是定義在對象實例的方法),主要是String.fromCharCode()。該方法的參數是一個或多個數值,代表 Unicode 碼點,返回值是這些碼點組成的字符串

String.fromCharCode() // ""
String.fromCharCode(97) // "a"
String.fromCharCode(104, 101, 108, 108, 111) // "hello"

String.fromCharCode方法的參數爲空,就返回空字符串;否則,返回參數對應的 Unicode 字符串。

注意,該方法不支持 Unicode 碼點大於0xFFFF的字符,即傳入的參數不能大於0xFFFF(即十進制的 65535)

String.fromCharCode(0x20BB7) // "ஷ"
String.fromCharCode(0x20BB7) === String.fromCharCode(0x0BB7) // true

上面代碼中,String.fromCharCode參數0x20BB7大於0xFFFF,導致返回結果出錯。0x20BB7對應的字符是漢字𠮷,但是返回結果卻是另一個字符(碼點0x0BB7)。這是因爲String.fromCharCode發現參數值大於0xFFFF,就會忽略多出的位(即忽略0x20BB7裏面的2)。這種現象的根本原因在於,碼點大於0xFFFF的字符佔用四個字節,而 JavaScript 默認支持兩個字節的字符。這種情況下,必須把0x20BB7拆成兩個字符表示

String.fromCharCode(0xD842, 0xDFB7) // "𠮷"

實例屬性

String.prototype.length

字符串實例的length屬性返回字符串的長度

'abc'.length // 3

實例方法

String.prototype.charAt()

charAt方法返回指定位置的字符,參數是從0開始編號的位置;這個方法也完全可以用數組下標替代;如果參數爲負數,或大於等於字符串的長度,charAt返回空字符串

var s = new String('abc');
s.charAt(1) // "b"
s.charAt(s.length - 1) // "c"
'abc'[1] // "b"
'abc'.charAt(-1) // ""

String.prototype.charCodeAt()

charCodeAt方法返回字符串指定位置的 Unicode 碼點(十進制表示),相當於String.fromCharCode()的逆操作;如果沒有任何參數,charCodeAt返回首字符的 Unicode 碼點;如果參數爲負數,或大於等於字符串的長度,charCodeAt返回NaN

'abc'.charCodeAt(1) // 98
'abc'.charCodeAt() // 97
'abc'.charCodeAt(-1) // NaN

注意,charCodeAt方法返回的 Unicode 碼點不會大於65536(0xFFFF),也就是說,只返回兩個字節的字符的碼點。如果遇到碼點大於 65536 的字符(四個字節的字符),必需連續使用兩次charCodeAt,不僅讀入charCodeAt(i),還要讀入charCodeAt(i+1),將兩個值放在一起,才能得到準確的字符

String.prototype.concat()

concat方法用於連接兩個字符串,返回一個新字符串,不改變原字符串;該方法可以接受多個參數

var s1 = 'abc';
var s2 = 'def';
s1.concat(s2) // "abcdef"
s1 // "abc"
'a'.concat('b', 'c') // "abc"

如果參數不是字符串,concat方法會將其先轉爲字符串,然後再連接

var one = 1;
var two = 2;
var three = '3';
''.concat(one, two, three) // "123"
one + two + three // "33"

String.prototype.slice()

slice方法用於從原字符串取出子字符串並返回,不改變原字符串。它的第一個參數是子字符串的開始位置,第二個參數是子字符串的結束位置(不含該位置);如果省略第二個參數,則表示子字符串一直到原字符串結束;如果參數是負值,表示從結尾開始倒數計算的位置,即該負值加上字符串長度;如果第一個參數大於第二個參數,slice方法返回一個空字符串

'JavaScript'.slice(0, 4) // "Java"
'JavaScript'.slice(4) // "Script"
'JavaScript'.slice(-6) // "Script"
'JavaScript'.slice(-2, -1) // "p"
'JavaScript'.slice(2, 1) // ""

String.prototype.substring()

substring方法用於從原字符串取出子字符串並返回,不改變原字符串,跟slice方法很相像。它的第一個參數表示子字符串的開始位置,第二個位置表示結束位置(返回結果不含該位置);如果省略第二個參數,則表示子字符串一直到原字符串的結束;如果第一個參數大於第二個參數,substring方法會自動更換兩個參數的位置;如果參數是負數,substring方法會自動將負數轉爲0

'JavaScript'.substring(0, 4) // "Java"
'JavaScript'.substring(4) // "Script"
'JavaScript'.substring(10, 4) // "Script"
// 等同於
'JavaScript'.substring(4, 10) // "Script"
'JavaScript'.substring(4, -3) // "Java"

由於substring某些規則違反直覺,因此不建議使用substring方法,應該優先使用slice

String.prototype.substr()

substr方法用於從原字符串取出子字符串並返回,不改變原字符串,跟slice和substring方法的作用相同。substr方法的第一個參數是子字符串的開始位置(從0開始計算),第二個參數是子字符串的長度;如果省略第二個參數,則表示子字符串一直到原字符串的結束;如果第一個參數是負數,表示倒數計算的字符位置。如果第二個參數是負數,將被自動轉爲0,因此會返回空字符串

'JavaScript'.substr(4, 6) // "Script"
'JavaScript'.substr(4) // "Script"
'JavaScript'.substr(-6) // "Script"
'JavaScript'.substr(4, -1) // ""

String.prototype.indexOf(),String.prototype.lastIndexOf()

indexOf方法用於確定一個字符串在另一個字符串中第一次出現的位置,返回結果是匹配開始的位置。如果返回-1,就表示不匹配;indexOf方法還可以接受第二個參數,表示從該位置開始向後匹配

'hello world'.indexOf('o') // 4
'JavaScript'.indexOf('script') // -1
'hello world'.indexOf('o', 6) // 7

lastIndexOf方法的用法跟indexOf方法一致,主要的區別是lastIndexOf從尾部開始匹配,indexOf則是從頭部開始匹配;另外,lastIndexOf的第二個參數表示從該位置起向前匹配

'hello world'.lastIndexOf('o') // 7
'hello world'.lastIndexOf('o', 6) // 4

String.prototype.trim()

trim方法用於去除字符串兩端的空格,返回一個新字符串,不改變原字符串;該方法去除的不僅是空格,還包括製表符(t、v)、換行符(n)和回車符(r)

'  hello world  '.trim() // "hello world"
'\r\nabc \t'.trim() // 'abc'

String.prototype.toLowerCase(),String.prototype.toUpperCase()

toLowerCase方法用於將一個字符串全部轉爲小寫,toUpperCase則是全部轉爲大寫。它們都返回一個新字符串,不改變原字符串

'Hello World'.toLowerCase() // "hello world"
'Hello World'.toUpperCase() // "HELLO WORLD"

String.prototype.match()

match方法用於確定原字符串是否匹配某個子字符串,返回一個數組,成員爲匹配的第一個字符串。如果沒有找到匹配,則返回null;返回的數組還有index屬性和input屬性,分別表示匹配字符串開始的位置和原始字符串

var matches = 'cat, bat, sat, fat'.match('at');
'cat, bat, sat, fat'.match('at') // ["at"]
'cat, bat, sat, fat'.match('xt') // null
matches.index // 1
matches.input // "cat, bat, sat, fat"

String.prototype.search(),String.prototype.replace()

search方法的用法基本等同於match,但是返回值爲匹配的第一個位置。如果沒有找到匹配,則返回-1;search方法還可以使用正則表達式作爲參數

'cat, bat, sat, fat'.search('at') // 1

replace方法用於替換匹配的子字符串,一般情況下只替換第一個匹配(除非使用帶有g修飾符的正則表達式);replace方法還可以使用正則表達式作爲參數

'aaa'.replace('a', 'b') // "baa"

String.prototype.split()

split方法按照給定規則分割字符串,返回一個由分割出來的子字符串組成的數組;如果分割規則爲空字符串,則返回數組的成員是原字符串的每一個字符;如果省略參數,則返回數組的唯一成員就是原字符串;如果滿足分割規則的兩個部分緊鄰着(即兩個分割符中間沒有其他字符),則返回數組之中會有一個空字符串;如果滿足分割規則的部分處於字符串的開頭或結尾(即它的前面或後面沒有其他字符),則返回數組的第一個或最後一個成員是一個空字符串;split方法還可以接受第二個參數,限定返回數組的最大成員數;split方法還可以使用正則表達式作爲參數

'a|b|c'.split('|') // ["a", "b", "c"]
'a|b|c'.split('') // ["a", "|", "b", "|", "c"]
'a|b|c'.split() // ["a|b|c"]
'a||c'.split('|') // ['a', '', 'c']
'|b|c'.split('|') // ["", "b", "c"]
'a|b|'.split('|') // ["a", "b", ""]
'a|b|c'.split('|', 2) // ["a", "b"]
'a|b|c'.split('|', 3) // ["a", "b", "c"]
'a|b|c'.split('|', 4) // ["a", "b", "c"]

String.prototype.localeCompare()

localeCompare方法用於比較兩個字符串。它返回一個整數,如果小於0,表示第一個字符串小於第二個字符串;如果等於0,表示兩者相等;如果大於0,表示第一個字符串大於第二個字符串;該方法的最大特點,就是會考慮自然語言的順序。舉例來說,正常情況下,大寫的英文字母小於小寫字母

'apple'.localeCompare('banana') // -1
'apple'.localeCompare('apple') // 0
'B' > 'a' // false

JavaScript 採用的是 Unicode 碼點比較,B的碼點是66,而a的碼點是97。但是,localeCompare方法會考慮自然語言的排序情況,將B排在a的前面。

'B'.localeCompare('a') // 1

上面代碼中,localeCompare方法返回整數1,表示B較大。localeCompare還可以有第二個參數,指定所使用的語言(默認是英語),然後根據該語言的規則進行比較

'ä'.localeCompare('z', 'de') // -1
'ä'.localeCompare('z', 'sv') // 1

上面代碼中,de表示德語,sv表示瑞典語。德語中,ä小於z,所以返回-1;瑞典語中,ä大於z,所以返回1

Math 對象

Math是 JavaScript 的原生對象,提供各種數學功能。該對象不是構造函數,不能生成實例,所有的屬性和方法都必須在Math對象上調用

靜態屬性

Math對象的靜態屬性,提供以下一些數學常數。

//Math.E:常數e
Math.E // 2.718281828459045
//Math.LN2:2 的自然對數
Math.LN2 // 0.6931471805599453
//Math.LN10:10 的自然對數
Math.LN10 // 2.302585092994046
//Math.LOG2E:以 2 爲底的e的對數
Math.LOG2E // 1.4426950408889634
//Math.LOG10E:以 10 爲底的e的對數
Math.LOG10E // 0.4342944819032518
//Math.PI:常數π
Math.PI // 3.141592653589793
//Math.SQRT1_2:0.5 的平方根
Math.SQRT1_2 // 0.7071067811865476
//Math.SQRT2:2 的平方根
Math.SQRT2 // 1.4142135623730951

這些屬性都是隻讀的,不能修改

靜態方法

Math對象提供以下一些靜態方法。

Math.abs():絕對值
Math.ceil():向上取整
Math.floor():向下取整
Math.max():最大值
Math.min():最小值
Math.pow():指數運算
Math.sqrt():平方根
Math.log():自然對數
Math.exp():e的指數
Math.round():四捨五入
Math.random():隨機數

Math.abs()

Math.abs方法返回參數值的絕對值

Math.max(),Math.min()

Math.max方法返回參數之中最大的那個值,Math.min返回最小的那個值。如果參數爲空, Math.min返回Infinity, Math.max返回-Infinity

Math.max(2, -1, 5) // 5
Math.min(2, -1, 5) // -1
Math.min() // Infinity
Math.max() // -Infinity

Math.floor(),Math.ceil()

Math.floor方法返回小於參數值的最大整數(地板值);Math.ceil方法返回大於參數值的最小整數(天花板值)

Math.floor(3.2) // 3
Math.floor(-3.2) // -4
Math.ceil(3.2) // 4
Math.ceil(-3.2) // -3

這兩個方法可以結合起來,實現一個總是返回數值的整數部分的函數

function ToInteger(x) {
  x = Number(x);
  return x < 0 ? Math.ceil(x) : Math.floor(x);
}
ToInteger(3.2) // 3
ToInteger(3.5) // 3
ToInteger(3.8) // 3
ToInteger(-3.2) // -3
ToInteger(-3.5) // -3
ToInteger(-3.8) // -3

Math.round()

Math.round方法用於四捨五入;要注意它對負數的處理(主要是對0.5的處理)

Math.round(0.1) // 0
Math.round(0.5) // 1
Math.round(0.6) // 1
Math.round(-1.1) // -1
Math.round(-1.5) // -1
Math.round(-1.6) // -2

Math.pow()

Math.pow方法返回以第一個參數爲底數、第二個參數爲冪的指數值

Math.pow(2, 3) // 8
var r = 20;
var area = Math.PI * Math.pow(r, 2) //計算圓面積

Math.sqrt()

Math.sqrt方法返回參數值的平方根。如果參數是一個負值,則返回NaN

Math.sqrt(4) // 2
Math.sqrt(-4) // NaN

Math.log()

Math.log方法返回以e爲底的自然對數值。如果要計算以10爲底的對數,可以先用Math.log求出自然對數,然後除以Math.LN10;求以2爲底的對數,可以除以Math.LN2

Math.log(Math.E) // 1
Math.log(10) // 2.302585092994046
Math.log(100)/Math.LN10 // 2
Math.log(8)/Math.LN2 // 3

Math.exp()

Math.exp方法返回常數e的參數次方

Math.exp(1) // 2.718281828459045
Math.exp(3) // 20.085536923187668

Math.random()

Math.random()返回0到1之間的一個僞隨機數,可能等於0,但是一定小於1

//任意範圍的隨機數生成函數
function getRandomArbitrary(min, max) {
  return Math.random() * (max - min) + min;
}
getRandomArbitrary(1.5, 6.5) // 2.4942810038223864
//任意範圍的隨機整數生成函數
function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}
getRandomInt(1, 6) // 5

三角函數方法

Math對象還提供一系列三角函數方法

//Math.sin():返回參數的正弦(參數爲弧度值)
Math.sin(0) // 0
Math.sin(Math.PI / 2) // 1
//Math.cos():返回參數的餘弦(參數爲弧度值)
Math.cos(0) // 1
//Math.tan():返回參數的正切(參數爲弧度值)
Math.tan(0) // 0
//Math.asin():返回參數的反正弦(返回值爲弧度值)
Math.asin(1) // 1.5707963267948966
//Math.acos():返回參數的反餘弦(返回值爲弧度值)
Math.acos(1) // 0
//Math.atan():返回參數的反正切(返回值爲弧度值)
Math.atan(1) // 0.7853981633974483

Date 對象

Date對象是 JavaScript 原生的時間庫。它以國際標準時間(UTC)1970年1月1日00:00:00作爲時間的零點,可以表示的時間範圍是前後各1億天(單位爲毫秒)

普通函數的用法

Date對象可以作爲普通函數直接調用,返回一個代表當前時間的字符串

注意,即使帶有參數,Date作爲普通函數使用時,返回的還是當前時間

Date() // "Tue Mar 12 2019 13:53:36 GMT+0800 (中國標準時間)"
Date(2000, 1, 1) // "Tue Mar 12 2019 13:53:36 GMT+0800 (中國標準時間)"

構造函數的用法

Date還可以當作構造函數使用。對它使用new命令,會返回一個Date對象的實例。如果不加參數,實例代表的就是當前時間

var today = new Date();

Date實例有一個獨特的地方。其他對象求值的時候,都是默認調用.valueOf()方法,但是Date實例求值的時候,默認調用的是toString()方法。這導致對Date實例求值,返回的是一個字符串,代表該實例對應的時間

var today = new Date();
today // "Tue Mar 12 2019 13:56:06 GMT+0800 (中國標準時間)"
// 等同於
today.toString() // "Tue Mar 12 2019 13:56:06 GMT+0800 (中國標準時間)"

上面代碼中,today是Date的實例,直接求值等同於調用toString方法。作爲構造函數時,Date對象可以接受多種格式的參數,返回一個該參數對應的時間實例

// 參數爲時間零點開始計算的毫秒數
new Date(1546704000000) // Sun Jan 06 2019 00:00:00 GMT+0800 (中國標準時間)
// 參數爲日期字符串
new Date('January 6, 2019'); // Sun Jan 06 2019 00:00:00 GMT+0800 (中國標準時間)
// 參數爲多個整數,代表年、月、日、小時、分鐘、秒、毫秒
new Date(2019, 0, 1, 0, 0, 0, 0) // Tue Jan 01 2019 00:00:00 GMT+0800 (中國標準時間)

關於Date構造函數的參數,有幾點說明:

1.參數可以是負整數,代表1970年元旦之前的時間

new Date(-1378218728000) // Fri Apr 30 1926 17:27:52 GMT+0800 (中國標準時間)

2.只要是能被Date.parse()方法解析的字符串,都可以當作參數

new Date('2019-2-15')
new Date('2019/2/15')
new Date('02/15/2019')
new Date('2019-FEB-15')
new Date('FEB, 15, 2019')
new Date('FEB 15, 2019')
new Date('February, 15, 2019')
new Date('February 15, 2019')
new Date('15 Feb 2019')
new Date('15, February, 2019') // Fri Feb 15 2019 00:00:00 GMT+0800 (中國標準時間)

上面多種日期字符串的寫法,返回的都是同一個時間

3.參數爲年、月、日等多個整數時,年和月是不能省略的,其他參數都可以省略的。也就是說,這時至少需要兩個參數,因爲如果只使用“年”這一個參數,Date會將其解釋爲毫秒數
2.只要是能被Date.parse()方法解析的字符串,都可以當作參數

new Date(2019) //Thu Jan 01 1970 08:00:02 GMT+0800 (中國標準時間)
new Date(2019, 0) // Tue Jan 01 2019 00:00:00 GMT+0800 (中國標準時間)
new Date(2019, 0, 1) // Tue Jan 01 2019 00:00:00 GMT+0800 (中國標準時間)
new Date(2019, 0, 1, 0) // Tue Jan 01 2019 00:00:00 GMT+0800 (中國標準時間)
new Date(2019, 0, 1, 0, 0, 0, 0) // Tue Jan 01 2019 00:00:00 GMT+0800 (中國標準時間)

上面代碼中,不管有幾個參數,返回的都是2019年1月1日零點

各個參數的取值範圍如下:

1.年:使用四位數年份,比如2000。如果寫成兩位數或個位數,則加上1900,即10代表1910年。如果是負數,表示公元前。

2.月:0表示一月,依次類推,11表示12月。

3.日:1到31。

4.小時:0到23。

5.分鐘:0到59。

6.秒:0到59

7.毫秒:0到999

注意:月份從0開始計算,天數從1開始計算。另外,除了日期的默認值爲1,小時、分鐘、秒鐘和毫秒的默認值都是0。這些參數如果超出了正常範圍,會被自動折算。比如,如果月設爲15,就折算爲下一年的4月;日期設爲0,就代表上個月的最後一天;參數還可以使用負數,表示扣去的時間

new Date(2019, -1) // Sat Dec 01 2018 00:00:00 GMT+0800 (中國標準時間)
new Date(2019, 0, -1) // Sun Dec 30 2018 00:00:00 GMT+0800 (中國標準時間)

日期的運算

類型自動轉換時,Date實例如果轉爲數值,則等於對應的毫秒數;如果轉爲字符串,則等於對應的日期字符串。所以,兩個日期實例對象進行減法運算時,返回的是它們間隔的毫秒數;進行加法運算時,返回的是兩個字符串連接而成的新字符串

var d1 = new Date(2000, 2, 1);
var d2 = new Date(2000, 3, 1);
d2 - d1 // 2678400000
d2 + d1 // "Sat Apr 01 2000 00:00:00 GMT+0800 (CST)Wed Mar 01 2000 00:00:00 GMT+0800 (CST)"

靜態方法

Date.now()

Date.now方法返回當前時間距離時間零點(1970年1月1日 00:00:00 UTC)的毫秒數,相當於 Unix 時間戳乘以1000

Date.now() // 1364026285194

Date.parse()

Date.parse方法用來解析日期字符串,返回該時間距離時間零點(1970年1月1日 00:00:00)的毫秒數。日期字符串應該符合 RFC 2822 和 ISO 8061 這兩個標準,即YYYY-MM-DDTHH:mm:ss.sssZ格式,其中最後的Z表示時區。但是,其他格式也可以被解析

Date.parse('Aug 9, 1995')
Date.parse('January 26, 2011 13:51:50')
Date.parse('Mon, 25 Dec 1995 13:30:00 GMT')
Date.parse('Mon, 25 Dec 1995 13:30:00 +0430')
Date.parse('2011-10-10')
Date.parse('2011-10-10T14:48:00')

上面的日期字符串都可以解析。如果解析失敗,返回NaN

Date.UTC()

Date.UTC方法接受年、月、日等變量作爲參數,返回該時間距離時間零點(1970年1月1日 00:00:00 UTC)的毫秒數

// 格式
Date.UTC(year, month[, date[, hrs[, min[, sec[, ms]]]]])
// 用法
Date.UTC(2011, 0, 1, 2, 3, 4, 567) // 1293847384567

該方法的參數用法與Date構造函數完全一致,比如月從0開始計算,日期從1開始計算。區別在於Date.UTC方法的參數,會被解釋爲 UTC 時間(世界標準時間),Date構造函數的參數會被解釋爲當前時區的時間

實例方法

Date的實例對象,有幾十個自己的方法,除了valueOf和toString,可以分爲以下三類

to類:從Date對象返回一個字符串,表示指定的時間。
get類:獲取Date對象的日期和時間。
set類:設置Date對象的日期和時間

Date.prototype.valueOf()

valueOf方法返回實例對象距離時間零點(1970年1月1日00:00:00 UTC)對應的毫秒數,該方法等同於getTime方法

var d = new Date();
d.valueOf() // 1552374623595
d.getTime() // 1552374623595

預期爲數值的場合,Date實例會自動調用該方法,所以可以用該方法計算時間的間隔

to 類方法

Date.prototype.toString()

toString方法返回一個完整的日期字符串。toString是默認的調用方法,所以如果直接讀取Date實例,就相當於調用這個方法

var d = new Date(2019, 0, 1);
d.toString() // "Tue Jan 01 2019 00:00:00 GMT+0800 (中國標準時間)"
d // "Tue Jan 01 2019 00:00:00 GMT+0800 (中國標準時間)"
Date.prototype.toUTCString()

toUTCString方法返回對應的 UTC 時間,也就是比北京時間晚8個小時

var d = new Date(2019, 0, 1);
d.toUTCString() // "Mon, 31 Dec 2018 16:00:00 GMT"
Date.prototype.toISOString()

toISOString方法返回對應時間的 ISO8601 寫法

var d = new Date(2019, 0, 1);
d.toISOString() // "2018-12-31T16:00:00.000Z"

注意,toISOString方法返回的總是 UTC 時區的時間

Date.prototype.toJSON()

toJSON方法返回一個符合 JSON 格式的 ISO 日期字符串,與toISOString方法的返回結果完全相同

var d = new Date(2019, 0, 1);
d.toJSON() // "2018-12-31T16:00:00.000Z"
Date.prototype.toDateString()

toDateString方法返回日期字符串(不含小時、分和秒)

var d = new Date(2019, 0, 1);
d.toDateString() // "Tue Jan 01 2019"
Date.prototype.toTimeString()

toTimeString方法返回時間字符串(不含年月日)

var d = new Date(2019, 0, 1);
d.toTimeString() // "00:00:00 GMT+0800 (中國標準時間)"
本地時間

以下三種方法,可以將 Date 實例轉爲表示本地時間的字符串。

Date.prototype.toLocaleString():完整的本地時間。

Date.prototype.toLocaleDateString():本地日期(不含小時、分和秒)。

Date.prototype.toLocaleTimeString():本地時間(不含年月日)。

這三個方法都有兩個可選的參數

dateObj.toLocaleString([locales[, options]])
dateObj.toLocaleDateString([locales[, options]])
dateObj.toLocaleTimeString([locales[, options]])

這兩個參數中,locales是一個指定所用語言的字符串,options是一個配置對象

var d = new Date(2019, 0, 1);
d.toLocaleString('en-US') // "1/1/2019, 12:00:00 AM"
d.toLocaleString('zh-CN') // "2019/1/1 上午12:00:00"
d.toLocaleDateString('en-US') // "1/1/2019"
d.toLocaleDateString('zh-CN') // "2019/1/1"
d.toLocaleTimeString('en-US') // "12:00:00 AM"
d.toLocaleTimeString('zh-CN') // "上午12:00:00"

get 類方法

1.Date對象提供了一系列get*方法,用來獲取實例對象某個方面的值。

2.getTime():返回實例距離1970年1月1日00:00:00的毫秒數,等同於valueOf方法

3.getDate():返回實例對象對應每個月的幾號(從1開始)

4.getDay():返回星期幾,星期日爲0,星期一爲1,以此類推

5.getFullYear():返回四位的年份

6.getMonth():返回月份(0表示1月,11表示12月)

7.getHours():返回小時(0-23)

8.getMilliseconds():返回毫秒(0-999)

9.getMinutes():返回分鐘(0-59)

10.getSeconds():返回秒(0-59)

11.getTimezoneOffset():返回當前時間與 UTC 的時區差異,以分鐘表示,返回結果考慮到了夏令時因素

這些get*方法返回的都是整數,不同方法返回值的範圍不一樣。

分鐘和秒:0 到 59

小時:0 到 23

星期:0(星期天)到 6(星期六)

日期:1 到 31

月份:0(一月)到 11(十二月)

var d = new Date('January 6, 2019');
d.getDate() // 6
d.getMonth() // 0
d.getFullYear() // 2019
d.getTimezoneOffset() // -480

-480表示 UTC 比當前時間少480分鐘,即當前時區比 UTC 早8個小時

上面get*方法返回的都是當前時區的時間,Date對象還提供了這些方法對應的 UTC 版本,用來返回 UTC 時間。

getUTCDate()/getUTCFullYear()/getUTCMonth()/getUTCDay()/getUTCHours()/getUTCMinutes()/getUTCSeconds()/getUTCMilliseconds()

set 類方法

Date對象提供了一系列set*方法,用來設置實例對象的各個方面。

setDate(date):設置實例對象對應的每個月的幾號(1-31),返回改變後毫秒時間戳
setFullYear(year [, month, date]):設置四位年份
setHours(hour [, min, sec, ms]):設置小時(0-23)
setMilliseconds():設置毫秒(0-999)
setMinutes(min [, sec, ms]):設置分鐘(0-59)
setMonth(month [, date]):設置月份(0-11)
setSeconds(sec [, ms]):設置秒(0-59)
setTime(milliseconds):設置毫秒時間戳

這些方法基本是跟get*方法一一對應的,但是沒有setDay方法,因爲星期幾是計算出來的,而不是設置的。另外,需要注意的是,凡是涉及到設置月份,都是從0開始算的,即0是1月,11是12月

var d = new Date ('January 6, 2019');
d // Sun Jan 06 2019 00:00:00 GMT+0800 (中國標準時間)
d.setDate(9) // 1546963200000
d // Wed Jan 09 2019 00:00:00 GMT+0800 (中國標準時間)

set類方法和get類方法,可以結合使用,得到相對時間

var d = new Date();
d.setDate(d.getDate() + 1000); // 將日期向後推1000天
d.setHours(d.getHours() + 6); // 將時間設爲6小時後
d.setFullYear(d.getFullYear() - 1); // 將年份設爲去年

set*系列方法除了setTime(),都有對應的 UTC 版本,即設置 UTC 時區的時間

setUTCDate()/setUTCFullYear()/setUTCHours()/setUTCMilliseconds()/setUTCMinutes()/setUTCMonth()/setUTCSeconds()

RegExp 對象

RegExp對象提供正則表示式的功能

概述

正則表達式(regular expression)是一種表達文本模式(即字符串結構)的方法,有點像字符串的模板,常常用來按照“給定模式”匹配文本。比如,正則表達式給出一個 Email 地址的模式,然後用它來確定一個字符串是否爲 Email 地址。JavaScript 的正則表達式體系是參照 Perl 5 建立的。新建正則表達式有兩種方法

1.使用字面量,以斜槓表示開始和結束

var regex = /xyz/;

2.使用RegExp構造函數

var regex = new RegExp('xyz');

兩種寫法是等價的,都新建了一個內容爲xyz的正則表達式對象。它們的主要區別是,第一種方法在引擎編譯代碼時就會新建正則表達式,第二種方法在運行時新建正則表達式,所以前者的效率較高。而且,前者比較便利和直觀,所以實際應用中,基本上都採用字面量定義正則表達式。RegExp構造函數還可以接受第二個參數,表示修飾符

var regex = new RegExp('xyz', 'i');
// 等價於
var regex = /xyz/i;

實例屬性

正則對象的實例屬性分成兩類:

1.修飾符相關,返回一個布爾值,表示對應的修飾符是否設置

(1).RegExp.prototype.ignoreCase:返回一個布爾值,表示是否設置了i修飾符

(2).RegExp.prototype.global:返回一個布爾值,表示是否設置了g修飾符

(3).RegExp.prototype.multiline:返回一個布爾值,表示是否設置了m修飾符

上面三個屬性都是隻讀的

var r = /abc/igm;
r.ignoreCase // true
r.global // true
r.multiline // true

2.與修飾符無關的屬性

(1).RegExp.prototype.lastIndex:返回一個整數,表示下一次開始搜索的位置。該屬性可讀寫,但是隻在進行連續搜索時有意義

(2).RegExp.prototype.source:返回正則表達式的字符串形式(不包括反斜槓),該屬性只讀

var r = /abc/igm;
r.lastIndex // 0
r.source // "abc"

實例方法

RegExp.prototype.test()

正則實例對象的test方法返回一個布爾值,表示當前模式是否能匹配參數字符串

/cat/.test('cats and dogs') // true

如果正則表達式帶有g修飾符,則每一次test方法都從上一次結束的位置開始向後匹配

var r = /x/g;
var s = '_x_x';
r.lastIndex // 0
r.test(s) // true
r.lastIndex // 2
r.test(s) // true
r.lastIndex // 4
r.test(s) // false

帶有g修飾符時,可以通過正則對象的lastIndex屬性指定開始搜索的位置

var r = /x/g;
var s = '_x_x';
r.lastIndex = 4;
r.test(s) // false
r.lastIndex // 0
r.test(s) //true

上面代碼指定從字符串的第五個位置開始搜索,這個位置爲空,所以返回false。同時,lastIndex屬性重置爲0,所以第二次執行r.test(s)會返回true。

注意,帶有g修飾符時,正則表達式內部會記住上一次的lastIndex屬性,這時不應該更換所要匹配的字符串,否則會有一些難以察覺的錯誤

var r = /bb/g;
r.test('bb') // true
r.test('-bb-') // false

lastIndex屬性只對同一個正則表達式有效

var count = 0;
while (/a/g.test('babaa')) count++;

上面代碼會導致無限循環,因爲while循環的每次匹配條件都是一個新的正則表達式,導致lastIndex屬性總是等於0。如果正則模式是一個空字符串,則匹配所有字符串

new RegExp('').test('abc') // true

RegExp.prototype.exec()

正則實例對象的exec方法,用來返回匹配結果。如果發現匹配,就返回一個數組,成員是匹配成功的子字符串,否則返回null

var s = '_x_x';
var r1 = /x/;
var r2 = /y/;
r1.exec(s) // ["x"]
r2.exec(s) // null

如果正則表示式包含圓括號(即含有“組匹配”),則返回的數組會包括多個成員。第一個成員是整個匹配成功的結果,後面的成員就是圓括號對應的匹配成功的組。也就是說,第二個成員對應第一個括號,第三個成員對應第二個括號,以此類推。整個數組的length屬性等於組匹配的數量再加1

var s = '_x_x';
var r = /_(x)/;
r.exec(s) // ["_x", "x"]

exec方法的返回數組還包含以下兩個屬性:

1.input:整個原字符串

2.index:整個模式匹配成功的開始位置(從0開始計數)

var r = /a(b+)a/;
var arr = r.exec('_abbba_aba_');
arr // ["abbba", "bbb"]
arr.index // 1
arr.input // "_abbba_aba_"

上面代碼中的index屬性等於1,是因爲從原字符串的第二個位置開始匹配成功。如果正則表達式加上g修飾符,則可以使用多次exec方法,下一次搜索的位置從上一次匹配成功結束的位置開始

var reg = /a/g;
var str = 'abc_abc_abc'
var r1 = reg.exec(str);
r1 // ["a"]
r1.index // 0
reg.lastIndex // 1
var r2 = reg.exec(str);
r2 // ["a"]
r2.index // 4
reg.lastIndex // 5
var r3 = reg.exec(str);
r3 // ["a"]
r3.index // 8
reg.lastIndex // 9
var r4 = reg.exec(str);
r4 // null
reg.lastIndex // 0

利用g修飾符允許多次匹配的特點,可以用一個循環完成全部匹配

var reg = /a/g;
var str = 'abc_abc_abc'
while(true) {
  var match = reg.exec(str);
  if (!match) break;
  console.log('#' + match.index + ':' + match[0]);
}
// #0:a
// #4:a
// #8:a

正則實例對象的lastIndex屬性不僅可讀,還可寫。設置了g修飾符的時候,只要手動設置了lastIndex的值,就會從指定位置開始匹配

字符串的實例方法

字符串的實例方法之中,有4種與正則表達式有關

1.String.prototype.match():返回一個數組,成員是所有匹配的子字符串。

2.String.prototype.search():按照給定的正則表達式進行搜索,返回一個整數,表示匹配開始的位置。

3.String.prototype.replace():按照給定的正則表達式進行替換,返回替換後的字符串。

4.String.prototype.split():按照給定規則進行字符串分割,返回一個數組,包含分割後的各個成員

String.prototype.match()

字符串實例對象的match方法對字符串進行正則匹配,返回匹配結果

var s = '_x_x';
var r1 = /x/;
var r2 = /y/;
s.match(r1) // ["x"]
s.match(r2) // null

從上面代碼可以看到,字符串的match方法與正則對象的exec方法非常類似:匹配成功返回一個數組,匹配失敗返回null。如果正則表達式帶有g修飾符,則該方法與正則對象的exec方法行爲不同,會一次性返回所有匹配成功的結果

var s = 'abba';
var r = /a/g;
s.match(r) // ["a", "a"]
r.exec(s) // ["a"]

設置正則表達式的lastIndex屬性,對match方法無效,匹配總是從字符串的第一個字符開始

String.prototype.search()

字符串對象的search方法,返回第一個滿足條件的匹配結果在整個字符串中的位置。如果沒有任何匹配,則返回-1

'_x_x'.search(/x/) // 1

String.prototype.replace()

字符串對象的replace方法可以替換匹配的值。它接受兩個參數,第一個是正則表達式,表示搜索模式,第二個是替換的內容

str.replace(search, replacement)

正則表達式如果不加g修飾符,就替換第一個匹配成功的值,否則替換所有匹配成功的值

'aaa'.replace('a', 'b') // "baa"
'aaa'.replace(/a/, 'b') // "baa"
'aaa'.replace(/a/g, 'b') // "bbb"

replace方法的一個應用,就是消除字符串首尾兩端的空格

var str = '  #id div.class  ';
str.replace(/^\s+|\s+$/g, '') // "#id div.class"

replace方法的第二個參數可以使用美元符號$,用來指代所替換的內容。

$&:匹配的子字符串

$`:匹配結果前面的文本

$':匹配結果後面的文本

$n:匹配成功的第n組內容,n是從1開始的自然數

$$:指代美元符號$

'hello world'.replace(/(\w+)\s(\w+)/, '$2 $1') // "world hello"
'abc'.replace('b', '[$`-$&-$\']') // "a[a-b-c]c"

上面代碼中,第一個例子是將匹配的組互換位置,第二個例子是改寫匹配的值。replace方法的第二個參數還可以是一個函數,將每一個匹配內容替換爲函數返回值

'3 and 5'.replace(/[0-9]+/g, function (match) {
  return 2 * match;
}) // "6 and 10"
var a = 'The quick brown fox jumped over the lazy dog.';
var pattern = /quick|brown|lazy/ig;
a.replace(pattern, function replacer(match) {
  return match.toUpperCase();
}); // The QUICK BROWN fox jumped over the LAZY dog.

作爲replace方法第二個參數的替換函數,可以接受多個參數。其中,第一個參數是捕捉到的內容,第二個參數是捕捉到的組匹配(有多少個組匹配,就有多少個對應的參數)。此外,最後還可以添加兩個參數,倒數第二個參數是捕捉到的內容在整個字符串中的位置(比如從第五個位置開始),最後一個參數是原字符串。下面是一個網頁模板替換的例子

var prices = {  'p1': '$1.99',  'p2': '$9.99',  'p3': '$5.00'};
var template = '<span id="p1"></span><span id="p2"></span><span id="p3"></span>';
template.replace(
  /(<span id=")(.*?)(">)(<\/span>)/g,
  function(match, $1, $2, $3, $4){
    return $1 + $2 + $3 + prices[$2] + $4;
  }
); // "<span id="p1">$1.99</span><span id="p2">$9.99</span><span id="p3">$5.00</span>"

上面代碼的捕捉模式中,有四個括號,所以會產生四個組匹配,在匹配函數中用$1到$4表示。匹配函數的作用是將價格插入模板中

String.prototype.split()

字符串對象的split方法按照正則規則分割字符串,返回一個由分割後的各個部分組成的數組

str.split(separator, [limit])

該方法接受兩個參數,第一個參數是正則表達式,表示分隔規則,第二個參數是返回數組的最大成員數

// 非正則分隔
'a,  b,c, d'.split(',') // [ 'a', '  b', 'c', ' d' ]
// 正則分隔,去除多餘的空格
'a,  b,c, d'.split(/, */) // [ 'a', 'b', 'c', 'd' ]
// 指定返回數組的最大成員
'a,  b,c, d'.split(/, */, 2) [ 'a', 'b' ]

上面代碼使用正則表達式,去除了子字符串的逗號後面的空格

'aaa*a*'.split(/a*/) // [ '', '*', '*' ]
'aaa**a*'.split(/a*/) // ["", "*", "*", "*"]

上面代碼的分割規則是0次或多次的a,由於正則默認是貪婪匹配,所以例一的第一個分隔符是aaa,第二個分割符是a,將字符串分成三個部分,包含開始處的空字符串。例二的第一個分隔符是aaa,第二個分隔符是0個a(即空字符),第三個分隔符是a,所以將字符串分成四個部分。如果正則表達式帶有括號,則括號匹配的部分也會作爲數組成員返回

'aaa*a*'.split(/(a*)/) // [ '', 'aaa', '*', 'a', '*' ]

匹配規則

正則表達式的規則很複雜

字面量字符和元字符

大部分字符在正則表達式中就是字面的含義,比如/a/匹配a,/b/匹配b。如果在正則表達式中,某個字符只表示它字面的含義(就像前面的a和b),那麼它們就叫做“字面量字符”(literal characters)

/dog/.test('old dog') // true

除了字面量字符以外,還有一部分字符有特殊含義,不代表字面的意思。它們叫做“元字符”(metacharacters),主要有以下幾個

點字符(.)

點字符(.)匹配除回車(r)、換行(n) 、行分隔符(u2028)和段分隔符(u2029)以外的所有字符。注意,對於碼點大於0xFFFF字符,點字符不能正確匹配,會認爲這是兩個字符

/c.t/

c.t匹配c和t之間包含任意一個字符的情況,只要這三個字符在同一行,比如cat、c2t、c-t等等,但是不匹配coot

位置字符

位置字符用來提示字符所處的位置,主要有兩個字符:

1.^ 表示字符串的開始位置
2.$ 表示字符串的結束位置

// test必須出現在開始位置
/^test/.test('test123') // true
// test必須出現在結束位置
/test$/.test('new test') // true
// 從開始位置到結束位置只有test
/^test$/.test('test') // true
/^test$/.test('test test') // false
選擇符(|)

豎線符號(|)在正則表達式中表示“或關係”(OR),即cat|dog表示匹配cat或dog

/11|22|33/.test('911') // true

選擇符會包括它前後的多個字符,比如/ab|cd/指的是匹配ab或者cd,而不是指匹配b或者c。如果想修改這個行爲,可以使用圓括號

/a( |\t)b/.test('a\tb') // true

上面代碼指的是,a和b之間有一個空格或者一個製表符。其他的元字符還包括、*、+、?、()、[]、{}等

轉義符

正則表達式中那些有特殊含義的元字符,如果要匹配它們本身,就需要在它們前面要加上反斜槓。比如要匹配+,就要寫成+

/1+1/.test('1+1') // false
/1\+1/.test('1+1') // true

正則表達式中,需要反斜槓轉義的,一共有12個字符:^、.、[、$、(、)、|、*、+、?、{和。需要特別注意的是,如果使用RegExp方法生成正則對象,轉義需要使用兩個斜槓,因爲字符串內部會先轉義一次

(new RegExp('1\+1')).test('1+1')  // false  
(new RegExp('1\\+1')).test('1+1')  // true

上面代碼中,RegExp作爲構造函數,參數是一個字符串。但是,在字符串內部,反斜槓也是轉義字符,所以它會先被反斜槓轉義一次,然後再被正則表達式轉義一次,因此需要兩個反斜槓轉義

特殊字符

正則表達式對一些不能打印的特殊字符,提供了表達方法

cX 表示Ctrl-[X],其中的X是A-Z之中任一個英文字母,用來匹配控制字符

[b] 匹配退格鍵(U+0008),不要與b混淆

n 匹配換行鍵

r 匹配回車鍵

t 匹配製表符 tab(U+0009)

v 匹配垂直製表符(U+000B)

f 匹配換頁符(U+000C)

0 匹配null字符(U+0000)

xhh 匹配一個以兩位十六進制數(x00-xFF)表示的字符

uhhhh 匹配一個以四位十六進制數(u0000-uFFFF)表示的 Unicode 字符

字符類

字符類(class)表示有一系列字符可供選擇,只要匹配其中一個就可以了。所有可供選擇的字符都放在方括號內,比如[xyz] 表示x、y、z之中任選一個匹配

/[abc]/.test('hello world') // false
/[abc]/.test('apple') // true

有兩個字符在字符類中有特殊含義:

脫字符(^)

如果方括號內的第一個字符是[^],則表示除了字符類之中的字符,其他字符都可以匹配。比如,1表示除了x、y、z之外都可以匹配

/[^abc]/.test('hello world') // true
/[^abc]/.test('bbc') // false

如果方括號內沒有其他字符,即只有[^],就表示匹配一切字符,其中包括換行符。相比之下,點號作爲元字符(.)是不包括換行符的

var s = 'Please yes\nmake my day!';
s.match(/yes.*day/) // null
s.match(/yes[^]*day/) // [ 'yes\nmake my day']

上面代碼中,字符串s含有一個換行符,點號不包括換行符,所以第一個正則表達式匹配失敗;第二個正則表達式[^]包含一切字符,所以匹配成功。注意,脫字符只有在字符類的第一個位置纔有特殊含義,否則就是字面含義

連字符(-)

某些情況下,對於連續序列的字符,連字符(-)用來提供簡寫形式,表示字符的連續範圍。比如,[abc]可以寫成[a-c],[0123456789]可以寫成[0-9],同理[A-Z]表示26個大寫字母

/a-z/.test('b') // false
/[a-z]/.test('b') // true

上面代碼中,當連字號(dash)不出現在方括號之中,就不具備簡寫的作用,只代表字面的含義,所以不匹配字符b。只有當連字號用在方括號之中,才表示連續的字符序列。以下都是合法的字符類簡寫形式

[0-9.,]
[0-9a-fA-F]
[a-zA-Z0-9-]
[1-31]

上面代碼中最後一個字符類[1-31],不代表1到31,只代表1到3。連字符還可以用來指定 Unicode 字符的範圍

var str = "\u0130\u0131\u0132";
/[\u0128-\uFFFF]/.test(str) // true

另外,不要過分使用連字符,設定一個很大的範圍,否則很可能選中意料之外的字符。最典型的例子就是[A-z],表面上它是選中從大寫的A到小寫的z之間52個字母,但是由於在 ASCII 編碼之中,大寫字母與小寫字母之間還有其他字符,結果就會出現意料之外的結果

/[A-z]/.test('\\') // true

預定義模式

預定義模式指的是某些常見模式的簡寫方式。

\d 匹配0-9之間的任一數字,相當於[0-9]。
\D 匹配所有0-9以外的字符,相當於[^0-9]。
\w 匹配任意的字母、數字和下劃線,相當於[A-Za-z0-9_]。
\W 除所有字母、數字和下劃線以外的字符,相當於[^A-Za-z0-9_]。
\s 匹配空格(包括換行符、製表符、空格符等),相等於[ \t\r\n\v\f]。
\S 匹配非空格的字符,相當於[^ \t\r\n\v\f]。
\b 匹配詞的邊界。
\B 匹配非詞邊界,即在詞的內部

通常,正則表達式遇到換行符(n)就會停止匹配

var html = "<b>Hello</b>\n<i>world!</i>";
/.*/.exec(html)[0] // "<b>Hello</b>"
/[\S\s]*/.exec(html)[0] // "<b>Hello</b>\n<i>world!</i>"

字符串html包含一個換行符,結果點字符(.)不匹配換行符,導致匹配結果可能不符合原意。這時使用s字符類,就能包括換行符

重複類

模式的精確匹配次數,使用大括號({})表示。{n}表示恰好重複n次,{n,}表示至少重複n次,{n,m}表示重複不少於n次,不多於m次

/lo{2}k/.test('look') // true
/lo{2,5}k/.test('looook') // true

量詞符

量詞符用來設定某個模式出現的次數。

// ? 問號表示某個模式出現0次或1次,等同於{0, 1};t 出現0次或1次
/t?est/.test('test') // true
/t?est/.test('est') // true
//+ 加號表示某個模式出現1次或多次,等同於{1,}; t 出現1次或多次
/t+est/.test('ttest') // true
/t+est/.test('est') // false
//* 星號表示某個模式出現0次或多次,等同於{0,}; t 出現0次或多次
/t*est/.test('tttest') // true
/t*est/.test('est') // true

貪婪模式

上面的三個量詞符,默認情況下都是最大可能匹配,即匹配直到下一個字符不滿足匹配規則爲止。這被稱爲貪婪模式

var s = 'aaa';
s.match(/a+/) // ["aaa"]

上面代碼中,模式是/a+/,表示匹配1個或多個a,那麼到底會匹配幾個a呢?因爲默認是貪婪模式,會一直匹配到字符a不出現爲止,所以匹配結果是3個a。如果想將貪婪模式改爲非貪婪模式,可以在量詞符後面加一個問號

var s = 'aaa';
s.match(/a+?/) // ["a"]

上面代碼中,模式結尾添加了一個問號/a+?/,這時就改爲非貪婪模式,一旦條件滿足,就不再往下匹配。除了非貪婪模式的加號,還有非貪婪模式的星號(*)和非貪婪模式的問號(?)

//+?:表示某個模式出現1次或多次,匹配時採用非貪婪模式;*?:表示某個模式出現0次或多次,匹配時採用非貪婪模式;??:表示某個模式出現0次或1次,匹配時採用非貪婪模式
'abb'.match(/ab*b/) // ["abb"]
'abb'.match(/ab*?b/) // ["ab"]
'abb'.match(/ab?b/) // ["abb"]
'abb'.match(/ab??b/) // ["ab"]

修飾符

修飾符(modifier)表示模式的附加規則,放在正則模式的最尾部。修飾符可以單個使用,也可以多個一起使用

g 修飾符

默認情況下,第一次匹配成功後,正則對象就停止向下匹配了。g修飾符表示全局匹配(global),加上它以後,正則對象將匹配全部符合條件的結果,主要用於搜索和替換

var regex = /b/;
var str = 'abba';
regex.test(str); // true
regex.test(str); // true
regex.test(str); // true

上面代碼中,正則模式不含g修飾符,每次都是從字符串頭部開始匹配。所以連續做了三次匹配都返回true

var regex = /b/g;
var str = 'abba';
regex.test(str); // true
regex.test(str); // true
regex.test(str); // false

上面代碼中,正則模式含有g修飾符,每次都是從上一次匹配成功處,開始向後匹配。因爲字符串abba只有兩個b,所以前兩次匹配結果爲true,第三次匹配結果爲false

i 修飾符

默認情況下,正則對象區分字母的大小寫,加上i修飾符以後表示忽略大小寫(ignoreCase)

/abc/.test('ABC') // false
/abc/i.test('ABC') // true
m 修飾符

m修飾符表示多行模式(multiline),會修改^和$的行爲。默認情況下(即不加m修飾符時),^和$匹配字符串的開始處和結尾處,加上m修飾符以後,^和$還會匹配行首和行尾,即^和$會識別換行符(n)

/world$/.test('hello world\n') // false
/world$/m.test('hello world\n') // true

上面的代碼中,字符串結尾處有一個換行符。如果不加m修飾符,匹配不成功,因爲字符串的結尾不是world;加上以後,$可以匹配行尾

/^b/m.test('a\nb') // true

上面代碼要求匹配行首的b,如果不加m修飾符,就相當於b只能處在字符串的開始處。加上b修飾符以後,換行符n也會被認爲是一行的開始

組匹配

概述

正則表達式的括號表示分組匹配,括號中的模式可以用來匹配分組的內容

/fred+/.test('fredd') // true
/(fred)+/.test('fredfred') // true

上面代碼中,第一個模式沒有括號,結果+只表示重複字母d,第二個模式有括號,結果+就表示匹配fred這個詞。

var m = 'abcabc'.match(/(.)b(.)/);
m // ['abc', 'a', 'c']

上面代碼中,正則表達式/(.)b(.)/一共使用兩個括號,第一個括號捕獲a,第二個括號捕獲c

注意,使用組匹配時,不宜同時使用g修飾符,否則match方法不會捕獲分組的內容

var m = 'abcabc'.match(/(.)b(.)/g);
m // ['abc', 'abc']

上面代碼使用帶g修飾符的正則表達式,結果match方法只捕獲了匹配整個表達式的部分。這時必須使用正則表達式的exec方法,配合循環,才能讀到每一輪匹配的組捕獲

var str = 'abcabc';
var reg = /(.)b(.)/g;
while (true) {
  var result = reg.exec(str);
  if (!result) break;
  console.log(result);
}
// ["abc", "a", "c"]
// ["abc", "a", "c"]

正則表達式內部,還可以用n引用括號匹配的內容,n是從1開始的自然數,表示對應順序的括號

/(.)b(.)\1b\2/.test("abcabc") // true

上面的代碼中,1表示第一個括號匹配的內容(即a),2表示第二個括號匹配的內容(即c)。括號還可以嵌套

/y((..)\2)\1/.test('yabababab') // true

上面代碼中,1指向外層括號,2指向內層括號

var html = '<b class="hello">Hello</b><i>world</i>';
var tag = /<(\w+)([^>]*)>(.*?)<\/\1>/g;
var match = tag.exec(html);
match[1] // "b"
match[2] // " class="hello""
match[3] // "Hello"
match = tag.exec(html);
match[1] // "i"
match[2] // ""
match[3] // "world"
非捕獲組

(?:x)稱爲非捕獲組(Non-capturing group),表示不返回該組匹配的內容,即匹配的結果中不計入這個括號。

非捕獲組的作用請考慮這樣一個場景,假定需要匹配foo或者foofoo,正則表達式就應該寫成/(foo){1, 2}/,但是這樣會佔用一個組匹配。這時,就可以使用非捕獲組,將正則表達式改爲/(?:foo){1, 2}/,它的作用與前一個正則是一樣的,但是不會單獨輸出括號內部的內容

var m = 'abc'.match(/(?:.)b(.)/);
m // ["abc", "c"]

上面代碼中的模式,一共使用了兩個括號。其中第一個括號是非捕獲組,所以最後返回的結果中沒有第一個括號,只有第二個括號匹配的內容

// 正常匹配
var url = /(http|ftp):\/\/([^/\r\n]+)(\/[^\r\n]*)?/;
url.exec('http://google.com/'); // ["http://google.com/", "http", "google.com", "/"]
// 非捕獲組匹配
var url = /(?:http|ftp):\/\/([^/\r\n]+)(\/[^\r\n]*)?/;
url.exec('http://google.com/'); // ["http://google.com/", "google.com", "/"]

上面的代碼中,前一個正則表達式是正常匹配,第一個括號返回網絡協議;後一個正則表達式是非捕獲匹配,返回結果中不包括網絡協議

先行斷言

x(?=y)稱爲先行斷言(Positive look-ahead),x只有在y前面才匹配,y不會被計入返回結果。比如,要匹配後面跟着百分號的數字,可以寫成/d+(?=%)/。“先行斷言”中,括號裏的部分是不會返回的

var m = 'abc'.match(/b(?=c)/);
m // ["b"]
先行否定斷言

x(?!y)稱爲先行否定斷言(Negative look-ahead),x只有不在y前面才匹配,y不會被計入返回結果。比如,要匹配後面跟的不是百分號的數字,就要寫成/d+(?!%)/

/\d+(?!\.)/.exec('3.14') // ["14"]

上面代碼中,正則表達式指定,只有不在小數點前面的數字纔會被匹配,因此返回的結果就是14。“先行否定斷言”中,括號裏的部分是不會返回的

var m = 'abd'.match(/b(?!c)/);
m // ['b']

JSON 對象

JSON 格式

JSON 格式(JavaScript Object Notation 的縮寫)是一種用於數據交換的文本格式,2001年由 Douglas Crockford 提出,目的是取代繁瑣笨重的 XML 格式。相比 XML 格式,JSON 格式有兩個顯著的優點:書寫簡單,一目瞭然;符合 JavaScript 原生語法,可以由解釋引擎直接處理,不用另外添加解析代碼。所以,JSON 迅速被接受,已經成爲各大網站交換數據的標準格式,並被寫入標準。每個 JSON 對象就是一個值,可能是一個數組或對象,也可能是一個原始類型的值。總之,只能是一個值,不能是兩個或更多的值。JSON 對值的類型和格式有嚴格的規定

1.字符串必須使用雙引號表示,不能使用單引號
2.數組或對象最後一個成員的後面,不能加逗號
3.對象的鍵名必須放在雙引號裏面
{ name: "張三", 'age': 32 }  
4.原始類型的值只有四種:字符串、數值(必須以十進制表示)、布爾值和null(不能使用NaN, Infinity, -Infinity和undefined)
[32, 64, 128, 0xFFF]
{ "name": "張三", "age": undefined }
5.複合類型的值只能是數組或對象,不能是函數、正則表達式對象、日期對象
{ "name": "張三",
  "birthday": new Date('Fri, 26 Aug 2011 07:13:10 GMT'),
  "getName": function () {
      return this.name;
  }
}

注意,null、空數組和空對象都是合法的 JSON 值

JSON 對象

JSON對象是 JavaScript 的原生對象,用來處理 JSON 格式數據。它有兩個靜態方法:JSON.stringify()和JSON.parse()

JSON.stringify()

基本用法

JSON.stringify方法用於將一個值轉爲 JSON 字符串。該字符串符合 JSON 格式,並且可以被JSON.parse方法還原

JSON.stringify('abc') // ""abc""
JSON.stringify([1, "false", false]) // '[1,"false",false]'
JSON.stringify({ name: "張三" }) // '{"name":"張三"}'

注意,對於原始類型的字符串,轉換結果會帶雙引號

JSON.stringify('foo') === "foo" // false
JSON.stringify('foo') === "\"foo\"" // true

如果對象的屬性是undefined、函數或 XML 對象,該屬性會被JSON.stringify過濾

var obj = {
  a: undefined,
  b: function () {}
};
JSON.stringify(obj) // "{}"

如果數組的成員是undefined、函數或 XML 對象,則這些值被轉成null

var arr = [undefined, function () {}];
JSON.stringify(arr) // "[null,null]"

正則對象會被轉成空對象

JSON.stringify(/foo/) // "{}"

JSON.stringify方法會忽略對象的不可遍歷的屬性

var obj = {};
Object.defineProperties(obj, {
  'foo': { value: 1, enumerable: true },
  'bar': { value: 2, enumerable: false }
});
JSON.stringify(obj); // "{"foo":1}"

上面代碼中,bar是obj對象的不可遍歷屬性,JSON.stringify方法會忽略這個屬性

第二個參數

JSON.stringify方法還可以接受一個數組,作爲第二個參數,指定需要轉成字符串的屬性

var obj = {
  'prop1': 'value1',
  'prop2': 'value2',
  'prop3': 'value3'
};
var selectedProperties = ['prop1', 'prop2'];
JSON.stringify(obj, selectedProperties) // "{"prop1":"value1","prop2":"value2"}"

上面代碼中,JSON.stringify方法的第二個參數指定,只轉prop1和prop2兩個屬性。這個類似白名單的數組,只對對象的屬性有效,對數組無效

JSON.stringify(['a', 'b'], ['0']) // "["a","b"]"
JSON.stringify({0: 'a', 1: 'b'}, ['0']) // "{"0":"a"}"

第二個參數還可以是一個函數,用來更改JSON.stringify的返回值

function f(key, value) {
  if (typeof value === "number") {
    value = 2 * value;
  }
  return value;
}
JSON.stringify({ a: 1, b: 2 }, f) // '{"a": 2,"b": 4}'

注意,這個處理函數是遞歸處理所有的鍵

var o = {a: {b: 1}};
function f(key, value) {
  console.log("["+ key +"]:" + value);
  return value;
}
JSON.stringify(o, f)
// []:[object Object]
// [a]:[object Object]
// [b]:1
// '{"a":{"b":1}}'

上面代碼中,對象o一共會被f函數處理三次,最後那行是JSON.stringify的輸出。第一次鍵名爲空,鍵值是整個對象o;第二次鍵名爲a,鍵值是{b: 1};第三次鍵名爲b,鍵值爲1。遞歸處理中,每一次處理的對象,都是前一次返回的值

var o = {a: 1};
function f(key, value) {
  if (typeof value === 'object') {
    return {b: 2};
  }
  return value * 2;
}
JSON.stringify(o, f) // "{"b": 4}"

如果處理函數返回undefined或沒有返回值,則該屬性會被忽略

function f(key, value) {
  if (typeof(value) === "string") {
    return undefined;
  }
  return value;
}
JSON.stringify({ a: "abc", b: 123 }, f) // '{"b": 123}'
第三個參數

JSON.stringify還可以接受第三個參數,用於增加返回的 JSON 字符串的可讀性。如果是數字,表示每個屬性前面添加的空格(最多不超過10個);如果是字符串(不超過10個字符),則該字符串會添加在每行前面

JSON.stringify({ p1: 1, p2: 2 }, null, 2);
/*
"{
  "p1": 1,
  "p2": 2
}"
*/
JSON.stringify({ p1:1, p2:2 }, null, '|-');
/*
"{
|-"p1": 1,
|-"p2": 2
}"
*/
參數對象的 toJSON 方法

如果參數對象有自定義的toJSON方法,那麼JSON.stringify會使用這個方法的返回值作爲參數,而忽略原對象的其他屬性

一個普通的對象

var user = {
  firstName: '三',
  lastName: '張',
  get fullName(){
    return this.lastName + this.firstName;
  }
};
JSON.stringify(user) // "{"firstName":"三","lastName":"張","fullName":"張三"}"

爲這個對象加上toJSON方法

var user = {
  firstName: '三',
  lastName: '張',
  get fullName(){
    return this.lastName + this.firstName;
  },
  toJSON: function () {
    return {
      name: this.lastName + this.firstName
    };
  }
};
JSON.stringify(user) // "{"name":"張三"}"

上面代碼中,JSON.stringify發現參數對象有toJSON方法,就直接使用這個方法的返回值作爲參數,而忽略原對象的其他參數。Date對象就有一個自己的toJSON方法

var date = new Date('2015-01-01');
date.toJSON() // "2015-01-01T00:00:00.000Z"
JSON.stringify(date) // ""2015-01-01T00:00:00.000Z""

toJSON方法的一個應用是,將正則對象自動轉爲字符串。因爲JSON.stringify默認不能轉換正則對象,但是設置了toJSON方法以後,就可以轉換正則對象了

var obj = {  reg: /foo/ };
// 不設置 toJSON 方法時
JSON.stringify(obj) // "{"reg":{}}"
// 設置 toJSON 方法時
RegExp.prototype.toJSON = RegExp.prototype.toString;
JSON.stringify(/foo/) // ""/foo/""

上面代碼在正則對象的原型上面部署了toJSON方法,將其指向toString方法,因此遇到轉換成JSON時,正則對象就先調用toJSON方法轉爲字符串,然後再被JSON.stingify方法處理

JSON.parse()

JSON.parse方法用於將 JSON 字符串轉換成對應的值

JSON.parse('true') // true
JSON.parse('[1, 5, "false"]') // [1, 5, "false"]
JSON.parse('null') // null
var o = JSON.parse('{"name": "張三"}');
o.name // 張三

如果傳入的字符串不是有效的 JSON 格式,JSON.parse方法將報錯

JSON.parse("'String'") // SyntaxError: Unexpected token ILLEGAL

上面代碼中,雙引號字符串中是一個單引號字符串,因爲單引號字符串不符合 JSON 格式,所以報錯。爲了處理解析錯誤,可以將JSON.parse方法放在try...catch代碼塊中

try {
  JSON.parse("'String'");
} catch(e) {
  console.log('parsing error');
}

JSON.parse方法可以接受一個處理函數,作爲第二個參數,用法與JSON.stringify方法類似

function f(key, value) {
  if (key === 'a') {
    return value + 10;
  }
  return value;
}
JSON.parse('{"a": 1, "b": 2}', f) // {a: 11, b: 2}

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