1
if ( typeof v === 'undefined') { // true
}
if ( typeof null === 'object') { // true
}
2
Number(null) //0
5 + null //5
Number(undefined) //NaN
5 + undefined //NaN
3 整數和浮點數
JavaScript 語言的底層根本沒有整數,所有數字都是小數(64位浮點數)。容易造成混淆的是,某些運算只有整數才能完成,此時 JavaScript 會自動把64位浮點數,轉成32位整數,然後再進行運算,參見《運算符》一章的“位運算”部分。
由於浮點數不是精確的值,所以涉及小數的比較和運算要特別小心。
1 === 1.0 // true
0.1 + 0.2 === 0.3 // false
0.3 / 0.1
// 2.9999999999999996
(0.3 - 0.2) === (0.2 - 0.1)
// false
4
精度最多隻能到53個二進制位,這意味着,絕對值小於2的53次方的整數,即-253到253,都可以精確表示。
Math.pow(2, 53)
// 9007199254740992
Math.pow(2, 53) + 1
// 9007199254740992
Math.pow(2, 53) + 2
// 9007199254740994
Math.pow(2, 53) + 3
// 9007199254740996
Math.pow(2, 53) + 4
// 900719925474099
Number.MAX_VALUE // 1.7976931348623157e+308
Number.MIN_VALUE // 5e-324
5 數值的進制
使用字面量(literal)直接表示一個數值時,JavaScript 對整數提供四種進制的表示方法:十進制、十六進制、八進制、二進制。
十進制:沒有前導0的數值。
八進制:有前綴0o或0O的數值,或者有前導0、且只用到0-7的八個阿拉伯數字的數值。
十六進制:有前綴0x或0X的數值。
二進制:有前綴0b或0B的數值。
6 正零和負零
-0 === +0 // true
0 === -0 // true
0 === +0 // true
+0 // 0
-0 // 0
(-0).toString() // '0'
(+0).toString() // '0'
(1 / +0) === (1 / -0) // false
7 NaN not a number
typeof NaN // 'number'
NaN === NaN // false
[NaN].indexOf(NaN) // -1
Boolean(NaN) // false
NaN + 32 // NaN
NaN - 32 // NaN
NaN * 32 // NaN
NaN / 32 // NaN
// 場景一
Math.pow(2, 1024)
// Infinity
// 場景二
0 / 0 // NaN
1 / 0 // Infinity
Infinity === -Infinity // false
1 / -0 // -Infinity
-1 / -0 // Infinity
Infinity > 1000 // true
-Infinity < -1000 // true
Infinity > NaN // false
-Infinity > NaN // false
Infinity < NaN // false
-Infinity < NaN // false
7parseInt()
如果parseInt的參數不是字符串,則會先轉爲字符串再轉換。
parseInt(1.23) // 1
// 等同於
parseInt('1.23') // 1
字符串轉爲整數的時候,是一個個字符依次轉換,如果遇到不能轉爲數字的字符,就不再進行下去,返回已經轉好的部分。
parseInt('8a') //8
parseInt('12**') // 12
parseInt('12.34') // 12
parseInt('15e2') // 15
parseInt('15px') // 15
parseInt('abc') // NaN
parseInt('.3') // NaN
parseInt('') // NaN
parseInt('+') // NaN
parseInt('+1') // 1
所以,parseInt的返回值只有兩種可能,要麼是一個十進制整數,要麼是NaN。
如果字符串以0x或0X開頭,parseInt會將其按照十六進制數解析。
paeseInt('0x10') //16
如果字符串以0開頭,將其按照10進制解析。
parseInt('011') //11
對於那些會自動轉爲科學計數法的數字,parseInt會將科學計數法的表示方法視爲字符串,因此導致一些奇怪的結果。
parseInt(1000000000000000000000.5) // 1
// 等同於
parseInt('1e+21') // 1
parseInt(0.0000008) // 8
// 等同於
parseInt('8e-7') // 8
parseInt方法還可以接受第二個參數(2到36之間),表示被解析的值的進制,返回該值對應的十進制數。默認情況下,parseInt的第二個參數爲10,即默認是十進制轉十進制。
parseInt('1000') // 1000
// 等同於
parseInt('1000', 10) // 1000
parseInt('1000', 2) // 8
parseInt('1000', 6) // 216
parseInt('1000', 8) // 512
如果第二個參數不是數值,會被自動轉爲一個整數。這個整數只有在2到36之間,才能得到有意義的結果,超出這個範圍,則返回NaN。如果第二個參數是0、undefined和null,則直接忽略。
parseInt('10', 37) // NaN
parseInt('10', 1) // NaN
parseInt('10', 0) // 10
parseInt('10', null) // 10
parseInt('10', undefined) // 10
如果字符串包含對於指定進制無意義的字符,則從最高位開始,只返回可以轉換的數值。如果最高位無法轉換,則直接返回NaN。
parseInt('1546', 2) // 1
parseInt('546', 2) // NaN
前面說過,如果parseInt的第一個參數不是字符串,會被先轉爲字符串。這會導致一些令人意外的結果。
parseInt(0x11, 36) // 43
parseInt(0x11, 2) // 1
// 等同於
parseInt(String(0x11), 36)
parseInt(String(0x11), 2)
// 等同於
parseInt('17', 36)
parseInt('17', 2)
上面代碼中,十六進制的0x11會被先轉爲十進制的17,再轉爲字符串。然後,再用36進制或二進制解讀字符串17,最後返回結果43和1。
這種處理方式,對於八進制的前綴0,尤其需要注意。
parseInt(011, 2) // NaN
// 等同於
parseInt(String(011), 2)
// 等同於
parseInt(String(9), 2)
上面代碼中,第一行的011會被先轉爲字符串9,因爲9不是二進制的有效字符,所以返回NaN。如果直接計算parseInt(‘011’, 2),011則是會被當作二進制處理,返回3。
JavaScript 不再允許將帶有前綴0的數字視爲八進制數,而是要求忽略這個0。但是,爲了保證兼容性,大部分瀏覽器並沒有部署這一條規定。
8parseFloat
parseFloat(true) // NaN
Number(true) // 1
parseFloat(null) // NaN
Number(null) // 0
parseFloat('') // NaN
Number('') // 0
parseFloat('123.45#') // 123.45
Number('123.45#') // NaN
9 isNaN
但是,對於空數組和只有一個數值成員的數組,isNaN返回false。
isNaN([]) // false
isNaN([123]) // false
isNaN(['123']) // false
上面代碼之所以返回false,原因是這些數組能被Number函數轉成數值,請參見《數據類型轉換》一章。
因此,使用isNaN之前,最好判斷一下數據類型。
function myIsNaN(value) {
return typeof value === 'number' && isNaN(value);
}
判斷NaN更可靠的方法是,利用NaN爲唯一不等於自身的值的這個特點,進行判斷。
function myIsNaN(value) {
return value !== value;
}
10 標識符
下面這些都是合法的標識符。
arg0
_tmp
$elem
π
下面這些則是不合法的標識符。
1a // 第一個字符不能是數字
23 // 同上
*** // 標識符不能包含星號
a+b // 標識符不能包含加號
-d // 標識符不能包含減號或連詞線
中文是合法的標識符,可以用作變量名。
var 臨時變量 = 1;
JavaScript 有一些保留字,不能用作標識符:arguments、break、case、catch、class、const、continue、debugger、default、delete、do、else、enum、eval、export、extends、false、finally、for、function、if、implements、import、in、instanceof、interface、let、new、null、package、private、protected、public、return、static、super、switch、this、throw、true、try、typeof、var、void、while、with、yield。
11 字符串
如果長字符串必須分成多行,可以在每一行的尾部使用反斜槓。
var longString = 'Long \
long \
long \
string';
longString
// "Long long long string"
上面代碼表示,加了反斜槓以後,原來寫在一行的字符串,可以分成多行書寫。但是,輸出的時候還是單行,效果與寫在同一行完全一樣。注意,反斜槓的後面必須是換行符,而不能有其他字符(比如空格),否則會報錯。
連接運算符(+)可以連接多個單行字符串,將長字符串拆成多行書寫,輸出的時候也是單行
var longString = 'Long '
+ 'long '
+ 'long '
+ 'string';
如果想輸出多行字符串,有一種利用多行註釋的變通方法。
(function () { /*
line 1
line 2
line 3
*/}).toString().split('\n').slice(1, -1).join('\n')
// "line 1
// line 2
// line 3"
反斜槓(\)在字符串內有特殊含義,用來表示一些特殊字符,所以又稱爲轉義符。
需要用反斜槓轉義的特殊字符,主要有下面這些。
\0 :null(\u0000)
\b :後退鍵(\u0008)
\f :換頁符(\u000C)
\n :換行符(\u000A)
\r :回車鍵(\u000D)
\t :製表符(\u0009)
\v :垂直製表符(\u000B)
\' :單引號(\u0027)
\" :雙引號(\u0022)
\\ :反斜槓(\u005C)
字符串可以被視爲字符數組,因此可以使用數組的方括號運算符,用來返回某個位置的字符(位置編號從0開始)。
var s = 'hello';
s[0] // "h"
s[1] // "e"
s[4] // "o"
// 直接對字符串使用方括號運算符
'hello'[1] // "e"
但是,字符串與數組的相似性僅此而已。實際上,無法改變字符串之中的單個字符。
var s = 'hello';
delete s[0];
s // "hello"
s[1] = 'a';
s // "hello"
s[5] = '!';
s // "hello"
12 Base64 轉碼
JavaScript 原生提供兩個 Base64 相關的方法。
btoa():任意值轉爲 Base64 編碼
atob():Base64 編碼轉爲原來的值
var string = 'Hello World!';
btoa(string) // "SGVsbG8gV29ybGQh"
atob('SGVsbG8gV29ybGQh') // "Hello World!"
注意,這兩個方法不適合非 ASCII 碼的字符,會報錯。
btoa('你好') // 報錯
要將非 ASCII 碼字符轉爲 Base64 編碼,必須中間插入一個轉碼環節,再使用這兩個方法。
function b64Encode(str) {
return btoa(encodeURIComponent(str));
}
function b64Decode(str) {
return decodeURIComponent(atob(str));
}
b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE"
b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好"
13 object
var obj = {};
if ('toString' in obj) {
console.log(obj.hasOwnProperty('toString')) // false
}
for…in循環有兩個使用注意點。
它遍歷的是對象所有可遍歷(enumerable)的屬性,會跳過不可遍歷的屬性。
它不僅遍歷對象自身的屬性,還遍歷繼承的屬性。
舉例來說,對象都繼承了toString屬性,但是for…in循環不會遍歷到這個屬性。
var person = { name: '老張' };
for (var key in person) {
if (person.hasOwnProperty(key)) {
console.log(key);
}
}
// name
14 function
上面代碼第二行,調用f的時候,f只是被聲明瞭,還沒有被賦值,等於undefined,所以會報錯。因此,如果同時採用function命令和賦值語句聲明同一個函數,最後總是採用賦值語句的定義。
var f = function () {
console.log('1');
}
function f() {
console.log('2');
}
f() // 1
函數的toString方法返回一個字符串,內容是函數的源碼。
function f() {
a();
b();
c();
}
f.toString()
// function f() {
// a();
// b();
// c();
// }
對於頂層函數來說,函數外部聲明的變量就是全局變量(global variable),它可以在函數內部讀取。
var v = 1;
function f() {
console.log(v);
}
f()
// 1
上面的代碼表明,函數f內部可以讀取全局變量v。
在函數內部定義的變量,外部無法讀取,稱爲“局部變量”(local variable)。
function f(){
var v = 1;
}
v // ReferenceError: v is not defined
上面代碼中,變量v在函數內部定義,所以是一個局部變量,函數之外就無法讀取。
函數內部定義的變量,會在該作用域內覆蓋同名全局變量。
var v = 1;
function f(){
var v = 2;
console.log(v);
}
f() // 2
v // 1
上面代碼中,變量v同時在函數的外部和內部有定義。結果,在函數內部定義,局部變量v覆蓋了全局變量v。
注意,對於var命令來說,局部變量只能在函數內部聲明,在其他區塊中聲明,一律都是全局變量。
在其他區塊中聲明,一律都是全局變量。
在其他區塊中聲明,一律都是全局變量。
if (true) {
var x = 5;
}
console.log(x); // 5
上面代碼中,變量x在條件判斷區塊之中聲明,結果就是一個全局變量,可以在區塊之外讀取。
函數本身也是一個值,也有自己的作用域。它的作用域與變量一樣,就是其聲明時所在的作用域,與其運行時所在的作用域無關。
arguments
需要注意的是,雖然arguments很像數組,但它是一個對象。數組專有的方法(比如slice和forEach),不能在arguments對象上直接使用。
如果要讓arguments對象使用數組方法,真正的解決方法是將arguments轉爲真正的數組。下面是兩種常用的轉換方法:slice方法和逐一填入新數組。
var args = Array.prototype.slice.call(arguments);
// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
閉包
閉包的最大用處有兩個,一個是可以讀取函數內部的變量,另一個就是讓這些變量始終保持在內存中,即閉包可以使得它誕生環境一直存在。請看下面的例子,閉包使得內部變量記住上一次調用時的運算結果。
function createIncrementor(start) {
return function () {
return start++;
};
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7
上面代碼中,start是函數createIncrementor的內部變量。通過閉包,start的狀態被保留了,每一次調用都是在上一次調用的基礎上進行計算。從中可以看到,閉包inc使得函數createIncrementor的內部環境,一直存在。所以,閉包可以看作是函數內部作用域的一個接口。
爲什麼會這樣呢?原因就在於inc始終在內存中,而inc的存在依賴於createIncrementor,因此也始終在內存中,不會在調用結束後,被垃圾回收機制回收。
閉包的另一個用處,是封裝對象的私有屬性和私有方法。
立即調用的函數表達式(IIFE)
有時,我們需要在定義函數之後,立即調用該函數。這時,你不能在函數的定義之後加上圓括號,這會產生語法錯誤。
function(){ /* code */ }();
// SyntaxError: Unexpected token (
產生這個錯誤的原因是,function這個關鍵字即可以當作語句,也可以當作表達式。
// 語句
function f() {}
// 表達式
var f = function f() {}
爲了避免解析上的歧義,JavaScript 引擎規定,如果function關鍵字出現在行首,一律解釋成語句。因此,JavaScript 引擎看到行首是function關鍵字之後,認爲這一段都是函數的定義,不應該以圓括號結尾,所以就報錯了。
解決方法就是不要讓function出現在行首,讓引擎將其理解成一個表達式。最簡單的處理,就是將其放在一個圓括號裏面。
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();
上面兩種寫法都是以圓括號開頭,引擎就會認爲後面跟的是一個表示式,而不是函數定義語句,所以就避免了錯誤。這就叫做“立即調用的函數表達式”(Immediately-Invoked Function Expression),簡稱 IIFE。
注意,上面兩種寫法最後的分號都是必須的。如果省略分號,遇到連着兩個 IIFE,可能就會報錯。
// 報錯
(function(){ /* code */ }())
(function(){ /* code */ }())
上面代碼的兩行之間沒有分號,JavaScript 會將它們連在一起解釋,將第二行解釋爲第一行的參數。
推而廣之,任何讓解釋器以表達式來處理函數定義的方法,都能產生同樣的效果,比如下面三種寫法。
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
甚至像下面這樣寫,也是可以的。
!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();
通常情況下,只對匿名函數使用這種“立即執行的函數表達式”。它的目的有兩個:一是不必爲函數命名,避免了污染全局變量;二是 IIFE 內部形成了一個單獨的作用域,可以封裝一些外部無法讀取的私有變量。
// 寫法一
var tmp = newData;
processData(tmp);
storeData(tmp);
// 寫法二
(function () {
var tmp = newData;
processData(tmp);
storeData(tmp);
}());
上面代碼中,寫法二比寫法一更好,因爲完全避免了污染全局變量。
15 數組
// 設置負值
[].length = -1
// RangeError: Invalid array length
// 數組元素個數大於等於2的32次方
[].length = Math.pow(2, 32)
// RangeError: Invalid array length
// 設置字符串
[].length = 'abc'
// RangeError: Invalid array length
值得注意的是,由於數組本質上是一種對象,所以可以爲數組添加屬性,但是這不影響length屬性的值。
var a = [];
a['p'] = 'abc';
a.length // 0
a[2.1] = 'abc';
a.length // 0
上面代碼將數組的鍵分別設爲字符串和小數,結果都不影響length屬性。因爲,length屬性的值就是等於最大的數字鍵加1,而這個數組沒有整數鍵,所以length屬性保持爲0。
如果數組的鍵名是添加超出範圍的數值,該鍵名會自動轉爲字符串。
var arr = [];
arr[-1] = 'a';
arr[Math.pow(2, 32)] = 'b';
arr.length // 0
arr[-1] // "a"
arr[4294967296] // "b"
上面代碼中,我們爲數組arr添加了兩個不合法的數字鍵,結果length屬性沒有發生變化。這些數字鍵都變成了字符串鍵名。最後兩行之所以會取到值,是因爲取鍵值時,數字鍵名會默認轉爲字符串。
數組的slice方法可以將“類似數組的對象”變成真正的數組。
var arr = Array.prototype.slice.call(arrayLike);
除了轉爲真正的數組,“類似數組的對象”還有一個辦法可以使用數組的方法,就是通過call()把數組的方法放到對象上面。
function print(value, index) {
console.log(index + ' : ' + value);
}
Array.prototype.forEach.call(arrayLike, print);
上面代碼中,arrayLike代表一個類似數組的對象,本來是不可以使用數組的forEach()方法的,但是通過call(),可以把forEach()嫁接到arrayLike上面調用。
下面的例子就是通過這種方法,在arguments對象上面調用forEach方法。
// forEach 方法
function logArgs() {
Array.prototype.forEach.call(arguments, function (elem, i) {
console.log(i + '. ' + elem);
});
}
// 等同於 for 循環
function logArgs() {
for (var i = 0; i < arguments.length; i++) {
console.log(i + '. ' + arguments[i]);
}
}
字符串也是類似數組的對象,所以也可以用Array.prototype.forEach.call遍歷。
Array.prototype.forEach.call('abc', function (chr) {
console.log(chr);
});
// a
// b
// c
注意,這種方法比直接使用數組原生的forEach要慢,所以最好還是先將“類似數組的對象”轉爲真正的數組,然後再直接調用數組的forEach方法。
var arr = Array.prototype.slice.call('abc');
arr.forEach(function (chr) {
console.log(chr);
});
// a
// b
// c
16 比較
NaN === NaN // false
+0 === -0 // true
undefined === undefined // true
null === null // true
兩個複合類型(對象、數組、函數)的數據比較時,不是比較它們的值是否相等,而是比較它們是否指向同一個地址。
{} === {} // false
[] === [] // false
(function () {} === function () {}) // false
如果兩個變量引用同一個對象,則它們相等。
var v1 = {};
var v2 = v1;
v1 === v2 // true
1 == 1.0
// 等同於
1 === 1.0
上面代碼中,數組[1]與數值進行比較,會先轉成數值,再進行比較;與字符串進行比較,會先轉成字符串,再進行比較;與布爾值進行比較,對象和布爾值都會先轉成數值,再進行比較。
17 void
請看下面的代碼。
<script>
function f() {
console.log('Hello World');
}
</script>
<a href="http://example.com" onclick="f(); return false;">點擊</a>
上面代碼中,點擊鏈接後,會先執行onclick的代碼,由於onclick返回false,所以瀏覽器不會跳轉到 example.com。
void運算符可以取代上面的寫法。
<a href="javascript: void(f())">文字</a>
下面是一個更實際的例子,用戶點擊鏈接提交表單,但是不產生頁面跳轉。
<a href="javascript: void(document.form.submit())">
提交
</a>
18 數據類型的轉換
// 數值:轉換後還是原來的值
Number(324) // 324
// 字符串:如果可以被解析爲數值,則轉換爲相應的數值
Number('324') // 324
// 字符串:如果不可以被解析爲數值,返回 NaN
Number('324abc') // NaN
// 空字符串轉爲0
Number('') // 0
// 布爾值:true 轉成 1,false 轉成 0
Number(true) // 1
Number(false) // 0
// undefined:轉成 NaN
Number(undefined) // NaN
// null:轉成0
Number(null) // 0
19 try…catch…finally
function f() {
try {
console.log(0);
throw 'bug';
} catch(e) {
console.log(1);
return true; // 這句原本會延遲到 finally 代碼塊結束再執行
console.log(2); // 不會運行
} finally {
console.log(3);
return false; // 這句會覆蓋掉前面那句 return
console.log(4); // 不會運行
}
console.log(5); // 不會運行
}
var result = f();
// 0
// 1
// 3
result
// false
20 Object
var obj = Object(1);
obj instanceof Object // true
obj instanceof Number // true
var obj = Object('foo');
obj instanceof Object // true
obj instanceof String // true
var obj = Object(true);
obj instanceof Object // true
obj instanceof Boolean // true
上面代碼中,Object函數的參數是各種原始類型的值,轉換成對象就是原始類型值對應的包裝對象。
如果Object方法的參數是一個對象,它總是返回該對象,即不用轉換。
var arr = [];
var obj = Object(arr); // 返回原數組
obj === arr // true
var value = {};
var obj = Object(value) // 返回原對象
obj === value // true
var fn = function () {};
var obj = Object(fn); // 返回原函數
obj === fn // true
利用這一點,可以寫一個判斷變量是否爲對象的函數。
function isObject(value) {
return value === Object(value);
}
isObject([]) // true
isObject(true) // false
對於一般的對象來說,Object.keys()和Object.getOwnPropertyNames()返回的結果是一樣的。只有涉及不可枚舉屬性時,纔會有不一樣的結果。Object.keys方法只返回可枚舉的屬性(詳見《對象屬性的描述對象》一章),Object.getOwnPropertyNames方法還返回不可枚舉的屬性名。
var a = ['Hello', 'World'];
Object.keys(a) // ["0", "1"]
Object.getOwnPropertyNames(a) // ["0", "1", "length"]
上面代碼中,數組的length屬性是不可枚舉的屬性,所以只出現在Object.getOwnPropertyNames方法的返回結果中。
由於 JavaScript 沒有提供計算對象屬性個數的方法,所以可以用這兩個方法代替。
var obj = {
p1: 123,
p2: 456
};
Object.keys(obj).length // 2
Object.getOwnPropertyNames(obj).length // 2
一般情況下,幾乎總是使用Object.keys方法,遍歷對象的屬性。
這就是說,Object.prototype.toString可以看出一個值到底是什麼類型。
Object.prototype.toString.call(2) // "[object Number]"
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(Math) // "[object Math]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call([]) // "[object Array]"
利用這個特性,可以寫出一個比typeof運算符更準確的類型判斷函數。
var type = function (o){
var s = Object.prototype.toString.call(o);
return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};
type({}); // "object"
type([]); // "array"
type(5); // "number"
type(null); // "null"
type(); // "undefined"
type(/abcd/); // "regex"
type(new Date()); // "date"
在上面這個type函數的基礎上,還可以加上專門判斷某種類型數據的方法。
var type = function (o){
var s = Object.prototype.toString.call(o);
return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};
['Null',
'Undefined',
'Object',
'Array',
'String',
'Number',
'Boolean',
'Function',
'RegExp'
].forEach(function (t) {
type['is' + t] = function (o) {
return type(o) === t.toLowerCase();
};
});
type.isObject({}) // true
type.isNumber(NaN) // true
type.isRegExp(/abc/) // true
Object.defineProperty()方法允許通過屬性描述對象,定義或修改一個屬性,然後返回修改後的對象,它的用法如下。
Object.defineProperty(object, propertyName, attributesObject)
Object.defineProperty方法接受三個參數,依次如下。
object:屬性所在的對象
propertyName:字符串,表示屬性名
attributesObject:屬性描述對象
舉例來說,定義obj.p可以寫成下面這樣。
var obj = Object.defineProperty({}, 'p', {
value: 123,
writable: false,
enumerable: true,
configurable: false
});
obj.p // 123
obj.p = 246;
obj.p // 123
如果一次性定義或修改多個屬性,可以使用Object.defineProperties()方法。
var obj = Object.defineProperties({}, {
p1: { value: 123, enumerable: true },
p2: { value: 'abc', enumerable: true },
p3: { get: function () { return this.p1 + this.p2 },
enumerable:true,
configurable:true
}
});
obj.p1 // 123
obj.p2 // "abc"
obj.p3 // "123abc"
對象的拷貝
有時,我們需要將一個對象的所有屬性,拷貝到另一個對象,可以用下面的方法實現。
var extend = function (to, from) {
for (var property in from) {
to[property] = from[property];
}
return to;
}
extend({}, {
a: 1
})
// {a: 1}
上面這個方法的問題在於,如果遇到存取器定義的屬性,會只拷貝值。
extend({}, {
get a() { return 1 }
})
// {a: 1}
爲了解決這個問題,我們可以通過Object.defineProperty方法來拷貝屬性。
var extend = function (to, from) {
for (var property in from) {
if (!from.hasOwnProperty(property)) continue;
Object.defineProperty(
to,
property,
Object.getOwnPropertyDescriptor(from, property)
);
}
return to;
}
extend({}, { get a(){ return 1 } })
// { get a(){ return 1 } })
上面代碼中,hasOwnProperty那一行用來過濾掉繼承的屬性,否則可能會報錯,因爲Object.getOwnPropertyDescriptor讀不到繼承屬性的屬性描述對象。
有時需要凍結對象的讀寫狀態,防止對象被改變。JavaScript 提供了三種凍結方法,最弱的一種是Object.preventExtensions,其次是Object.seal,最強的是Object.freeze。
21 Array
Array.isArray方法返回一個布爾值,表示參數是否爲數組。它可以彌補typeof運算符的不足。
var arr = [1, 2, 3];
typeof arr // "object"
Array.isArray(arr) // true
valueOf方法是一個所有對象都擁有的方法,表示對該對象求值。不同對象的valueOf方法不盡一致,數組的valueOf方法返回數組本身。
var arr = [1, 2, 3];
arr.valueOf() // [1, 2, 3]
toString方法也是對象的通用方法,數組的toString方法返回數組的字符串形式。
var arr = [1, 2, 3];
arr.toString() // "1,2,3"
var arr = [1, 2, 3, [4, 5, 6]];
arr.toString() // "1,2,3,4,5,6"
push方法用於在數組的末端添加一個或多個元素,並返回添加新元素後的數組長度。注意,該方法會改變原數組。
pop方法用於刪除數組的最後一個元素,並返回該元素。注意,該方法會改變原數組。
shift()方法用於刪除數組的第一個元素,並返回該元素。注意,該方法會改變原數組。
unshift()方法用於在數組的第一個位置添加元素,並返回添加新元素後的數組長度。注意,該方法會改變原數組。
push和pop結合使用,就構成了“後進先出”的棧結構(stack)。
push()和shift()結合使用,就構成了“先進先出”的隊列結構(queue)。
var arr = [];
arr.push(1, 2);
arr.push(3);
arr.pop();
arr // [1, 2]
var a = ['1','2]
a.unshift('3')
a // [3,1,2]
join()方法以指定參數作爲分隔符,將所有數組成員連接爲一個字符串返回。如果不提供參數,默認用逗號分隔。
var a = [1, 2, 3, 4];
a.join(' ') // '1 2 3 4'
a.join(' | ') // "1 | 2 | 3 | 4"
a.join() // "1,2,3,4"
如果數組成員是undefined或null或空位,會被轉成空字符串。
[undefined, null].join('#')
// '#'
['a',, 'b'].join('-')
// 'a--b'
通過call方法,這個方法也可以用於字符串或類似數組的對象。
Array.prototype.join.call('hello', '-')
// "h-e-l-l-o"
var obj = { 0: 'a', 1: 'b', length: 2 };
Array.prototype.join.call(obj, '-')
// 'a-b'
concat
如果數組成員包括對象,concat方法返回當前數組的一個淺拷貝。所謂“淺拷貝”,指的是新數組拷貝的是對象的引用。
var obj = { a: 1 };
var oldArray = [obj];
var newArray = oldArray.concat();
obj.a = 2;
newArray[0].a // 2
slice方法用於提取目標數組的一部分,返回一個新數組,原數組不變。
arr.slice(start, end)
;
var a = ['a', 'b', 'c'];
a.slice(0) // ["a", "b", "c"]
a.slice(1) // ["b", "c"]
a.slice(1, 2) // ["b"]
a.slice(2, 6) // ["c"]
a.slice() // ["a", "b", "c"]
它的第一個參數爲起始位置(從0開始),第二個參數爲終止位置(但該位置的元素本身不包括在內)。如果省略第二個參數,則一直返回到原數組的最後一個成員。
slice方法的一個重要應用,是將類似數組的對象轉爲真正的數組。
Array.prototype.slice.call({ 0: 'a', 1: 'b', length: 2 })
// ['a', 'b']
Array.prototype.slice.call(document.querySelectorAll("div"));
Array.prototype.slice.call(arguments);
splice方法用於刪除原數組的一部分成員,並可以在刪除的位置添加新的數組成員,返回值是被刪除的元素。注意,該方法會改變原數組。
arr.splice(start, count, addElement1, addElement2, ...);
splice的第一個參數是刪除的起始位置(從0開始),第二個參數是被刪除的元素個數。如果後面還有更多的參數,則表示這些就是要被插入數組的新元素。
如果只是單純地插入元素,splice方法的第二個參數可以設爲0。
var a = [1, 1, 1];
a.splice(1, 0, 2) // []
a // [1, 2, 1, 1]
刪除並插入數據
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(4, 2, 1, 2) // ["e", "f"]
a // ["a", "b", "c", "d", 1, 2]
sort方法對數組成員進行排序,默認是按照字典順序排序。排序後,原數組將被改變。
['d', 'c', 'b', 'a'].sort()
// ['a', 'b', 'c', 'd']
[4, 3, 2, 1].sort()
// [1, 2, 3, 4]
[11, 101].sort()
// [101, 11]
[10111, 1101, 111].sort()
// [10111, 1101, 111]
上面代碼的最後兩個例子,需要特殊注意。sort()方法不是按照大小排序,而是按照字典順序。也就是說,數值會被先轉成字符串,再按照字典順序進行比較,所以101排在11的前面。
如果想讓sort方法按照自定義方式排序,可以傳入一個函數作爲參數。
[10111, 1101, 111].sort(function (a, b) {
return a - b;
})
// [111, 1101, 10111]
map方法將數組的所有成員依次傳入參數函數,然後把每一次的執行結果組成一個新數組返回
[1, 2, 3].map(function(elem, index, arr) {
return elem * index;
});
// [0, 2, 6]
map方法還可以接受第二個參數,用來綁定回調函數內部的this變量
var arr = ['a', 'b', 'c'];
[1, 2].map(function (e) {
return this[e];
}, arr)
// ['b', 'c']
forEach方法與map方法很相似,也是對數組的所有成員依次執行參數函數。但是,forEach方法不返回值,只用來操作數據。這就是說,如果數組遍歷的目的是爲了得到返回值,那麼使用map方法,否則使用forEach方法。
forEach方法也可以接受第二個參數,綁定參數函數的this變量。
var out = [];
[1, 2, 3].forEach(function(elem) {
this.push(elem * elem);
}, out);
out // [1, 4, 9]
注意,forEach方法無法中斷執行,總是會將所有成員遍歷完。如果希望符合某種條件時,就中斷遍歷,要使用for循環。
var arr = [1, 2, 3];
for (var i = 0; i < arr.length; i++) {
if (arr[i] === 2) break;
console.log(arr[i]);
}
// 1
filter方法用於過濾數組成員,滿足條件的成員組成一個新數組返回。
reduce方法和reduceRight方法依次處理數組的每個成員,最終累計爲一個值。它們的差別是,reduce是從左到右處理(從第一個成員到最後一個成員),reduceRight則是從右到左(從最後一個成員到第一個成員),其他完全一樣。
[1, 2, 3, 4, 5].reduce(function (a, b) {
console.log(a, b);
return a + b;
})
// 1 2
// 3 3
// 6 4
// 10 5
//最後結果:15
reduce方法和reduceRight方法的第一個參數都是一個函數。該函數接受以下四個參數。
累積變量,默認爲數組的第一個成員
當前變量,默認爲數組的第二個成員
當前位置(從0開始)
原數組
這四個參數之中,只有前兩個是必須的,後兩個則是可選的。
如果要對累積變量指定初值,可以把它放在reduce方法和reduceRight方法的第二個參數。
[1, 2, 3, 4, 5].reduce(function (a, b) {
return a + b;
}, 10);
// 25
上面代碼指定參數a的初值爲10,所以數組從10開始累加,最終結果爲25。注意,這時b是從數組的第一個成員開始遍歷。
由於這兩個方法會遍歷數組,所以實際上還可以用來做一些遍歷相關的操作。比如,找出字符長度最長的數組成員。
function findLongest(entries) {
return entries.reduce(function (longest, entry) {
return entry.length > longest.length ? entry : longest;
}, '');
}
findLongest(['aaa', 'bb', 'c']) // "aaa"
上面代碼中,reduce的參數函數會將字符長度較長的那個數組成員,作爲累積值。這導致遍歷所有成員之後,累積值就是字符長度最長的那個成員。
indexOf方法返回給定元素在數組中第一次出現的位置,如果沒有出現則返回-1。
var a = ['a', 'b', 'c'];
a.indexOf('b') // 1
a.indexOf('y') // -1
indexOf方法還可以接受第二個參數,表示搜索的開始位置。
['a', 'b', 'c'].indexOf('a', 1) // -1
上面代碼從1號位置開始搜索字符a,結果爲-1,表示沒有搜索到。
lastIndexOf方法返回給定元素在數組中最後一次出現的位置,如果沒有出現則返回-1。
var a = [2, 5, 9, 2];
a.lastIndexOf(2) // 3
a.lastIndexOf(7) // -1
注意,這兩個方法不能用來搜索NaN的位置,即它們無法確定數組成員是否包含NaN。
[NaN].indexOf(NaN) // -1
[NaN].lastIndexOf(NaN) // -1
這是因爲這兩個方法內部,使用嚴格相等運算符(===)進行比較,而NaN是唯一一個不等於自身的值。
22 Number
(123).toLocaleString('zh-Hans-CN', { style: 'currency', currency: 'CNY' })
// "¥123.00"
23 String
var s1 = 'abc';
var s2 = new String('abc');
typeof s1 // "string"
typeof s2 // "object"
s2.valueOf() // "abc"
這種現象的根本原因在於,碼點大於0xFFFF的字符佔用四個字節,而 JavaScript 默認支持兩個字節的字符。這種情況下,必須把0x20BB7拆成兩個字符表示。
String.fromCharCode(0xD842, 0xDFB7)
// "𠮷"
slice方法用於從原字符串取出子字符串並返回,不改變原字符串。它的第一個參數是子字符串的開始位置,第二個參數是子字符串的結束位置(不含該位置)。
'JavaScript'.slice(0, 4) // "Java"
如果省略第二個參數,則表示子字符串一直到原字符串結束。
'JavaScript'.slice(4) // "Script"
如果參數是負值,表示從結尾開始倒數計算的位置,即該負值加上字符串長度。
'JavaScript'.slice(-6) // "Script"
'JavaScript'.slice(0, -6) // "Java"
'JavaScript'.slice(-2, -1) // "p"
如果第一個參數大於第二個參數,slice方法返回一個空字符串。
'JavaScript'.slice(2, 1) // ""
substring方法用於從原字符串取出子字符串並返回,不改變原字符串,跟slice方法很相像。它的第一個參數表示子字符串的開始位置,第二個位置表示結束位置(返回結果不含該位置)。
'JavaScript'.substring(0, 4) // "Java"
如果省略第二個參數,則表示子字符串一直到原字符串的結束。
'JavaScript'.substring(4) // "Script"
如果第一個參數大於第二個參數,substring方法會自動更換兩個參數的位置。
'JavaScript'.substring(10, 4) // "Script"
// 等同於
'JavaScript'.substring(4, 10) // "Script"
上面代碼中,調換substring方法的兩個參數,都得到同樣的結果。
如果參數是負數,substring方法會自動將負數轉爲0。
'JavaScript'.substring(-3) // "JavaScript"
'JavaScript'.substring(4, -3) // "Java"
上面代碼中,第二個例子的參數-3會自動變成0,等同於’JavaScript’.substring(4, 0)。由於第二個參數小於第一個參數,會自動互換位置,所以返回Java。
由於這些規則違反直覺,因此不建議使用substring方法,應該優先使用slice。
substr方法用於從原字符串取出子字符串並返回,不改變原字符串,跟slice和substring方法的作用相同。
substr方法的第一個參數是子字符串的開始位置(從0開始計算),第二個參數是子字符串的長度。
'JavaScript'.substr(4, 6) // "Script"
如果省略第二個參數,則表示子字符串一直到原字符串的結束。
'JavaScript'.substr(4) // "Script"
如果第一個參數是負數,表示倒數計算的字符位置。如果第二個參數是負數,將被自動轉爲0,因此會返回空字符串。
'JavaScript'.substr(-6) // "Script"
'JavaScript'.substr(4, -1) // ""
split方法還可以接受第二個參數,限定返回數組的最大成員數。
'a|b|c'.split('|', 0) // []
'a|b|c'.split('|', 1) // ["a"]
'a|b|c'.split('|', 2) // ["a", "b"]
'a|b|c'.split('|', 3) // ["a", "b", "c"]
'a|b|c'.split('|', 4) // ["a", "b", "c"]
24 Math
下面是計算圓面積的方法。
var radius = 20;
var area = Math.PI * Math.pow(radius, 2);
Math.random()
任意範圍的隨機數生成函數如下。
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
getRandomArbitrary(1.5, 6.5)
// 2.4942810038223864
任意範圍的隨機整數生成函數如下。
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
getRandomInt(1, 6) // 5
25 Date
只要是能被Date.parse()方法解析的字符串,都可以當作參數。
new Date('2013-2-15')
new Date('2013/2/15')
new Date('02/15/2013')
new Date('2013-FEB-15')
new Date('FEB, 15, 2013')
new Date('FEB 15, 2013')
new Date('February, 15, 2013')
new Date('February 15, 2013')
new Date('15 Feb 2013')
new Date('15, February, 2013')
// Fri Feb 15 2013 00:00:00 GMT+0800 (CST)
以下三種方法,可以將 Date 實例轉爲表示本地時間的字符串。
Date.prototype.toLocaleString():完整的本地時間。
Date.prototype.toLocaleDateString():本地日期(不含小時、分和秒)。
Date.prototype.toLocaleTimeString():本地時間(不含年月日)。
下面是用法實例。
var d = new Date(2013, 0, 1);
d.toLocaleString()
// 中文版瀏覽器爲"2013年1月1日 上午12:00:00"
// 英文版瀏覽器爲"1/1/2013 12:00:00 AM"
d.toLocaleDateString()
// 中文版瀏覽器爲"2013年1月1日"
// 英文版瀏覽器爲"1/1/2013"
d.toLocaleTimeString()
// 中文版瀏覽器爲"上午12:00:00"
// 英文版瀏覽器爲"12:00:00 AM"
26 RegExp
正則實例對象的test方法返回一個布爾值,表示當前模式是否能匹配參數字符串。
/cat/.test('cats and dogs') // true
上面代碼驗證參數字符串之中是否包含cat,結果返回true。
如果正則表達式帶有g修飾符,則每一次test方法都從上一次結束的位置開始向後匹配。
var r = /x/g;
var s = '_x_x';
r.lastIndex // 0
r.test(s) // true
r.lastIndex // 2
r.test(s) // true
r.lastIndex // 4
r.test(s) // false
正則實例對象的exec方法,用來返回匹配結果。如果發現匹配,就返回一個數組,成員是匹配成功的子字符串,否則返回null。
var s = '_x_x';
var r1 = /x/;
var r2 = /y/;
r1.exec(s) // ["x"]
r2.exec(s) // null
字符串的實例方法之中,有4種與正則表達式有關。
String.prototype.match():返回一個數組,成員是所有匹配的子字符串。
String.prototype.search():按照給定的正則表達式進行搜索,返回一個整數,表示匹配開始的位置。
String.prototype.replace():按照給定的正則表達式進行替換,返回替換後的字符串。
String.prototype.split():按照給定規則進行字符串分割,返回一個數組,包含分割後的各個成員。
match
var s = 'abba';
var r = /a/g;
s.match(r) // ["a", "a"]
r.exec(s) // ["a"]
var s = 'abab'
var r = /a/g;
字符串對象的replace方法可以替換匹配的值。它接受兩個參數,第一個是正則表達式,表示搜索模式,第二個是替換的內容。
str.replace(search, replacement)
正則表達式如果不加g修飾符,就替換第一個匹配成功的值,否則替換所有匹配成功的值。
'aaa'.replace('a', 'b') // "baa"
'aaa'.replace(/a/, 'b') // "baa"
'aaa'.replace(/a/g, 'b') // "bbb"
replace方法的一個應用,就是消除字符串首尾兩端的空格。
var str = ' #id div.class ';
str.replace(/^\s+|\s+$/g, '')
// "#id div.class"
replace方法的第二個參數可以使用美元符號$,用來指代所替換的內容。
$&:匹配的子字符串。
$`:匹配結果前面的文本。
$’:匹配結果後面的文本。
$n:匹配成功的第n組內容,n是從1開始的自然數。
$。
'hello world'.replace(/(\w+)\s(\w+)/, '$2 $1')
// "world hello"
'abc'.replace('b', '[$`-$&-$\']')
// "a[a-b-c]c"
上面代碼中,第一個例子是將匹配的組互換位置,第二個例子是改寫匹配的值。
replace方法的第二個參數還可以是一個函數,將每一個匹配內容替換爲函數返回值。
'3 and 5'.replace(/[0-9]+/g, function (match) {
return 2 * match;
})
// "6 and 10"
var a = 'The quick brown fox jumped over the lazy dog.';
var pattern = /quick|brown|lazy/ig;
a.replace(pattern, function replacer(match) {
return match.toUpperCase();
});
// The QUICK BROWN fox jumped over the LAZY dog.
作爲replace方法第二個參數的替換函數,可以接受多個參數。其中,第一個參數是捕捉到的內容,第二個參數是捕捉到的組匹配(有多少個組匹配,就有多少個對應的參數)。此外,最後還可以添加兩個參數,倒數第二個參數是捕捉到的內容在整個字符串中的位置(比如從第五個位置開始),最後一個參數是原字符串。下面是一個網頁模板替換的例子。
var prices = {
'p1': '$1.99',
'p2': '$9.99',
'p3': '$5.00'
};
var template = '<span id="p1"></span>'
+ '<span id="p2"></span>'
+ '<span id="p3"></span>';
template.replace(
/(<span id=")(.*?)(">)(<\/span>)/g,
function(match, $1, $2, $3, $4){
return $1 + $2 + $3 + prices[$2] + $4;
}
);
// "<span id="p1">$1.99</span><span id="p2">$9.99</span><span id="p3">$5.00</span>"
上面代碼的捕捉模式中,有四個括號,所以會產生四個組匹配,在匹配函數中用$1到$4表示。匹配函數的作用是將價格插入模板中。
字符串對象的split方法按照正則規則分割字符串,返回一個由分割後的各個部分組成的數組。
str.split(separator, [limit])
該方法接受兩個參數,第一個參數是正則表達式,表示分隔規則,第二個參數是返回數組的最大成員數。
// 非正則分隔
'a, b,c, d'.split(',')
// [ 'a', ' b', 'c', ' d' ]
// 正則分隔,去除多餘的空格
'a, b,c, d'.split(/, */)
// [ 'a', 'b', 'c', 'd' ]
// 指定返回數組的最大成員
'a, b,c, d'.split(/, */, 2)
[ 'a', 'b' ]
27 JSON
上面代碼將各種類型的值,轉成 JSON 字符串。
注意,對於原始類型的字符串,轉換結果會帶雙引號。
JSON.stringify('foo') === "foo" // false
JSON.stringify('foo') === "\"foo\"" // true
上面代碼中,字符串foo,被轉成了"“foo”"。這是因爲將來還原的時候,內層雙引號可以讓 JavaScript 引擎知道,這是一個字符串,而不是其他類型的值。
JSON.stringify(false) // "false"
JSON.stringify('false') // "\"false\""
上面代碼中,如果不是內層的雙引號,將來還原的時候,引擎就無法知道原始值是布爾值還是字符串。
如果對象的屬性是undefined、函數或 XML 對象,該屬性會被JSON.stringify過濾。
var obj = {
a: undefined,
b: function () {}
};
JSON.stringify(obj) // "{}"
上面代碼中,對象obj的a屬性是undefined,而b屬性是一個函數,結果都被JSON.stringify過濾。
如果數組的成員是undefined、函數或 XML 對象,則這些值被轉成null。
var arr = [undefined, function () {}];
JSON.stringify(arr) // "[null,null]"
JSON.stringify方法會忽略對象的不可遍歷的屬性。
JSON.stringify方法還可以接受一個數組,作爲第二個參數,指定需要轉成字符串的屬性。
var obj = {
'prop1': 'value1',
'prop2': 'value2',
'prop3': 'value3'
};
var selectedProperties = ['prop1', 'prop2'];
JSON.stringify(obj, selectedProperties)
// "{"prop1":"value1","prop2":"value2"}"
上面代碼中,JSON.stringify方法的第二個參數指定,只轉prop1和prop2兩個屬性。
這個類似白名單的數組,只對對象的屬性有效,對數組無效。
JSON.stringify(['a', 'b'], ['0'])
// "["a","b"]"
JSON.stringify({0: 'a', 1: 'b'}, ['0'])
第二個參數還可以是一個函數,用來更改JSON.stringify的返回值。
function f(key, value) {
if (typeof value === "number") {
value = 2 * value;
}
return value;
}
JSON.stringify({ a: 1, b: 2 }, f)
// '{"a": 2,"b": 4}'
上面代碼中的f函數,接受兩個參數,分別是被轉換的對象的鍵名和鍵值。如果鍵值是數值,就將它乘以2,否則就原樣返回。
注意,這個處理函數是遞歸處理所有的鍵。
遞歸處理中,每一次處理的對象,都是前一次返回的值。
var o = {a: 1};
function f(key, value) {
if (typeof value === 'object') {
return {b: 2};
}
return value * 2;
}
JSON.stringify(o, f)
// "{"b": 4}"
第一次鍵名爲空,鍵值是整個對象o;第二次鍵名爲a,鍵值爲1。
JSON.stringify還可以接受第三個參數,用於增加返回的 JSON 字符串的可讀性。如果是數字,表示每個屬性前面添加的空格(最多不超過10個);如果是字符串(不超過10個字符),則該字符串會添加在每行前面。
JSON.stringify({ p1: 1, p2: 2 }, null, 2);
/*
"{
"p1": 1,
"p2": 2
}"
*/
JSON.stringify({ p1:1, p2:2 }, null, '|-');
/*
"{
|-"p1": 1,
|-"p2": 2
}"
*/
如果傳入的字符串不是有效的 JSON 格式,JSON.parse方法將報錯。
JSON.parse("'String'") // illegal single quotes
// SyntaxError: Unexpected token ILLEGAL
上面代碼中,雙引號字符串中是一個單引號字符串,因爲單引號字符串不符合 JSON 格式,所以報錯。
爲了處理解析錯誤,可以將JSON.parse方法放在try…catch代碼塊中。
try {
JSON.parse("'String'");
} catch(e) {
console.log('parsing error');
}
JSON.parse方法可以接受一個處理函數,作爲第二個參數,用法與JSON.stringify方法類似。
function f(key, value) {
if (key === 'a') {
return value + 10;
}
return value;
}
JSON.parse('{"a": 1, "b": 2}', f)
28 object
另一個解決辦法,構造函數內部判斷是否使用new命令,如果發現沒有使用,則直接返回一個實例對象。
function Fubar(foo, bar) {
if (!(this instanceof Fubar)) {
return new Fubar(foo, bar);
}
this._foo = foo;
this._bar = bar;
}
Fubar(1, 2)._foo // 1
(new Fubar(1, 2))._foo // 1
new 命令的原理
使用new命令時,它後面的函數依次執行下面的步驟。
創建一個空對象,作爲將要返回的對象實例。
將這個空對象的原型,指向構造函數的prototype屬性。
將這個空對象賦值給函數內部的this關鍵字。
開始執行構造函數內部的代碼。
也就是說,構造函數內部,this指的是一個新生成的空對象,所有針對this的操作,都會發生在這個空對象上。構造函數之所以叫“構造函數”,就是說這個函數的目的,就是操作一個空對象(即this對象),將其“構造”爲需要的樣子。
如果構造函數內部有return語句,而且return後面跟着一個對象,new命令會返回return語句指定的對象;否則,就會不管return語句,返回this對象。
var Vehicle = function () {
this.price = 1000;
return 1000;
};
(new Vehicle()) === 1000
// false
上面代碼中,構造函數Vehicle的return語句返回一個數值。這時,new命令就會忽略這個return語句,返回“構造”後的this對象。
但是,如果return語句返回的是一個跟this無關的新對象,new命令會返回這個新對象,而不是this對象。這一點需要特別引起注意。
var Vehicle = function (){
this.price = 1000;
return { price: 2000 };
};
(new Vehicle()).price
// 2000
上面代碼中,構造函數Vehicle的return語句,返回的是一個新對象。new命令會返回這個對象,而不是this對象。
另一方面,如果對普通函數(內部沒有this關鍵字的函數)使用new命令,則會返回一個空對象。
function getMessage() {
return 'this is a message';
}
var msg = new getMessage();
msg // {}
typeof msg // "object"
上面代碼中,getMessage是一個普通函數,返回一個字符串。對它使用new命令,會得到一個空對象。這是因爲new命令總是返回一個對象,要麼是實例對象,要麼是return語句指定的對象。本例中,return語句返回的是字符串,所以new命令就忽略了該語句。
new命令簡化的內部流程,可以用下面的代碼表示。
function _new(/* 構造函數 */ constructor, /* 構造函數參數 */ params) {
// 將 arguments 對象轉爲數組
var args = [].slice.call(arguments);
// 取出構造函數
var constructor = args.shift();
// 創建一個空對象,繼承構造函數的 prototype 屬性
var context = Object.create(constructor.prototype);
// 執行構造函數
var result = constructor.apply(context, args);
// 如果返回結果是對象,就直接返回,否則返回 context 對象
return (typeof result === 'object' && result != null) ? result : context;
}
// 實例
var actor = _new(Person, '張三', 28);
函數體裏面的this.x就是指當前運行環境的x。
var f = function () {
console.log(this.x);
}
var x = 1;
var obj = {
f: f,
x: 2,
};
// 單獨執行
f() // 1
// obj 環境執行
obj.f() // 2
上面代碼中,函數f在全局環境執行,this.x指向全局環境的x;在obj環境執行,this.x指向obj.x。
對象的方法
如果對象的方法裏面包含this,this的指向就是方法運行時所在的對象。該方法賦值給另一個對象,就會改變this的指向。
但是,這條規則很不容易把握。請看下面的代碼。
var obj ={
foo: function () {
console.log(this);
}
};
obj.foo() // obj
上面代碼中,obj.foo方法執行時,它內部的this指向obj。
但是,下面這幾種用法,都會改變this的指向。
// 情況一
(obj.foo = obj.foo)() // window
// 情況二
(false || obj.foo)() // window
// 情況三
(1, obj.foo)() // window
上面代碼中,obj.foo就是一個值。這個值真正調用的時候,運行環境已經不是obj了,而是全局環境,所以this不再指向obj。
可以這樣理解,JavaScript 引擎內部,obj和obj.foo儲存在兩個內存地址,稱爲地址一和地址二。obj.foo()這樣調用時,是從地址一調用地址二,因此地址二的運行環境是地址一,this指向obj。但是,上面三種情況,都是直接取出地址二進行調用,這樣的話,運行環境就是全局環境,因此this指向全局環境。上面三種情況等同於下面的代碼。
// 情況一
(obj.foo = function () {
console.log(this);
})()
// 等同於
(function () {
console.log(this);
})()
// 情況二
(false || function () {
console.log(this);
})()
// 情況三
(1, function () {
console.log(this);
})()
如果this所在的方法不在對象的第一層,這時this只是指向當前一層的對象,而不會繼承更上面的層。
var a = {
p: 'Hello',
b: {
m: function() {
console.log(this.p);
}
}
};
a.b.m() // undefined
上面代碼中,a.b.m方法在a對象的第二層,該方法內部的this不是指向a,而是指向a.b,因爲實際執行的是下面的代碼。
var b = {
m: function() {
console.log(this.p);
}
};
var a = {
p: 'Hello',
b: b
};
(a.b).m() // 等同於 b.m()
如果要達到預期效果,只有寫成下面這樣。
var a = {
b: {
m: function() {
console.log(this.p);
},
p: 'Hello'
}
};
如果這時將嵌套對象內部的方法賦值給一個變量,this依然會指向全局對象。
var a = {
b: {
m: function() {
console.log(this.p);
},
p: 'Hello'
}
};
var hello = a.b.m;
hello() // undefined
上面代碼中,m是多層對象內部的一個方法。爲求簡便,將其賦值給hello變量,結果調用時,this指向了頂層對象。爲了避免這個問題,可以只將m所在的對象賦值給hello,這樣調用時,this的指向就不會變。
var hello = a.b;
hello.m() // Hello
數組的map和foreach方法,允許提供一個函數作爲參數。這個函數內部不應該使用this。
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
this.p.forEach(function (item) {
console.log(this.v + ' ' + item);
});
}
}
o.f()
// undefined a1
// undefined a2
上面代碼中,foreach方法的回調函數中的this,其實是指向window對象,因此取不到o.v的值。原因跟上一段的多層this是一樣的,就是內層的this不指向外部,而指向頂層對象。
解決這個問題的一種方法,就是前面提到的,使用中間變量固定this。
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
var that = this;
this.p.forEach(function (item) {
console.log(that.v+' '+item);
});
}
}
o.f()
// hello a1
// hello a2
另一種方法是將this當作foreach方法的第二個參數,固定它的運行環境。
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
this.p.forEach(function (item) {
console.log(this.v + ' ' + item);
}, this);
}
}
o.f()
// hello a1
// hello a2
this的動態切換,固然爲 JavaScript 創造了巨大的靈活性,但也使得編程變得困難和模糊。有時,需要把this固定下來,避免出現意想不到的情況。JavaScript 提供了call、apply、bind這三個方法,來切換/固定this的指向。
Function.prototype.call()
函數實例的call方法,可以指定函數內部this的指向(即函數執行時所在的作用域),然後在所指定的作用域中,調用該函數。
var obj = {};
var f = function () {
return this;
};
f() === window // true
f.call(obj) === obj // true
上面代碼中,全局環境運行函數f時,this指向全局環境(瀏覽器爲window對象);call方法可以改變this的指向,指定this指向對象obj,然後在對象obj的作用域中運行函數f
call方法還可以接受多個參數。
func.call(thisValue, arg1, arg2, ...)
call的第一個參數就是this所要指向的那個對象,後面的參數則是函數調用時所需的參數。
function add(a, b) {
return a + b;
}
add.call(this, 1, 2) // 3
Function.prototype.apply()
apply方法的作用與call方法類似,也是改變this指向,然後再調用該函數。唯一的區別就是,它接收一個數組作爲函數執行時的參數,使用格式如下。
func.apply(thisValue, [arg1, arg2, ...])
function f(x, y){
console.log(x + y);
}
f.call(null, 1, 1) // 2
f.apply(null, [1, 1]) // 2
上面代碼中,f函數本來接受兩個參數,使用apply方法以後,就變成可以接受一個數組作爲參數。
利用這一點,可以做一些有趣的應用。
(1)找出數組最大元素
JavaScript 不提供找出數組最大元素的函數。結合使用apply方法和Math.max方法,就可以返回數組的最大元素。
var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15
(2)將數組的空元素變爲undefined
通過apply方法,利用Array構造函數將數組的空元素變成undefined。
Array.apply(null, ['a', ,'b'])
// [ 'a', undefined, 'b' ]
空元素與undefined的差別在於,數組的forEach方法會跳過空元素,但是不會跳過undefined。因此,遍歷內部元素的時候,會得到不同的結果。
var a = ['a', , 'b'];
function print(i) {
console.log(i);
}
a.forEach(print)
// a
// b
Array.apply(null, a).forEach(print)
// a
// undefined
// b
(3)轉換類似數組的對象
另外,利用數組對象的slice方法,可以將一個類似數組的對象(比如arguments對象)轉爲真正的數組。
Array.prototype.slice.apply({0: 1, length: 1}) // [1]
Array.prototype.slice.apply({0: 1}) // []
Array.prototype.slice.apply({0: 1, length: 2}) // [1, undefined]
Array.prototype.slice.apply({length: 1}) // [undefined]
上面代碼的apply方法的參數都是對象,但是返回結果都是數組,這就起到了將對象轉成數組的目的。從上面代碼可以看到,這個方法起作用的前提是,被處理的對象必須有length屬性,以及相對應的數字鍵。
(4)綁定回調函數的對象
前面的按鈕點擊事件的例子,可以改寫如下。
var o = new Object();
o.f = function () {
console.log(this === o);
}
var f = function (){
o.f.apply(o);
// 或者 o.f.call(o);
};
// jQuery 的寫法
$('#button').on('click', f);
bind還可以接受更多的參數,將這些參數綁定原函數的參數。
var add = function (x, y) {
return x * this.m + y * this.n;
}
var obj = {
m: 2,
n: 2
};
var newAdd = add.bind(obj, 5);
newAdd(5) // 20
上面代碼中,bind方法除了綁定this對象,還將add函數的第一個參數x綁定成5,然後返回一個新函數newAdd,這個函數只要再接受一個參數y就能運行了。
如果bind方法的第一個參數是null或undefined,等於將this綁定到全局對象,函數運行時this指向頂層對象(瀏覽器爲window)。
function add(x, y) {
return x + y;
}
var plus5 = add.bind(null, 5);
plus5(10) // 15
上面代碼中,函數add內部並沒有this,使用bind方法的主要目的是綁定參數x,以後每次運行新函數plus5,就只需要提供另一個參數y就夠了。而且因爲add內部沒有this,所以bind的第一個參數是null,不過這裏如果是其他對象,也沒有影響。
bind方法有一些使用注意點。
(1)每一次返回一個新函數
bind方法每運行一次,就返回一個新函數,這會產生一些問題。比如,監聽事件的時候,不能寫成下面這樣。
element.addEventListener('click', o.m.bind(o));
上面代碼中,click事件綁定bind方法生成的一個匿名函數。這樣會導致無法取消綁定,所以,下面的代碼是無效的。
element.removeEventListener('click', o.m.bind(o));
正確的方法是寫成下面這樣:
var listener = o.m.bind(o);
element.addEventListener('click', listener);
// ...
element.removeEventListener('click', listener);
(2)結合回調函數使用
回調函數是 JavaScript 最常用的模式之一,但是一個常見的錯誤是,將包含this的方法直接當作回調函數。解決方法就是使用bind方法,將counter.inc綁定counter。
var counter = {
count: 0,
inc: function () {
'use strict';
this.count++;
}
};
function callIt(callback) {
callback();
}
callIt(counter.inc.bind(counter));
counter.count // 1
上面代碼中,callIt方法會調用回調函數。這時如果直接把counter.inc傳入,調用時counter.inc內部的this就會指向全局對象。使用bind方法將counter.inc綁定counter以後,就不會有這個問題,this總是指向counter。
還有一種情況比較隱蔽,就是某些數組方法可以接受一個函數當作參數。這些函數內部的this指向,很可能也會出錯。
var obj = {
name: '張三',
times: [1, 2, 3],
print: function () {
this.times.forEach(function (n) {
console.log(this.name);
});
}
};
obj.print()
// 沒有任何輸出
上面代碼中,obj.print內部this.times的this是指向obj的,這個沒有問題。但是,forEach方法的回調函數內部的this.name卻是指向全局對象,導致沒有辦法取到值。稍微改動一下,就可以看得更清楚。
obj.print = function () {
this.times.forEach(function (n) {
console.log(this === window);
});
};
obj.print()
// true
// true
// true
解決這個問題,也是通過bind方法綁定this。
obj.print = function () {
this.times.forEach(function (n) {
console.log(this.name);
}.bind(this));
};
obj.print()
// 張三
// 張三
// 張三
(3)結合call方法使用
利用bind方法,可以改寫一些 JavaScript 原生方法的使用形式,以數組的slice方法爲例。
[1, 2, 3].slice(0, 1) // [1]
// 等同於
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]
上面的代碼中,數組的slice方法從[1, 2, 3]裏面,按照指定位置和長度切分出另一個數組。這樣做的本質是在[1, 2, 3]上面調用Array.prototype.slice方法,因此可以用call方法表達這個過程,得到同樣的結果。
call方法實質上是調用Function.prototype.call方法,因此上面的表達式可以用bind方法改寫。
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]
上面代碼的含義就是,將Array.prototype.slice變成Function.prototype.call方法所在的對象,調用時就變成了Array.prototype.slice.call。類似的寫法還可以用於其他數組方法。
var push = Function.prototype.call.bind(Array.prototype.push);
var pop = Function.prototype.call.bind(Array.prototype.pop);
var a = [1 ,2 ,3];
push(a, 4)
a // [1, 2, 3, 4]
pop(a)
a // [1, 2, 3]
如果再進一步,將Function.prototype.call方法綁定到Function.prototype.bind對象,就意味着bind的調用形式也可以被改寫。
function f() {
console.log(this.v);
}
var o = { v: 123 };
var bind = Function.prototype.call.bind(Function.prototype.bind);
bind(f, o)() // 123
上面代碼的含義就是,將Function.prototype.bind方法綁定在Function.prototype.call上面,所以bind方法就可以直接使用,不需要在函數實例上使用。
29 對象繼承
如果讓構造函數的prototype屬性指向一個數組,就意味着實例對象可以調用數組方法。
var MyArray = function () {};
MyArray.prototype = new Array();
MyArray.prototype.constructor = MyArray;
var mine = new MyArray();
mine.push(1, 2, 3);
mine.length // 3
mine instanceof Array // true
上面代碼中,mine是構造函數MyArray的實例對象,由於MyArray.prototype指向一個數組實例,使得mine可以調用數組方法(這些方法定義在數組實例的prototype對象上面)。最後那行instanceof表達式,用來比較一個對象是否爲某個構造函數的實例,結果就是證明mine爲Array的實例,instanceof運算符的詳細解釋詳見後文。
constructor 屬性 #
prototype對象有一個constructor屬性,默認指向prototype對象所在的構造函數。
function P() {}
P.prototype.constructor === P // true
所以,修改原型對象時,一般要同時修改constructor屬性的指向。
// 壞的寫法
C.prototype = {
method1: function (...) { ... },
// ...
};
// 好的寫法
C.prototype = {
constructor: C,
method1: function (...) { ... },
// ...
};
// 更好的寫法
C.prototype.method1 = function (...) { ... };
上面代碼中,要麼將constructor屬性重新指向原來的構造函數,要麼只在原型對象上添加方法,這樣可以保證instanceof運算符不會失真。
如果不能確定constructor屬性是什麼函數,還有一個辦法:通過name屬性,從實例得到構造函數的名稱。
function Foo() {}
var f = new Foo();
f.constructor.name // "Foo"
由於任意對象(除了null)都是Object的實例,所以instanceof運算符可以判斷一個值是否爲非null的對象。
var obj = { foo: 123 };
obj instanceof Object // true
null instanceof Object // false
上面代碼中,除了null,其他對象的instanceOf Object的運算結果都是true。
instanceof的原理是檢查右邊構造函數的prototype屬性,是否在左邊對象的原型鏈上。有一種特殊情況,就是左邊對象的原型鏈上,只有null對象。這時,instanceof判斷會失真。
var obj = Object.create(null);
typeof obj // "object"
Object.create(null) instanceof Object // false
上面代碼中,Object.create(null)返回一個新對象obj,它的原型是null(Object.create的詳細介紹見後文)。右邊的構造函數Object的prototype屬性,不在左邊的原型鏈上,因此instanceof就認爲obj不是Object的實例。但是,只要一個對象的原型不是null,instanceof運算符的判斷就不會失真。
instanceof運算符的一個用處,是判斷值的類型。
var x = [1, 2, 3];
var y = {};
x instanceof Array // true
y instanceof Object // true
上面代碼中,instanceof運算符判斷,變量x是數組,變量y是對象。
注意,instanceof運算符只能用於對象,不適用原始類型的值。
var s = 'hello';
s instanceof String // false
上面代碼中,字符串不是String對象的實例(因爲字符串不是對象),所以返回false。
此外,對於undefined和null,instanceof運算符總是返回false。
undefined instanceof Object // false
null instanceof Object // false
利用instanceof運算符,還可以巧妙地解決,調用構造函數時,忘了加new命令的問題。
function Fubar (foo, bar) {
if (this instanceof Fubar) {
this._foo = foo;
this._bar = bar;
} else {
return new Fubar(foo, bar);
}
}
讓一個構造函數繼承另一個構造函數,是非常常見的需求。這可以分成兩步實現。第一步是在子類的構造函數中,調用父類的構造函數。
function Sub(value) {
Super.call(this)
this.prop = value
}
上面代碼中,Sub是子類的構造函數,this是子類的實例。在實例上調用父類的構造函數Super,就會讓子類實例具有父類實例的屬性。
第二步,是讓子類的原型指向父類的原型,這樣子類就可以繼承父類原型。
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';
上面代碼中,Sub.prototype是子類的原型,要將它賦值爲Object.create(Super.prototype),而不是直接等於Super.prototype。否則後面兩行對Sub.prototype的操作,會連父類的原型Super.prototype一起修改掉。
另外一種寫法是Sub.prototype等於一個父類實例。
Sub.prototype = new Super();
上面這種寫法也有繼承的效果,但是子類會具有父類實例的方法。有時,這可能不是我們需要的,所以不推薦使用這種寫法。
舉例來說,下面是一個Shape構造函數。
function Shape() {
this.x = 0;
this.y = 0;
}
Shape.prototype.move = function (x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};
我們需要讓Rectangle構造函數繼承Shape。
// 第一步,子類繼承父類的實例
function Rectangle() {
Shape.call(this); // 調用父類構造函數
}
// 另一種寫法
function Rectangle() {
this.base = Shape;
this.base();
}
// 第二步,子類繼承父類的原型
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
採用這樣的寫法以後,instanceof運算符會對子類和父類的構造函數,都返回true。
var rect = new Rectangle();
rect instanceof Rectangle // true
rect instanceof Shape // true
上面代碼中,子類是整體繼承父類。有時只需要單個方法的繼承,這時可以採用下面的寫法。
ClassB.prototype.print = function() {
ClassA.prototype.print.call(this);
// some code
}
上面代碼中,子類B的print方法先調用父類A的print方法,再部署自己的代碼。這就等於繼承了父類A的print方法。
模塊是實現特定功能的一組屬性和方法的封裝。
簡單的做法是把模塊寫成一個對象,所有的模塊成員都放到這個對象裏面。
var module1 = new Object({
_count : 0,
m1 : function (){
//...
},
m2 : function (){
//...
}
});
上面的函數m1和m2,都封裝在module1對象裏。使用的時候,就是調用這個對象的屬性。
module1.m1();
但是,這樣的寫法會暴露所有模塊成員,內部狀態可以被外部改寫。比如,外部代碼可以直接改變內部計數器的值。
module1._count = 5;
封裝私有變量:構造函數的寫法
我們可以利用構造函數,封裝私有變量。
function StringBuilder() {
var buffer = [];
this.add = function (str) {
buffer.push(str);
};
this.toString = function () {
return buffer.join('');
};
}
上面代碼中,buffer是模塊的私有變量。一旦生成實例對象,外部是無法直接訪問buffer的。但是,這種方法將私有變量封裝在構造函數中,導致構造函數與實例對象是一體的,總是存在於內存之中,無法在使用完成後清除。這意味着,構造函數有雙重作用,既用來塑造實例對象,又用來保存實例對象的數據,違背了構造函數與實例對象在數據上相分離的原則(即實例對象的數據,不應該保存在實例對象以外)。同時,非常耗費內存。
function StringBuilder() {
this._buffer = [];
}
StringBuilder.prototype = {
constructor: StringBuilder,
add: function (str) {
this._buffer.push(str);
},
toString: function () {
return this._buffer.join('');
}
};
這種方法將私有變量放入實例對象中,好處是看上去更自然,但是它的私有變量可以從外部讀寫,不是很安全。
封裝私有變量:立即執行函數的寫法
另一種做法是使用“立即執行函數”(Immediately-Invoked Function Expression,IIFE),將相關的屬性和方法封裝在一個函數作用域裏面,可以達到不暴露私有成員的目的。
var module1 = (function () {
var _count = 0;
var m1 = function () {
//...
};
var m2 = function () {
//...
};
return {
m1 : m1,
m2 : m2
};
})();
使用上面的寫法,外部代碼無法讀取內部的_count變量。
console.info(module1._count); //undefined
上面的module1就是 JavaScript 模塊的基本寫法。下面,再對這種寫法進行加工。
模塊的放大模式
如果一個模塊很大,必須分成幾個部分,或者一個模塊需要繼承另一個模塊,這時就有必要採用“放大模式”
(augmentation)。
var module1 = (function (mod){
mod.m3 = function () {
//...
};
return mod;
})(module1);
上面的代碼爲module1模塊添加了一個新方法m3(),然後返回新的module1模塊。
在瀏覽器環境中,模塊的各個部分通常都是從網上獲取的,有時無法知道哪個部分會先加載。如果採用上面的寫法,第一個執行的部分有可能加載一個不存在空對象,這時就要採用"寬放大模式"(Loose augmentation)。
var module1 = (function (mod) {
//...
return mod;
})(window.module1 || {});
與"放大模式"相比,“寬放大模式”就是“立即執行函數”的參數可以是空對象。
輸入全局變量
獨立性是模塊的重要特點,模塊內部最好不與程序的其他部分直接交互。
爲了在模塊內部調用全局變量,必須顯式地將其他變量輸入模塊。
var module1 = (function ($, YAHOO) {
//...
})(jQuery, YAHOO);
上面的module1模塊需要使用 jQuery 庫和 YUI 庫,就把這兩個庫(其實是兩個模塊)當作參數輸入module1。這樣做除了保證模塊的獨立性,還使得模塊之間的依賴關係變得明顯。
立即執行函數還可以起到命名空間的作用。
(function($, window, document) {
function go(num) {
}
function handleEvents() {
}
function initialize() {
}
function dieCarouselDie() {
}
//attach to the global scope
window.finalCarousel = {
init : initialize,
destroy : dieCarouselDie
}
})( jQuery, window, document );
上面代碼中,finalCarousel對象輸出到全局,對外暴露init和destroy接口,內部方法go、handleEvents、initialize、dieCarouselDie都是外部無法調用的。
30 Object 對象的相關方法
// 空對象的原型是 Object.prototype
Object.getPrototypeOf({}) === Object.prototype // true
// Object.prototype 的原型是 null
Object.getPrototypeOf(Object.prototype) === null // true
// 函數的原型是 Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype // true
Object.setPrototypeOf方法爲參數對象設置原型,返回該參數對象。它接受兩個參數,第一個是現有對象,第二個是原型對象。
var a = {};
var b = {x: 1};
Object.setPrototypeOf(a, b);
Object.getPrototypeOf(a) === b // true
a.x // 1
上面代碼中,Object.setPrototypeOf方法將對象a的原型,設置爲對象b,因此a可以共享b的屬性。
new命令可以使用Object.setPrototypeOf方法模擬。
var F = function () {
this.foo = 'bar';
};
var f = new F();
// 等同於
var f = Object.setPrototypeOf({}, F.prototype);
F.call(f);
上面代碼中,new命令新建實例對象,其實可以分成兩步。第一步,將一個空對象的原型設爲構造函數的prototype屬性(上例是F.prototype);第二步,將構造函數內部的this綁定這個空對象,然後執行構造函
Object.create()
生成實例對象的常用方法是,使用new命令讓構造函數返回一個實例。但是很多時候,只能拿到一個實例對象,它可能根本不是由構建函數生成的,那麼能不能從一個實例對象,生成另一個實例對象呢?
JavaScript 提供了Object.create方法,用來滿足這種需求。該方法接受一個對象作爲參數,然後以它爲原型,返回一個實例對象。該實例完全繼承原型對象的屬性。
// 原型對象
var A = {
print: function () {
console.log('hello');
}
};
// 實例對象
var B = Object.create(A);
Object.getPrototypeOf(B) === A // true
B.print() // hello
B.print === A.print // true
上面代碼中,Object.create方法以A對象爲原型,生成了B對象。B繼承了A的所有屬性和方法。
實際上,Object.create方法可以用下面的代碼代替。
if (typeof Object.create !== 'function') {
Object.create = function (obj) {
function F() {}
F.prototype = obj;
return new F();
};
}
上面代碼表明,Object.create方法的實質是新建一個空的構造函數F,然後讓F.prototype屬性指向參數對象obj,最後返回一個F的實例,從而實現讓該實例繼承obj的屬性。
下面三種方式生成的新對象是等價的。
var obj1 = Object.create({});
var obj2 = Object.create(Object.prototype);
var obj3 = new Object();
如果想要生成一個不繼承任何屬性(比如沒有toString和valueOf方法)的對象,可以將Object.create的參數設爲null。
var obj = Object.create(null);
obj.valueOf()
// TypeError: Object [object Object] has no method 'valueOf'
Object.create方法生成的對象,繼承了它的原型對象的構造函數。
function A() {}
var a = new A();
var b = Object.create(a);
b.constructor === A // true
b instanceof A // true
上面代碼中,b對象的原型是a對象,因此繼承了a對象的構造函數A。
獲取原型對象方法的比較
如前所述,__proto__屬性指向當前對象的原型對象,即構造函數的prototype屬性。
var obj = new Object();
obj.__proto__ === Object.prototype
// true
obj.__proto__ === obj.constructor.prototype
// true
上面代碼首先新建了一個對象obj,它的__proto__屬性,指向構造函數(Object或obj.constructor)的prototype屬性。
因此,獲取實例對象obj的原型對象,有三種方法。
obj.proto
obj.constructor.prototype
Object.getPrototypeOf(obj)
上面三種方法之中,前兩種都不是很可靠。__proto__屬性只有瀏覽器才需要部署,其他環境可以不部署。而obj.constructor.prototype在手動改變原型對象時,可能會失效。
var P = function () {};
var p = new P();
var C = function () {};
C.prototype = p;
var c = new C();
c.constructor.prototype === p // false
上面代碼中,構造函數C的原型對象被改成了p,但是實例對象的c.constructor.prototype卻沒有指向p。所以,在改變原型對象時,一般要同時設置constructor屬性。
C.prototype = p;
C.prototype.constructor = C;
var c = new C();
c.constructor.prototype === p // true
因此,推薦使用第三種Object.getPrototypeOf方法,獲取原型對象。
對象的拷貝
如果要拷貝一個對象,需要做到下面兩件事情。
確保拷貝後的對象,與原對象具有同樣的原型。
確保拷貝後的對象,與原對象具有同樣的實例屬性。
下面就是根據上面兩點,實現的對象拷貝函數。
function copyObject(orig) {
var copy = Object.create(Object.getPrototypeOf(orig));
copyOwnPropertiesFrom(copy, orig);
return copy;
}
function copyOwnPropertiesFrom(target, source) {
Object
.getOwnPropertyNames(source)
.forEach(function (propKey) {
var desc = Object.getOwnPropertyDescriptor(source, propKey);
Object.defineProperty(target, propKey, desc);
});
return target;
}
另一種更簡單的寫法,是利用 ES2017 才引入標準的Object.getOwnPropertyDescriptors方法。
function copyObject(orig) {
return Object.create(
Object.getPrototypeOf(orig),
Object.getOwnPropertyDescriptors(orig)
);
}
上面代碼想在用戶每次輸入文本後,立即將字符轉爲大寫。但是實際上,它只能將本次輸入前的字符轉爲大寫,因爲瀏覽器此時還沒接收到新的文本,所以this.value取不到最新輸入的那個字符。只有用setTimeout改寫,上面的代碼才能發揮作用。
document.getElementById('input-box').onkeypress = function() {
var self = this;
setTimeout(function() {
self.value = self.value.toUpperCase();
}, 0);
}
上面代碼將代碼放入setTimeout之中,就能使得它在瀏覽器接收到文本之後觸發。
由於setTimeout(f, 0)實際上意味着,將任務放到瀏覽器最早可得的空閒時段執行,所以那些計算量大、耗時長的任務,常常會被放到幾個小部分,分別放到setTimeout(f, 0)裏面執行。
var div = document.getElementsByTagName('div')[0];
// 寫法一
for (var i = 0xA00000; i < 0xFFFFFF; i++) {
div.style.backgroundColor = '#' + i.toString(16);
}
// 寫法二
var timer;
var i=0x100000;
function func() {
timer = setTimeout(func, 0);
div.style.backgroundColor = '#' + i.toString(16);
if (i++ == 0xFFFFFF) clearTimeout(timer);
}
timer = setTimeout(func, 0);
上面代碼有兩種寫法,都是改變一個網頁元素的背景色。寫法一會造成瀏覽器“堵塞”,因爲 JavaScript 執行速度遠高於 DOM,會造成大量 DOM 操作“堆積”,而寫法二就不會,這就是setTimeout(f, 0)的好處。
另一個使用這種技巧的例子是代碼高亮的處理。如果代碼塊很大,一次性處理,可能會對性能造成很大的壓力,那麼將其分成一個個小塊,一次處理一塊,比如寫成setTimeout(highlightNext, 50)的樣子,性能壓力就會減輕。
31document
document.createEvent() #
document.createEvent方法生成一個事件對象(Event實例),該對象可以被element.dispatchEvent方法使用,觸發指定事件。
var event = document.createEvent(type);
document.createEvent方法的參數是事件類型,比如UIEvents、MouseEvents、MutationEvents、HTMLEvents。
var event = document.createEvent('Event');
event.initEvent('build', true, true);
document.addEventListener('build', function (e) {
console.log(e.type); // "build"
}, false);
document.dispatchEvent(event);
上面代碼新建了一個名爲build的事件實例,然後觸發該事件。
document.addEventListener(),document.removeEventListener(),document.dispatchEvent()
這三個方法用於處理document節點的事件。它們都繼承自EventTarget接口,詳細介紹參見《EventTarget 接口》一章。
// 添加事件監聽函數
document.addEventListener('click', listener, false);
// 移除事件監聽函數
document.removeEventListener('click', listener, false);
// 觸發事件
var event = new Event('click');
document.dispatchEvent(event);
document.queryCommandSupported()
document.queryCommandSupported()方法返回一個布爾值,表示瀏覽器是否支持document.execCommand()的某個命令。
if (document.queryCommandSupported('SelectAll')) {
console.log('瀏覽器支持選中可編輯區域的所有內容');
}
document.queryCommandEnabled()
document.queryCommandEnabled()方法返回一個布爾值,表示當前是否可用document.execCommand()的某個命令。比如,bold(加粗)命令只有存在文本選中時纔可用,如果沒有選中文本,就不可用。
// HTML 代碼爲
// <input type="button" value="Copy" onclick="doCopy()">
function doCopy(){
// 瀏覽器是否支持 copy 命令(選中內容複製到剪貼板)
if (document.queryCommandSupported('copy')) {
copyText('你好');
}else{
console.log('瀏覽器不支持');
}
}
function copyText(text) {
var input = document.createElement('textarea');
document.body.appendChild(input);
input.value = text;
input.focus();
input.select();
// 當前是否有選中文字
if (document.queryCommandEnabled('copy')) {
var success = document.execCommand('copy');
input.remove();
console.log('Copy Ok');
} else {
console.log('queryCommandEnabled is false');
}
}
上面代碼中,先判斷瀏覽器是否支持copy命令(允許可編輯區域的選中內容,複製到剪貼板),如果支持,就新建一個臨時文本框,裏面寫入內容“你好”,並將其選中。然後,判斷是否選中成功,如果成功,就將“你好”複製到剪貼板,再刪除那個臨時文本框。
Element.getBoundingClientRect() #
Element.getBoundingClientRect方法返回一個對象,提供當前元素節點的大小、位置等信息,基本上就是 CSS 盒狀模型的所有信息。
var rect = obj.getBoundingClientRect();
上面代碼中,getBoundingClientRect方法返回的rect對象,具有以下屬性(全部爲只讀)。
x:元素左上角相對於視口的橫座標
y:元素左上角相對於視口的縱座標
height:元素高度
width:元素寬度
left:元素左上角相對於視口的橫座標,與x屬性相等
right:元素右邊界相對於視口的橫座標(等於x + width)
top:元素頂部相對於視口的縱座標,與y屬性相等
bottom:元素底部相對於視口的縱座標(等於y + height)
由於元素相對於視口(viewport)的位置,會隨着頁面滾動變化,因此表示位置的四個屬性值,都不是固定不變的。如果想得到絕對位置,可以將left屬性加上window.scrollX,top屬性加上window.scrollY。
注意,getBoundingClientRect方法的所有屬性,都把邊框(border屬性)算作元素的一部分。也就是說,都是從邊框外緣的各個點來計算。因此,width和height包括了元素本身 + padding + border。
另外,上面的這些屬性,都是繼承自原型的屬性,Object.keys會返回一個空數組,這一點也需要注意。
var rect = document.body.getBoundingClientRect();
Object.keys(rect) // []
上面代碼中,rect對象沒有自身屬性,而Object.keys方法只返回對象自身的屬性,所以返回了一個空數組。
Element.getClientRects()
Element.getClientRects方法返回一個類似數組的對象,裏面是當前元素在頁面上形成的所有矩形(所以方法名中的Rect用的是複數)。每個矩形都有bottom、height、left、right、top和width六個屬性,表示它們相對於視口的四個座標,以及本身的高度和寬度。
對於盒狀元素(比如
),該方法返回的對象中只有該元素一個成員。對於行內元素(比如、、),該方法返回的對象有多少個成員,取決於該元素在頁面上佔據多少行。這是它和Element.getBoundingClientRect()方法的主要區別,後者對於行內元素總是返回一個矩形。
Hello World Hello World Hello World
上面代碼是一個行內元素,如果它在頁面上佔據三行,getClientRects方法返回的對象就有三個成員,如果它在頁面上佔據一行,getClientRects方法返回的對象就只有一個成員。
var el = document.getElementById('inline');
el.getClientRects().length // 3
el.getClientRects()[0].left // 8
el.getClientRects()[0].right // 113.908203125
el.getClientRects()[0].bottom // 31.200000762939453
el.getClientRects()[0].height // 23.200000762939453
el.getClientRects()[0].width // 105.908203125
這個方法主要用於判斷行內元素是否換行,以及行內
31 事件
如果希望事件到某個節點爲止,不再傳播,可以使用事件對象的stopPropagation方法。
// 事件傳播到 p 元素後,就不再向下傳播了
p.addEventListener('click', function (event) {
event.stopPropagation();
}, true);
// 事件冒泡到 p 元素後,就不再向上冒泡了
p.addEventListener('click', function (event) {
event.stopPropagation();
}, false);
上面代碼中,stopPropagation方法分別在捕獲階段和冒泡階段,阻止了事件的傳播。
但是,stopPropagation方法只會阻止事件的傳播,不會阻止該事件觸發
節點的其他click事件的監聽函數。也就是說,不是徹底取消click事件。
p.addEventListener('click', function (event) {
event.stopPropagation();
console.log(1);
});
p.addEventListener('click', function(event) {
// 會觸發
console.log(2);
});
上面代碼中,p元素綁定了兩個click事件的監聽函數。stopPropagation方法只能阻止這個事件的傳播,不能取消這個事件,因此,第二個監聽函數會觸發。輸出結果會先是1,然後是2。
如果想要徹底取消該事件,不再觸發後面所有click的監聽函數,可以使用stopImmediatePropagation方法。
p.addEventListener('click', function (event) {
event.stopImmediatePropagation();
console.log(1);
});
p.addEventListener('click', function(event) {
// 不會被觸發
console.log(2);
});
由於事件會在冒泡階段向上傳播到父節點,因此可以把子節點的監聽函數定義在父節點上,由父節點的監聽函數統一處理多個子元素的事件。這種方法叫做事件的代理(delegation)。
var ul = document.querySelector('ul');
ul.addEventListener('click', function (event) {
if (event.target.tagName.toLowerCase() === 'li') {
// some code
}
});
32 Worker
然後,讀取這一段嵌入頁面的腳本,用 Worker 來處理。
var blob = new Blob([document.querySelector('#worker').textContent]);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);
worker.onmessage = function (e) {
// e.data === 'some message'
};
上面代碼中,先將嵌入網頁的腳本代碼,轉成一個二進制對象,然後爲這個二進制對象生成 URL,再讓 Worker 加載這個 URL。這樣就做到了,主線程和 Worker 的代碼都在同一個網頁上面。