JavaScript 學習筆記

筆記摘錄自廖雪峯的官方網站


入門

基本語法

賦值:
下面的一行代碼就是一個完整的賦值語句:

varx=1;

數據類型:

Number:

JavaScript不區分整數和浮點數,統一用Number表示,以下都是合法的Number類型:

123;//整數123
0.456;//浮點數0.456
1.2345e3;//科學計數法表示1.2345x1000,等同於1234.5
-99;//負數
NaN;//NaN表示NotaNumber,當無法計算結果時用NaN表示
Infinity;//Infinity表示無限大,當數值超過了JavaScript的Number所能表示的最大值時,就表示爲Infinity

計算機由於使用二進制,所以,有時候用十六進制表示整數比較方便,十六進制用0x前綴和0-9,a-f表示,例如:0xff000xa5b4c3d2,等等,它們和十進制表示的數值完全一樣。

Number可以直接做四則運算,規則和數學一致:

1+2;//3
(1+2)*5/2;//7.5
2/0;//Infinity
0/0;//NaN
10%3;//1
10.5%3;//1.5

%是求餘運算。

字符串:

字符串是以單引號’或雙引號”括起來的任意文本,比如'abc'"xyz"等等。

布爾值:

true;//這是一個truefalse;//這是一個false2>1;//這是一個true2>=3;//這是一個false

邏輯運算符:

&&運算是與運算,||運算是或運算,!運算是非運算

比較運算符

對Number做比較時,可以通過比較運算符得到一個布爾值:

2>5;//false
5>=2;//true
7==7;//true

JavaScript允許對任意數據類型做比較:

false==0;//true
false===0;//false

要特別注意相等運算符==。JavaScript在設計時,有兩種比較運算符:

第一種是==比較,它會自動轉換數據類型再比較,很多時候,會得到非常詭異的結果;

第二種是===比較,它不會自動轉換數據類型,如果數據類型不一致,返回false,如果一致,再比較。

由於JavaScript這個設計缺陷,不要使用==比較,始終堅持使用===比較。

另一個例外是NaN這個特殊的Number與所有其他值都不相等,包括它自己:

NaN===NaN;//false

唯一能判斷NaN的方法是通過isNaN()函數:

isNaN(NaN);//true

null和undefined

null表示一個“空”的值,它和0以及空字符串”不同,0是一個數值,”表示長度爲0的字符串,而null表示“空”。

類似的undefined,它表示“未定義”,大多數情況下,我們都應該用null。undefined僅僅在判斷函數參數是否傳遞的情況下有用。

數組

JavaScript的數組可以包括任意數據類型。例如:

[1,2,3.14,'Hello',null,true];

另一種創建數組的方法是通過Array()函數實現:

newArray(1,2,3);//創建了數組[1,2,3]

數組的元素可以通過索引來訪問。請注意,索引的起始值爲0:

vararr=[1,2,3.14,'Hello',null,true];
arr[0];//返回索引爲0的元素,即1
arr[5];//返回索引爲5的元素,即true
arr[6];//索引超出了範圍,返回undefined

對象

JavaScript的對象是一組由鍵-值組成的無序集合,例如:

varperson={
name:'Bob',
age:20,
tags:['js','web','mobile'],
city:'Beijing',
hasCar:true,
zipcode:null
};

JavaScript對象的鍵都是字符串類型,值可以是任意數據類型。

要獲取一個對象的屬性,我們用對象變量.屬性名的方式:

person.name;//'Bob'
person.zipcode;//null

變量

變量在JavaScript中就是用一個變量名錶示,變量名是大小寫英文、數字、$和_的組合,且不能用數字開頭。變量名也不能是JavaScript的關鍵字,如ifwhile等。

申明一個變量用var語句,比如:

vara;//申明瞭變量a,此時a的值爲undefined
var$b=1;//申明瞭變量$b,同時給$b賦值,此時$b的值爲1
vars_007='007';//s_007是一個字符串
varAnswer=true;//Answer是一個布爾值true
vart=null;//t的值是null

在JavaScript中,使用等號=對變量進行賦值。可以把任意數據類型賦值給變量,同一個變量可以反覆賦值,而且可以是不同類型的變量,但是要注意只能用var申明一次,例如:

vara=123;//a的值是整數123
a='ABC';//a變爲字符串

這種變量本身類型不固定的語言稱之爲動態語言,與之對應的是靜態語言

在同一個頁面的不同的JavaScript文件中,如果都不用var申明,恰好都使用了變量i,將造成變量i互相影響,產生難以調試的錯誤結果。

使用var申明的變量則不是全局變量,它的範圍被限制在該變量被申明的函數體內(函數的概念將稍後講解),同名變量在不同的函數體內互不衝突。

爲了修補JavaScript這一嚴重設計缺陷,ECMA在後續規範中推出了strict模式,在strict模式下運行的JavaScript代碼,強制通過var申明變量,未使用var申明變量就使用的,將導致運行錯誤。

啓用strict模式的方法是在JavaScript代碼的第一行寫上:'usestrict';

字符串

ASCII字符可以以\x##形式的十六進制表示,例如'\x41'完全等同於'A'

還可以用\u####表示一個Unicode字符'\u4e2d\u6587'完全等同於'中文'

多行字符串用`…`表示:

`這是一個
多行
字符串`;

數組

JavaScript的Array可以包含任意數據類型,並通過索引來訪問每個元素。

要取得Array的長度,直接訪問length屬性

vararr=[1,2,3.14,'Hello',null,true];
arr.length;//6

請注意,如果通過索引賦值時,索引超過了範圍,同樣會引起Array大小的變化:

var arr = [1, 2, 3];
arr[5] = 'x';
arr; // arr變爲[1, 2, 3, undefined, undefined, 'x']

大多數其他編程語言不允許直接改變數組的大小,越界訪問索引會報錯。然而,JavaScript的Array卻不會有任何錯誤。在編寫代碼時,不建議直接修改Array的大小,訪問索引時要確保索引不會越界。

indexOf

與String類似,Array也可以通過indexOf()來搜索一個指定的元素的位置:

var arr = [10, 20, '30', 'xyz'];
arr.indexOf(10); // 元素10的索引爲0
arr.indexOf(20); // 元素20的索引爲1
arr.indexOf(30); // 元素30沒有找到,返回-1
arr.indexOf('30'); // 元素'30'的索引爲2

slice

slice()就是對應String的substring()版本,它截取Array的部分元素,然後返回一個新的Array:

var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
arr.slice(0, 3); // 從索引0開始,到索引3結束,但不包括索引3: ['A', 'B', 'C']
arr.slice(3); // 從索引3開始到結束: ['D', 'E', 'F', 'G']

注意到slice()的起止參數包括開始索引,不包括結束索引。

如果不給slice()傳遞任何參數,它就會從頭到尾截取所有元素。利用這一點,我們可以很容易地複製一個Array。

push和pop

push()向Array的末尾添加若干元素,pop()則把Array的最後一個元素刪除掉:

var arr = [1, 2];
arr.push('A', 'B'); // 返回Array新的長度: 4
arr; // [1, 2, 'A', 'B']
arr.pop(); // pop()返回'B'
arr; // [1, 2, 'A']
arr.pop(); arr.pop(); arr.pop(); // 連續pop 3次
arr; // []
arr.pop(); // 空數組繼續pop不會報錯,而是返回undefined
arr; // []

unshift和shift

如果要往Array的頭部添加若干元素,使用unshift()方法,shift()方法則把Array的第一個元素刪掉:

var arr = [1, 2];
arr.unshift('A', 'B'); // 返回Array新的長度: 4
arr; // ['A', 'B', 1, 2]
arr.shift(); // 'A'
arr; // ['B', 1, 2]
arr.shift(); arr.shift(); arr.shift(); // 連續shift 3次
arr; // []
arr.shift(); // 空數組繼續shift不會報錯,而是返回undefined
arr; // []

sort

sort()可以對當前Array進行排序,它會直接修改當前Array的元素位置,直接調用時,按照默認順序排序:

var arr = ['B', 'C', 'A'];
arr.sort();
arr; // ['A', 'B', 'C']

reverse

reverse()把整個Array的元素反轉:

var arr = ['one', 'two', 'three'];
arr.reverse(); 
arr; // ['three', 'two', 'one']

splice

splice()方法是修改Array的“萬能方法”,它可以從指定的索引開始刪除若干元素,然後再從該位置添加若干元素:

var arr = ['Microsoft', 'Apple', 'Yahoo', 'AOL', 'Excite', 'Oracle'];
// 從索引2開始刪除3個元素,然後再添加兩個元素:
arr.splice(2, 3, 'Google', 'Facebook'); // 返回刪除的元素 ['Yahoo', 'AOL', 'Excite']
arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']
// 只刪除,不添加:
arr.splice(2, 2); // ['Google', 'Facebook']
arr; // ['Microsoft', 'Apple', 'Oracle']
// 只添加,不刪除:
arr.splice(2, 0, 'Google', 'Facebook'); // 返回[],因爲沒有刪除任何元素
arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']

concat

concat()方法把當前的Array和另一個Array連接起來,並返回一個新的Array:

var arr = ['A', 'B', 'C'];
var added = arr.concat([1, 2, 3]);
added; // ['A', 'B', 'C', 1, 2, 3]
arr; // ['A', 'B', 'C']

concat()方法並沒有修改當前Array,而是返回了一個新的Array。

實際上,concat()方法可以接收任意個元素和Array,並且自動把Array拆開,然後全部添加到新的Array裏:

var arr = ['A', 'B', 'C'];
arr.concat(1, 2, [3, 4]); // ['A', 'B', 'C', 1, 2, 3, 4]

join

join()方法是一個非常實用的方法,它把當前Array的每個元素都用指定的字符串連接起來,然後返回連接後的字符串:

var arr = ['A', 'B', 'C', 1, 2, 3];
arr.join('-'); // 'A-B-C-1-2-3'

多維數組

如果數組的某個元素又是一個Array,則可以形成多維數組,例如:

如果數組的某個元素又是一個Array,則可以形成多維數組,例如:

上述Array包含3個元素,其中頭兩個元素本身也是Array。

對象

JavaScript的對象是一種無序的集合數據類型,它由若干鍵值對組成。

var xiaoming = {
    name: '小明',
    birth: 1990,
    school: 'No.1 Middle School',
    height: 1.70,
    weight: 65,
    score: null
};

上述對象申明瞭一個name屬性,值是’小明’,birth屬性,值是1990,以及其他一些屬性。最後,把這個對象賦值給變量xiaoming後,就可以通過變量xiaoming來獲取小明的屬性了:

xiaoming.name; // '小明'
xiaoming.birth; // 1990

由於JavaScript的對象是動態類型,你可以自由地給一個對象添加或刪除屬性:

var xiaoming = {
    name: '小明'
};
xiaoming.age; // undefined
xiaoming.age = 18; // 新增一個age屬性
xiaoming.age; // 18
delete xiaoming.age; // 刪除age屬性
xiaoming.age; // undefined
delete xiaoming['name']; // 刪除name屬性
xiaoming.name; // undefined
delete xiaoming.school; // 刪除一個不存在的school屬性也不會報錯

如果我們要檢測xiaoming是否擁有某一屬性,可以用in操作符:

var xiaoming = {
    name: '小明',
    birth: 1990,
    school: 'No.1 Middle School',
    height: 1.70,
    weight: 65,
    score: null
};
'name' in xiaoming; // true
'grade' in xiaoming; // false

不過要小心,如果in判斷一個屬性存在,這個屬性不一定是xiaoming的,它可能是xiaoming繼承得到的:

'toString' in xiaoming; // true

因爲toString定義在object對象中,而所有對象最終都會在原型鏈上指向object,所以xiaoming也擁有toString屬性。

要判斷一個屬性是否是xiaoming自身擁有的,而不是繼承得到的,可以用hasOwnProperty()方法:

var xiaoming = {
    name: '小明'
};
xiaoming.hasOwnProperty('name'); // true
xiaoming.hasOwnProperty('toString'); // false

條件判斷

JavaScript使用if () { ... } else { ... }來進行條件判斷。

JavaScript把nullundefined0NaN和空字符串”視爲false,其他值一概視爲true,因此上述代碼條件判斷的結果是true

循環

JavaScript的循環有兩種,一種是for循環,通過初始條件、結束條件和遞增條件來循環執行語句塊:

for循環

var x = 0;
var i;
for (i=1; i<=10000; i++) {
    x = x + i;
}
x; // 50005000

i=1 這是初始條件,將變量i置爲1;
i<=10000 這是判斷條件,滿足時就繼續循環,不滿足就退出循環;
i++ 這是每次循環後的遞增條件,由於每次循環後變量i都會加1,因此它終將在若干次循環後不滿足判斷條件i<=10000而退出循環。

for循環最常用的地方是利用索引來遍歷數組:

var arr = ['Apple', 'Google', 'Microsoft'];
var i, x;
for (i=0; i<arr.length; i++) {
    x = arr[i];
    alert(x);
}

for … in

for循環的一個變體是for ... in循環,它可以把一個對象的所有屬性依次循環出來:

var o = {
    name: 'Jack',
    age: 20,
    city: 'Beijing'
};
for (var key in o) {
    alert(key); // 'name', 'age', 'city'
}

要過濾掉對象繼承的屬性,用hasOwnProperty()來實現:

var o = {
    name: 'Jack',
    age: 20,
    city: 'Beijing'
};
for (var key in o) {
    if (o.hasOwnProperty(key)) {
        alert(key); // 'name', 'age', 'city'
    }
}

由於Array也是對象,而它的每個元素的索引被視爲對象的屬性,因此,for … in循環可以直接循環出Array的索引:

var a = ['A', 'B', 'C'];
for (var i in a) {
    alert(i); // '0', '1', '2'
    alert(a[i]); // 'A', 'B', 'C'
}

for … in對Array的循環得到的是String而不是Number。

while

while循環只有一個判斷條件,條件滿足,就不斷循環,條件不滿足時則退出循環。

var x = 0;
var n = 99;
while (n > 0) {
    x = x + n;
    n = n - 2;
}
x; // 2500

在循環內部變量n不斷自減,直到變爲-1時,不再滿足while條件,循環退出。

do … while

do { … } while()循環,它和while循環的唯一區別在於,不是在每次循環開始的時候判斷條件,而是在每次循環完成的時候判斷條件

var n = 0;
do {
    n = n + 1;
} while (n < 100);
n; // 100

do { ... } while()循環要小心,循環體會至少執行1次,而forwhile循環則可能一次都不執行。

Map和Set

Map是一組鍵值對的結構,具有極快的查找速度。

用JavaScript寫一個Map如下:

var m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);
m.get('Michael'); // 95

初始化Map需要一個二維數組,或者直接初始化一個空Map。Map具有以下方法:

var m = new Map(); // 空Map
m.set('Adam', 67); // 添加新的key-value
m.set('Bob', 59);
m.has('Adam'); // 是否存在key 'Adam': true
m.get('Adam'); // 67
m.delete('Adam'); // 刪除key 'Adam'
m.get('Adam'); // undefined

多次對一個key放入value,後面的值會把前面的值沖掉

Set

Set和Map類似,算是其key的集合,不存儲value。由於key不能重複,所以,在Set中,沒有重複的元素。

要創建一個Set,需要提供一個Array作爲輸入,或者直接創建一個空Set:

var s1 = new Set(); // 空Set
var s2 = new Set([1, 2, 3]); // 含1, 2, 3

重複元素在Set中自動被過濾:

重複元素在Set中自動被過濾:

通過add(key)方法可以添加元素到Set中,可以重複添加,但不會有效果:

>>> s.add(4)
>>> s
{1, 2, 3, 4}
>>> s.add(4)
>>> s
{1, 2, 3, 4}

通過delete(key)方法可以刪除元素:

var s = new Set([1, 2, 3]);
s; // Set {1, 2, 3}
s.delete(3);
s; // Set {1, 2}

iterable

遍歷Array可以採用下標循環,遍歷MapSet就無法使用下標。爲了統一集合類型,ES6標準引入了新的iterable類型,ArrayMapSet都屬於iterable類型。

具有iterable類型的集合可以通過新的for ... of循環來遍歷。

用for … of循環遍歷集合,用法如下:

var a = ['A', 'B', 'C'];
var s = new Set(['A', 'B', 'C']);
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
for (var x of a) { // 遍歷Array
    alert(x);
}
for (var x of s) { // 遍歷Set
    alert(x);
}
for (var x of m) { // 遍歷Map
    alert(x[0] + '=' + x[1]);
}

for … in循環由於歷史遺留問題,它遍歷的實際上是對象的屬性名稱。
for … of循環則只循環集合本身的元素。

然而,更好的方式是直接使用iterable內置的forEach方法,它接收一個函數,每次迭代就自動回調該函數。

var a = ['A', 'B', 'C'];
a.forEach(function (element, index, array) {
    // element: 指向當前元素的值
    // index: 指向當前索引
    // array: 指向Array對象本身
    alert(element);
});

如果對某些參數不感興趣,由於JavaScript的函數調用不要求參數必須一致,因此可以忽略它們。例如,只需要獲得Arrayelement

var a = ['A', 'B', 'C'];
a.forEach(function (element) {
    alert(element);
});

函數

函數定義和調用

定義

在JavaScript中,定義函數的方式如下:

function abs(x) {
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
}

上述abs()函數的定義如下:

  • function指出這是一個函數定義;
  • abs是函數的名稱;
  • (x)括號內列出函數的參數,多個參數以,分隔;
  • { … }之間的代碼是函數體,可以包含若干語句,甚至可以沒有任何語句。

函數體內部的語句在執行時,一旦執行到return時,函數就執行完畢,並將結果返回。因此,函數內部通過條件判斷和循環可以實現非常複雜的邏輯。

如果沒有return語句,函數執行完畢後也會返回結果,只是結果爲undefined。

由於JavaScript的函數也是一個對象,上述定義的abs()函數實際上是一個函數對象,而函數名abs可以視爲指向該函數的變量。

因此,第二種定義函數的方式如下:

var abs = function (x) {
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
};

在這種方式下,function (x) { ... }是一個匿名函數,它沒有函數名。但是,這個匿名函數賦值給了變量abs,所以,通過變量abs就可以調用該函數。

上述兩種定義完全等價,注意第二種方式按照完整語法需要在函數體末尾加一個;,表示賦值語句結束。

調用

調用函數時,按順序傳入參數即可:

abs(10); // 返回10
abs(-9); // 返回9

arguments

JavaScript還有一個免費贈送的關鍵字arguments,它只在函數內部起作用,並且永遠指向當前函數的調用者傳入的所有參數。arguments類似Array但它不是一個Array:

function foo(x) {
    alert(x); // 10
    for (var i=0; i<arguments.length; i++) {
        alert(arguments[i]); // 10, 20, 30
    }
}
foo(10, 20, 30);

rest參數

由於JavaScript函數允許接收任意個參數,於是我們就不得不用arguments來獲取所有參數:

function foo(a, b) {
    var i, rest = [];
    if (arguments.length > 2) {
        for (i = 2; i<arguments.length; i++) {
            rest.push(arguments[i]);
        }
    }
    console.log('a = ' + a);
    console.log('b = ' + b);
    console.log(rest);
}

爲了獲取除了已定義參數a、b之外的參數,我們不得不用arguments,並且循環要從索引2開始以便排除前兩個參數,這種寫法很彆扭。

ES6標準引入了rest參數,上面的函數可以改寫爲:

function foo(a, b, ...rest) {
    console.log('a = ' + a);
    console.log('b = ' + b);
    console.log(rest);
}

foo(1, 2, 3, 4, 5);
// 結果:
// a = 1
// b = 2
// Array [ 3, 4, 5 ]

foo(1);
// 結果:
// a = 1
// b = undefined
// Array []

rest參數只能寫在最後,前面用…標識,從運行結果可知,傳入的參數先綁定ab,多餘的參數以數組形式交給變量rest,所以,不再需要arguments我們就獲取了全部參數。

如果傳入的參數連正常定義的參數都沒填滿,也不要緊,rest參數會接收一個空數組(注意不是undefined)。

作用域

在JavaScript中,用var申明的變量實際上是有作用域的。

如果一個變量在函數體內部申明,則該變量的作用域爲整個函數體,在函數體外不可引用該變量。

如果兩個不同的函數各自申明瞭同一個變量,那麼該變量只在各自的函數體內起作用。換句話說,不同函數內部的同名變量互相獨立,互不影響。

由於JavaScript的函數可以嵌套,此時,內部函數可以訪問外部函數定義的變量,反過來則不行:

'use strict';

function foo() {
    var x = 1;
    function bar() {
        var y = x + 1; // bar可以訪問foo的變量x!
    }
    var z = y + 1; // ReferenceError! foo不可以訪問bar的變量y!
}

如果內部函數和外部函數的變量名重名怎麼辦?
JavaScript的函數在查找變量時從自身函數定義開始,從“內”向“外”查找。如果內部函數定義了與外部函數重名的變量,則內部函數的變量將“屏蔽”外部函數的變量。

全局作用域

不在任何函數內定義的變量就具有全局作用域。實際上,JavaScript默認有一個全局對象window,全局作用域的變量實際上被綁定到window的一個屬性:

'use strict';

var course = 'Learn JavaScript';
alert(course); // 'Learn JavaScript'
alert(window.course); // 'Learn JavaScript'

因此,直接訪問全局變量course和訪問window.course是完全一樣的。

名字空間

全局變量會綁定到window上,不同的JavaScript文件如果使用了相同的全局變量,或者定義了相同名字的頂層函數,都會造成命名衝突,並且很難被發現。

減少衝突的一個方法是把自己的所有變量和函數全部綁定到一個全局變量中。例如:

// 唯一的全局變量MYAPP:
var MYAPP = {};

// 其他變量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;

// 其他函數:
MYAPP.foo = function () {
    return 'foo';
};

由於JavaScript的變量作用域實際上是函數內部,我們在for循環等語句塊中是無法定義具有局部作用域的變量的:

'use strict';

function foo() {
    for (var i=0; i<100; i++) {
        //
    }
    i += 100; // 仍然可以引用變量i
}

爲了解決塊級作用域,ES6引入了新的關鍵字let,用let替代var可以申明一個塊級作用域的變量:

'use strict';

function foo() {
    var sum = 0;
    for (let i=0; i<100; i++) {
        sum += i;
    }
    i += 1; // SyntaxError
}

常量

ES6標準引入了新的關鍵字const來定義常量,const與let都具有塊級作用域:

'use strict';

const PI = 3.14;
PI = 3; // 某些瀏覽器不報錯,但是無效果!
PI; // 3.14

方法

我們給xiaoming綁定一個函數,就可以做更多的事情。比如,寫個age()方法,返回xiaoming的年齡:

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: function () {
        var y = new Date().getFullYear();
        return y - this.birth;
    }
};

xiaoming.age; // function xiaoming.age()
xiaoming.age(); // 今年調用是25,明年調用就變成26了

apply

雖然在一個獨立的函數調用中,根據是否是strict模式,this指向undefinedwindow,不過,我們還是可以控制this的指向的!

要指定函數的this指向哪個對象,可以用函數本身的apply方法,它接收兩個參數,第一個參數就是需要綁定的this變量,第二個參數是Array,表示函數本身的參數。

apply修復getAge()調用:

function getAge() {
    var y = new Date().getFullYear();
    return y - this.birth;
}

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge
};

xiaoming.age(); // 25
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 參數爲空

裝飾器

利用apply(),我們還可以動態改變函數的行爲。

JavaScript的所有對象都是動態的,即使內置的函數,我們也可以重新指向新的函數。

現在假定我們想統計一下代碼一共調用了多少次parseInt(),可以把所有的調用都找出來,然後手動加上count += 1,不過這樣做太傻了。最佳方案是用我們自己的函數替換掉默認的parseInt():

var count = 0;
var oldParseInt = parseInt; // 保存原函數

window.parseInt = function () {
    count += 1;
    return oldParseInt.apply(null, arguments); // 調用原函數
};

// 測試:
parseInt('10');
parseInt('20');
parseInt('30');
count; // 3

高階函數

高階函數英文叫Higher-order function。那麼什麼是高階函數?

JavaScript的函數其實都指向某個變量。既然變量可以指向函數,函數的參數能接收變量,那麼一個函數就可以接收另一個函數作爲參數,這種函數就稱之爲高階函數。

一個最簡單的高階函數:

function add(x, y, f) {
    return f(x) + f(y);
}

當我們調用add(-5, 6, Math.abs)時,參數xyf分別接收-56和函數Math.abs根據函數定義,我們可以推導計算過程爲:

x = -5;
y = 6;
f = Math.abs;
f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11;
return 11;

編寫高階函數,就是讓函數的參數能夠接收別的函數。

map/reduce

map

舉例說明,比如我們有一個函數f(x)=x^2,要把這個函數作用在一個數組[1, 2, 3, 4, 5, 6, 7, 8, 9]上,就可以用map實現如下:
pic

由於map()方法定義在JavaScript的Array中,我們調用Arraymap()方法,傳入我們自己的函數,就得到了一個新的Array作爲結果:

function pow(x) {
    return x * x;
}

var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]

map()作爲高階函數,事實上它把運算規則抽象了,因此,我們不但可以計算簡單的f(x)=x2,還可以計算任意複雜的函數。

reduce

Arrayreduce()把一個函數作用在這個Array[x1, x2, x3...]上,這個函數必須接收兩個參數,reduce()把結果繼續和序列的下一個元素做累積計算,其效果就是:

[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)

比方說對一個Array求和,就可以用reduce實現:

var arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
    return x + y;
}); // 25

filter

filter也是一個常用的操作,它用於把Array的某些元素過濾掉,然後返回剩下的元素。 和map()類似,Arrayfilter()也接收一個函數。和map()不同的是,filter()把傳入的函數依次作用於每個元素,然後根據返回值是true還是false決定保留還是丟棄該元素。

例如,在一個Array中,刪掉偶數,只保留奇數,可以這麼寫:

var arr = [1, 2, 4, 5, 6, 9, 10, 15];
var r = arr.filter(function (x) {
    return x % 2 !== 0;
});
r; // [1, 5, 9, 15]

filter()接收的回調函數,其實可以有多個參數。通常我們僅使用第一個參數,表示Array的某個元素。回調函數還可以接收另外兩個參數,表示元素的位置和數組本身:

var arr = ['A', 'B', 'C'];
var r = arr.filter(function (element, index, self) {
    console.log(element); // 依次打印'A', 'B', 'C'
    console.log(index); // 依次打印0, 1, 2
    console.log(self); // self就是變量arr
    return true;
});

利用filter,可以巧妙地去除Array的重複元素:

'use strict';

var r, arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry'];

r = arr.filter(function (element, index, self) {
    return self.indexOf(element) === index;
});

sort

通常規定,對於兩個元素xy,如果認爲x < y,則返回-1,如果認爲x == y,則返回0,如果認爲x > y,則返回1,這樣,排序算法就不用關心具體的比較過程,而是根據比較結果直接排序。

JavaScript的Array的sort()方法就是用於排序的,但是排序結果可能讓你大吃一驚:

// 看上去正常的結果:
['Google', 'Apple', 'Microsoft'].sort(); // ['Apple', 'Google', 'Microsoft'];

// apple排在了最後:
['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple']

// 無法理解的結果:
[10, 20, 1, 2].sort(); // [1, 10, 2, 20]

第二個排序把apple排在了最後,是因爲字符串根據ASCII碼進行排序,而小寫字母a的ASCII碼在大寫字母之後。

第三個排序結果是什麼鬼?簡單的數字排序都能錯? 這是因爲Array的sort()方法默認把所有元素先轉換爲String再排序,結果’10’排在了’2’的前面,因爲字符’1’比字符’2’的ASCII碼小。

如果不知道sort()方法的默認排序規則,直接對數字排序,絕對栽進坑裏!

幸運的是,sort()方法也是一個高階函數,它還可以接收一個比較函數來實現自定義的排序。

要按數字大小排序,我們可以這麼寫:

var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
    if (x < y) {
        return -1;
    }
    if (x > y) {
        return 1;
    }
    return 0;
}); // [1, 2, 10, 20]

sort()方法會直接對Array進行修改,它返回的結果仍是當前Array

var a1 = ['B', 'A', 'C'];
var a2 = a1.sort();
a1; // ['A', 'B', 'C']
a2; // ['A', 'B', 'C']
a1 === a2; // true, a1和a2是同一對象

閉包

我們來實現一個對Array的求和。通常情況下,求和的函數是這樣定義的:

function sum(arr) {
    return arr.reduce(function (x, y) {
        return x + y;
    });
}

sum([1, 2, 3, 4, 5]); // 15

但是,如果不需要立刻求和,而是在後面的代碼中,根據需要再計算怎麼辦?可以不返回求和的結果,而是返回求和的函數!

function lazy_sum(arr) {
    var sum = function () {
        return arr.reduce(function (x, y) {
            return x + y;
        });
    }
    return sum;
}

當我們調用lazy_sum()時,返回的並不是求和結果,而是求和函數:

var f = lazy_sum([1, 2, 3, 4, 5]); // function sum()

調用函數f時,才真正計算求和的結果:

f(); // 15

在這個例子中,我們在函數lazy_sum中又定義了函數sum,並且,內部函數sum可以引用外部函數lazy_sum的參數和局部變量,當lazy_sum返回函數sum時,相關參數和變量都保存在返回的函數中,這種稱爲“閉包(Closure)”的程序結構擁有極大的威力。

當我們調用lazy_sum()時,每次調用都會返回一個新的函數,即使傳入相同的參數:

var f1 = lazy_sum([1, 2, 3, 4, 5]);
var f2 = lazy_sum([1, 2, 3, 4, 5]);
f1 === f2; // false

f1()f2()的調用結果互不影響。

generator

一個函數是一段完整的代碼,調用一個函數就是傳入參數,然後返回結果:

function foo(x) {
    return x + x;
}

var r = foo(1); // 調用foo函數

函數在執行過程中,如果沒有遇到return語句(函數末尾如果沒有return,就是隱含的return undefined;),控制權無法交回被調用的代碼。 generator跟函數很像,定義如下:

function* foo(x) {
    yield x + 1;
    yield x + 2;
    return x + 3;
}

generator和函數不同的是,generatorfunction*定義(注意多出的*號),並且,除了return語句,還可以用yield返回多次。

我們以一個著名的斐波那契數列爲例,它由0,1開頭:

0 1 1 2 3 5 8 13 21 34 ...

要編寫一個產生斐波那契數列的函數,可以這麼寫:

function fib(max) {
    var
        t,
        a = 0,
        b = 1,
        arr = [0, 1];
    while (arr.length < max) {
        t = a + b;
        a = b;
        b = t;
        arr.push(t);
    }
    return arr;
}

// 測試:
fib(5); // [0, 1, 1, 2, 3]
fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

函數只能返回一次,所以必須返回一個Array。但是,如果換成generator,就可以一次返回一個數,不斷返回多次。用generator改寫如下:

function* fib(max) {
    var
        t,
        a = 0,
        b = 1,
        n = 1;
    while (n < max) {
        yield a;
        t = a + b;
        a = b;
        b = t;
        n ++;
    }
    return a;
}

直接調用試試:

fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}

直接調用一個generator和調用函數不一樣,fib(5)僅僅是創建了一個generator對象,還沒有去執行它。 調用generator對象有兩個方法,一是不斷地調用generator對象的next()方法:

var f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: true}

next()方法會執行generator的代碼,然後,每次遇到yield x;就返回一個對象{value: x, done: true/false},然後“暫停”。返回的value就是yield的返回值,done表示這個generator是否已經執行結束了。如果donetrue,則value就是return的返回值。 當執行到donetrue時,這個generator對象就已經全部執行完畢,不要再繼續調用next()了。

第二個方法是直接用for ... of循環迭代generator對象,這種方式不需要我們自己判斷done

for (var x of fib(5)) {
    console.log(x); // 依次輸出0, 1, 1, 2, 3
}

標準對象

在JavaScript的世界裏,一切都是對象。

但是某些對象還是和其他對象不太一樣。爲了區分對象的類型,我們用typeof操作符獲取對象的類型,它總是返回一個字符串:

typeof 123; // 'number'
typeof NaN; // 'number'
typeof 'str'; // 'string'
typeof true; // 'boolean'
typeof undefined; // 'undefined'
typeof Math.abs; // 'function'
typeof null; // 'object'
typeof []; // 'object'
typeof {}; // 'object'

包裝對象

除了這些類型外,JavaScript還提供了包裝對象,熟悉Java的小夥伴肯定很清楚intInteger這種曖昧關係。

numberbooleanstring都有包裝對象。沒錯,在JavaScript中,字符串也區分string類型和它的包裝類型。包裝對象用new創建:

var n = new Number(123); // 123,生成了新的包裝類型
var b = new Boolean(true); // true,生成了新的包裝類型
var s = new String('str'); // 'str',生成了新的包裝類型

雖然包裝對象看上去和原來的值一模一樣,顯示出來也是一模一樣,但他們的類型已經變爲object了!所以,包裝對象和原始值用===比較會返回false

typeof new Number(123); // 'object'
new Number(123) === 123; // false

typeof new Boolean(true); // 'object'
new Boolean(true) === true; // false

typeof new String('str'); // 'object'
new String('str') === 'str'; // false

如果我們在使用NumberBooleanString時,沒有寫new會發生什麼情況? 此時,Number()Boolean()String()被當做普通函數,把任何類型的數據轉換爲numberbooleanstring類型(注意不是其包裝類型):

var n = Number('123'); // 123,相當於parseInt()或parseFloat()
typeof n; // 'number'

var b = Boolean('true'); // true
typeof b; // 'boolean'

var b2 = Boolean('false'); // true! 'false'字符串轉換結果爲true!因爲它是非空字符串!
var b3 = Boolean(''); // false

var s = String(123.45); // '123.45'
typeof s; // 'string'

總結一下,有這麼幾條規則需要遵守:

  • 不要使用new Number()new Boolean()new String()創建包裝對象;

  • parseInt()parseFloat()來轉換任意類型到number

  • String()來轉換任意類型到string,或者直接調用某個對象的toString()方法;

  • 通常不必把任意類型轉換爲boolean再判斷,因爲可以直接寫if (myVar) {...};

  • typeof操作符可以判斷出numberbooleanstringfunctionundefined

  • 判斷Array要使用Array.isArray(arr)

  • 判斷null請使用myVar === null

  • 判斷某個全局變量是否存在用typeof window.myVar === 'undefined'

  • 函數內部判斷某個變量是否存在用typeof myVar === 'undefined'

Date

在JavaScript中,Date對象用來表示日期和時間。

var now = new Date();
now; // Wed Jun 24 2015 19:49:22 GMT+0800 (CST)
now.getFullYear(); // 2015, 年份
now.getMonth(); // 5, 月份,注意月份範圍是0~11,5表示六月
now.getDate(); // 24, 表示24號
now.getDay(); // 3, 表示星期三
now.getHours(); // 19, 24小時制
now.getMinutes(); // 49, 分鐘
now.getSeconds(); // 22, 秒
now.getMilliseconds(); // 875, 毫秒數
now.getTime(); // 1435146562875, 以number形式表示的時間戳

如果要創建一個指定日期和時間的Date對象,可以用:

var d = new Date(2015, 5, 19, 20, 15, 30, 123);
d; // Fri Jun 19 2015 20:15:30 GMT+0800 (CST)

注意 JavaScript的月份範圍用整數表示是0~11,0表示一月,1表示二月……,所以要表示6月,我們傳入的是5!

第二種創建一個指定日期和時間的方法是解析一個符合ISO 8601格式的字符串:

var d = Date.parse('2015-06-24T19:49:22.875+08:00');
d; // 1435146562875

但它返回的不是Date對象,而是一個時間戳。不過有時間戳就可以很容易地把它轉換爲一個Date:

var d = new Date(1435146562875);
d; // Wed Jun 24 2015 19:49:22 GMT+0800 (CST)

時區

Date對象表示的時間總是按瀏覽器所在時區顯示的,不過我們既可以顯示本地時間,也可以顯示調整後的UTC時間:

var d = new Date(1435146562875);
d.toLocaleString(); // '2015/6/24 下午7:49:22',本地時間(北京時區+8:00),顯示的字符串與操作系統設定的格式有關
d.toUTCString(); // 'Wed, 24 Jun 2015 11:49:22 GMT',UTC時間,與本地時間相差8小時

在JavaScript中如何進行時區轉換呢?

實際上,只要我們傳遞的是一個number類型的時間戳,我們就不用關心時區轉換。任何瀏覽器都可以把一個時間戳正確轉換爲本地時間。

時間戳是一個自增的整數,它表示從1970年1月1日零時整的GMT時區開始的那一刻,到現在的毫秒數。

假設瀏覽器所在電腦的時間是準確的,那麼世界上無論哪個時區的電腦,它們此刻產生的時間戳數字都是一樣的,所以,時間戳可以精確地表示一個時刻,並且與時區無關。

所以,我們只需要傳遞時間戳,或者把時間戳從數據庫裏讀出來,再讓JavaScript自動轉換爲當地時間就可以了。

要獲取當前時間戳,可以用:

if (Date.now) {
    alert(Date.now()); // 老版本IE沒有now()方法
} else {
    alert(new Date().getTime());
}

RegExp

JavaScript有兩種方式創建一個正則表達式:

第一種方式是直接通過/正則表達式/寫出來,第二種方式是通過new RegExp('正則表達式')創建一個RegExp對象。 兩種寫法是一樣的:

var re1 = /ABC\-001/;
var re2 = new RegExp('ABC\\-001');

re1; // /ABC\-001/
re2; // /ABC\-001/

注意,如果使用第二種寫法,因爲字符串的轉義問題,字符串的兩個\\實際上是一個\

先看看如何判斷正則表達式是否匹配:

var re = /^\d{3}\-\d{3,8}$/;
re.test('010-12345'); // true
re.test('010-1234x'); // false
re.test('010 12345'); // false

RegExp對象的test()方法用於測試給定的字符串是否符合條件。

切分字符串

用正則表達式切分字符串比用固定的字符更靈活,請看正常的切分代碼:

'a b   c'.split(' '); // ['a', 'b', '', '', 'c']

無法識別連續的空格,用正則表達式試試:

'a b   c'.split(/\s+/); // ['a', 'b', 'c']

無論多少個空格都可以正常分割。加入,試試:

'a,b, c  d'.split(/[\s\,]+/); // ['a', 'b', 'c', 'd']

分組

除了簡單地判斷是否匹配之外,正則表達式還有提取子串的強大功能。用()表示的就是要提取的分組(Group)

比如: ^(\d{3})-(\d{3,8})$分別定義了兩個組,可以直接從匹配的字符串中提取出區號和本地號碼:

var re = /^(\d{3})-(\d{3,8})$/;
re.exec('010-12345'); // ['010-12345', '010', '12345']
re.exec('010 12345'); // null

貪婪匹配

正則匹配默認是貪婪匹配,也就是匹配儘可能多的字符。舉例如下,匹配出數字後面的0

var re = /^(\d+)(0*)$/;
re.exec('102300'); // ['102300', '102300', '']

由於\d+採用貪婪匹配,直接把後面的0全部匹配了,結果0*只能匹配空字符串了。

必須讓\d+採用非貪婪匹配(也就是儘可能少匹配),才能把後面的0匹配出來,加個?就可以讓\d+採用非貪婪匹配:

var re = /^(\d+?)(0*)$/;
re.exec('102300'); // ['102300', '1023', '00']

全局搜索

JavaScript的正則表達式還有幾個特殊的標誌,最常用的是g,表示全局匹配:

var r1 = /test/g;
// 等價於:
var r2 = new RegExp('test', 'g');

全局匹配可以多次執行exec()方法來搜索一個匹配的字符串。當我們指定g標誌後,每次運行exec(),正則表達式本身會更新lastIndex屬性,表示上次匹配到的最後索引:

var s = 'JavaScript, VBScript, JScript and ECMAScript';
var re=/[a-zA-Z]+Script/g;

// 使用全局匹配:
re.exec(s); // ['JavaScript']
re.lastIndex; // 10

re.exec(s); // ['VBScript']
re.lastIndex; // 20

re.exec(s); // ['JScript']
re.lastIndex; // 29

re.exec(s); // ['ECMAScript']
re.lastIndex; // 44

re.exec(s); // null,直到結束仍沒有匹配到

全局匹配類似搜索,因此不能使用/^…$/,那樣只會最多匹配一次。

正則表達式還可以指定i標誌,表示忽略大小寫,m標誌,表示執行多行匹配。

JSON

JSON是JavaScript Object Notation的縮寫,它是一種數據交換格式。

序列化

讓我們先把小明這個對象序列化成JSON格式的字符串:

var xiaoming = {
    name: '小明',
    age: 14,
    gender: true,
    height: 1.65,
    grade: null,
    'middle-school': '\"W3C\" Middle School',
    skills: ['JavaScript', 'Java', 'Python', 'Lisp']
};

JSON.stringify(xiaoming); // '{"name":"小明","age":14,"gender":true,"height":1.65,"grade":null,"middle-school":"\"W3C\" Middle School","skills":["JavaScript","Java","Python","Lisp"]}'

要輸出得好看一些,可以加上參數,按縮進輸出:

JSON.stringify(xiaoming, null, '  ');

結果:

{
  "name": "小明",
  "age": 14,
  "gender": true,
  "height": 1.65,
  "grade": null,
  "middle-school": "\"W3C\" Middle School",
  "skills": [
    "JavaScript",
    "Java",
    "Python",
    "Lisp"
  ]
}

第二個參數用於控制如何篩選對象的鍵值,如果我們只想輸出指定的屬性,可以傳入Array

JSON.stringify(xiaoming, ['name', 'skills'], '  ');

結果:

{
  "name": "小明",
  "skills": [
    "JavaScript",
    "Java",
    "Python",
    "Lisp"
  ]
}

還可以傳入一個函數,這樣對象的每個鍵值對都會被函數先處理:

function convert(key, value) {
    if (typeof value === 'string') {
        return value.toUpperCase();
    }
    return value;
}

JSON.stringify(xiaoming, convert, '  ');

上面的代碼把所有屬性值都變成大寫:

{
  "name": "小明",
  "age": 14,
  "gender": true,
  "height": 1.65,
  "grade": null,
  "middle-school": "\"W3C\" MIDDLE SCHOOL",
  "skills": [
    "JAVASCRIPT",
    "JAVA",
    "PYTHON",
    "LISP"
  ]
}

如果我們還想要精確控制如何序列化小明,可以給xiaoming定義一個toJSON()的方法,直接返回JSON應該序列化的數據:

var xiaoming = {
    name: '小明',
    age: 14,
    gender: true,
    height: 1.65,
    grade: null,
    'middle-school': '\"W3C\" Middle School',
    skills: ['JavaScript', 'Java', 'Python', 'Lisp'],
    toJSON: function () {
        return { // 只輸出name和age,並且改變了key:
            'Name': this.name,
            'Age': this.age
        };
    }
};

JSON.stringify(xiaoming); // '{"Name":"小明","Age":14}'

反序列化

拿到一個JSON格式的字符串,我們直接用JSON.parse()把它變成一個JavaScript對象:

JSON.parse('[1,2,3,true]'); // [1, 2, 3, true]
JSON.parse('{"name":"小明","age":14}'); // Object {name: '小明', age: 14}
JSON.parse('true'); // true
JSON.parse('123.45'); // 123.45

JSON.parse()還可以接收一個函數,用來轉換解析出的屬性:

JSON.parse('{"name":"小明","age":14}', function (key, value) {
    // 把number * 2:
    if (key === 'name') {
        return value + '同學';
    }
    return value;
}); // Object {name: '小明同學', age: 14}

面向對象編程

JavaScript不區分類和實例的概念,而是通過原型(prototype)來實現面向對象編程。

原型是指當我們想要創建xiaoming這個具體的學生時,我們並沒有一個Student類型可用。那怎麼辦?

這麼一個現成的對象:

var robot = {
    name: 'Robot',
    height: 1.6,
    run: function () {
        console.log(this.name + ' is running...');
    }
};

這個robot對象有名字,有身高,還會跑,有點像小明,於是我們把它改名爲Student,然後創建出xiaoming:

var Student = {
    name: 'Robot',
    height: 1.2,
    run: function () {
        console.log(this.name + ' is running...');
    }
};

var xiaoming = {
    name: '小明'
};

xiaoming.__proto__ = Student;

注意最後一行代碼把xiaoming的原型指向了對象Student,看上去xiaoming彷彿是從Student繼承下來的:

xiaoming.name; // '小明'
xiaoming.run(); // 小明 is running...

xiaoming有自己的name屬性,但並沒有定義run()方法。不過,由於小明是從Student繼承而來,只要Studentrun()方法,xiaoming也可以調用:
pic

創建對象

在編寫JavaScript代碼時,不要直接用obj.__proto__去改變一個對象的原型,並且,低版本的IE也無法使用__proto__Object.create()方法可以傳入一個原型對象,並創建一個基於該原型的新對象,但是新對象什麼屬性都沒有,因此,我們可以編寫一個函數來創建xiaoming

// 原型對象:
var Student = {
    name: 'Robot',
    height: 1.2,
    run: function () {
        console.log(this.name + ' is running...');
    }
};

function createStudent(name) {
    // 基於Student原型創建一個新對象:
    var s = Object.create(Student);
    // 初始化新對象:
    s.name = name;
    return s;
}

var xiaoming = createStudent('小明');
xiaoming.run(); // 小明 is running...
xiaoming.__proto__ === Student; // true

構造函數

除了直接用{ ... }創建一個對象外,JavaScript還可以用一種構造函數的方法來創建對象。它的用法是,先定義一個構造函數:

function Student(name) {
    this.name = name;
    this.hello = function () {
        alert('Hello, ' + this.name + '!');
    }
}

這確實是一個普通函數,但是在JavaScript中,可以用關鍵字new來調用這個函數,並返回一個對象:

var xiaoming = new Student('小明');
xiaoming.name; // '小明'
xiaoming.hello(); // Hello, 小明!

注意,如果不寫new,這就是一個普通函數,它返回undefined。但是,如果寫了new,它就變成了一個構造函數,它綁定的this指向新創建的對象,並默認返回this,也就是說,不需要在最後寫return this;。

新創建的xiaoming的原型鏈是:

xiaoming ----> Student.prototype ----> Object.prototype ----> null

也就是說,xiaoming的原型指向函數Student的原型。如果你又創建了xiaohong、xiaojun,那麼這些對象的原型與xiaoming是一樣的:

xiaoming ↘
xiaohong -→ Student.prototype ----> Object.prototype ----> null
xiaojun  ↗

用new Student()創建的對象還從原型上獲得了一個constructor屬性,它指向函數Student本身:

pic

我們還可以編寫一個createStudent()函數,在內部封裝所有的new操作。一個常用的編程模式像這樣:

function Student(props) {
    this.name = props.name || '匿名'; // 默認值爲'匿名'
    this.grade = props.grade || 1; // 默認值爲1
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
};

function createStudent(props) {
    return new Student(props || {})
}

這個createStudent()函數有幾個巨大的優點:一是不需要new來調用,二是參數非常靈活,可以不傳,也可以這麼傳:

var xiaoming = createStudent({
    name: '小明'
});

xiaoming.grade; // 1

如果創建的對象有很多屬性,我們只需要傳遞需要的某些屬性,剩下的屬性可以用默認值。由於參數是一個Object,我們無需記憶參數的順序。如果恰好從JSON拿到了一個對象,就可以直接創建出xiaoming。

原型繼承

JavaScript由於採用原型繼承,我們無法直接擴展一個Class,因爲根本不存在Class這種類型。 但是辦法還是有的。我們先回顧Student構造函數:

function Student(props) {
    this.name = props.name || 'Unnamed';
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
}

以及Student的原型鏈:
這裏寫圖片描述

要基於Student擴展出PrimaryStudent,可以先定義出PrimaryStudent

function PrimaryStudent(props) {
    // 調用Student構造函數,綁定this變量:
    Student.call(this, props);
    this.grade = props.grade || 1;
}

但是,調用了Student構造函數不等於繼承了Student,PrimaryStudent創建的對象的原型是:

new PrimaryStudent() ----> PrimaryStudent.prototype ----> Object.prototype ----> null

必須想辦法把原型鏈修改爲:

new PrimaryStudent() ----> PrimaryStudent.prototype ----> Student.prototype ----> Object.prototype ----> null

這樣,原型鏈對了,繼承關係就對了。新的基於PrimaryStudent創建的對象不但能調用 PrimaryStudent.prototype定義的方法,也可以調用Student.prototype定義的方法。

如果用最簡單粗暴的方法這麼幹:

PrimaryStudent.prototype = Student.prototype;

如果這樣的話,PrimaryStudent和Student共享一個原型對象,是不行的!

我們必須藉助一箇中間對象來實現正確的原型鏈,這個中間對象的原型要指向Student.prototype。爲了實現這一點,中間對象可以用一個空函數F來實現:

// PrimaryStudent構造函數:
function PrimaryStudent(props) {
    Student.call(this, props);
    this.grade = props.grade || 1;
}

// 空函數F:
function F() {
}

// 把F的原型指向Student.prototype:
F.prototype = Student.prototype;

// 把PrimaryStudent的原型指向一個新的F對象,F對象的原型正好指向Student.prototype:
PrimaryStudent.prototype = new F();

// 把PrimaryStudent原型的構造函數修復爲PrimaryStudent:
PrimaryStudent.prototype.constructor = PrimaryStudent;

// 繼續在PrimaryStudent原型(就是new F()對象)上定義方法:
PrimaryStudent.prototype.getGrade = function () {
    return this.grade;
};

// 創建xiaoming:
var xiaoming = new PrimaryStudent({
    name: '小明',
    grade: 2
});
xiaoming.name; // '小明'
xiaoming.grade; // 2

// 驗證原型:
xiaoming.__proto__ === PrimaryStudent.prototype; // true
xiaoming.__proto__.__proto__ === Student.prototype; // true

// 驗證繼承關係:
xiaoming instanceof PrimaryStudent; // true
xiaoming instanceof Student; // true

用一張圖來表示新的原型鏈:
這裏寫圖片描述

注意,函數F僅用於橋接,我們僅創建了一個new F()實例,而且,沒有改變原有的Student定義的原型鏈。 如果把繼承這個動作用一個inherits()函數封裝起來,還可以隱藏F的定義,並簡化代碼:

function inherits(Child, Parent) {
    var F = function () {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
}

這個inherits()函數可以複用:

function Student(props) {
    this.name = props.name || 'Unnamed';
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
}

function PrimaryStudent(props) {
    Student.call(this, props);
    this.grade = props.grade || 1;
}

// 實現原型繼承鏈:
inherits(PrimaryStudent, Student);

// 綁定其他方法到PrimaryStudent原型:
PrimaryStudent.prototype.getGrade = function () {
    return this.grade;
};

JavaScript的原型繼承實現方式就是:

  1. 定義新的構造函數,並在內部用call()調用希望“繼承”的構造函數,並綁定this;

  2. 藉助中間函數F實現原型鏈繼承,最好通過封裝的inherits函數完成;

  3. 繼續在新的構造函數的原型上定義新方法。

class繼承

新的關鍵字class從ES6開始正式被引入到JavaScript中。class的目的就是讓定義類更簡單。 我們先回顧用函數實現Student的方法:

function Student(name) {
    this.name = name;
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
}

如果用新的class關鍵字來編寫Student,可以這樣寫:

class Student {
    constructor(name) {
        this.name = name;
    }

    hello() {
        alert('Hello, ' + this.name + '!');
    }
}

比較一下就可以發現,class的定義包含了構造函數constructor和定義在原型對象上的函數hello()(注意沒有function關鍵字),這樣就避免了Student.prototype.hello = function () {...}這樣分散的代碼。

創建一個Student對象代碼和前面章節完全一樣:

var xiaoming = new Student('小明');
xiaoming.hello();

用class定義對象的另一個巨大的好處是繼承更方便了。想一想我們從 Student派生一個PrimaryStudent需要編寫的代碼量。現在,原型繼承的中間對象,原型對象的構造函數等等都不需要考慮了,直接通過extends來實現:

class PrimaryStudent extends Student {
    constructor(name, grade) {
        super(name); // 記得用super調用父類的構造方法!
        this.grade = grade;
    }

    myGrade() {
        alert('I am at grade ' + this.grade);
    }
}

瀏覽器

瀏覽器對象

window

window對象不但充當全局作用域,而且表示瀏覽器窗口。 window對象有innerWidthinnerHeight屬性,可以獲取瀏覽器窗口的內部寬度和高度。內部寬高是指除去菜單欄、工具欄、邊框等佔位元素後,用於顯示網頁的淨寬高。

// 可以調整瀏覽器窗口大小試試:
alert('window inner size: ' + window.innerWidth + ' x ' + window.innerHeight);

對應的,還有一個outerWidth和outerHeight屬性,可以獲取瀏覽器窗口的整個寬高。

navigator對象表示瀏覽器的信息,最常用的屬性包括:

  • navigator.appName:瀏覽器名稱;
  • navigator.appVersion:瀏覽器版本;
  • navigator.language:瀏覽器設置的語言;
  • navigator.platform:操作系統類型;
  • navigator.userAgent:瀏覽器設定的User-Agent字符串。

screen

screen對象表示屏幕的信息,常用的屬性有:

  • screen.width:屏幕寬度,以像素爲單位;
  • screen.height:屏幕高度,以像素爲單位;
  • screen.colorDepth:返回顏色位數,如8、16、24。

location

location對象表示當前頁面的URL信息。例如,一個完整的URL:
http://www.example.com:8080/path/index.html?a=1&b=2#TOP

可以用location.href獲取。要獲得URL各個部分的值,可以這麼寫:

location.protocol; // 'http'
location.host; // 'www.example.com'
location.port; // '8080'
location.pathname; // '/path/index.html'
location.search; // '?a=1&b=2'
location.hash; // 'TOP'

要加載一個新頁面,可以調用location.assign()。如果要重新加載當前頁面,調用location.reload()方法非常方便。

document

document對象表示當前頁面。由於HTML在瀏覽器中以DOM形式表示爲樹形結構,document對象就是整個DOM樹的根節點。 documenttitle屬性是從HTML文檔中的<title>xxx</title>讀取的,但是可以動態改變:

document.title = '努力學習JavaScript!';

要查找DOM樹的某個節點,需要從document對象開始查找。最常用的查找是根據ID和Tag Name。 我們先準備HTML數據:

<dl id="drink-menu" style="border:solid 1px #ccc;padding:6px;">
    <dt>摩卡</dt>
    <dd>熱摩卡咖啡</dd>
    <dt>酸奶</dt>
    <dd>北京老酸奶</dd>
    <dt>果汁</dt>
    <dd>鮮榨蘋果汁</dd>
</dl>

用document對象提供的getElementById()getElementsByTagName()可以按ID獲得一個DOM節點和按Tag名稱獲得一組DOM節點:

var menu = document.getElementById('drink-menu');
var drinks = document.getElementsByTagName('dt');
var i, s, menu, drinks;

menu = document.getElementById('drink-menu');
menu.tagName; // 'DL'

drinks = document.getElementsByTagName('dt');
s = '提供的飲料有:';
for (i=0; i<drinks.length; i++) {
    s = s + drinks[i].innerHTML + ',';
}
alert(s);

document對象還有一個cookie屬性,可以獲取當前頁面的Cookie

Cookie是由服務器發送的key-value標示符。因爲HTTP協議是無狀態的,但是服務器要區分到底是哪個用戶發過來的請求,就可以用Cookie來區分。

當一個用戶成功登錄後,服務器發送一個Cookie給瀏覽器,例如user=ABC123XYZ(加密的字符串)…,此後,瀏覽器訪問該網站時,會在請求頭附上這個Cookie,服務器根據Cookie即可區分出用戶。

Cookie還可以存儲網站的一些設置,例如,頁面顯示的語言等等。 JavaScript可以通過document.cookie讀取到當前頁面的Cookie:

document.cookie; // 'v=123; remember=true; prefer=zh'

操作DOM

由於HTML文檔被瀏覽器解析後就是一棵DOM樹,要改變HTML的結構,就需要通過JavaScript來操作DOM。

始終記住DOM是一個樹形結構。操作一個DOM節點實際上就是這麼幾個操作:

  • 更新:更新該DOM節點的內容,相當於更新了該DOM節點表示的HTML的內容;
  • 遍歷:遍歷該DOM節點下的子節點,以便進行進一步操作;
  • 添加:在該DOM節點下新增一個子節點,相當於動態增加了一個HTML節點;
  • 刪除:將該節點從HTML中刪除,相當於刪掉了該DOM節點的內容以及它包含的所有子節點。

在操作一個DOM節點前,我們需要通過各種方式先拿到這個DOM節點。最常用的方法是document.getElementById()document.getElementsByTagName(),以及CSS選擇器document.getElementsByClassName()

由於ID在HTML文檔中是唯一的,所以document.getElementById()可以直接定位唯一的一個DOM節點。

document.getElementsByTagName()document.getElementsByClassName()總是返回一組DOM節點。要精確地選擇DOM,可以先定位父節點,再從父節點開始選擇,以縮小範圍:

// 返回ID爲'test'的節點:
var test = document.getElementById('test');

// 先定位ID爲'test-table'的節點,再返回其內部所有tr節點:
var trs = document.getElementById('test-table').getElementsByTagName('tr');

// 先定位ID爲'test-div'的節點,再返回其內部所有class包含red的節點:
var reds = document.getElementById('test-div').getElementsByClassName('red');

// 獲取節點test下的所有直屬子節點:
var cs = test.children;

// 獲取節點test下第一個、最後一個子節點:
var first = test.firstElementChild;
var last = test.lastElementChild;

第二種方法是使用querySelector()querySelectorAll(),需要了解selector語法,然後使用條件來獲取節點,更加方便:

// 通過querySelector獲取ID爲q1的節點:
var q1 = document.querySelector('#q1');

// 通過querySelectorAll獲取q1節點內的符合條件的所有節點:
var ps = q1.querySelectorAll('div.highlighted > p');

嚴格地講,我們這裏的DOM節點是指Element,但是DOM節點實際上是Node,在HTML中,Node包括Element、Comment、CDATA_SECTION等很多種,以及根節點Document類型,但是,絕大多數時候我們只關心Element,也就是實際控制頁面結構的Node,其他類型的Node忽略即可。根節點Document已經自動綁定爲全局變量document。

更新DOM

可以直接修改節點的文本,方法有兩種:

一種是修改innerHTML屬性,這個方式非常強大,不但可以修改一個DOM節點的文本內容,還可以直接通過HTML片段修改DOM節點內部的子樹:

// 獲取<p id="p-id">...</p>
var p = document.getElementById('p-id');
// 設置文本爲abc:
p.innerHTML = 'ABC'; // <p id="p-id">ABC</p>
// 設置HTML:
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ';
// <p>...</p>的內部結構已修改

用innerHTML時要注意,是否需要寫入HTML。如果寫入的字符串是通過網絡拿到了,要注意對字符編碼來避免XSS攻擊。

第二種是修改innerText或textContent屬性,這樣可以自動對字符串進行HTML編碼,保證無法設置任何HTML標籤:

// 獲取<p id="p-id">...</p>
var p = document.getElementById('p-id');
// 設置文本:
p.innerText = '<script>alert("Hi")</script>';
// HTML被自動編碼,無法設置一個<script>節點:
// <p id="p-id">&lt;script&gt;alert("Hi")&lt;/script&gt;</p>

兩者的區別在於讀取屬性時,innerText不返回隱藏元素的文本,而textContent返回所有文本。

DOM節點的style屬性對應所有的CSS,可以直接獲取或設置。因爲CSS允許font-size這樣的名稱,但它並非JavaScript有效的屬性名,所以需要在JavaScript中改寫爲駝峯式命名fontSize:

// 獲取<p id="p-id">...</p>
var p = document.getElementById('p-id');
// 設置CSS:
p.style.color = '#ff0000';
p.style.fontSize = '20px';
p.style.paddingTop = '2em';

插入DOM

當我們獲得了某個DOM節點,想在這個DOM節點內插入新的DOM,應該如何做?

如果這個DOM節點是空的,例如,<div></div>,那麼,直接使用innerHTML = '<span>child</span>'就可以修改DOM節點的內容,相當於“插入”了新的DOM節點。

如果這個DOM節點不是空的,那就不能這麼做,因爲innerHTML會直接替換掉原來的所有子節點。 有兩個辦法可以插入新的節點。

一個是使用appendChild,把一個子節點添加到父節點的最後一個子節點。例如:

<!-- HTML結構 -->
<p id="js">JavaScript</p>
<div id="list">
    <p id="java">Java</p>
    <p id="python">Python</p>
    <p id="scheme">Scheme</p>
</div>

<p id="js">JavaScript</p>添加到<div id="list">的最後一項:

var
    js = document.getElementById('js'),
    list = document.getElementById('list');
list.appendChild(js);

現在,HTML結構變成了這樣:

<!-- HTML結構 -->
<div id="list">
    <p id="java">Java</p>
    <p id="python">Python</p>
    <p id="scheme">Scheme</p>
    <p id="js">JavaScript</p>
</div>

因爲我們插入的js節點已經存在於當前的文檔樹,因此這個節點首先會從原先的位置刪除,再插入到新的位置。

更多的時候我們會從零創建一個新的節點,然後插入到指定位置:

var
    list = document.getElementById('list'),
    haskell = document.createElement('p');
haskell.id = 'haskell';
haskell.innerText = 'Haskell';
list.appendChild(haskell);

這樣我們就動態添加了一個新的節點:

<!-- HTML結構 -->
<div id="list">
    <p id="java">Java</p>
    <p id="python">Python</p>
    <p id="scheme">Scheme</p>
    <p id="haskell">Haskell</p>
</div>

insertBefore
如果我們要把子節點插入到指定的位置怎麼辦?

可以使用parentElement.insertBefore(newElement, referenceElement);,子節點會插入到referenceElement之前。

還是以上面的HTML爲例,假定我們要把Haskell插入到Python之前:

<!-- HTML結構 -->
<div id="list">
    <p id="java">Java</p>
    <p id="python">Python</p>
    <p id="scheme">Scheme</p>
</div>

可以這麼寫:

var
    list = document.getElementById('list'),
    ref = document.getElementById('python'),
    haskell = document.createElement('p');
haskell.id = 'haskell';
haskell.innerText = 'Haskell';
list.insertBefore(haskell, ref);

新的HTML結構如下:

<!-- HTML結構 -->
<div id="list">
    <p id="java">Java</p>
    <p id="haskell">Haskell</p>
    <p id="python">Python</p>
    <p id="scheme">Scheme</p>
</div>

刪除DOM

刪除一個DOM節點就比插入要容易得多。

要刪除一個節點,首先要獲得該節點本身以及它的父節點,然後,調用父節點的removeChild把自己刪掉:

// 拿到待刪除節點:
var self = document.getElementById('to-be-removed');
// 拿到父節點:
var parent = self.parentElement;
// 刪除:
var removed = parent.removeChild(self);
removed === self; // true

對於如下HTML結構:

<div id="parent">
    <p>First</p>
    <p>Second</p>
</div>

當我們用如下代碼刪除子節點時:

var parent = document.getElementById('parent');
parent.removeChild(parent.children[0]);
parent.removeChild(parent.children[1]); // <-- 瀏覽器報錯

瀏覽器報錯:parent.children[1]不是一個有效的節點。原因就在於,當<p>First</p>節點被刪除後,parent.children的節點數量已經從2變爲了1,索引[1]已經不存在了。

因此,刪除多個節點時,要注意children屬性時刻都在變化。

操作表單

用JavaScript操作表單和操作DOM是類似的,因爲表單本身也是DOM樹。

不過表單的輸入框、下拉框等可以接收用戶輸入,所以用JavaScript來操作表單,可以獲得用戶輸入的內容,或者對一個輸入框設置新的內容。

HTML表單的輸入控件主要有以下幾種:

  • 文本框,對應的<input type="text">,用於輸入文本;
  • 口令框,對應的<input type="password">,用於輸入口令;
  • 單選框,對應的<input type="radio">,用於選擇一項;
  • 複選框,對應的<input type="checkbox">,用於選擇多項;
  • 下拉框,對應的<select>,用於選擇一項;
  • 隱藏文本,對應的<input type="hidden">,用戶不可見,但表單提交時會把隱藏文本發送到服務器。

獲取值

如果我們獲得了一個<input>節點的引用,就可以直接調用value獲得對應的用戶輸入值:

// <input type="text" id="email">
var input = document.getElementById('email');
input.value; // '用戶輸入的值'

這種方式可以應用於text、password、hidden以及select。

但是,對於單選框和複選框,value屬性返回的永遠是HTML預設的值,而我們需要獲得的實際是用戶是否“勾上了”選項,所以應該用checked判斷:

// <label><input type="radio" name="weekday" id="monday" value="1"> Monday</label>
// <label><input type="radio" name="weekday" id="tuesday" value="2"> Tuesday</label>
var mon = document.getElementById('monday');
var tue = document.getElementById('tuesday');
mon.value; // '1'
tue.value; // '2'
mon.checked; // true或者false
tue.checked; // true或者false

設置值

設置值和獲取值類似,對於text、password、hidden以及select,直接設置value就可以

// <input type="text" id="email">
var input = document.getElementById('email');
input.value = '[email protected]'; // 文本框的內容已更新

對於單選框和複選框,設置checkedtruefalse即可。

提交表單

方式一是通過<form>元素的submit()方法提交一個表單,例如,響應一個<button>click事件,在JavaScript代碼中提交表單:

<!-- HTML -->
<form id="test-form">
    <input type="text" name="test">
    <button type="button" onclick="doSubmitForm()">Submit</button>
</form>

<script>
function doSubmitForm() {
    var form = document.getElementById('test-form');
    // 可以在此修改form的input...
    // 提交form:
    form.submit();
}
</script>

這種方式的缺點是擾亂了瀏覽器對form的正常提交。瀏覽器默認點擊<button type="submit">時提交表單,或者用戶在最後一個輸入框按回車鍵。

因此,第二種方式是響應<form>本身的onsubmit事件,在提交form時作修改:

<!-- HTML -->
<form id="test-form" onsubmit="return checkForm()">
    <input type="text" name="test">
    <button type="submit">Submit</button>
</form>

<script>
function checkForm() {
    var form = document.getElementById('test-form');
    // 可以在此修改form的input...
    // 繼續下一步:
    return true;
}
</script>

在檢查和修改<input>時,要充分利用<input type="hidden">來傳遞數據。

例如,很多登錄表單希望用戶輸入用戶名和口令,但是,安全考慮,提交表單時不傳輸明文口令,而是口令的MD5。普通JavaScript開發人員會直接修改<input>

<!-- HTML -->
<form id="login-form" method="post" onsubmit="return checkForm()">
    <input type="text" id="username" name="username">
    <input type="password" id="password" name="password">
    <button type="submit">Submit</button>
</form>

<script>
function checkForm() {
    var pwd = document.getElementById('password');
    // 把用戶輸入的明文變爲MD5:
    pwd.value = toMD5(pwd.value);
    // 繼續下一步:
    return true;
}
</script>

這個做法看上去沒啥問題,但用戶輸入了口令提交時,口令框的顯示會突然從幾個*變成32個*(因爲MD5有32個字符)。

要想不改變用戶的輸入,可以利用<input type="hidden">實現:

<!-- HTML -->
<form id="login-form" method="post" onsubmit="return checkForm()">
    <input type="text" id="username" name="username">
    <input type="password" id="input-password">
    <input type="hidden" id="md5-password" name="password">
    <button type="submit">Submit</button>
</form>

<script>
function checkForm() {
    var input_pwd = document.getElementById('input-password');
    var md5_pwd = document.getElementById('md5-password');
    // 把用戶輸入的明文變爲MD5:
    md5_pwd.value = toMD5(input_pwd.value);
    // 繼續下一步:
    return true;
}
</script>

注意到id爲md5-password<input>標記了name="password",而用戶輸入的id爲input-password<input>沒有name屬性。沒有name屬性的<input>的數據不會被提交。

操作文件

在HTML表單中,可以上傳文件的唯一控件就是<input type="file">

注意:當一個表單包含<input type="file">時,表單的enctype必須指定爲multipart/form-datamethod必須指定爲post,瀏覽器才能正確編碼並以multipart/form-data格式發送表單的數據。

出於安全考慮,瀏覽器只允許用戶點擊<input type="file">來選擇本地文件,用JavaScript對<input type="file">的value賦值是沒有任何效果的。當用戶選擇了上傳某個文件後,JavaScript也無法獲得該文件的真實路徑。

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