js基礎易忽略點

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六個屬性,表示它們相對於視口的四個座標,以及本身的高度和寬度。

對於盒狀元素(比如

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 的代碼都在同一個網頁上面。

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