從新介紹 JavaScript(JS 教程)

概覽

JavaScript 是一種多範式的動態語言,它包含類型、運算符、標準內置( built-in)對象和方法。它的語法來源於 Java 和 C,因此這兩種語言的許多語法特性一樣適用於 JavaScript。JavaScript 經過原型鏈而不是類來支持面向對象編程(有關 ES6 類的內容參考這裏Classes,有關對象原型參考見此繼承與原型鏈)。JavaScript一樣支持函數編程-由於它們也是對象,函數也能夠被保存在變量中,而且像其餘對象同樣被傳遞。javascript

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

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

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位整型不支持的操做,這是爲了便於進行位操做。

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

Math.sin(3.5);
var circumference = 2 * Math.PI * r;
複製代碼

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

parseInt("123", 10); // 123
parseInt("010", 10); // 10
複製代碼

一些老版本的瀏覽器會將首字符爲「0」的字符串當作八進制數字,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, -InfinityNaN則返回false

isFinite(1/0); // false
isFinite(Infinity); // false
isFinite(-Infinity); // false
isFinite(NaN); // false

isFinite(0); // true
isFinite(2e64); // true

isFinite("0"); // true
// 若是是純數值類型的檢測,則返回 false:
Number.isFinite("0"); // false
複製代碼

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

字符串

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

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

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

"hello".length; // 5
複製代碼

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

"hello".charAt(0); // "h"
"hello, world".replace("world", "mars"); // "hello, mars"
"hello".toUpperCase(); // "HELLO"
複製代碼

其餘類型

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

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

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

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

Boolean(""); // false
Boolean(234); // true
複製代碼

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

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

變量

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

**let** 語句聲明一個塊級做用域的本地變量,而且可選的將其初始化爲一個值。

let a;
let name = 'Simon';
複製代碼

下面是使用 **let** 聲明變量做用域的例子:

// myLetVariable is *not* visible out here

for (let myLetVariable = 0; myLetVariable < 5; myLetVariable++) {
  // myLetVariable is only visible in here
}

// myLetVariable is *not* visible out here
複製代碼

**const** 容許聲明一個不可變的常量。這個常量在定義域內老是可見的。

const Pi = 3.14; // 設置 Pi 的值 
Pi = 1; // 將會拋出一個錯誤由於你改變了一個常量的值。
複製代碼

**var** 是最多見的聲明變量的關鍵字。它沒有其餘兩個關鍵字的種種限制。這是由於它是傳統上在 JavaScript 聲明變量的惟一方法。使用 var 聲明的變量在它所聲明的整個函數都是可見的。

var a;
var name = "simon";
複製代碼

一個使用 var 聲明變量的語句塊的例子:

// myVarVariable *is* visible out here 

for (var myVarVariable = 0; myVarVariable < 5; myVarVariable++) { 
  // myVarVariable is visible to the whole function 
} 

// myVarVariable *is* visible out here
複製代碼

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

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

運算符

JavaScript的算術操做符包括 +-*/% ——求餘(與模運算相同)。賦值使用 = 運算符,此外還有一些複合運算符,如 +=-=,它們等價於 x = x *operator* 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 語言相似。可使用 ifelse 來定義條件語句,還能夠連起來使用:

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++) {
  // 將會執行五次
}
複製代碼

JavaScript 也還包括其餘兩種重要的 for 循環: for...of

for (let value of array) {
  // do something with value
}
複製代碼

for...in

for (let property in object) {
  // do something with object property
}
複製代碼

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

var name = o && o.getName();
複製代碼

或用於緩存值(當錯誤值無效時):

var name = cachedName || (cachedName = getName());
複製代碼

相似地,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 語句是可選的。switchcase 均可以使用須要運算才能獲得結果的表達式;在 switch 的表達式和 case 的表達式是使用 === 嚴格相等運算符進行比較的:

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

對象

JavaScript 中的對象,Object,能夠簡單理解成「名稱-值」對(而不是鍵值對:如今,ES 2015 的映射表(Map),比對象更接近鍵值對),不難聯想 JavaScript 中的對象與下面這些概念相似:

  • Python 中的字典(Dictionary)
  • Perl 和 Ruby 中的散列/哈希(Hash)
  • C/C++ 中的散列表(Hash table)
  • Java 中的散列映射表(HashMap)
  • PHP 中的關聯數組(Associative array)

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

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

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

var obj = new Object();
複製代碼

和:

var obj = {};
複製代碼

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

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

var obj = {
    name: "Carrot",
    "for": "Max",//'for' 是保留字之一,使用'_for'代替
    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;
複製代碼

和:

// bracket notation
obj['name'] = 'Simon';
var name = obj['name'];
// can use a variable to define a key
var user = prompt('what is your key?')
obj[user] = prompt('what is its value?')
複製代碼

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

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. 解釋對象原型和對象原型鏈能夠參見:繼承與原型鏈

**注意:**從 EcmaScript 6 開始,對象鍵能夠在建立時使用括號表示法由變量定義。{[phoneType]: 12345} 能夠用來替換 var userPhone = {}; userPhone[phoneType] = 12345 .

數組

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]
}
複製代碼

ES2015 引入了更加簡潔的 for...of 循環,能夠用它來遍歷可迭代對象,例如數組:

for (const currentValue of a) {
  // Do something with currentValue
}
複製代碼

遍歷數組的另外一種方法是使用 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);
複製代碼

除了 forEach()push(),Array(數組)類還自帶了許多方法。建議查看 Array 方法的完整文檔

方法名稱 描述
a.toString() 返回一個包含數組中全部元素的字符串,每一個元素經過逗號分隔。
a.toLocaleString() 根據宿主環境的區域設置,返回一個包含數組中全部元素的字符串,每一個元素經過逗號分隔。
a.concat(item1[, item2[, ...[, itemN]]]) 返回一個數組,這個數組包含原先 aitem一、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(item1[, item2[, ...[, itemN]]]) 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
複製代碼

這個就有用多了,可是卻有些冗長。爲了使代碼變短一些,咱們可使用剩餘參數來替換arguments的使用。在這方法中,咱們能夠傳遞任意數量的參數到函數中同時儘可能減小咱們的代碼。這個剩餘參數操做符在函數中以:...variable 的形式被使用,它將包含在調用函數時使用的未捕獲整個參數列表到這個變量中。咱們一樣也能夠將 for 循環替換爲 for...of 循環來返回咱們變量的值。

function avg(...args) {
  var sum = 0;
  for (let value of args) {
    sum += value;
  }
  return sum / args.length;
}

avg(2, 3, 4, 5); // 3.5
複製代碼

在上面這段代碼中,全部被傳入該函數的參數都被變量 args 所持有。

須要注意的是,不管「剩餘參數操做符」被放置到函數聲明的哪裏,它都會把除了本身以前的全部參數存儲起來。好比函數:function avg(firstValue, ...args) 會把傳入函數的第一個值存入 firstValue,其餘的參數存入 args。這是雖然一個頗有用的語言特性,卻也會帶來新的問題。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,咱們將在後面討論。這也正說明了一個事實——函數也是對象。

經過使用展開語法,你也能夠得到一樣的效果。

好比說:avg(...numbers)

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) { // TEXT_NODE 文本節點
        return elm.nodeValue.length;
    }
    var count = 0;
    for (var i = 0, child; child = elm.childNodes[i]; i++) {
        count += countChars(child);
    }
    return count;
}
複製代碼

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

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 其實是指向全局對象的,並無名爲 firstlast 的全局變量,因此它們兩個的返回值都會是 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 parentFunc() {
  var a = 1;

  function nestedFunc() {
    var b = 4; // parentFunc 沒法訪問 b
    return a + b;
  }
  return nestedFunc(); // 5
}
複製代碼

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

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

閉包

閉包是 JavaScript 中最強大的抽象概念之一——但它也是最容易形成困惑的。它到底是作什麼的呢?

function makeAdder(a) {
  return function(b) {
    return a + b;
  }
}
var add5 = makeAdder(5);
var add20 = makeAdder(20);
add5(6); // ?
add20(7); // ?
複製代碼

makeAdder 這個名字自己,便應該能說明函數是用來作什麼的:它會用一個參數來建立一個新的「adder」函數,再用另外一個參數來調用被建立的函數時,makeAdder 會將一前一後兩個參數相加。

從被建立的函數的視角來看的話,這兩個參數的來源問題會更顯而易見:新函數自帶一個參數——在新函數被建立時,便欽定、欽點了前一個參數(如上方代碼中的 a、5 和 20,參考 makeAdder 的結構,它應當位於新函數外部);新函數被調用時,又接收了後一個參數(如上方代碼中的 b、6 和 7,位於新函數內部)。最終,新函數被調用的時候,前一個參數便會和由外層函數傳入的後一個參數相加。

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

x(6); // 返回 11
y(7); // 返回 27
複製代碼

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

因此,當調用 makeAdder 時,解釋器建立了一個做用域對象,它帶有一個屬性:a,這個屬性被看成參數傳入 makeAdder 函數。而後 makeAdder 返回一個新建立的函數(暫記爲 adder)。一般,JavaScript 的垃圾回收器會在這時回收 makeAdder 建立的做用域對象(暫記爲 b),可是,makeAdder 的返回值,新函數 adder,擁有一個指向做用域對象 b 的引用。最終,做用域對象 b 不會被垃圾回收器回收,直到沒有任何引用指向新函數 adder

做用域對象組成了一個名爲做用域鏈(scope chain)的(調用)鏈。它和 JavaScript 的對象系統使用的原型(prototype)鏈相相似。

一個閉包,就是 一個函數 與其 被建立時所帶有的做用域對象 的組合。閉包容許你保存狀態——因此,它們能夠用來代替對象。這個 StackOverflow 帖子裏有一些關於閉包的詳細介紹。

相關文章
相關標籤/搜索