js 常用 -- 你懂的

在路上發表於2020-06-17

全篇來自廖雪峰老師的教程,本篇近作為筆記使用。

1、== 和===的區別

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

奇怪吧?因為:這屬於 javascript 的一個缺陷

  • == 會自動轉換資料型別再比較,很多時候,會得到非常詭異的結果;
  • === 不會自動轉換資料型別,如果資料型別不一致,返回 false,如果一致,再比較;

例外情況:
(1) NaN

NaN === Nan;  // false

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

isNaN(NAN);  // true

2、null 和 undefined**

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

3、變數

(1)命名
變數名是大小寫英文、數字、$ 和_的組合,且不能用數字開頭
注意:變數名也可以用中文,但是,請不要給自己找麻煩。
(2)變數定義
Javascript 定義變數:

var a = 123;  // a的值是整數123
a = 'ABC';  // a變為字串

python 也是動態語言:

a = 123
a = "ABC"

Java 是靜態語言,定義變數時必須指定變數型別:

int a = 123;  // a是整數型別變數,型別用int申明
a = "ABC";  // 錯誤:不能把字串賦給整形變數

4、陣列

有一種陣列長度說改就改:

// 直接給Array的length賦一個新的值會導致Array大小的變化:
var arr = [1, 2, 3];
arr.length; // 3
arr.length = 6;
arr; // arr變為[1, 2, 3, undefined, undefined, undefined]
arr.length = 2;
arr; // arr變為[1, 2]

// 如果透過索引賦值時,索引超過了範圍,同樣會引起Array大小的變化:
var arr = [1, 2, 3];
arr[5] = 'x';
arr; // arr變為[1, 2, 3, undefined, undefined, 'x']

陣列常用函式:
(1)索引:透過 indexOf() 來搜尋一個指定元素的位置

var arr = [10, 20, '30', 'xyz'];
arr.indexOf(10);  // 元素10的索引為0

(2)擷取:slice() 可以擷取 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']

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

var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
var aCopy = arr.slice();

(4)末尾新增和刪除任意元素:push() 向 Array 的末尾新增若干元素,pop() 則把 Array 的最後一個元素刪除掉:

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

(5)頭部新增和頭部刪除:如果要往 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(); // 空陣列繼續shift不會報錯,而是返回undefined

(6)排序: sort() 可以對當前 Array 進行排序,它會直接修改當前 Array 的元素位置,直接呼叫時,按照預設順序排序:

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

(7)反轉: reverse() 把整個 Array 的元素給掉個個,也就是反轉

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

(8)萬能方法: 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']

(9)連線: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。

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

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

(11)多維陣列:
如果陣列的某個元素又是一個 Array,則可以形成多維陣列,例如:

var arr = [[1, 2, 3], [400, 500, 600], '-'];

5、物件

要判斷一個屬性是否是物件自身擁有的,而不是繼承得到的,可以用 hasOwnProperty() 方法。不要用 in,繼承得到的屬性也會返回 true;

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

6、條件判斷

JavaScript 把 null、undefined、0、NaN 和空字串' '視為 false,其他值一概視為 true.

7、iterable

for...in 迴圈問題:遍歷的實際上是物件的屬性名稱
for...of,遍歷的是物件的元素

更好的遍歷方式:

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


// Set:Set與Array類似,但Set沒有索引,因此回撥函式的前兩個引數都是元素本身:
var s = new Set(['A', 'B', 'C']);
s.forEach(function (element, sameElement, set) {
    console.log(element);
});

// Map: Map的回撥函式引數依次為value、key和map本身
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
m.forEach(function (value, key, map) {
    console.log(value);
});

// 如果只對某些引數感興趣,可以忽略其他引數
var a = ['A', 'B', 'C'];
a.forEach(function (element) {
    console.log(element);
});

8、函式

(1)函式多傳參或少傳參問題

由於 JavaScript 允許傳入任意個引數而不影響呼叫:

  • 因此傳入的引數比定義的引數多也沒有問題,雖然函式內部並不需要這些引數;
  • 傳入的引數少,也不會報錯,函式引數將收到 undefined,計算結果為 NaN

為避免收到 undefined,可以對引數進行檢查:

function abs(x) {
    if (typeof x !== 'number') {
        throw 'Not a number';
    }
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
}

(2)arguments

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

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

/*
x = 10
arg 0 = 10
arg 1 = 20
arg 2 = 30
*/

arguments 最常用於判斷傳入引數的個數

(3)rest 引數

如果使用 arguments 獲取除已定義引數之外的引數,比較麻煩。所以,ES6 標準引入了 rest 函式,用於獲取多餘的傳參。

function fooo (a, b, ...rest) {
    console.log('a = ' + a);
    console.log('b = ' + b);
    console.log('rest = ' + rest);
}
// 多傳參:rest會獲取多餘引數
fooo(1, 2, 3, 4, 5)
/*
a = 1
b = 2
rest = 3,4,5
 */

// 少傳參:rest引數會接受一個空陣列
fooo(1)
/*
a = 1
b = undefined
[]
*/

(4)return 語句坑

由於 javascript 在行末自動新增分號,所以 return 語句需要寫在同一行,或{...}中

// 正常的單行return
function foooo() {
    return { name: 'foo' };
}
foooo(); // { name: 'foo' }

// 多行return的坑
function fooooo() {
    return
    { name: 'foo' };
}
fooooo(); // undefined

// 正確的多行return寫法
function foo() {
    return { // 這裡不會自動加分號,因為{表示語句尚未結束
        name: 'foo'
    };
}

(5)變數作用域與解構賦值

A. var申明的變數是有作用域的,作用域為整個函式體;
  • 由於 JavaScript 的函式可以巢狀,此時,內部函式可以訪問外部函式定義的變數,反過來則不行
  • JavaScript 的函式在查詢變數時從自身函式定義開始,從 “內” 向 “外” 查詢。如果內部函式定義了與外部函式重名的變數,則內部函式的變數將 “遮蔽” 外部函式的變數。
B. 變數提升

JavaScript 的函式定義有個特點,它會先掃描整個函式體的語句,把所有申明的變數 “提升” 到函式頂部

function foo() {
    var x = 'Hello, ' + y;
    console.log(x);
    var y = 'Bob';
}
foo();  // Hello, undefined
/*
因為JavaScript引擎自動提升了變數y的宣告,所以函式不會報錯;
但是不會提升變數y的賦值,所以變數y的值為undefined
*/

常見寫法:用一個 var 在函式開頭 申明所有變數

function foo() {
    var
        x = 1, // x初始化為1
        y = x + 1, // y初始化為2
        z, i; // z和i為undefined
    // 其他語句:
    for (i=0; i<100; i++) {
        ...
    }
}
C. 全域性作用域

不在任何函式內定義的變數就具有全域性作用域。實際上,JavaScript 預設有一個全域性物件 window,全域性作用域的變數實際上被繫結到 window 的一個屬性。

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

你可能猜到了,由於函式定義有兩種方式,以變數方式 var foo = function () {}定義的函式實際上也是一個全域性變數,因此,頂層函式的定義也被視為一個全域性變數,並繫結到 window 物件;
我們每次直接呼叫的 alert() 函式其實也是 window 的一個變數:

function foo() {
    alert('foo');
}

foo(); // 直接呼叫foo()
window.foo(); // 透過window.foo()呼叫

說明,JavaScript 只有一個全域性作用域。任何變數(函式也視為變數),如果沒有在當前函式作用域中找到,就會繼續網上查詢,最後如果在全域性作用域中也沒找到,則報 ReferennceError 錯誤。

D. 名字空間

全域性變數會繫結到 window 上,不同的 JavaScript 檔案如果使用了相同的全域性變數,或者定義了相同名字的頂層函式,都會造成命名衝突,並且很難被發現。
減少衝突的一個方法是把自己的所有變數和函式全部繫結到一個全域性變數中。例如:

// 唯一的全域性變數MYAPP:
var MYAPP = {};

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

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

把自己的程式碼全部放入唯一的名字空間 MYAPP 中,會大大減少全域性變數衝突的可能。jQuery、YUI、underscore 也這麼做。

E. 區域性作用域

var 定義的變數作用域:函式內部
let 定義的變數作用域:塊

// var定義變數作用域在函式內
function foo() {
    for (var i=0; i<100; i++) {
        //
    }
    i += 100; // 仍然可以引用變數i
}

// let定義變數可以達到塊級作用域
function foo() {
    var sum = 0;
    for (let i=0; i<100; i++) {
        sum += i;
    }
    // SyntaxError:
    i += 1;
}

F. 常量

(ES6)const 申明常量。const 和 let 都具有塊級作用域。

const PI = 3.14;
PI = 3; // 某些瀏覽器不報錯,但是無效果!
PI; // 3.14
G. 解構賦值

ES6 引入:解構賦值,可以同時對一組變數進行賦值。

// 傳統寫法
var array = ['hello', 'Javascript', 'ES6']
var x = array[0];
var y = array[1];
var z = array[2];

// 如果瀏覽器支援解構賦值就不會報錯:
var [x, y, z] = ['hello', 'JavaScript', 'ES6'];

注意,對陣列元素進行解構賦值時,多個變數要用 [...] 括起來。
如果陣列本身還有巢狀,也可以透過下面的形式進行解構賦值,注意巢狀層次和位置要保持一致:

let [x, [y, z]] = ['hello', ['JavaScript', 'ES6']];
x; // 'hello'
y; // 'JavaScript'
z; // 'ES6'

解構賦值還可以忽略某些元素:

let [, , z] = ['hello', 'JavaScript', 'ES6']; // 忽略前兩個元素,只對z賦值第三個元素
z; // 'ES6'

使用解構賦值,從物件中取出若干屬性,便於快速獲取物件的指定屬性。

var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school'
};
var {name, age, passport} = person;

對一個物件進行解構賦值時,同樣可以直接對巢狀的物件屬性進行賦值,只要保證對應的層次是一致的:

var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school',
    address: {
        city: 'Beijing',
        street: 'No.1 Road',
        zipcode: '100001'
    }
};
var {name, address: {city, zip}} = person;
name; // '小明'
city; // 'Beijing'
zip; // undefined, 因為屬性名是zipcode而不是zip
// 注意: address不是變數,而是為了讓city和zip獲得巢狀的address物件的屬性:
address; // Uncaught ReferenceError: address is not defined

使用解構賦值對物件屬性進行賦值時,如果對應的屬性不存在,變數將被賦值為 undefined,這和引用一個不存在的屬性獲得 undefined 是一致的。如果要使用的變數名和屬性名不一致,可以用下面的語法獲取:

var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school'
};

// 把passport屬性賦值給變數id:
let {name, passport:id} = person;
name; // '小明'
id; // 'G-12345678'
// 注意: passport不是變數,而是為了讓變數id獲得passport屬性:
passport; // Uncaught ReferenceError: passport is not defined

解構賦值還可以使用預設值,這樣就避免了不存在的屬性返回 undefined 的問題:

var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678'
};

// 如果 person 物件沒有 single 屬性,預設賦值為 true:

var {name, single=true} = person;
name; // '小明'
single; // true

有些時候,如果變數已經被宣告瞭,再次賦值的時候,正確的寫法也會報語法錯誤:

// 宣告變數:
var x, y;
// 解構賦值:
{x, y} = { name: '小明', x: 100, y: 200};
// 語法錯誤: Uncaught SyntaxError: Unexpected token =

這是因為 JavaScript 引擎把{開頭的語句當作了塊處理,於是=不再合法。解決方法是用小括號括起來:

({x, y} = { name: '小明', x: 100, y: 200});
H. 使用場景
  • 解構賦值可以簡化程式碼 比如交換兩個變數的值 javascript var x=1, y=2; [x, y] = [y, x]

4、方法

(1)函式內部的 this 到底指向誰?

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

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge()
}
xiaoming.age();  // 30,正常結果
getAge();  // NaN

JavaScript 函式中如果呼叫了 this,那麼 this 到底指向誰?

  • 如果以物件對方法形式呼叫,比如 xiaoming.age(),該函式的 this 指向被呼叫的函式,也就是 xiaoming,符合預期。
  • 如果單獨呼叫函式,比如 getAge(),此時,該函式的 this 指向全域性物件,也就是 window。 注意:如果以物件的方法形式呼叫,格式必須是: obj.xxx();

apply 與 call

指定函式的 this 指向哪個物件,可以使用 apply 和 call:

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

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge()
}
xiaoming.age(); // 30
getAge().apply(xiaoming, [函式引數])
getAge().call(xiaoming, 函式引數)
// 對於普通函式呼叫,通常把this幫定為null
Math.max.apply(null, [1, 3, 5])
Math.max.call(null, 1, 3, 5)

裝飾器

javaScript 的所有物件都是動態的,即使是內建的函式,也可以重新指向新的函式。
比如統計呼叫了多少次 parceInt(),可以如下:

'use strict';

var count = 0;
var oldParseInt = parseInt;  //儲存原函式

parseInt = function () {  // 動態改變parseInt函式的行為
    count += 1;
    return oldParseInt.apply(null, arguments);  // 呼叫原函式
}

parseInt('10');
parseInt('20');
parseInt('30');
console.log(count)

高階函式

接收函式作為引數的函式,稱之為高階函式。
一個最簡單的高階函式:

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

(1)map

由於 map() 方法定義在 JavaScript 的 Array 中,我們呼叫 Array 的 map() 方法,傳入我們自己的函式,就得到了一個新的 Array 作為結果:
map() 只接受一個入參的函式;

function pow(x) {
    return x * x;
}
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var results = arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
console.log(results);

注意:map() 傳入的引數是 pow,即函式物件本身。
map() 作為高階函式,實際上把運算規則抽象了。

(2)reduce

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

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

(3)filter

filter() 接收一個函式,根據返回值是 true 還是 false 決定保留還是丟棄該元素。filter 接收的回撥函式可以接收多個引數;
比如,去除 Array 中的重複元素:

var r, arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry'];
r = arr.filter(function ( element, index, self ) {
    return self.indexOf(element) === index;
})

(4)sort

驚掉大牙的 sort() 結果

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

原因:因為 Array 的 sort() 方法預設把所有元素先轉換為 String 再排序,結果'10'排在了'2'的前面,因為字元'1'比字元'2'的 ASCII 碼小。

sort() 是高階函式,可以接收一個比較函式來實現自定義的排序

var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
    let a = 0;
    if (x < y) {
        a = -1;
    }
    if (x > y) {
        a = 1;
    }
    return a;
});
console.log(arr); // [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是同一物件

every

every() 方法可以判斷陣列的所有元素是否滿足測試條件。
例如:判斷一個包含若干字串的陣列,判斷所有字串睡佛歐滿足指定的測試條件:

var arr = ['Apple', 'pear', 'orange']
console.log(arr.every(function (s) {
    return s.length > 0;
}))  // true 因為每個元素都滿足s.length>0

console.log(arr.every(function (s) {
    return s.toLowerCase() === s;
}))  // false 因為不是每個元素都全部是小寫

find

find() 方法用於查詢符合條件的第一個元素,如果找到了,返回這個元素,否則,返回 undefined;

var arr = ['Apple', 'pear', 'orange']
console.log(arr.find(function (s) {
    return s.toLowerCase() === s;
}));  // 'pear', 因為pear全部是小寫

console.log(arr.find(function (s) {
    return s.toUpperCase() === s;
}));  // undefined, 因為沒有全部是大寫的元素

findIndex

findIndex() 和 find() 類似,也是查詢符合條件的第一個元素,不同之處在於 findIndex() 會返回這個元素的索引,如果沒有找到,返回-1;

forEach

forEach() 和 map() 類似,它也把每個元素依次作用於傳入的函式,但不會返回新的陣列。forEach() 常用於遍歷陣列,因此,傳入的函式不需要返回值

閉包

高階函式除了可以接收函式作為引數外,還可以把函式作為結果值返回。

箭頭函式

ES6 標準新增了一種新的函式:Arrow Function(箭頭函式)
箭頭函式相當於一個匿名函式:

x => x * x
//相當於
function (x) {
  return x * x;
}

generator

gennerator(生成器)是 ES6 標準引入的新的資料型別。一個 generator 看上去像一個函式,但可以往返多次。
generator 的函式,在每次呼叫 next() 的時候執行,遇到 yield 語句返回,再次執行時從上次返回的 yield 語句處繼續執行。

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

呼叫 generator 物件有兩種方法:

  • next():next() 方法會執行 generator 的程式碼,然後,每次遇到 yield,就返回一個物件{value: x, done: true/false},然後 “暫停”。返回的 value 就是 yield 的返回值,done 表示這個 generator 是否已經執行結束了。如果 done 為 true,則 value 就是 return 的返回值。 當執行到 done 為 true 時,這個 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: false}
f.next(); // {value: undefined, done: true}
  • 直接用 for ... of 迴圈迭代 generator 物件,這種方式不需要我們自己判斷 done。
var fib_obj = fib(10);
for (var x of fib_obj) {
    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'

包裝物件

注意: 閒的蛋疼也不要使用包裝物件,因為包裝物件有神奇的 “催眠魔力 “。

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 運算子可以判斷出 number、boolean、string、function 和 undefined;
  • 判斷 Array 要使用 Array.isArray(arr);
  • 判斷 null 請使用 myVar === null;
  • 判斷某個全域性變數是否存在用 typeof window.myVar === 'undefined';
  • 函式內部判斷某個變數是否存在用 typeof myVar === 'undefined'。

注意:

  • null 和 undefined 沒有 toSrting() 方法;
  • number 物件呼叫 toString() 報錯 SyntaxError:
123.toString(); // SyntaxError
// number轉string要特殊處理以下
123..toString(); // '123', 注意是兩個點!
(123).toString(); // '123'

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 神坑:JavaScript 的月份範圍用整數表示是 0~11,0 表示一月,1 表示二月……。此處應該是設計者的 BUG。

// 標準時間轉換為時間戳
var d = Date.parse('2015-06-24T19:49:22.875+08:00');
d; // 1435146562875

// 時間戳轉換為標準時間
var d = new Date(1435146562875);
d; // Wed Jun 24 2015 19:49:22 GMT+0800 (CST)
d.getMonth(); // 5

時區

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小時

時間戳:時間戳是一個自增的整數,它表示從1970年1月1日零時整的 GMT 時區開始的那一刻,到現在的毫秒數。假設瀏覽器所在電腦的時間是準確的,那麼世界上無論哪個時區的電腦,它們此刻產生的時間戳數字都是一樣的,所以,時間戳可以精確地表示一個時刻,並且與時區無關。

RegExp 正規表示式

在正規表示式中,如果直接給出字元,就是精確匹配。匹配規則如下:

正則基礎

  • 用\d 可以匹配一個數字;
  • \w 可以匹配一個字母或數字;
  • \s 可以匹配一個空格(也包括 Tab 等空白符),所以\s+ 表示至少有一個空格,例如匹配' ','\t\t'等;
  • 要匹配變長的字元,在正規表示式中,用 * 表示任意個字元(包括 0 個),用 + 表示至少一個字元,用?表示 0 個或 1 個字元,用{n}表示 n 個字元,用{n,m}表示 n-m 個字元:

進階

要做更精確地匹配,可以用 [] 表示範圍,比如:
(1)[0-9a-zA-Z_] 可以匹配一個數字、字母或者下劃線;
(2)[0-9a-zA-Z_]+ 可以匹配至少由一個數字、字母或者下劃線組成的字串,比如'a100','0_Z','js2015'等等;
(3)[a-zA-Z_\$][0-9a-zA-Z_\$]* 可以匹配由字母或下劃線、$ 開頭,後接任意個由一個數字、字母或者下劃線、$ 組成的字串,也就是 JavaScript 允許的變數名;
(4)[a-zA-Z_\$][0-9a-zA-Z_\$]{0, 19}更精確地限制了變數的長度是 1-20 個字元(前面 1 個字元 + 後面最多 19 個字元)。
(5)A|B 可以匹配 A 或 B,所以 (J|j) ava(S|s) cript 可以匹配'JavaScript'、'Javascript'、'javaScript'或者'javascript'。
(6)表示行的開頭,\d 表示必須以數字開頭。
(7)$ 表示行的結束,\d$ 表示必須以數字結束。

你可能注意到了,js 也可以匹配'jsp',但是加上js$ 就變成了整行匹配,就只能匹配'js'了。

正則的使用

JavaScript 有兩種方式建立正規表示式:

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

切分字串

// 用正規表示式切分字串比用固定的字元更靈活,請看正常的切分程式碼:
'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']
// 再加入;試試:
'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

如果正規表示式中定義了組,就可以在 RegExp 物件上用 exec() 方法提取出子串來。

  • exec() 方法在匹配成功後,會返回一個 Array,第一個元素是正規表示式匹配到的整個字串,後面的字串表示匹配成功的子串。
  • exec() 方法在匹配失敗時返回 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

發明者:道格拉斯·克羅克福特(Douglas Crockford)
誕生時間:2002 年
JSON 中資料結構:

  • number:和 JavaScript 的 number 完全一致;
  • boolean:就是 JavaScript 的 true 或 false;
  • string:就是 JavaScript 的 string;
  • null:就是 JavaScript 的 null;
  • array:就是 JavaScript 的 Array 表示方式——[];
  • object:就是 JavaScript 的{ ... }表示方式。 JSON 字符集:必須是 UTF-8 注意:為了統一解析,JSON 的字串規定必須用雙引號"",Object 的鍵也必須用雙引號""。

JavaScript 中可以直接使用 JSON,因為 javascript 內建了 JSON。

序列化

將 JavaScript 物件序列化成 JSON 格式的字串

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

var s = JSON.stringify(xiaoming);
console.log(s);

// 要輸出的好看些,按照縮排輸出:
var s = JSON.stringify(xiaoming, null, '  ');   
console.log(s);

// 第二個參數列示輸出指定的屬性
var s = JSON.stringify(xiaoming, ['name', 'height'], '  ');

// 還可以傳入一個函式,這樣物件的每個鍵值對都會被函式先處理
function convert(key, value) {
    if (typeof value === 'string') {
        return value.toUpperCase();
    }
    return value;
}

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

// 如果還想要精確控制如何序列化物件,可以給物件定義一個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.parse() 將 JSON 格式的字串轉為 JavaScript 物件;

let js_obj = JSON.parse('{"Name":"小明","Age":14}')
console.log(js_obj);

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

let js_obj1 = JSON.parse('{"Name":"小明","Age":14}', function (key, value) {
    if (key === 'Name') {
        return value + '小夥伴';
    }
    return value;
})

console.log(JSON.stringify(js_obj1))

/* 輸出:
{"Name":"小明小夥伴","Age":14}
*/

物件導向程式設計

JavaScript 的所有資料都可以看成物件。

JavaScript 的物件導向程式設計和大多數其他語言如 Java、C# 的物件導向程式設計都不太一樣。
Java 的物件導向有兩個基本概念:

  • 類:類是物件的類系 ing 模版,例如,定義 Student 類來表示學生,類本身是一種型別;
  • 例項:例項誰根據類建立的物件,例如 Student 類可以建立出 xiaoming、xiaohong 等多個例項;

JavaScript 不區分類和例項的概念,而是透過原型 (prototype) 來實現物件導向程式設計。

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 的原型鏈和 Java 的 Class 區別就在,JS 沒有 “Class” 的概念,所有物件都是例項,所謂繼承關係不過是吧一個物件的原型指向另一個物件而已。

建立物件

JavaScript 對每個建立的物件都會設定一個原型,指向它的原型物件。
當我們用 obj.xxx 訪問一個物件的屬性時,JavaScript 引擎現在當前物件上查詢該屬性,如果沒有找到,就到其原型物件上找,如果還沒有找到,就一直上溯到 Object.prototype 物件,最後,如果還沒有找到,就只能返回 undefiined。

例如:建立一個 Array 物件:

var arr = [1, 2, 3];

其原型鏈是:

arr  ----->  Array.prototype  ----->  Object.prototype  -----> null

Array.prototype 定義了 indexOf()、shift() 等方法,因此所有的 Array 物件可以直接呼叫這些方法。
注意:如果原型鏈很長,那麼訪問一個物件的屬性就會因為花更多的時間查詢而變得更慢,因此要注意不要把原型鏈搞的太長。

建構函式

除了直接用{ ... }建立一個物件外,還可以用建構函式的方法來建立物件。

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

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

注意:如果不寫 new,這就是個普通函式,它返回 undefined。如果寫了 new,它就變成了一個建構函式,它繫結的 this 指向新建立的物件,並預設返回 this。

忘記寫 new 怎麼辦?

千萬不要忘記寫 new。在 strict 模式下,this.name = name 將報錯,因為 this 繫結為 undefined,在非 strict 模式下,this.name = name 不報錯,因為 this 繫結為 window,於是無意間建立了全域性變數 name,並且返回 undefined,這個結果更糟糕。

所以,呼叫建構函式千萬不要忘記寫 new。為了區分普通函式和建構函式,按照約定,建構函式首字母應當大寫,而普通函式首字母應當小寫,這樣,一些語法檢查工具如 jslint 將可以幫你檢測到漏寫的 new。

最後,可以編寫一個 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 || {})
}

var xiaoming = createStudent({name: '小明'});
console.log(xiaoming.grade);

原型繼承

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

  • 定義新的建構函式,並在內部用 call() 呼叫希望 “繼承” 的建構函式,並繫結 this;
  • 藉助中間函式 F 實現原型鏈繼承,最好透過封裝的 inherits 函式完成;
  • 繼續在新的建構函式的原型上定義新方法。
function Student(props) {
    this.name = props.name || 'Unnamed';
}

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

// PrimaryStudent建構函式
function PrimaryStudent (props) {
    // 呼叫Student建構函式,繫結this變數
    Student.call(this, props);
    this.grade = props.grade || 1;
}

// 原型繼承函式
function inherits (Child, Parent) {
    // 空函式F
    var F = function () {};
    // 把F的原型指向Parent.prototype:
    F.prototype = Parent.prototype;
    // 把Child的原型指向一個新的F物件,F物件的原型正好指向Parent.prototype:
    Child.prototype = new F();
    // 把Child原型的建構函式修復為Child:
    Child.prototype.constructor = Child;
}

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

class 繼承

原型繼承理解困難,實現需要編寫大量程式碼,有沒有更簡單的辦法?有
新的關鍵字 class 從 ES6 開始正式被引入到 JavaScript 中。class 的目的就是讓定義類更簡單。

(1)class 定義原型

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

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

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

(2)class 繼承

// extends則表示原型鏈物件來自Student
class PrimaryStudent extends Student{
    // 建構函式
    constructor (name, grade) {
        //透過super(name)來呼叫父類的建構函式
        super(name);
        this.grade = grade;
    }

    myGrade() {
        return this.grade;
    }
}

瀏覽器

瀏覽器物件

(1)window

window 物件不但充當全域性作用域,而且表示瀏覽器視窗。
內部寬高:指除去選單欄、工具欄、邊框等佔位元素後,用於顯示網頁的淨寬高。

  • window.innerWidth:瀏覽器視窗的內部寬度;
  • window.innerHeight:瀏覽器視窗的內部高度;

(2)navigator

navigator 物件表示瀏覽器的資訊,最常用的屬性包括:

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

請注意,navigator 的資訊可以很容易地被使用者修改,所以 JavaScript 讀取的值不一定是正確的

(3)screen

screen 物件表示螢幕的資訊,常用的屬性有:

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

(4)location

location 物件表示當前頁面的 URL 資訊。

  • 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():載入新頁面

(5)document

document 物件表示當前頁面。由於 HTML 在瀏覽器中以 DOM 形式表示為樹形結構,document 物件就是整個 DOM 樹的根節點。

document 的 title 屬性是從 HTML 文件中的

xxx讀取的,但是可以動態改變:
document.title = '努力學習JavaScript!';

要查詢 DOM 樹的某個節點,需要從 document 物件開始查詢。最常用的查詢是根據 ID 和 Tag Name。

  • 獲取頁面物件:用 document 物件提供的 getElementById() 和 getElementsByTagName() 可以按 ID 獲得一個 DOM 節點和按 Tag 名稱獲得一組 DOM 節點
  • 獲取頁面 cookie:JavaScript 可以透過 document.cookie 讀取到當前頁面的 Cookie: javascript document.cookie; // 'v=123; remember=true; prefer=zh' 由於 JavaScript 可以讀取頁面的 Cookie,而使用者的登入資訊通常也儲存在 Cookie 中,這就造成了巨大的安全隱患。為了解決這個問題,伺服器在設定 Cookie 時,可以使用 httponly,設定了 httponly 的 Cookie 將不能被 JavaScript 讀取。這個行為由瀏覽器實現,主流瀏覽器均支援 httponly 選項。 設定後利用開發者工具檢視,會顯示 HttpOnly 的✅

(6)history

任何時候,都不應該使用 history 這個物件了。

操作 DOM

由於 HTML 文件被瀏覽器解析後就是一棵 DOM 樹,要改變 HTML 的結構,就需要透過 JavaScript 來操作 DOM。
始終記住 DOM 是一個樹形結構。操作一個 DOM 節點實際上就是這麼幾個操作:

  • 更新:更新該 DOM 節點的內容,相當於更新了該 DOM 節點表示的 HTML 的內容;
  • 遍歷:遍歷該 DOM 節點下的子節點,以便進行進一步操作;
  • 新增:在該 DOM 節點下新增一個子節點,相當於動態增加了一個 HTML 節點;
  • 刪除:將該節點從 HTML 中刪除,相當於刪掉了該 DOM 節點的內容以及它包含的所有子節點。 ```javascript // 返回 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 獲取 ID 為 q1 的節點:
var q1 = document.querySelector('#q1');

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


#### 更新DOM
更新方式由兩種:
(1)修改innerHTML屬性:這個方式非常強大,不但可以修改一個DOM節點的文字內容,還可以直接透過HTML片段修改DOM節點內部的子樹:
```javascript
// 獲取<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>的內部結構已修改

(2)修改 innerText 或 textContent 屬性,這樣可以自動對字串進行 HTML 編碼,保證無法設定任何 HTML 標籤:

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

(3)修改 css 屬性

// 獲取<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

(1)如果這個 DOM 節點是空的,例如,

,那麼,直接使用 innerHTML = 'child'就可以修改 DOM 節點的內容,相當於 “插入” 了新的 DOM 節點。

(2)如果這個 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>
    

    JavaScript

    新增到 的最後一項:
    var
    js = document.getElementById('js'),
    list = document.getElementById('list');
    list.appendChild(js);
    
  • 有時會從零建立一個新的節點,然後插入到指定的位置

    var
    list = document.getElementById('list'),
    haskell = document.createElement('p');
    haskell.id = 'haskell';
    haskell.innerText = 'Haskell';
    list.appendChild(haskell);
    
  • (3)insertBefore
    使用 parentElement.insertBefore(newElement, referenceElement);,子節點會插入到 referenceElement 之前。

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

    (4)刪除 DOM

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

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

    刪除多個節點時,注意 children 屬性時刻都在變化:

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

    瀏覽器報錯:parent.children[1] 不是一個有效的節點。原因就在於,當

    First

    節點被刪除後,parent.children 的節點數量已經從 2 變為了 1,索引 [1] 已經不存在了。

    操作表單

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

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

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

    文字框,對應的,用於輸入文字;

    口令框,對應的,用於輸入口令;

    單選框,對應的,用於選擇一項;

    核取方塊,對應的,用於選擇多項;

    下拉框,對應的,用於選擇一項;

    隱藏文字,對應的,使用者不可見,但表單提交時會把隱藏文字傳送到伺服器。

    (1)獲取值
    如果我們獲得了一個節點的引用,就可以直接呼叫 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>
    var mon = document.getElementById('monday');
    mon.value; // '1'
    mon.checked; // true或者false
    

    (2)設定值
    設定值和獲取值類似,對於 text、password、hidden 以及 select,直接設定 value 就可以:

    // <input type="text" id="email">
    var input = document.getElementById('email');
    input.value = 'test@example.com'; // 文字框的內容已更新
    

    對於單選框和核取方塊,設定 checked 為 true 或 false 即可。

    (3)HTML5 控制元件
    HTML5 新增了大量標準控制元件,常用的包括 date、datetime、datetime-local、color 等,它們都使用標籤:

    <input type="date" value="2015-07-01">
    

    <input type="datetime-local" value="2015-07-01T02:03:04">
    

    <input type="color" value="#ff0000">
    

    不支援 HTML5 的瀏覽器無法識別新的控制元件,會把它們當做 type="text"來顯示。支援 HTML5 的瀏覽器將獲得格式化的字串。例如,type="date"型別的 input 的 value 將保證是一個有效的 YYYY-MM-DD 格式的日期,或者空字串。

    (4)提交表單
    JavaScript 以兩種方式來處理表單的提交:
    A. 第一種方式:透過

    元素的 submit() 方法提交一個表單。
    例如,響應一個的 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 的正常提交。

    B. 第二種方式:響應

    本身的 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>
    

    注意要 return true 來告訴瀏覽器繼續提交,如果 return false,瀏覽器將不會繼續提交 form,這種情況通常對應使用者輸入有誤,提示使用者錯誤資訊後終止提交 form。
    在檢查和修改時,要充分利用來傳遞資料。

    注意:沒有 name 屬性的的資料不會被提交。

    操作檔案

    在 HTML 表單中,可以上傳檔案的唯一控制元件就是。
    注意:當一個表單包含<input type="file">時,表單的 enctype 必須指定為 multipart/form-data,method 必須指定為 post,瀏覽器才能正確編碼並以 multipart/form-data 格式傳送表單的資料。

    通常,上傳的檔案都由後臺伺服器處理,JavaScript 可以在提交表單時對副檔名做檢查,以便防止使用者上傳無效格式的檔案:

    var f = document.getElementById('test-file-upload');
    var filename = f.value; // 'C:\fakepath\test.png'
    if (!filename || !(filename.endsWith('.jpg') || filename.endsWith('.png') || filename.endsWith('.gif'))) {
        alert('Can only upload image file.');
        return false;
    }
    

    File API

    HTML5 新增的 File API 允許 JavaScript 讀取檔案內容,獲取更多的檔案資訊。
    HTML5 的 File API 提供了FileFileReader兩個主要物件,可以獲得檔案資訊並讀取檔案。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>demo</title>
        <script src="test.js"></script>
    
    </head>
    <body>
    <form method="post" action="http://localhost/test" enctype="multipart/form-data">
        <p>圖片預覽</p>
        <div id="test-image-preview"
             style="border: 1px solid #ccc; width: 100%; height: 200px; background-size: contain; background-repeat: no-repeat; background-position: center center;"></div>
        <p>
            <input type="file" id="test-image-file" name="test" onclick="f()">
        </p>
        <p id="test-file-info"></p>
    </form>
    
    </body>
    </html>
    
    function f () {
        var
            fileInput = document.getElementById('test-image-file'),
            info = document.getElementById('test-file-info'),
            preview = document.getElementById('test-image-preview');
    // 監聽change事件:
        fileInput.addEventListener('change', function () {
            // 清除背景圖片:
            preview.style.backgroundImage = '';
            // 檢查檔案是否選擇:
            if (!fileInput.value) {
                info.innerHTML = '沒有選擇檔案...';
                return;
            }
            // 獲取File引用:
            var file = fileInput.files[0];
            console.log(file);
            // 獲取File資訊:
            info.innerHTML = '檔案: ' + file.name + '<br>' +
                '大小: ' + file.size + '<br>' +
                '修改: ' + file.lastModifiedDate;
            if (file.type !== 'image/jpeg' && file.type !== 'image/png' && file.type !== 'image/gif') {
                alert('不是有效的圖片檔案!');
                return false;
            }
            // 讀取檔案:
            var reader = new FileReader();
            reader.onload = function(e) {
                var
                    data = e.target.result; // 'data:image/jpeg;base64,/9j/4AAQSk...(base64編碼)...'
                preview.style.backgroundImage = 'url(' + data + ')';
            };
            // 以DataURL的形式讀取檔案:
            reader.readAsDataURL(file);
        });
    }
    

    上面的程式碼演示瞭如何透過 HTML5 的 File API 讀取檔案內容。以 DataURL 的形式讀取到的檔案是一個字串,類似於 data:image/jpeg;base64,/9j/4AAQSk...(base64 編碼)...,常用於設定影像。如果需要伺服器端處理,把字串 base64,後面的字元傳送給伺服器並用 Base64 解碼就可以得到原始檔案的二進位制內容。

    回撥

    在 JavaScript 中,瀏覽器的 JavaScript 執行引擎在執行 JavaScript 程式碼時,總是以單執行緒模式執行,也就是說,任何時候,JavaScript 程式碼都不可能同時有多於 1 個執行緒在執行。
    在 JS 中,執行多工實際上都是非同步呼叫,比如上面的程式碼:

    reader.readAsDataURL(file);
    

    就會發起一個非同步操作來讀取檔案內容。因為是非同步操作,所以我們在 JavaScript 程式碼中就不知道什麼時候操作結束,因此需要先設定一個回撥函式:

    reader.onload = function(e) {
        // 當檔案讀取完成後,自動呼叫此函式:
    };
    

    當檔案讀取完成後,JavaScript 引擎將自動呼叫我們設定的回撥函式。執行回撥函式時,檔案已經讀取完畢,所以我們可以在回撥函式內部安全地獲得檔案內容。

    AJAX(Asynchronous JavaScript and XML,即用 JS 執行非同步網路請求)

    function success(text) {
        var textarea = document.getElementById('test-response-text');
        textarea.value = text;
    }
    
    function fail(code) {
        var textarea = document.getElementById('test-response-text');
        textarea.value = 'Error code: ' + code;
    }
    
    var request = new XMLHttpRequest(); // 新建XMLHttpRequest物件
    
    request.onreadystatechange = function () { // 狀態發生變化時,函式被回撥
        if (request.readyState === 4) { // 成功完成
            // 判斷響應結果:
            if (request.status === 200) {
                // 成功,透過responseText拿到響應的文字:
                return success(request.responseText);
            } else {
                // 失敗,根據響應碼判斷失敗原因:
                return fail(request.status);
            }
        } else {
            // HTTP請求還在繼續...
        }
    }
    
    // 傳送請求:
    request.open('GET', 'https://testerhome.com/');
    request.send();
    
    alert('請求已傳送,請等待響應...');
    

    當建立了 XMLHttpRequest 物件後,要先設定 onreadystatechange 的回撥函式。在回撥函式中,通常我們只需透過 readyState === 4 判斷請求是否完成,如果已完成,再根據 status === 200 判斷是否是一個成功的響應。

    XMLHttpRequest 物件的 open() 方法有 3 個引數,第一個引數指定是 GET 還是 POST,第二個引數指定 URL 地址,第三個引數指定是否使用非同步,預設是 true,所以不用寫。

    注意,千萬不要把第三個引數指定為 false,否則瀏覽器將停止響應,直到 AJAX 請求完成。如果這個請求耗時 10 秒,那麼 10 秒內你會發現瀏覽器處於 “假死” 狀態。

    最後呼叫 send() 方法才真正傳送請求。GET 請求不需要引數,POST 請求需要把 body 部分以字串或者 FormData 物件傳進去。

    安全限制

    瀏覽器的同源策略:預設情況下,JavaScript 在傳送 AJAX 請求時,URL 的域名必須與當前頁面完全一致。
    完全一致的意思是:

    • 域名要相同;
    • 協議要相同;
    • 埠號要相同;(有的瀏覽器口子松一點,允許埠不同,大多數瀏覽器都會嚴格遵守這個限制)

    解決 JavaScript 無法請求外域的 URL 問題

    • 【Flash】透過 Flash 外掛傳送 HTTP 請求,這種方式可以繞過瀏覽器的安全限制,但必須按照 Flash,並且跟 Flash 互動。不過 Flash 基本被拋棄了。
    • 【代理伺服器】透過在同源域名下架設一個代理伺服器來轉發,JavaScript 負責把請求傳送到代理伺服器,代理伺服器再把結果返回,這樣就遵守了同源策略。這種方式麻煩之處在於需要伺服器端額外做開發。 javascript '/proxy?url=http://www.sina.com.cn'
    • 【JSONP】JSONP 有個限制,只能用 GET 請求,並且要求返回 JavaScript。這種方式跨域實際上是利用了瀏覽器允許跨域引用 JavaScript 資源:
    • 【CORS】如果瀏覽器支援 HTML5,一勞永逸地使用新的跨域策略:CORS(Cross-Origin Resource Sharing) Origin 表示本域,也就是瀏覽器當前頁面的域。當 JavaScript 向外域(如 sina.com)發起請求後,瀏覽器收到響應後,首先檢查 Access-Control-Allow-Origin 是否包含本域,如果是,則此次跨域請求成功,如果不是,則請求失敗,JavaScript 將無法獲取到響應的任何資料。 假設本域是 my.com,外域是 sina.com,只要響應頭 Access-Control-Allow-Origin 為http://my.com,或者是 *,本次請求就可以成功。 上面這種跨域請求,稱之為 “簡單請求”。簡單請求包括 GET、HEAD 和 POST(POST 的 Content-Type 型別,僅限 application/x-www-form-urlencoded、multipart/form-data 和 text/plain),並且不能出現任何自定義頭(例如 X-Custom: 12345),通常能滿足 90% 的需求。

    Promise

    非同步執行網路操作

    var ajax = ajaxGet('http://...');
    ajax.ifSuccess(success)
        .ifFail(fail);
    

    這種 “承諾將來會執行” 的物件在 JavaScript 中稱為 Promise 物件。

    Promise 示例:

    new Promise(function (resolve, reject) {
        console.log('start new Promise...');
        var timeOut = Math.random() * 2;
        console.log('set timeout to: ' + timeOut + ' seconds.');
        setTimeout(function () {  // 承諾執行的程式碼
            if (timeOut < 1) {
                console.log('call resolve()...');
                resolve('200 OK');
            }
            else {
                console.log('call reject()...');
                reject('timeout in ' + timeOut + ' seconds.');
            }
        }, timeOut * 1000);
    }).then(function (r) {  // 處理結果程式碼
        console.log('Done: ' + r);
    }).catch(function (reason) {  // 處理結果程式碼
        console.log('Failed: ' + reason);
    });
    

    非同步 - 序列 - 執行多個任務:

    // 其中,job1、job2和job3都是Promise物件
    job1.then(job2).then(job3).catch(handleError);
    

    例項:

    // 0.5秒後返回input*input的計算結果:
    function multiply(input) {
        return new Promise(function (resolve, reject) {
            console.log('calculating ' + input + ' x ' + input + '...');
            setTimeout(resolve, 500, input * input);
        });
    }
    
    // 0.5秒後返回input+input的計算結果:
    function add(input) {
        return new Promise(function (resolve, reject) {
            console.log('calculating ' + input + ' + ' + input + '...');
            setTimeout(resolve, 500, input + input);
        });
    }
    
    var p = new Promise(function (resolve, reject) {
        console.log('start new Promise...');
        resolve(123);
    });
    
    p.then(multiply)
        .then(add)
        .then(multiply)
        .then(add)
        .then(function (result) {
            console.log('Got value: ' + result);
        });
    
    

    並行 - 執行 - 多個任務

    並行執行多個任務,並返回結果:

    var p1 = new Promise(function (resolve, reject) {
        setTimeout(resolve, 500, 'P1');
    });
    var p2 = new Promise(function (resolve, reject) {
        setTimeout(resolve, 600, 'P2');
    });
    // 同時執行p1和p2,並在它們都完成後執行then:
    Promise.all([p1, p2]).then(function (results) {
        console.log(results); // 獲得一個Array: ['P1', 'P2']
    });
    

    為了容錯,執行多個同樣功能的非同步任務,只需要獲得先返回的結果即可:

    var p1 = new Promise(function (resolve, reject) {
        setTimeout(resolve, 500, 'P1');
    });
    var p2 = new Promise(function (resolve, reject) {
        setTimeout(resolve, 600, 'P2');
    });
    Promise.race([p1, p2]).then(function (result) {
        console.log(result); // 'P1'
    });
    

    由於 p1 執行較快,Promise 的 then() 將獲得結果'P1'。p2 仍在繼續執行,但執行結果將被丟棄。

    Canvas(繪製圖表和動畫的幕布)

    jQuery

    jQuery 是一個 JavaScript 函式庫。

    jQuery 是一個輕量級的"寫的少,做的多"的 JavaScript 庫。

    jQuery 庫包含以下功能:

    HTML 元素選取
    HTML 元素操作
    CSS 操作
    HTML 事件函式
    JavaScript 特效和動畫
    HTML DOM 遍歷和修改
    AJAX
    Utilities

    錯誤處理

    最後請注意,catch 和 finally 可以不必都出現。也就是說,try 語句一共有三種形式:

    完整的 try ... catch ... finally:

    try {
        ...
    } catch (e) {
        ...
    } finally {
        ...
    }
    

    只有 try ... catch,沒有 finally:

    try {
        ...
    } catch (e) {
        ...
    }
    

    只有 try ... finally,沒有 catch:

    try {
        ...
    } finally {
        ...
    }
    

    錯誤型別

    JavaScript 有一個標準的 Error 物件表示錯誤,還有從 Error 派生的 TypeError、ReferenceError 等錯誤物件。我們在處理錯誤時,可以透過 catch(e) 捕獲的變數 e 訪問錯誤物件:

    丟擲錯誤

    throw 丟擲錯誤:throw new Error('錯誤名');

    錯誤傳遞

    如果在一個函式內部發生了錯誤,它自身沒有捕獲,錯誤就會被拋到外層呼叫函式,如果外層函式也沒有捕獲,該錯誤會一直沿著函式呼叫鏈向上丟擲,直到被 JavaScript 引擎捕獲,程式碼終止執行。

    所以,我們不必在每一個函式內部捕獲錯誤,只需要在合適的地方來個統一捕獲,一網打盡:

    非同步錯誤處理

    牢記:JS 引擎是一個事件驅動的執行引擎,程式碼總是以單執行緒執行,而回撥函式的執行需要等到下一個滿足條件的時間出現後,才會被執行。

    JS 的非同步程式碼,無法在呼叫時捕獲,原因是:在捕獲的當時,回撥函式並未執行。
    例如:呼叫時捕獲非同步程式碼的異常,是捕獲不到的

    function printTime() {
        throw new Error();
    }
    
    try {
        setTimeout(printTime, 1000);
        console.log('done');
    } catch (e) {
        console.log('error');
    }
    
    

    underscore

    成熟可靠的第三方開源庫,使用統一的函式來實現 map()、filter() 這些操作;
    jQuery 在載入時,會把自身繫結到唯一的全域性變數 $ 上,underscore 與其類似,會把自身繫結到唯一的全域性變數_上,這也是為啥它的名字叫 underscore 的原因。

    Collections

    underscore 為集合類物件提供了一致的介面。集合類指 Array、Object,暫不支援 Map 和 Set。

    map/filter

    和 Array 的 map() 與 filter() 類似,但是 underscore 的 map() 和 filter() 可以作用於 Object。當作用於 Object 時,傳入的函式為 function (value, key),第一個引數接收 value,第二個引數接收 key:

    var upper = _.map(obj, function (value, key) {
        return key + "=" + value.toUpperCase();
    });
    

    every/some

    當集合的所有元素都滿足條件時,.every() 函式返回 true,當集合的至少一個元素滿足條件時,.some() 函式返回 true:

    'use strict';
    // 所有元素都大於0?
    _.every([1, 4, 7, -3, -9], (x) => x > 0); // false
    // 至少一個元素大於0?
    _.some([1, 4, 7, -3, -9], (x) => x > 0); // true
    

    max / min

    這兩個函式直接返回集合中最大和最小的數:

    groupBy

    shuffle / sample

    shuffle() 用洗牌演算法隨機打亂一個集合:

    'use strict';
    // 注意每次結果都不一樣:
    _.shuffle([1, 2, 3, 4, 5, 6]); // [3, 5, 4, 6, 2, 1]
    

    sample() 則是隨機選擇一個或多個元素:

    'use strict';
    // 注意每次結果都不一樣:
    // 隨機選1個:
    _.sample([1, 2, 3, 4, 5, 6]); // 2
    // 隨機選3個:
    _.sample([1, 2, 3, 4, 5, 6], 3); // [6, 1, 4]
    

    Arrarys

    first / last

    這兩個函式分別取第一個和最後一個元素

    flatten

    flatten() 接收一個 Array,無論這個 Array 裡面巢狀了多少個 Array,flatten() 最後都把它們變成一個一維陣列

    zip / unzip

    zip() 把兩個或多個陣列的所有元素按索引對齊,然後按索引合併成新陣列。例如,你有一個 Array 儲存了名字,另一個 Array 儲存了分數,現在,要把名字和分數給對上,用 zip() 輕鬆實現:

    var names = ['Adam', 'Lisa', 'Bart'];
    var scores = [85, 92, 59];
    _.zip(names, scores);
    // [['Adam', 85], ['Lisa', 92], ['Bart', 59]]
    

    unzip() 則是反過來:

    var namesAndScores = [['Adam', 85], ['Lisa', 92], ['Bart', 59]];
    _.unzip(namesAndScores);
    // [['Adam', 'Lisa', 'Bart'], [85, 92, 59]]
    

    object

    有時候你會想,與其用 zip(),為啥不把名字和分數直接對應成 Object 呢?別急,object() 函式就是幹這個的:

    'use strict';
    var names = ['Adam', 'Lisa', 'Bart'];
    var scores = [85, 92, 59];
    _.object(names, scores);
    // {Adam: 85, Lisa: 92, Bart: 59}
    

    注意_.object() 是一個函式,不是 JavaScript 的 Object 物件。

    range

    uniq

    使用_.uniq 對陣列元素進行不區分大小寫去重:

    var arr = ['Apple', 'orange', 'banana', 'ORANGE', 'apple', 'PEAR'];
    var result = _.uniq(arr, x=>x.toUpperCase());
    // ["Apple", "orange", "banana", "PEAR"]
    

    Function

    underscore 提供了大量 JS 本身沒有的高階函式;

    bind

    bind() 可以幫我們把 s 物件直接繫結在 fn() 的 this 指標上,以後呼叫 fn() 就可以直接正常呼叫了:

    // 普通程式碼
    var s = ' Hello  ';
    var fn = s.trim;
    // 呼叫call並傳入s物件作為this:
    fn.call(s)
    // 輸出Hello
    
    // 使用_.bind
    var s = ' Hello  ';
    var fn = _.bind(s.trim, s);
    fn();
    // 輸出Hello
    

    partial

    partial() 可以建立偏函式:即建立一個固定原函式的某一個引數的新函式.

    memoize

    如果一個函式呼叫開銷很大,我們就可能希望能把結果快取下來,以便後續呼叫時直接獲得結果。舉個例子,計算階乘就比較耗時:

    once

    delay

    delay() 可以讓一個函式延遲執行,效果和 setTimeout() 是一樣的,但是程式碼明顯簡單了:

    object

    underscore 也提供了大量針對 Object 的函式

    keys / allkeys

    keys() 可以非常方便地返回一個 object 自身所有的 key,但不包含從原型鏈繼承下來的:
    allKeys() 除了 object 自身的 key,還包含從原型鏈繼承下來的:

    function Student(name, age) {
        this.name = name;
        this.age = age;
    }
    Student.prototype.school = 'No.1 Middle School';
    var xiaoming = new Student('小明', 20);
    _.keys(xiaoming); // ['name', 'age']
    _.allKeys(xiaoming); // ['name', 'age', 'school']
    

    values

    和 keys() 類似,values() 返回 object 自身但不包含原型鏈繼承的所有值

    var obj = {
        name: '小明',
        age: 20
    };
    
    _.values(obj); // ['小明', 20]
    

    注意,沒有 allValues(),原因我也不知道。

    mapObject

    mapObject() 就是針對 object 的 map 版本:

    var obj = { a: 1, b: 2, c: 3 };
    // 注意傳入的函式簽名,value在前,key在後:
    _.mapObject(obj, (v, k) => 100 + v); // { a: 101, b: 102, c: 103 }
    

    invert

    invert() 把 object 的每個 key-value 來個交換,key 變成 value,value 變成 key:

    var obj = {
        Adam: 90,
        Lisa: 85,
        Bart: 59
    };
    _.invert(obj); // { '59': 'Bart', '85': 'Lisa', '90': 'Adam' }
    

    extend / extendOwn

    extend() 把多個 object 的 key-value 合併到第一個 object 並返回:

    var a = {name: 'Bob', age: 20};
    _.extend(a, {age: 15}, {age: 88, city: 'Beijing'}); // {name: 'Bob', age: 88, city: 'Beijing'}
    // 變數a的內容也改變了:
    a; // {name: 'Bob', age: 88, city: 'Beijing'}
    

    注意:如果有相同的 key,後面的 object 的 value 將覆蓋前面的 object 的 value。
    extendOwn() 和 extend() 類似,但獲取屬性時忽略從原型鏈繼承下來的屬性。

    clone

    如果我們要複製一個 object 物件,就可以用 clone() 方法,它會把原有物件的所有屬性都複製到新的物件中

    var source = {
        name: '小明',
        age: 20,
        skills: ['JavaScript', 'CSS', 'HTML']
    };
    var copied = _.clone(source);
    // copied物件為:
    {
      "name": "小明",
      "age": 20,
      "skills": [
        "JavaScript",
        "CSS",
        "HTML"
      ]
    }
    

    注意,clone() 是 “淺複製”。所謂 “淺複製” 就是說,兩個物件相同的 key 所引用的 value 其實是同一物件:

    source.skills === copied.skills; // true
    也就是說,修改 source.skills 會影響 copied.skills。

    isEqual

    isEqual() 對兩個 object 進行深度比較,如果內容完全相同,則返回 true:

    Chaining

    chain() 可以把物件包裝成能程序鏈式呼叫的方法:

    var r = _.chain([1, 4, 9, 16, 25])
             .map(Math.sqrt)
             .filter(x => x % 2 === 1)
             .value();
    

    Node.js

相關文章