JavaScript 是一種多範式的動態語言,它包含類型、運算符、標準內置( built-in)對象和方法。它的語法來源於 Java 和 C,因此這兩種語言的許多語法特性一樣適用於 JavaScript。JavaScript 經過原型鏈而不是類來支持面向對象編程(有關 ES6 類的內容參考這裏Classes
,有關對象原型參考見此繼承與原型鏈)。JavaScript一樣支持函數編程-由於它們也是對象,函數也能夠被保存在變量中,而且像其餘對象同樣被傳遞。javascript
先從任何編程語言都不可缺乏的組成部分——「類型」開始。JavaScript 程序能夠修改值(value),這些值都有各自的類型。JavaScript 中的類型包括:java
…哦,還有看上去有些…奇怪的 undefined
(未定義)類型和 null
(空)類型。此外還有Array
(數組)類型,以及分別用於表示日期和正則表達式的 Date
(日期)和 RegExp
(正則表達式),這三種類型都是特殊的對象。嚴格意義上說,Function(函數)也是一種特殊的對象。因此準確來講,JavaScript 中的類型應該包括這些:node
Number
(數字)git
String
(字符串)程序員
Boolean
(布爾)github
Symbol
(符號)(ES2015 新增)正則表達式
Object
編程
(對象)數組
null
(空)瀏覽器
undefined
(未定義)
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
, -Infinity
或 NaN則返回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 包含布爾類型,這個類型的變量有兩個可能的值,分別是 true
和 false
(二者都是關鍵字)。根據具體須要,JavaScript 按照以下規則將變量轉換成布爾類型:
false
、0
、空字符串(""
)、NaN
、null
和 undefined
被轉換爲 false
true
也可使用 Boolean()
函數進行顯式轉換:
Boolean(""); // false
Boolean(234); // true
複製代碼
不過通常不必這麼作,由於 JavaScript 會在須要一個布爾變量時隱式完成這個轉換操做(好比在 if
條件語句中)。因此,有時咱們能夠把轉換成布爾值後的變量分別稱爲 真值(true values)——即值爲 true 和 假值(false values)——即值爲 false;也能夠分別稱爲「真的」(truthy)和「假的」(falsy)。
JavaScript 支持包括 &&
(邏輯與)、||
(邏輯或)和!
(邏輯非)在內的一些邏輯運算符。下面會有所提到。
在 JavaScript 中聲明一個新變量的方法是使用關鍵字 let
、const
和 var
:
**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 開始將有所不一樣的, let
和 const
關鍵字容許你建立塊做用域的變量。
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 語言相似。可使用 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++) {
// 將會執行五次
}
複製代碼
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
語句是可選的。switch
和 case
均可以使用須要運算才能獲得結果的表達式;在 switch
的表達式和 case
的表達式是使用 ===
嚴格相等運算符進行比較的:
switch(1 + 3){
case 2 + 2:
yay();
break;
default:
neverhappens();
}
複製代碼
JavaScript 中的對象,Object,能夠簡單理解成「名稱-值」對(而不是鍵值對:如今,ES 2015 的映射表(Map),比對象更接近鍵值對),不難聯想 JavaScript 中的對象與下面這些概念相似:
這樣的數據結構設計合理,能應付各種複雜需求,因此被各種編程語言普遍採用。正由於 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]]]) |
返回一個數組,這個數組包含原先 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(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
其實是指向全局對象的,並無名爲 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 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 帖子裏有一些關於閉包的詳細介紹。