JavaScript 相關記錄

JavaScript 堪稱世界上被人誤解最深的編程語言。雖然常被嘲爲「玩具語言」,但在它看似簡潔的外衣下,還隱藏着強大的語言特性。 JavaScript 目前普遍應用於衆多知名應用中,對於網頁和移動開發者來講,深刻理解 JavaScript 就尤有必要。javascript

先從這門語言的歷史談起是有必要的。在1995 年 Netscape 一位名爲 Brendan Eich 的工程師創造了 JavaScript,隨後在 1996 年初,JavaScript 首先被應用於 Netscape 2 瀏覽器上。最初的 JavaScript 名爲 LiveScript,後來由於 Sun Microsystem 的 Java 語言的興起和普遍使用,Netscape 出於宣傳和推廣的考慮,將它的名字從最初的 LiveScript 更改成 JavaScript——儘管二者之間並無什麼共同點。這即是以後混淆產生的根源。html

幾個月後,Microsoft 隨着 IE 3 推出了一個與之基本兼容的語言 JScript。又幾個月後,Netscape 將 JavaScript 提交至 Ecma International(一個歐洲標準化組織), ECMAScript 標準初版便在 1997 年誕生了,隨後在 1999 年以 ECMAScript 第三版的形式進行了更新,從那以後這個標準沒有發生過大的改動。因爲委員會在語言特性的討論上發生分歧,ECMAScript 第四版還沒有推出便被廢除,但隨後於 2009 年 12 月發佈的 ECMAScript 第五版引入了第四版草案加入的許多特性。第六版標準已經於2015年六月發佈。java

 爲熟悉起見,從這裏開始咱們用 「JavaScript」 替代 ECMAScript 。node

與大多數編程語言不一樣,JavaScript 沒有輸入或輸出的概念。它是一個在宿主環境(host environment)下運行的腳本語言,任何與外界溝通的機制都是由宿主環境提供的。瀏覽器是最多見的宿主環境,但在很是多的其餘程序中也包含 JavaScript 解釋器,如 Adobe Acrobat、Photoshop、SVG 圖像、Yahoo! 的 Widget 引擎,以及 Node.js 之類的服務器端環境。JavaScript 的實際應用遠不止這些,除此以外還有 NoSQL 數據庫(如開源的 Apache CouchDB)、嵌入式計算機,以及包括 GNOME (注:GNU/Linux 上最流行的 GUI 之一)在內的桌面環境等等。git

概覽

JavaScript 是一種面向對象的動態語言,它包含類型、運算符、標準內置( built-in)對象和方法。它的語法來源於 Java 和 C,因此這兩種語言的許多語法特性一樣適用於 JavaScript。須要注意的一個主要區別是 JavaScript 不支持類,類這一律念在 JavaScript 經過對象原型(object prototype)獲得延續(有關 ES6 類的內容參考這裏Classes)。另外一個主要區別是 JavaScript 中的函數也是對象,JavaScript 容許函數在包含可執行代碼的同時,能像其餘對象同樣被傳遞。程序員

先從任何編程語言都不可缺乏的組成部分——「類型」開始。JavaScript 程序能夠修改值(value),這些值都有各自的類型。JavaScript 中的類型包括:github

…哦,還有看上去有些…奇怪的 undefined(未定義)類型和 null(空)類型。此外還有Array(數組)類型,以及分別用於表示日期和正則表達式的 Date(日期)和 RegExp(正則表達式),這三種類型都是特殊的對象。嚴格意義上說,Function(函數)也是一種特殊的對象。因此準確來講,JavaScript 中的類型應該包括這些:web

JavaScript 還有一種內置Error(錯誤)類型,這個會在以後的介紹中提到;如今咱們先討論下上面這些類型。正則表達式

數字

根據語言規範,JavaScript 採用「IEEE 754 標準定義的雙精度64位格式」("double-precision 64-bit format IEEE 754 values")表示數字。據此咱們能獲得一個有趣的結論,和其餘編程語言(如 C 和 Java)不一樣,JavaScript 不區分整數值和浮點數值,全部數字在 JavaScript 中均用浮點數值表示,因此在進行數字運算的時候要特別注意。看看下面的例子:數據庫

0.1 + 0.2 = 0.30000000000000004

在具體實現時,整數值一般被視爲32位整型變量,在個別實現(如某些瀏覽器)中也以32位整型變量的形式進行存儲,直到它被用於執行某些32位整型不支持的操做,這是爲了便於進行位操做。進一步的詳細資料可參考 The Complete JavaScript Number Reference

JavaScript 支持標準的算術運算符,包括加法、減法、取模(或取餘)等等。還有一個以前沒有說起的內置對象 Math(數學對象),用以處理更多的高級數學函數和常數:

Math.sin(3.5); var d = Math.PI * (r + r);

你可使用內置函數 parseInt() 將字符串轉換爲整型。該函數的第二個參數表示字符串所表示數字的基(進制):

parseInt("123", 10); // 123 parseInt("010", 10); //10

若是調用時沒有提供第二個參數(字符串所表示數字的基),2013 年之前的 JavaScript 實現會返回一個意外的結果:

parseInt("010"); // 8 parseInt("0x10"); // 16

這是由於字符串以數字 0 開頭,parseInt()函數會把這樣的字符串視做八進制數字;同理,0x開頭的字符串則視爲十六進制數字。

若是想把一個二進制數字字符串轉換成整數值,只要把第二個參數設置爲 2 就能夠了:

parseInt("11", 2); // 3

JavaScript 還有一個相似的內置函數 parseFloat(),用以解析浮點數字符串,與parseInt()不一樣的地方是,parseFloat()只應用於解析十進制數字。

單元運算符 + 也能夠把數字字符串轉換成數值:

+ "42"; // 42 + "010"; // 10 + "0x10"; // 16

若是給定的字符串不存在數值形式,函數會返回一個特殊的值 NaN(Not a Number 的縮寫):

parseInt("hello", 10); // NaN

要當心NaN:若是把 NaN 做爲參數進行任何數學運算,結果也會是 NaN

NaN + 5; //NaN

可使用內置函數 isNaN() 來判斷一個變量是否爲 NaN

isNaN(NaN); // true

JavaScript 還有兩個特殊值:Infinity(正無窮)和 -Infinity(負無窮):

1 / 0; // Infinity -1 / 0; // -Infinity

可使用內置函數 isFinite() 來判斷一個變量是否爲 Infinity-Infinity 或 NaN

isFinite(1/0); // false isFinite(-Infinity); // false isFinite(NaN); // false
備註:  parseInt() 和  parseFloat() 函數會嘗試逐個解析字符串中的字符,直到趕上一個沒法被解析成數字的字符,而後返回該字符前全部數字字符組成的數字。使用運算符 "+" 將字符串轉換成數字,只要字符串中含有沒法被解析成數字的字符,該字符串都將被轉換成  NaN。請你用這兩種方法分別解析「10.2abc」這一字符串,比較獲得的結果,理解這兩種方法的區別。

字符串

JavaScript 中的字符串是一串Unicode 字符序列。這對於那些須要和多語種網頁打交道的開發者來講是個好消息。更準確地說,它們是一串UTF-16編碼單元的序列,每個編碼單元由一個 16 位二進制數表示。每個Unicode字符由一個或兩個編碼單元來表示。

若是想表示一個單獨的字符,只需使用長度爲 1 的字符串。

經過訪問字符串的  長度(編碼單元的個數)屬性能夠獲得它的長度。

"hello".length; // 5

這是咱們第一次碰到 JavaScript 對象。咱們有沒有提過你能夠像 objects  同樣使用字符串?是的,字符串也有methods(方法)能讓你操做字符串和獲取字符串的信息。

"hello".charAt(0); // "h" "hello, world".replace("hello", "goodbye"); // "goodbye, world" "hello".toUpperCase(); // "HELLO"

其餘類型

JavaScript 中 null 和 undefined 是不一樣的,前者表示一個空值(non-value),必須使用null關鍵字才能訪問,後者是「undefined(未定義)」類型的對象,表示一個未初始化的值,也就是尚未被分配的值。咱們以後再具體討論變量,但有一點能夠先簡單說明一下,JavaScript 容許聲明變量但不對其賦值,一個未被賦值的變量就是 undefined 類型。還有一點須要說明的是,undefined 其實是一個不容許修改的常量。

JavaScript 包含布爾類型,這個類型的變量有兩個可能的值,分別是 true 和 false(二者都是關鍵字)。根據具體須要,JavaScript 按照以下規則將變量轉換成布爾類型:

  1. false0、空字符串("")、NaNnull 和 undefined 被轉換爲 false
  2. 全部其餘值被轉換爲 true

也可使用 Boolean() 函數進行顯式轉換:

Boolean(""); // false Boolean(234); // true

不過通常不必這麼作,由於 JavaScript 會在須要一個布爾變量時隱式完成這個轉換操做(好比在 if條件語句中)。因此,有時咱們能夠把轉換成布爾值後的變量分別稱爲 真值(true values)——即值爲 true  和 假值(false values)——即值爲 false;也能夠分別稱爲「真的」(truthy)和「假的」(falsy)。

JavaScript 支持包括 &&(邏輯與)、|| (邏輯或)和!(邏輯非)在內的邏輯運算符。下面會有所提到。

變量

在 JavaScript 中聲明一個新變量的方法是使用關鍵字 var

var a; var name = "simon";

若是聲明瞭一個變量卻沒有對其賦值,那麼這個變量的類型就是 undefined

JavaScript 與其餘語言的(如 Java)的重要區別是在 JavaScript 中語句塊(blocks)是沒有做用域的,只有函數有做用域。所以若是在一個複合語句中(如 if 控制結構中)使用 var 聲明一個變量,那麼它的做用域是整個函數(複合語句在函數中)。 可是從 ECMAScript Edition 6 開始將有所不一樣的, let 和 const 關鍵字容許你建立塊做用域的變量。

運算符

JavaScript的算術操做符包括 +-*/ 和 % ——求餘(與模運算不一樣)。賦值使用 = 運算符,此外還有一些複合運算符,如 += 和 -=,它們等價於 x = x op y

x += 5; // 等價於 x = x + 5;

可使用 ++ 和 -- 分別實現變量的自增和自減。二者均可以做爲前綴或後綴操做符使用。

+ 操做符還能夠用來鏈接字符串:

"hello" + " world"; // hello world

若是你用一個字符串加上一個數字(或其餘值),那麼操做數都會被首先轉換爲字符串。以下所示:

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

這裏不難看出一個實用的技巧——經過與空字符串相加,能夠將某個變量快速轉換成字符串類型。

JavaScript 中的比較操做使用 <><= 和 >=,這些運算符對於數字和字符串都通用。相等的比較稍微複雜一些。由兩個「=(等號)」組成的相等運算符有類型自適應的功能,具體例子以下:

123 == "123" // true 1 == true; // true

若是在比較前不須要自動類型轉換,應該使用由三個「=(等號)」組成的相等運算符:

1 === true; //false 123 === "123"; // false

JavaScript 還支持 != 和 !== 兩種不等運算符,具體區別與兩種相等運算符的區別相似。

JavaScript 還提供了 位操做符

控制結構

JavaScript 的控制結構與其餘類 C 語言相似。可使用 if 和 else 來定義條件語句,還能夠連起來使用:

var name = "kittens"; if (name == "puppies") { name += "!"; } else if (name == "kittens") { name += "!!"; } else { name = "!" + name; } name == "kittens!!"; // true

JavaScript 支持 while 循環和 do-while 循環。前者適合常見的基本循環操做,若是須要循環體至少被執行一次則可使用 do-while

while (true) { // 一個無限循環! } var input; do { input = get_input(); } while (inputIsNotValid(input))

JavaScript 的 for 循環與 C 和 Java 中的相同,使用時能夠在一行代碼中提供控制信息。

for (var i = 0; i < 5; i++) { // 將會執行五次 }

&& 和 || 運算符使用短路邏輯(short-circuit logic),是否會執行第二個語句(操做數)取決於第一個操做數的結果。在須要訪問某個對象的屬性時,使用這個特性能夠事先檢測該對象是否爲空:

var name = o && o.getName();

或運算能夠用來設置默認值:

var name = otherName || "default";

相似地,JavaScript 也有一個用於條件表達式的三元操做符:

var allowed = (age > 18) ? "yes" : "no";

在須要多重分支時可使用  基於一個數字或字符串的switch 語句:

switch(action) { case 'draw': drawIt(); break; case 'eat': eatIt(); break; default: doNothing(); }

若是你不使用 break 語句,JavaScript 解釋器將會執行以後 case 中的代碼。除非是爲了調試,通常你並不須要這個特性,因此大多數時候不要忘了加上 break。

switch(a) { case 1: // 繼續向下 case 2: eatIt(); break; default: doNothing(); }

default 語句是可選的。switch 和 case 均可以使用須要運算才能獲得結果的表達式;在 switch 的表達式和 case 的表達式是使用 === 嚴格相等運算符進行比較的:

switch(1 + 3){ case 2 + 2: yay(); break; default: neverhappens(); }

對象

JavaScript 中的對象能夠簡單理解成「名稱-值」對,不難聯想 JavaScript 中的對象與下面這些概念相似:

  • Python 中的字典
  • Perl 和 Ruby 中的散列(哈希)
  • C/C++ 中的散列表
  • Java 中的 HashMap
  • PHP 中的關聯數組

這樣的數據結構設計合理,能應付各種複雜需求,因此被各種編程語言普遍採用。正由於 JavaScript 中的一切(除了核心類型,core object)都是對象,全部 JavaScript 程序必然與大量的散列表查找操做有着千絲萬縷的聯繫,而散列表擅長的正是高速查找。

「名稱」部分是一個 JavaScript 字符串,「值」部分能夠是任何 JavaScript 的數據類型——包括對象。這使用戶能夠根據具體需求,建立出至關複雜的數據結構。

有兩種簡單方法能夠建立一個空對象:

var obj = new Object();

和:

var obj = {};

這兩種方法在語義上是相同的。第二種更方便的方法叫做「對象字面量(object literal)」法。這種也是 JSON 格式的核心語法,通常咱們優先選擇第二種方法。

「對象字面量」也能夠用來在對象實例中定義一個對象:

var obj = { name: "Carrot", "for": "Max", details: { color: "orange", size: 12 } }

對象的屬性能夠經過鏈式(chain)表示方法進行訪問:

obj.details.color; // orange obj["details"]["size"]; // 12

下面的例子建立了一個對象原型,Person,和這個原型的實例,You。

function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 定義一個對象
var You = new Person("You", 24); 
// 咱們建立了一個新的 Person,名稱是 "You" 
// ("You" 是第一個參數, 24 是第二個參數..)

完成建立後,對象屬性能夠經過以下兩種方式進行賦值和訪問:

obj.name = "Simon" var name = obj.name;

和:

obj["name"] = "Simon"; var name = obj["name"];

這兩種方法在語義上也是相同的。第二種方法的優勢在於屬性的名稱被看做一個字符串,這就意味着它能夠在運行時被計算,缺點在於這樣的代碼有可能沒法在後期被解釋器優化。它也能夠被用來訪問某些以預留關鍵字做爲名稱的屬性的值:

obj.for = "Simon"; // 語法錯誤,由於 for 是一個預留關鍵字 obj["for"] = "Simon"; // 工做正常

注意:從 EcmaScript 5 開始,預留關鍵字能夠做爲對象的屬性名(reserved words may be used as object property names "in the buff")。 這意味着當定義對象字面量時不須要用雙引號了。參見 ES5 Spec.

關於對象和原型的詳情參見: Object.prototype.

數組

JavaScript 中的數組是一種特殊的對象。它的工做原理與普通對象相似(以數字爲屬性名,但只能經過[] 來訪問),但數組還有一個特殊的屬性——length(長度)屬性。這個屬性的值一般比數組最大索引大 1。

建立數組的傳統方法是:

var a = new Array(); a[0] = "dog"; a[1] = "cat"; a[2] = "hen"; a.length; // 3

使用數組字面量(array literal)法更加方便:

var a = ["dog", "cat", "hen"]; a.length; // 3

注意,Array.length 並不老是等於數組中元素的個數,以下所示:

var a = ["dog", "cat", "hen"]; a[100] = "fox"; a.length; // 101

記住:數組的長度是比數組最大索引值多一的數。

若是試圖訪問一個不存在的數組索引,會獲得 undefined

typeof(a[90]); // undefined

能夠經過以下方式遍歷一個數組:

for (var i = 0; i < a.length; i++) { // Do something with a[i] }

遍歷數組的另外一種方法是使用 for...in 循環。注意,若是有人向 Array.prototype 添加了新的屬性,使用這樣的循環這些屬性也一樣會被遍歷。因此並不推薦這種方法:

for (var i in a) { // Do something with a[i] }

ECMAScript 5 增長了遍歷數組的另外一個方法 forEach()

["dog", "cat", "hen"].forEach(function(currentValue, index, array) {
  // Do something with currentValue or array[index]
});

若是想在數組後追加元素,只須要:

a.push(item);

Array(數組)類自帶了許多方法。查看 array 方法的完整文檔

方法名稱 描述
a.toString() 返回一個包含數組中全部元素的字符串,每一個元素經過逗號分隔。
a.toLocaleString() 根據宿主環境的區域設置,返回一個包含數組中全部元素的字符串,每一個元素經過逗號分隔。
a.concat(item1[, item2[, ...[, itemN]]]) 返回一個數組,這個數組包含原先 a 和 item一、item二、……、itemN 中的全部元素。
a.join(sep) 返回一個包含數組中全部元素的字符串,每一個元素經過指定的 sep 分隔。
a.pop() 刪除並返回數組中的最後一個元素。
a.push(item1, ..., itemN) 將 item一、item二、……、itemN 追加至數組 a
a.reverse() 數組逆序(會更改原數組 a)。
a.shift() 刪除並返回數組中第一個元素。
a.slice(start, end) 返回子數組,以 a[start] 開頭,以 a[end] 前一個元素結尾。
a.sort([cmpfn]) 依據 cmpfn 返回的結果進行排序,若是未指定比較函數則按字符順序比較(即便元素是數字)。
a.splice(start, delcount[, item1[, ...[, itemN]]]) 從 start 開始,刪除 delcount 個元素,而後插入全部的 item
a.unshift([item]) 將 item 插入數組頭部,返回數組新長度(考慮undefined)。

函數

學習 JavaScript 最重要的就是要理解對象和函數兩個部分。最簡單的函數就像下面這個這麼簡單:

function add(x, y) { var total = x + y; return total; }

這個例子包括你須要瞭解的關於基本函數的全部部分。一個 JavaScript 函數能夠包含 0 個或多個已命名的變量。函數體中的表達式數量也沒有限制。你能夠聲明函數本身的局部變量。return 語句在返回一個值並結束函數。若是沒有使用 return 語句,或者一個沒有值的 return 語句,JavaScript 會返回undefined

已命名的參數更像是一個指示而沒有其餘做用。若是調用函數時沒有提供足夠的參數,缺乏的參數會被undefined 替代。

add(); // NaN // 不能在 undefined 對象上進行加法操做

你還能夠傳入多於函數自己須要參數個數的參數:

add(2, 3, 4); // 5 // 將前兩個值相加,4被忽略了

這看上去有點蠢。函數其實是訪問了函數體中一個名爲 arguments 的內部對象,這個對象就如同一個相似於數組的對象同樣,包括了全部被傳入的參數。讓咱們重寫一下上面的函數,使它能夠接收任意個數的參數:

function add() { var sum = 0; for (var i = 0, j = arguments.length; i < j; i++) { sum += arguments[i]; } return sum; } add(2, 3, 4, 5); // 14

這跟直接寫成 2 + 3 + 4 + 5 也沒什麼區別。接下來建立一個求平均數的函數:

function avg() { var sum = 0; for (var i = 0, j = arguments.length; i < j; i++) { sum += arguments[i]; } return sum / arguments.length; } avg(2, 3, 4, 5); // 3.5

這個頗有用,可是卻帶來了新的問題。avg() 函數處理一個由逗號鏈接的變量串,但若是想獲得一個數組的平均值該怎麼辦呢?能夠這麼修改函數:

function avgArray(arr) { var sum = 0; for (var i = 0, j = arr.length; i < j; i++) { sum += arr[i]; } return sum / arr.length; } avgArray([2, 3, 4, 5]); // 3.5

但若是能重用咱們已經建立的那個函數不是更好嗎?幸運的是 JavaScript 容許使用任意函數對象的apply() 方法來調用該函數,並傳遞給它一個包含了參數的數組。

avg.apply(null, [2, 3, 4, 5]); // 3.5

傳給 apply() 的第二個參數是一個數組,它將被看成 avg() 的參數使用,至於第一個參數 null,咱們將在後面討論。這也正說明一個事實——函數也是對象。

JavaScript 容許你建立匿名函數:

var avg = function() { var sum = 0; for (var i = 0, j = arguments.length; i < j; i++) { sum += arguments[i]; } return sum / arguments.length; };

這個函數在語義上與 function avg() 相同。你能夠在代碼中的任何地方定義這個函數,就像寫普通的表達式同樣。基於這個特性,有人發明出一些有趣的技巧。與 C 中的塊級做用域相似,下面這個例子隱藏了局部變量:

var a = 1; var b = 2; (function() { var b = 3; a += b; })(); a; // 4 b; // 2

JavaScript 容許以遞歸方式調用函數。遞歸在處理樹形結構(好比瀏覽器 DOM)時很是有用。

function countChars(elm) { if (elm.nodeType == 3) { // 文本節點 return elm.nodeValue.length; } var count = 0; for (var i = 0, child; child = elm.childNodes[i]; i++) { count += countChars(child); } return count; }

這裏須要說明一個潛在問題——既然匿名函數沒有名字,那該怎麼遞歸調用它呢?在這一點上,JavaScript 容許你命名這個函數表達式。你能夠命名當即調用的函數表達式(IIFES——Immediately Invoked Function Expressions),以下所示:

var charsInBody = (function counter(elm) { if (elm.nodeType == 3) { // 文本節點 return elm.nodeValue.length; } var count = 0; for (var i = 0, child; child = elm.childNodes[i]; i++) { count += counter(child); } return count; })(document.body);

如上所提供的函數表達式的名稱的做用域僅僅是該函數自身。這容許引擎去作更多的優化,而且這種實現更可讀、友好。該名稱也顯示在調試器和一些堆棧跟蹤中,節省了調試時的時間。

須要注意的是 JavaScript 函數是它們自己的對象——就和 JavaScript 其餘一切同樣——你能夠給它們添加屬性或者更改它們的屬性,這與前面的對象部分同樣。

自定義對象

備註:關於 JavaScript 中面向對象編程更詳細的信息,請參考  JavaScript 面向對象簡介

在經典的面嚮對象語言中,對象是指數據和在這些數據上進行的操做的集合。與 C++ 和 Java 不一樣,JavaScript 是一種基於原型的編程語言,並無 class 語句,而是把函數用做類。那麼讓咱們來定義一我的名對象,這個對象包括人的姓和名兩個域(field)。名字的表示有兩種方法:「名 姓(First Last)」或「姓, 名(Last, First)」。使用咱們前面討論過的函數和對象概念,能夠像這樣完成定義:

function makePerson(first, last) { return { first: first, last: last } } function personFullName(person) { return person.first + ' ' + person.last; } function personFullNameReversed(person) { return person.last + ', ' + person.first } s = makePerson("Simon", "Willison"); personFullName(s); // Simon Willison personFullNameReversed(s); // Willison, Simon

上面的寫法雖然能夠知足要求,可是看起來很麻煩,由於須要在全局命名空間中寫不少函數。既然函數自己就是對象,若是須要使一個函數隸屬於一個對象,那麼不可貴到:

function makePerson(first, last) { return { first: first, last: last, fullName: function() { return this.first + ' ' + this.last; }, fullNameReversed: function() { return this.last + ', ' + this.first; } } } s = makePerson("Simon", "Willison"); s.fullName(); // Simon Willison s.fullNameReversed(); // Willison, Simon

上面的代碼裏有一些咱們以前沒有見過的東西:關鍵字 this。當使用在函數中時,this 指代當前的對象,也就是調用了函數的對象。若是在一個對象上使用點或者方括號來訪問屬性或方法,這個對象就成了 this。若是並無使用「點」運算符調用某個對象,那麼 this 將指向全局對象(global object)。這是一個常常出錯的地方。例如:

s = makePerson("Simon", "Willison"); var fullName = s.fullName; fullName(); // undefined undefined

當咱們調用 fullName() 時,this 其實是指向全局對象的,並無名爲 first 或 last 的全局變量,因此它們兩個的返回值都會是 undefined

下面使用關鍵字 this 改進已有的 makePerson函數:

function Person(first, last) { this.first = first; this.last = last; this.fullName = function() { return this.first + ' ' + this.last; } this.fullNameReversed = function() { return this.last + ', ' + this.first; } } var s = new Person("Simon", "Willison");

咱們引入了另一個關鍵字:new,它和 this 密切相關。它的做用是建立一個嶄新的空對象,而後使用指向那個對象的 this 調用特定的函數。注意,含有 this 的特定函數不會返回任何值,只會修改this 對象自己。new 關鍵字將生成的 this 對象返回給調用方,而被 new 調用的函數成爲構造函數。習慣的作法是將這些函數的首字母大寫,這樣用 new 調用他們的時候就容易識別了。

不過這個改進的函數仍是和上一個例子同樣,單獨調用fullName() 時會產生相同的問題。

咱們的 Person 對象如今已經至關完善了,但還有一些不太好的地方。每次咱們建立一個 Person 對象的時候,咱們都在其中建立了兩個新的函數對象——若是這個代碼能夠共享不是更好嗎?

function personFullName() { return this.first + ' ' + this.last; } function personFullNameReversed() { return this.last + ', ' + this.first; } function Person(first, last) { this.first = first; this.last = last; this.fullName = personFullName; this.fullNameReversed = personFullNameReversed; }

這種寫法的好處是,咱們只須要建立一次方法函數,在構造函數中引用它們。那是否還有更好的方法呢?答案是確定的。

function Person(first, last) { this.first = first; this.last = last; } Person.prototype.fullName = function() { return this.first + ' ' + this.last; } Person.prototype.fullNameReversed = function() { return this.last + ', ' + this.first; }

Person.prototype 是一個能夠被Person的全部實例共享的對象。它是一個名叫原型鏈(prototype chain)的查詢鏈的一部分:當你試圖訪問一個 Person 沒有定義的屬性時,解釋器會首先檢查這個Person.prototype 來判斷是否存在這樣一個屬性。因此,任何分配給 Person.prototype 的東西對經過 this 對象構造的實例都是可用的。

這個特性功能十分強大,JavaScript 容許你在程序中的任什麼時候候修改原型(prototype)中的一些東西,也就是說你能夠在運行時(runtime)給已存在的對象添加額外的方法:

s = new Person("Simon", "Willison"); s.firstNameCaps(); // TypeError on line 1: s.firstNameCaps is not a function Person.prototype.firstNameCaps = function() { return this.first.toUpperCase() } s.firstNameCaps(); // SIMON

有趣的是,你還能夠給 JavaScript 的內置函數原型(prototype)添加東西。讓咱們給 String 添加一個方法用來返回逆序的字符串:

var s = "Simon"; s.reversed(); // TypeError on line 1: s.reversed is not a function String.prototype.reversed = function() { var r = ""; for (var i = this.length - 1; i >= 0; i--) { r += this[i]; } return r; } s.reversed(); // nomiS

定義新方法也能夠在字符串字面量上用(string literal)。

"This can now be reversed".reversed(); // desrever eb won nac sihT

正如我前面提到的,原型組成鏈的一部分。那條鏈的根節點是 Object.prototype,它包括toString() 方法——將對象轉換成字符串時調用的方法。這對於調試咱們的 Person 對象頗有用:

var s = new Person("Simon", "Willison"); s; // [object Object] Person.prototype.toString = function() { return '<Person: ' + this.fullName() + '>'; } s.toString(); // <Person: Simon Willison>

你是否還記得以前咱們說的 avg.apply() 中的第一個參數 null?如今咱們能夠回頭看看這個東西了。apply() 的第一個參數應該是一個被看成 this 來看待的對象。下面是一個 new 方法的簡單實現:

function trivialNew(constructor, ...args) { var o = {}; // 建立一個對象 constructor.apply(o, args); return o; }

這並非 new 的完整實現,由於它沒有建立原型(prototype)鏈。想舉例說明 new 的實現有些困難,由於你不會常常用到這個,可是適當瞭解一下仍是頗有用的。在這一小段代碼裏,...args(包括省略號)叫做剩餘參數(rest arguments)。如名所示,這個東西包含了剩下的參數。

所以調用

var bill = trivialNew(Person, "William", "Orange");

可認爲和調用以下語句是等效的

var bill = new Person("William", "Orange");

apply() 有一個姐妹函數,名叫 call,它也能夠容許你設置 this,但它帶有一個擴展的參數列表而不是一個數組。

function lastNameCaps() { return this.last.toUpperCase(); } var s = new Person("Simon", "Willison"); lastNameCaps.call(s); // 和如下方式等價 s.lastNameCaps = lastNameCaps; s.lastNameCaps();

內部函數

JavaScript 容許在一個函數內部定義函數,這一點咱們在以前的 makePerson() 例子中也見過。關於 JavaScript 中的嵌套函數,一個很重要的細節是它們能夠訪問父函數做用域中的變量:

function betterExampleNeeded() { var a = 1; function oneMoreThanA() { return a + 1; } return oneMoreThanA(); }

若是某個函數依賴於其餘的一兩個函數,而這一兩個函數對你其他的代碼沒有用處,你能夠將它們嵌套在會被調用的那個函數內部,這樣作能夠減小全局做用域下的函數的數量,這有利於編寫易於維護的代碼。

這也是一個減小使用全局變量的好方法。當編寫複雜代碼時,程序員每每試圖使用全局變量,將值共享給多個函數,但這樣作會使代碼很難維護。內部函數能夠共享父函數的變量,因此你可使用這個特性把一些函數捆綁在一塊兒,這樣能夠有效地防止「污染」你的全局命名空間——你能夠稱它爲「局部全局(local global)」。雖然這種方法應該謹慎使用,但它確實頗有用,應該掌握。

閉包

下面咱們將看到的是 JavaScript 中必須提到的功能最強大的抽象概念之一:閉包。但它可能也會帶來一些潛在的困惑。那它到底是作什麼的呢?

function makeAdder(a) { return function(b) { return a + b; } } var x = makeAdder(5); var y = makeAdder(20); x(6); // ? y(7); // ?

makeAdder 這個名字自己應該能說明函數是用來作什麼的:它建立了一個新的 adder 函數,這個函數自身帶有一個參數,它被調用的時候這個參數會被加在外層函數傳進來的參數上。

這裏發生的事情和前面介紹過的內嵌函數十分類似:一個函數被定義在了另一個函數的內部,內部函數能夠訪問外部函數的變量。惟一的不一樣是,外部函數被返回了,那麼常識告訴咱們局部變量「應該」再也不存在。可是它們卻仍然存在——不然 adder 函數將不能工做。也就是說,這裏存在 makeAdder 的局部變量的兩個不一樣的「副本」——一個是 a 等於5,另外一個是 a 等於20。那些函數的運行結果就以下所示:

x(6); // 返回 11 y(7); // 返回 27

下面來講說到底發生了什麼。每當 JavaScript 執行一個函數時,都會建立一個做用域對象(scope object),用來保存在這個函數中建立的局部變量。它和被傳入函數的變量一塊兒被初始化。這與那些保存的全部全局變量和函數的全局對象(global object)相似,但仍有一些很重要的區別,第一,每次函數被執行的時候,就會建立一個新的,特定的做用域對象;第二,與全局對象(在瀏覽器裏面是當作window 對象來訪問的)不一樣的是,你不能從 JavaScript 代碼中直接訪問做用域對象,也沒有能夠遍歷當前的做用域對象裏面屬性的方法。

因此當調用 makeAdder 時,解釋器建立了一個做用域對象,它帶有一個屬性:a,這個屬性被看成參數傳入 makeAdder 函數。而後 makeAdder 返回一個新建立的函數。一般 JavaScript 的垃圾回收器會在這時回收 makeAdder 建立的做用域對象,可是返回的函數卻保留一個指向那個做用域對象的引用。結果是這個做用域對象不會被垃圾回收器回收,直到指向 makeAdder 返回的那個函數對象的引用計數爲零。

做用域對象組成了一個名爲做用域鏈(scope chain)的鏈。它相似於原形(prototype)鏈同樣,被 JavaScript 的對象系統使用。

一個閉包就是一個函數和被建立的函數中的做用域對象的組合。

......

相關文章
相關標籤/搜索