標(biāo)簽 (label)
JavaScript 語言允許,語句的前面有標(biāo)簽(label),相當(dāng)于定位符,用于跳轉(zhuǎn)到程序的任意位置,標(biāo)簽的格式如下。
label:
語句
標(biāo)簽可以是任意的標(biāo)識符,但不能是保留字,語句部分可以是任意語句。
標(biāo)簽通常與break語句和continue語句配合使用,跳出特定的循環(huán)。
top:
for (var i = 0; i < 3; i++){
for (var j = 0; j < 3; j++){
if (i === 1 && j === 1) break top;
console.log('i=' + i + ', j=' + j);
}
}
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0
上面代碼為一個雙重循環(huán)區(qū)塊,break 命令后面加上了 top 標(biāo)簽(注意,top不用加引號),滿足條件時,直接跳出雙層循環(huán)。如果 break語句后面不使用標(biāo)簽,則只能跳出內(nèi)層循環(huán),進入下一次的外層循環(huán)。
標(biāo)簽也可以用于跳出代碼塊。
foo: {
console.log(1);
break foo;
console.log('本行不會輸出');
}
console.log(2);
// 1
// 2
上面代碼執(zhí)行到break foo,就會跳出區(qū)塊。
continue語句也可以與標(biāo)簽配合使用。
top:
for (var i = 0; i < 3; i++){
for (var j = 0; j < 3; j++){
if (i === 1 && j === 1) continue top;
console.log('i=' + i + ', j=' + j);
}
}
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0
// i=2, j=0
// i=2, j=1
// i=2, j=2
上面代碼中,continue命令后面有一個標(biāo)簽名,滿足條件時,會跳過當(dāng)前循環(huán),直接進入下一輪外層循環(huán)。如果continue語句后面不使用標(biāo)簽,則只能進入下一輪的內(nèi)層循環(huán)。
數(shù)據(jù)類型
簡介
- 數(shù)值
- 字符
- bloon
- undefined 表示“未定義”或不存在,即由于目前沒有定義,所以此處暫時沒有任何值
- null 表示空值,即此處的值為空。
- object
- Symbol 類型的值 (ES6新增的)
通常,數(shù)值、字符串、布爾值這三種類型,合稱為原始類型(primitive type)的值,即它們是最基本的數(shù)據(jù)類型,不能再細分了。對象則稱為合成類型(complex type)的值,因為一個對象往往是多個原始類型的值的合成,可以看作是一個存放各種值的容器。至于undefined和null,一般將它們看成兩個特殊值。
對象是最復(fù)雜的數(shù)據(jù)類型,又可以分成三個子類型。
- 狹義的對象(object)
- 數(shù)組(array)
- 函數(shù)(function)
狹義的對象和數(shù)組是兩種不同的數(shù)據(jù)組合方式,除非特別聲明,本教程的“對象”都特指狹義的對象。函數(shù)其實是處理數(shù)據(jù)的方法,JavaScript 把它當(dāng)成一種數(shù)據(jù)類型,可以賦值給變量,這為編程帶來了很大的靈活性,也為 JavaScript 的“函數(shù)式編程”奠定了基礎(chǔ)。
typeof 運算符
JavaScript 有三種方法,可以確定一個值到底是什么類型。
- typeof運算符
- instanceof運算符
- Object.prototype.toString方法
instanceof運算符和Object.prototype.toString方法,將在后文介紹。這里介紹typeof運算符。
// typeof運算符 可以返回一個值的類型
typeof 123 // "number"
typeof '123' // "string"
typeof false // "boolean"
function f() {}
typeof f // "function"
typeof undefined
// "undefined"
利用這一點,typeof可以用來檢查一個沒有聲明的變量,而不報錯。
v
// ReferenceError: v is not defined
typeof v
// "undefined"
上面代碼中,變量v沒有用var命令聲明,直接使用就會報錯。但是,放在typeof后面,就不報錯了,而是返回undefined。
實際編程中,這個特點通常用在判斷語句。
// 錯誤的寫法
if (v) {
// ...
}
// ReferenceError: v is not defined
// 正確的寫法
if (typeof v === "undefined") {
// ...
}
對象返回
typeof window // "object"
typeof {} // "object"
typeof [] // "object"
上面代碼中,空數(shù)組([])的類型也是object,這表示在 JavaScript 內(nèi)部,數(shù)組本質(zhì)上只是一種特殊的對象。這里順便提一下,instanceof運算符可以區(qū)分數(shù)組和對象。
var o = {};
var a = [];
o instanceof Array // false
a instanceof Array // true
null返回object。
typeof null // "object"
null的類型是object,這是由于歷史原因造成的。1995年的 JavaScript 語言第一版,只設(shè)計了五種數(shù)據(jù)類型(對象、整數(shù)、浮點數(shù)、字符串和布爾值),沒考慮null,只把它當(dāng)作object的一種特殊值。后來null獨立出來,作為一種單獨的數(shù)據(jù)類型,為了兼容以前的代碼,typeof null返回object就沒法改變了。
null 和 undefined 和布爾值
區(qū)別
- null是一個表示“空”的對象,轉(zhuǎn)為數(shù)值時為0;
- undefined是一個表示"此處無定義"的原始值,轉(zhuǎn)為數(shù)值時為NaN。
Number(null) // 0
5 + null // 5
Number(undefined) // NaN
5 + undefined // NaN
用法和含義
對于null和undefined,大致可以像下面這樣理解。
null表示空值,即該處的值現(xiàn)在為空。調(diào)用函數(shù)時,某個參數(shù)未設(shè)置任何值,這時就可以傳入null,表示該參數(shù)為空。比如,某個函數(shù)接受引擎拋出的錯誤作為參數(shù),如果運行過程中未出錯,那么這個參數(shù)就會傳入null,表示未發(fā)生錯誤。
undefined表示“未定義”,下面是返回undefined的典型場景。
// 變量聲明了,但沒有賦值
var i;
i // undefined
// 調(diào)用函數(shù)時,應(yīng)該提供的參數(shù)沒有提供,該參數(shù)等于 undefined
function f(x) {
return x;
}
f() // undefined
// 對象沒有賦值的屬性
var o = new Object();
o.p // undefined
// 函數(shù)沒有返回值時,默認返回 undefined
function f() {}
f() // undefined
布爾值
下列運算符會返回布爾值
- 前置邏輯運算符: ! (Not)
- 相等運算符:===,!==,==,!=
- 比較運算符:>,>=,<,<=
如果 JavaScript 預(yù)期某個位置應(yīng)該是布爾值,會將該位置上現(xiàn)有的值自動轉(zhuǎn)為布爾值。轉(zhuǎn)換規(guī)則是除了下面六個值被轉(zhuǎn)為false,其他值都視為true。
- undefined
- null
- false
- 0
- NaN
- ""或''(空字符串)
注意,空數(shù)組([])和空對象({})對應(yīng)的布爾值,都是true
數(shù)值
整數(shù)和浮點數(shù)
JavaScript 內(nèi)部,所有數(shù)字都是以64位浮點數(shù)形式儲存,即使整數(shù)也是如此。所以,1與1.0是相同的,是同一個數(shù)。
這就是說,JavaScript 語言的底層根本沒有整數(shù),所有數(shù)字都是小數(shù)(64位浮點數(shù))。容易造成混淆的是,某些運算只有整數(shù)才能完成,此時 JavaScript 會自動把64位浮點數(shù),轉(zhuǎn)成32位整數(shù),然后再進行運算。
由于浮點數(shù)不是精確的值,所以涉及小數(shù)的比較和運算要特別小心。
0.1 + 0.2 === 0.3
// false
0.3 / 0.1
// 2.9999999999999996
(0.3 - 0.2) === (0.2 - 0.1)
// false
數(shù)值精度
NaN
含義
NaN是 JavaScript 的特殊值,表示“非數(shù)字”(Not a Number),主要出現(xiàn)在將字符串解析成數(shù)字出錯的場合。
5 - 'x' // NaN
上面代碼運行時,會自動將字符串x轉(zhuǎn)為數(shù)值,但是由于x不是數(shù)值,所以最后得到結(jié)果為NaN,表示它是“非數(shù)字”(NaN)。
另外,一些數(shù)學(xué)函數(shù)的運算結(jié)果會出現(xiàn)NaN。
0 / 0 // NaN
需要注意的是,NaN不是獨立的數(shù)據(jù)類型,而是一個特殊數(shù)值,它的數(shù)據(jù)類型依然屬于Number,使用typeof運算符可以看得很清楚。
typeof NaN // 'number'
NaN不等于任何值,包括它本身。
NaN === NaN // false
數(shù)組的indexOf方法內(nèi)部使用的是嚴格相等運算符,所以該方法對NaN不成立。
[NaN].indexOf(NaN) // -1
NaN在布爾運算時被當(dāng)作false。
Boolean(NaN) // false
# NaN與任何數(shù)(包括它自己)的運算,得到的都是NaN。
NaN + 32 // NaN
NaN - 32 // NaN
NaN * 32 // NaN
NaN / 32 // NaN
Infinity
Infinity表示“無窮”,用來表示兩種場景。一種是一個正的數(shù)值太大,或一個負的數(shù)值太小,無法表示;另一種是非0數(shù)值除以0,得到Infinity。
// 場景一
Math.pow(2, 1024)
// Infinity
// 場景二
0 / 0 // NaN
1 / 0 // Infinity
上面代碼中,第一個場景是一個表達式的計算結(jié)果太大,超出了能夠表示的范圍,因此返回Infinity。第二個場景是0除以0會得到NaN,而非0數(shù)值除以0,會返回Infinity。
Infinity有正負之分,Infinity表示正的無窮,-Infinity表示負的無窮。
Infinity === -Infinity // false
1 / -0 // -Infinity
-1 / -0 // Infinity
上面代碼中,非零正數(shù)除以-0,會得到-Infinity,負數(shù)除以-0,會得到Infinity。
由于數(shù)值正向溢出(overflow)、負向溢出(underflow)和被0除,JavaScript 都不報錯,所以單純的數(shù)學(xué)運算幾乎沒有可能拋出錯誤。
Infinity大于一切數(shù)值(除了NaN),-Infinity小于一切數(shù)值(除了NaN)。
Infinity > 1000 // true
-Infinity < -1000 // true
Infinity與NaN比較,總是返回false。
Infinity > NaN // false
-Infinity > NaN // false
Infinity < NaN // false
-Infinity < NaN // false
Infinity的四則運算,符合無窮的數(shù)學(xué)計算規(guī)則。
5 * Infinity // Infinity
5 - Infinity // -Infinity
Infinity / 5 // Infinity
5 / Infinity // 0
0乘以Infinity,返回NaN;0除以Infinity,返回0;Infinity除以0,返回Infinity。
0 * Infinity // NaN
0 / Infinity // 0
Infinity / 0 // Infinity
Infinity加上或乘以Infinity,返回的還是Infinity。
Infinity減去或除以Infinity,得到NaN。
Infinity - Infinity // NaN
Infinity / Infinity // NaN
Infinity與null計算時,null會轉(zhuǎn)成0,等同于與0的計算。
null * Infinity // NaN
null / Infinity // 0
Infinity / null // Infinity
Infinity與undefined計算,返回的都是NaN。
undefined + Infinity // NaN
undefined - Infinity // NaN
undefined * Infinity // NaN
undefined / Infinity // NaN
Infinity / undefined // NaN
與數(shù)值相關(guān)的全局方法
parseInt() 字符串轉(zhuǎn)數(shù)字
parseFloat() 字符串轉(zhuǎn)浮點數(shù)
isNaN() 判斷是否為 NaN
isFinite() 方法返回一個布爾值,表示某個值是否為正常的數(shù)值。
除了Infinity、-Infinity、NaN和undefined這幾個值會返回false,isFinite對于其他的數(shù)值都會返回true。
(查看詳細資料)[https://wangdoc.com/javascript/types/number.html]
字符串與數(shù)組
字符串可以被視為字符數(shù)組,因此可以使用數(shù)組的方括號運算符,用來返回某個位置的字符(位置編號從0開始)。
如果方括號中的數(shù)字超過字符串的長度,或者方括號中根本不是數(shù)字,則返回undefined。
var s = 'hello';
s[0] // "h"
s[1] // "e"
s[4] // "o"
// 直接對字符串使用方括號運算符
'hello'[1] // "e"
'abc'[3] // undefined
'abc'[-1] // undefined
'abc'['x'] // undefined
但是,字符串與數(shù)組的相似性僅此而已。實際上,無法改變字符串之中的單個字符。
var s = 'hello';
delete s[0];
s // "hello"
s[1] = 'a';
s // "hello"
s[5] = '!';
s // "hello"
上面代碼表示,字符串內(nèi)部的單個字符無法改變和增刪,這些操作會默默地失敗。
JavaScript 字符集
每個字符在 JavaScript 內(nèi)部都是以16位(即2個字節(jié))的 UTF-16 格式儲存。
也就是說,JavaScript 的單位字符長度固定為16位長度,即2個字節(jié)。
Base64 轉(zhuǎn)碼
有時,文本里面包含一些不可打印的符號,比如 ASCII 碼0到31的符號都無法打印出來,這時可以使用 Base64 編碼,將它們轉(zhuǎn)成可以打印的字符。另一個場景是,有時需要以文本格式傳遞二進制數(shù)據(jù),那么也可以使用 Base64 編碼。
所謂 Base64 就是一種編碼方法,可以將任意值轉(zhuǎn)成 0~9、A~Z、a-z、+和/這64個字符組成的可打印字符。使用它的主要目的,不是為了加密,而是為了不出現(xiàn)特殊字符,簡化程序的處理。
JavaScript 原生提供兩個 Base64 相關(guān)的方法。
- btoa():任意值轉(zhuǎn)為 Base64 編碼
- atob():Base64 編碼轉(zhuǎn)為原來的值
注意,這兩個方法不適合非 ASCII 碼的字符,會報錯。
要將非 ASCII 碼字符轉(zhuǎn)為 Base64 編碼,必須中間插入一個轉(zhuǎn)碼環(huán)節(jié),再使用這兩個方法。
function b64Encode(str) {
return btoa(encodeURIComponent(str));
}
function b64Decode(str) {
return decodeURIComponent(atob(str));
}
b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE"
b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好"
對象
如果不同的變量名指向同一個對象,那么它們都是這個對象的引用,也就是說指向同一個內(nèi)存地址。修改其中一個變量,會影響到其他所有變量。
var o1 = {};
var o2 = o1;
o1.a = 1;
o2.a // 1
o2.b = 2;
o1.b // 2
但是,這種引用只局限于對象,如果兩個變量指向同一個原始類型的值。那么,變量這時都是值的拷貝。
var x = 1;
var y = x;
x = 2;
y // 1
# 上面的代碼中,當(dāng)x的值發(fā)生變化后,y的值并不變,這就表示y和x并不是指向同一個內(nèi)存地址。
表達式還是語句?
對象采用大括號表示,這導(dǎo)致了一個問題:如果行首是一個大括號,它到底是表達式還是語句?
{ foo: 123 }
JavaScript 引擎讀到上面這行代碼,會發(fā)現(xiàn)可能有兩種含義。第一種可能是,這是一個表達式,表示一個包含foo屬性的對象;第二種可能是,這是一個語句,表示一個代碼區(qū)塊,里面有一個標(biāo)簽foo,指向表達式123。
為了避免這種歧義,JavaScript 引擎的做法是,如果遇到這種情況,無法確定是對象還是代碼塊,一律解釋為代碼塊。
{ console.log(123) } // 123
上面的語句是一個代碼塊,而且只有解釋為代碼塊,才能執(zhí)行。
如果要解釋為對象,最好在大括號前加上圓括號。因為圓括號的里面,只能是表達式,所以確保大括號只能解釋為對象。
({ foo: 123 }) // 正確
({ console.log(123) }) // 報錯
這種差異在eval語句(作用是對字符串求值)中反映得最明顯。
eval('{foo: 123}') // 123
eval('({foo: 123})') // {foo: 123}
上面代碼中,如果沒有圓括號,eval將其理解為一個代碼塊;加上圓括號以后,就理解成一個對象。
屬性查看
# 查看一個對象本身的所有屬性,可以使用Object.keys方法。
var obj = {
key1: 1,
key2: 2
};
Object.keys(obj);
// ['key1', 'key2']
屬性的刪除:delete 命令
delete命令用于刪除對象的屬性,刪除成功后返回true。
var obj = { p: 1 };
Object.keys(obj) // ["p"]
delete obj.p // true
obj.p // undefined
Object.keys(obj) // []
上面代碼中,delete命令刪除對象obj的p屬性。刪除后,再讀取p屬性就會返回undefined,而且Object.keys方法的返回值也不再包括該屬性。
注意,刪除一個不存在的屬性,delete不報錯,而且返回true。
var obj = {};
delete obj.p // true
上面代碼中,對象obj并沒有p屬性,但是delete命令照樣返回true。因此,不能根據(jù)delete命令的結(jié)果,認定某個屬性是存在的。
只有一種情況,delete命令會返回false,那就是該屬性存在,且不得刪除。
var obj = Object.defineProperty({}, 'p', {
value: 123,
configurable: false
});
obj.p // 123
delete obj.p // false
上面代碼之中,對象obj的p屬性是不能刪除的,所以delete命令返回false
另外,需要注意的是,delete命令只能刪除對象本身的屬性,無法刪除繼承的屬性
var obj = {};
delete obj.toString // true
obj.toString // function toString() { [native code] }
上面代碼中,toString是對象obj繼承的屬性,雖然delete命令返回true,但該屬性并沒有被刪除,依然存在。這個例子還說明,即使delete返回true,該屬性依然可能讀取到值。
屬性是否存在:in 運算符
in運算符用于檢查對象是否包含某個屬性(注意,檢查的是鍵名,不是鍵值),如果包含就返回true,否則返回false。它的左邊是一個字符串,表示屬性名,右邊是一個對象。
var obj = { p: 1 };
'p' in obj // true
'toString' in obj // true
in運算符的一個問題是,它不能識別哪些屬性是對象自身的,哪些屬性是繼承的。就像上面代碼中,對象obj本身并沒有toString屬性,但是in運算符會返回true,因為這個屬性是繼承的。
這時,可以使用對象的hasOwnProperty方法判斷一下,是否為對象自身的屬性。
var obj = {};
if ('toString' in obj) {
console.log(obj.hasOwnProperty('toString')) // false
}
屬性的遍歷:for...in 循環(huán)
for...in循環(huán)用來遍歷一個對象的全部屬性。
var obj = {a: 1, b: 2, c: 3};
for (var i in obj) {
console.log('鍵名:', i);
console.log('鍵值:', obj[i]);
}
// 鍵名: a
// 鍵值: 1
// 鍵名: b
// 鍵值: 2
// 鍵名: c
// 鍵值: 3
for...in循環(huán)有兩個使用注意點。
- 它遍歷的是對象所有可遍歷(enumerable)的屬性,會跳過不可遍歷的屬性。
- 它不僅遍歷對象自身的屬性,還遍歷繼承的屬性。
舉例來說,對象都繼承了toString屬性,但是for...in循環(huán)不會遍歷到這個屬性。
對象obj繼承了toString屬性,該屬性不會被for...in循環(huán)遍歷到,因為它默認是“不可遍歷”的。關(guān)于對象屬性的可遍歷性,參見《標(biāo)準(zhǔn)庫》章節(jié)中 Object 一章的介紹。
如果繼承的屬性是可遍歷的,那么就會被for...in循環(huán)遍歷到。但是,一般情況下,都是只想遍歷對象自身的屬性,所以使用for...in的時候,應(yīng)該結(jié)合使用hasOwnProperty方法,在循環(huán)內(nèi)部判斷一下,某個屬性是否為對象自身的屬性。
var person = { name: '老張' };
for (var key in person) {
if (person.hasOwnProperty(key)) {
console.log(key);
}
}
// name
閉包
閉包的最大用處有兩個,一個是可以讀取函數(shù)內(nèi)部的變量,另一個就是讓這些變量始終保持在內(nèi)存中,即閉包可以使得它誕生環(huán)境一直存在。請看下面的例子,閉包使得內(nèi)部變量記住上一次調(diào)用時的運算結(jié)果。
function createIncrementor(start) {
return function () {
return start++;
};
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7
上面代碼中,start是函數(shù)createIncrementor的內(nèi)部變量。通過閉包,start的狀態(tài)被保留了,每一次調(diào)用都是在上一次調(diào)用的基礎(chǔ)上進行計算。從中可以看到,閉包inc使得函數(shù)createIncrementor的內(nèi)部環(huán)境,一直存在。所以,閉包可以看作是函數(shù)內(nèi)部作用域的一個接口。
為什么會這樣呢?原因就在于inc始終在內(nèi)存中,而inc的存在依賴于createIncrementor,因此也始終在內(nèi)存中,不會在調(diào)用結(jié)束后,被垃圾回收機制回收。
閉包的另一個用處,是封裝對象的私有屬性和私有方法。
function Person(name) {
var _age;
function setAge(n) {
_age = n;
}
function getAge() {
return _age;
}
return {
name: name,
getAge: getAge,
setAge: setAge
};
}
var p1 = Person('張三');
p1.setAge(25);
p1.getAge() // 25
上面代碼中,函數(shù)Person的內(nèi)部變量_age,通過閉包getAge和setAge,變成了返回對象p1的私有變量。
注意,外層函數(shù)每次運行,都會生成一個新的閉包,而這個閉包又會保留外層函數(shù)的內(nèi)部變量,所以內(nèi)存消耗很大。因此不能濫用閉包,否則會造成網(wǎng)頁的性能問題。
數(shù)組
數(shù)組(array)是按次序排列的一組值。每個值的位置都有編號(從0開始),整個數(shù)組用方括號表示。
任何類型的數(shù)據(jù),都可以放入數(shù)組。
var arr = [
{a: 1},
[1, 2, 3],
function() {return true;}
];
arr[0] // Object {a: 1}
arr[1] // [1, 2, 3]
arr[2] // function (){return true;}
如果數(shù)組的元素還是數(shù)組,就形成了多維數(shù)組。
JavaScript 使用一個32位整數(shù),保存數(shù)組的元素個數(shù)。這意味著,數(shù)組成員最多只有 4294967295 個(232 - 1)個,也就是說length屬性的最大值就是 4294967295。
只要是數(shù)組,就一定有l(wèi)ength屬性。該屬性是一個動態(tài)的值,等于鍵名中的最大整數(shù)加上1。
運算符
運算符重載
如果一個運算子是字符串,另一個運算子是非字符串,這時非字符串會轉(zhuǎn)成字符串,再連接在一起。
1 + 'a' // "1a"
false + 'a' // "falsea"
'3' + 4 + 5 // "345"
3 + 4 + '5' // "75"
加法運算符是在運行時決定,到底是執(zhí)行相加,還是執(zhí)行連接。也就是說,運算子的不同,導(dǎo)致了不同的語法行為,這種現(xiàn)象稱為“重載”(overload)。由于加法運算符存在重載,可能執(zhí)行兩種運算,使用的時候必須很小心。
除了加法運算符,其他算術(shù)運算符(比如減法、除法和乘法)都不會發(fā)生重載。它們的規(guī)則是:所有運算子一律轉(zhuǎn)為數(shù)值,再進行相應(yīng)的數(shù)學(xué)運算。
類型轉(zhuǎn)換
強制轉(zhuǎn)換
強制轉(zhuǎn)換主要指使用Number()、String()和Boolean()三個函數(shù),手動將各種類型的值,分別轉(zhuǎn)換成數(shù)字、字符串或者布爾值。
// 數(shù)值:轉(zhuǎn)換后還是原來的值
Number(324) // 324
// 字符串:如果可以被解析為數(shù)值,則轉(zhuǎn)換為相應(yīng)的數(shù)值
Number('324') // 324
// 字符串:如果不可以被解析為數(shù)值,返回 NaN
Number('324abc') // NaN
// 空字符串轉(zhuǎn)為0
Number('') // 0
// 布爾值:true 轉(zhuǎn)成 1,false 轉(zhuǎn)成 0
Number(true) // 1
Number(false) // 0
// undefined:轉(zhuǎn)成 NaN
Number(undefined) // NaN
// null:轉(zhuǎn)成0
Number(null) // 0
Number函數(shù)將字符串轉(zhuǎn)為數(shù)值,要比parseInt函數(shù)嚴格很多。基本上,只要有一個字符無法轉(zhuǎn)成數(shù)值,整個字符串就會被轉(zhuǎn)為NaN。