1 物件
概述
生成方法
物件(object)是 JavaScript 語言的核心概念,也是最重要的資料型別。

什麼是物件?簡單說,物件就是一組“鍵值對”(key-value)的集合,是一種無序的複合資料集合。

var obj = {
foo: ‘Hello’,
bar: ‘World’
};
上面程式碼中,大括號就定義了一個物件,它被賦值給變數obj,所以變數obj就指向一個物件。該物件內部包含兩個鍵值對(又稱為兩個“成員”),第一個鍵值對是foo: ‘Hello’,其中foo是“鍵名”(成員的名稱),字串Hello是“鍵值”(成員的值)。鍵名與鍵值之間用冒號分隔。第二個鍵值對是bar: ‘World’,bar是鍵名,World是鍵值。兩個鍵值對之間用逗號分隔。

鍵名
物件的所有鍵名都是字串(ES6 又引入了 Symbol 值也可以作為鍵名),所以加不加引號都可以。上面的程式碼也可以寫成下面這樣。

var obj = {
‘foo’: ‘Hello’,
‘bar’: ‘World’
};
如果鍵名是數值,會被自動轉為字串。

var obj = {
1: ‘a’,
3.2: ‘b’,
1e2: true,
1e-2: true,
.234: true,
0xFF: true
};

obj
// Object {
// 1: “a”,
// 3.2: “b”,
// 100: true,
// 0.01: true,
// 0.234: true,
// 255: true
// }

obj[‘100’] // true
上面程式碼中,物件obj的所有鍵名雖然看上去像數值,實際上都被自動轉成了字串。

如果鍵名不符合標識名的條件(比如第一個字元為數字,或者含有空格或運算子),且也不是數字,則必須加上引號,否則會報錯。

// 報錯
var obj = {
1p: ‘Hello World’
};

// 不報錯
var obj = {
‘1p’: ‘Hello World’,
‘h w’: ‘Hello World’,
‘p+q’: ‘Hello World’
};
上面物件的三個鍵名,都不符合標識名的條件,所以必須加上引號。

物件的每一個鍵名又稱為“屬性”(property),它的“鍵值”可以是任何資料型別。如果一個屬性的值為函式,通常把這個屬性稱為“方法”,它可以像函式那樣呼叫。

var obj = {
p: function (x) {
return 2 * x;
}
};

obj.p(1) // 2
上面程式碼中,物件obj的屬性p,就指向一個函式。

如果屬性的值還是一個物件,就形成了鏈式引用。

var o1 = {};
var o2 = { bar: ‘hello’ };

o1.foo = o2;
o1.foo.bar // “hello”
上面程式碼中,物件o1的屬性foo指向物件o2,就可以鏈式引用o2的屬性。

物件的屬性之間用逗號分隔,最後一個屬性後面可以加逗號(trailing comma),也可以不加。

var obj = {
p: 123,
m: function () { … },
}
上面的程式碼中,m屬性後面的那個逗號,有沒有都可以。

屬性可以動態建立,不必在物件宣告時就指定。

var obj = {};
obj.foo = 123;
obj.foo // 123
上面程式碼中,直接對obj物件的foo屬性賦值,結果就在執行時建立了foo屬性。

物件的引用
如果不同的變數名指向同一個物件,那麼它們都是這個物件的引用,也就是說指向同一個記憶體地址。修改其中一個變數,會影響到其他所有變數。

var o1 = {};
var o2 = o1;

o1.a = 1;
o2.a // 1

o2.b = 2;
o1.b // 2
上面程式碼中,o1和o2指向同一個物件,因此為其中任何一個變數新增屬性,另一個變數都可以讀寫該屬性。

此時,如果取消某一個變數對於原物件的引用,不會影響到另一個變數。

var o1 = {};
var o2 = o1;

o1 = 1;
o2 // {}
上面程式碼中,o1和o2指向同一個物件,然後o1的值變為1,這時不會對o2產生影響,o2還是指向原來的那個物件。

但是,這種引用只侷限於物件,如果兩個變數指向同一個原始型別的值。那麼,變數這時都是值的拷貝。

var x = 1;
var y = x;

x = 2;
y // 1
上面的程式碼中,當x的值發生變化後,y的值並不變,這就表示y和x並不是指向同一個記憶體地址。

表示式還是語句?
物件採用大括號表示,這導致了一個問題:如果行首是一個大括號,它到底是表示式還是語句?

{ foo: 123 }
JavaScript 引擎讀到上面這行程式碼,會發現可能有兩種含義。第一種可能是,這是一個表示式,表示一個包含foo屬性的物件;第二種可能是,這是一個語句,表示一個程式碼區塊,裡面有一個標籤foo,指向表示式123。

為了避免這種歧義,JavaScript 引擎的做法是,如果遇到這種情況,無法確定是物件還是程式碼塊,一律解釋為程式碼塊。

{ console.log(123) } // 123
上面的語句是一個程式碼塊,而且只有解釋為程式碼塊,才能執行。

如果要解釋為物件,最好在大括號前加上圓括號。因為圓括號的裡面,只能是表示式,所以確保大括號只能解釋為物件。

({ foo: 123 }) // 正確
({ console.log(123) }) // 報錯
這種差異在eval語句(作用是對字串求值)中反映得最明顯。

eval(’{foo: 123}’) // 123
eval(’({foo: 123})’) // {foo: 123}
上面程式碼中,如果沒有圓括號,eval將其理解為一個程式碼塊;加上圓括號以後,就理解成一個物件。
b 屬性的操作
屬性的讀取
讀取物件的屬性,有兩種方法,一種是使用點運算子,還有一種是使用方括號運算子。

var obj = {
p: ‘Hello World’
};

obj.p // “Hello World”
obj[‘p’] // “Hello World”
上面程式碼分別採用點運算子和方括號運算子,讀取屬性p。

請注意,如果使用方括號運算子,鍵名必須放在引號裡面,否則會被當作變數處理。

var foo = ‘bar’;

var obj = {
foo: 1,
bar: 2
};

obj.foo // 1
obj[foo] // 2
上面程式碼中,引用物件obj的foo屬性時,如果使用點運算子,foo就是字串;如果使用方括號運算子,但是不使用引號,那麼foo就是一個變數,指向字串bar。

方括號運算子內部還可以使用表示式。

obj[‘hello’ + ’ world’]
obj[3 + 3]
數字鍵可以不加引號,因為會自動轉成字串。

var obj = {
0.7: ‘Hello World’
};

obj[‘0.7’] // “Hello World”
obj[0.7] // “Hello World”
上面程式碼中,物件obj的數字鍵0.7,加不加引號都可以,因為會被自動轉為字串。

注意,數值鍵名不能使用點運算子(因為會被當成小數點),只能使用方括號運算子。

var obj = {
123: ‘hello world’
};

obj.123 // 報錯
obj[123] // “hello world”
上面程式碼的第一個表示式,對數值鍵名123使用點運算子,結果報錯。第二個表示式使用方括號運算子,結果就是正確的。

屬性的賦值
點運算子和方括號運算子,不僅可以用來讀取值,還可以用來賦值。

var obj = {};

obj.foo = ‘Hello’;
obj[‘bar’] = ‘World’;
上面程式碼中,分別使用點運算子和方括號運算子,對屬性賦值。

JavaScript 允許屬性的“後繫結”,也就是說,你可以在任意時刻新增屬性,沒必要在定義物件的時候,就定義好屬性。

var obj = { p: 1 };

// 等價於

var obj = {};
obj.p = 1;
屬性的檢視
檢視一個物件本身的所有屬性,可以使用Object.keys方法。

var obj = {
key1: 1,
key2: 2
};

Object.keys(obj);
// [‘key1’, ‘key2’]
屬性的刪除:delete 命令
delete命令用於刪除物件的屬性,刪除成功後返回true。

var obj = { p: 1 };
Object.keys(obj) // [“p”]

delete obj.p // true
obj.p // undefined
Object.keys(obj) // []
上面程式碼中,delete命令刪除物件obj的p屬性。刪除後,再讀取p屬性就會返回undefined,而且Object.keys方法的返回值也不再包括該屬性。

注意,刪除一個不存在的屬性,delete不報錯,而且返回true。

var obj = {};
delete obj.p // true
上面程式碼中,物件obj並沒有p屬性,但是delete命令照樣返回true。因此,不能根據delete命令的結果,認定某個屬性是存在的。

只有一種情況,delete命令會返回false,那就是該屬性存在,且不得刪除。

var obj = Object.defineProperty({}, ‘p’, {
value: 123,
configurable: false
});

obj.p // 123
delete obj.p // false
上面程式碼之中,物件obj的p屬性是不能刪除的,所以delete命令返回false(關於Object.defineProperty方法的介紹,請看《標準庫》的 Object 物件一章)。

另外,需要注意的是,delete命令只能刪除物件本身的屬性,無法刪除繼承的屬性(關於繼承參見《面向物件程式設計》章節)。

var obj = {};
delete obj.toString // true
obj.toString // function toString() { [native code] }
上面程式碼中,toString是物件obj繼承的屬性,雖然delete命令返回true,但該屬性並沒有被刪除,依然存在。這個例子還說明,即使delete返回true,該屬性依然可能讀取到值。

屬性是否存在:in 運算子
in運算子用於檢查物件是否包含某個屬性(注意,檢查的是鍵名,不是鍵值),如果包含就返回true,否則返回false。它的左邊是一個字串,表示屬性名,右邊是一個物件。

var obj = { p: 1 };
‘p’ in obj // true
‘toString’ in obj // true
in運算子的一個問題是,它不能識別哪些屬性是物件自身的,哪些屬性是繼承的。就像上面程式碼中,物件obj本身並沒有toString屬性,但是in運算子會返回true,因為這個屬性是繼承的。

這時,可以使用物件的hasOwnProperty方法判斷一下,是否為物件自身的屬性。

var obj = {};
if (‘toString’ in obj) {
console.log(obj.hasOwnProperty(‘toString’)) // false
}
屬性的遍歷:for…in 迴圈
for…in迴圈用來遍歷一個物件的全部屬性。

var obj = {a: 1, b: 2, c: 3};

for (var i in obj) {
console.log(‘鍵名:’, i);
console.log(‘鍵值:’, obj[i]);
}
// 鍵名: a
// 鍵值: 1
// 鍵名: b
// 鍵值: 2
// 鍵名: c
// 鍵值: 3
for…in迴圈有兩個使用注意點。

它遍歷的是物件所有可遍歷(enumerable)的屬性,會跳過不可遍歷的屬性。
它不僅遍歷物件自身的屬性,還遍歷繼承的屬性。
舉例來說,物件都繼承了toString屬性,但是for…in迴圈不會遍歷到這個屬性。

var obj = {};

// toString 屬性是存在的
obj.toString // toString() { [native code] }

for (var p in obj) {
console.log§;
} // 沒有任何輸出
上面程式碼中,物件obj繼承了toString屬性,該屬性不會被for…in迴圈遍歷到,因為它預設是“不可遍歷”的。關於物件屬性的可遍歷性,參見《標準庫》章節中 Object 一章的介紹。

如果繼承的屬性是可遍歷的,那麼就會被for…in迴圈遍歷到。但是,一般情況下,都是隻想遍歷物件自身的屬性,所以使用for…in的時候,應該結合使用hasOwnProperty方法,在迴圈內部判斷一下,某個屬性是否為物件自身的屬性。

var person = { name: ‘老張’ };

for (var key in person) {
if (person.hasOwnProperty(key)) {
console.log(key);
}
}
// name
cwith 語句
with語句的格式如下:

with (物件) {
語句;
}
它的作用是操作同一個物件的多個屬性時,提供一些書寫的方便。

// 例一
var obj = {
p1: 1,
p2: 2,
};
with (obj) {
p1 = 4;
p2 = 5;
}
// 等同於
obj.p1 = 4;
obj.p2 = 5;

// 例二
with (document.links[0]){
console.log(href);
console.log(title);
console.log(style);
}
// 等同於
console.log(document.links[0].href);
console.log(document.links[0].title);
console.log(document.links[0].style);
注意,如果with區塊內部有變數的賦值操作,必須是當前物件已經存在的屬性,否則會創造一個當前作用域的全域性變數。

var obj = {};
with (obj) {
p1 = 4;
p2 = 5;
}

obj.p1 // undefined
p1 // 4
上面程式碼中,物件obj並沒有p1屬性,對p1賦值等於創造了一個全域性變數p1。正確的寫法應該是,先定義物件obj的屬性p1,然後在with區塊內操作它。

這是因為with區塊沒有改變作用域,它的內部依然是當前作用域。這造成了with語句的一個很大的弊病,就是繫結物件不明確。

with (obj) {
console.log(x);
}
單純從上面的程式碼塊,根本無法判斷x到底是全域性變數,還是物件obj的一個屬性。這非常不利於程式碼的除錯和模組化,編譯器也無法對這段程式碼進行優化,只能留到執行時判斷,這就拖慢了執行速度。因此,建議不要使用with語句,可以考慮用一個臨時變數代替with。

with(obj1.obj2.obj3) {
console.log(p1 + p2);
}

// 可以寫成
var temp = obj1.obj2.obj3;
console.log(temp.p1 + temp.p2);
2函式
a 概述
函式的宣告
JavaScript 有三種宣告函式的方法。

(1)function 命令

function命令宣告的程式碼區塊,就是一個函式。function命令後面是函式名,函式名後面是一對圓括號,裡面是傳入函式的引數。函式體放在大括號裡面。

function print(s) {
console.log(s);
}
上面的程式碼命名了一個print函式,以後使用print()這種形式,就可以呼叫相應的程式碼。這叫做函式的宣告(Function Declaration)。

(2)函式表示式

除了用function命令宣告函式,還可以採用變數賦值的寫法。

var print = function(s) {
console.log(s);
};
這種寫法將一個匿名函式賦值給變數。這時,這個匿名函式又稱函式表示式(Function Expression),因為賦值語句的等號右側只能放表示式。

採用函式表示式宣告函式時,function命令後面不帶有函式名。如果加上函式名,該函式名只在函式體內部有效,在函式體外部無效。

var print = function x(){
console.log(typeof x);
};

x
// ReferenceError: x is not defined

print()
// function
上面程式碼在函式表示式中,加入了函式名x。這個x只在函式體內部可用,指代函式表示式本身,其他地方都不可用。這種寫法的用處有兩個,一是可以在函式體內部呼叫自身,二是方便除錯(除錯工具顯示函式呼叫棧時,將顯示函式名,而不再顯示這裡是一個匿名函式)。因此,下面的形式宣告函式也非常常見。

var f = function f() {};
需要注意的是,函式的表示式需要在語句的結尾加上分號,表示語句結束。而函式的宣告在結尾的大括號後面不用加分號。總的來說,這兩種宣告函式的方式,差別很細微,可以近似認為是等價的。

(3)Function 建構函式

第三種宣告函式的方式是Function建構函式。

var add = new Function(
‘x’,
‘y’,
‘return x + y’
);

// 等同於
function add(x, y) {
return x + y;
}
上面程式碼中,Function建構函式接受三個引數,除了最後一個引數是add函式的“函式體”,其他引數都是add函式的引數。

你可以傳遞任意數量的引數給Function建構函式,只有最後一個引數會被當做函式體,如果只有一個引數,該引數就是函式體。

var foo = new Function(
‘return “hello world”;’
);

// 等同於
function foo() {
return ‘hello world’;
}
Function建構函式可以不使用new命令,返回結果完全一樣。

總的來說,這種宣告函式的方式非常不直觀,幾乎無人使用。

函式的重複宣告
如果同一個函式被多次宣告,後面的宣告就會覆蓋前面的宣告。

function f() {
console.log(1);
}
f() // 2

function f() {
console.log(2);
}
f() // 2
上面程式碼中,後一次的函式宣告覆蓋了前面一次。而且,由於函式名的提升(參見下文),前一次宣告在任何時候都是無效的,這一點要特別注意。

圓括號運算子,return 語句和遞迴
呼叫函式時,要使用圓括號運算子。圓括號之中,可以加入函式的引數。

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

add(1, 1) // 2
上面程式碼中,函式名後面緊跟一對圓括號,就會呼叫這個函式。

函式體內部的return語句,表示返回。JavaScript 引擎遇到return語句,就直接返回return後面的那個表示式的值,後面即使還有語句,也不會得到執行。也就是說,return語句所帶的那個表示式,就是函式的返回值。return語句不是必需的,如果沒有的話,該函式就不返回任何值,或者說返回undefined。

函式可以呼叫自身,這就是遞迴(recursion)。下面就是通過遞迴,計算斐波那契數列的程式碼。

function fib(num) {
if (num === 0) return 0;
if (num === 1) return 1;
return fib(num - 2) + fib(num - 1);
}

fib(6) // 8
上面程式碼中,fib函式內部又呼叫了fib,計算得到斐波那契數列的第6個元素是8。

第一等公民
JavaScript 語言將函式看作一種值,與其它值(數值、字串、布林值等等)地位相同。凡是可以使用值的地方,就能使用函式。比如,可以把函式賦值給變數和物件的屬性,也可以當作引數傳入其他函式,或者作為函式的結果返回。函式只是一個可以執行的值,此外並無特殊之處。

由於函式與其他資料型別地位平等,所以在 JavaScript 語言中又稱函式為第一等公民。

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

// 將函式賦值給一個變數
var operator = add;

// 將函式作為引數和返回值
function a(op){
return op;
}
a(add)(1, 1)
// 2
函式名的提升
JavaScript 引擎將函式名視同變數名,所以採用function命令宣告函式時,整個函式會像變數宣告一樣,被提升到程式碼頭部。所以,下面的程式碼不會報錯。

f();

function f() {}
表面上,上面程式碼好像在宣告之前就呼叫了函式f。但是實際上,由於“變數提升”,函式f被提升到了程式碼頭部,也就是在呼叫之前已經聲明瞭。但是,如果採用賦值語句定義函式,JavaScript 就會報錯。

f();
var f = function (){};
// TypeError: undefined is not a function
上面的程式碼等同於下面的形式。

var f;
f();
f = function () {};
上面程式碼第二行,呼叫f的時候,f只是被聲明瞭,還沒有被賦值,等於undefined,所以會報錯。因此,如果同時採用function命令和賦值語句宣告同一個函式,最後總是採用賦值語句的定義。

var f = function () {
console.log(‘1’);
}

function f() {
console.log(‘2’);
}

f() // 1

b 函式的屬性和方法
name 屬性
函式的name屬性返回函式的名字。

function f1() {}
f1.name // “f1”
如果是通過變數賦值定義的函式,那麼name屬性返回變數名。

var f2 = function () {};
f2.name // “f2”
但是,上面這種情況,只有在變數的值是一個匿名函式時才是如此。如果變數的值是一個具名函式,那麼name屬性返回function關鍵字之後的那個函式名。

var f3 = function myName() {};
f3.name // ‘myName’
上面程式碼中,f3.name返回函式表示式的名字。注意,真正的函式名還是f3,而myName這個名字只在函式體內部可用。

name屬性的一個用處,就是獲取引數函式的名字。

var myFunc = function () {};

function test(f) {
console.log(f.name);
}

test(myFunc) // myFunc
上面程式碼中,函式test內部通過name屬性,就可以知道傳入的引數是什麼函式。

length 屬性
函式的length屬性返回函式預期傳入的引數個數,即函式定義之中的引數個數。

function f(a, b) {}
f.length // 2
上面程式碼定義了空函式f,它的length屬性就是定義時的引數個數。不管呼叫時輸入了多少個引數,length屬性始終等於2。

length屬性提供了一種機制,判斷定義時和呼叫時引數的差異,以便實現面向物件程式設計的”方法過載“(overload)。

toString()
函式的toString方法返回一個字串,內容是函式的原始碼。

function f() {
a();
b();
c();
}

f.toString()
// function f() {
// a();
// b();
// c();
// }
函式內部的註釋也可以返回。

function f() {/*
這是一個
多行註釋
*/}

f.toString()
// “function f(){/*
// 這是一個
// 多行註釋
// */}”
利用這一點,可以變相實現多行字串。

var multiline = function (fn) {
var arr = fn.toString().split(’\n’);
return arr.slice(1, arr.length - 1).join(’\n’);
};

function f() {/*
這是一個
多行註釋
*/}

multiline(f);
// " 這是一個
// 多行註釋"

c 函式作用域
定義
作用域(scope)指的是變數存在的範圍。在 ES5 的規範中,Javascript 只有兩種作用域:一種是全域性作用域,變數在整個程式中一直存在,所有地方都可以讀取;另一種是函式作用域,變數只在函式內部存在。ES6 又新增了塊級作用域,本教程不涉及。

函式外部宣告的變數就是全域性變數(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在條件判斷區塊之中宣告,結果就是一個全域性變數,可以在區塊之外讀取。

函式內部的變數提升
與全域性作用域一樣,函式作用域內部也會產生“變數提升”現象。var命令宣告的變數,不管在什麼位置,變數宣告都會被提升到函式體的頭部。

function foo(x) {
if (x > 100) {
var tmp = x - 100;
}
}

// 等同於
function foo(x) {
var tmp;
if (x > 100) {
tmp = x - 100;
};
}
函式本身的作用域
函式本身也是一個值,也有自己的作用域。它的作用域與變數一樣,就是其宣告時所在的作用域,與其執行時所在的作用域無關。

var a = 1;
var x = function () {
console.log(a);
};

function f() {
var a = 2;
x();
}

f() // 1
上面程式碼中,函式x是在函式f的外部宣告的,所以它的作用域繫結外層,內部變數a不會到函式f體內取值,所以輸出1,而不是2。

總之,函式執行時所在的作用域,是定義時的作用域,而不是呼叫時所在的作用域。

很容易犯錯的一點是,如果函式A呼叫函式B,卻沒考慮到函式B不會引用函式A的內部變數。

var x = function () {
console.log(a);
};

function y(f) {
var a = 2;
f();
}

y(x)
// ReferenceError: a is not defined
上面程式碼將函式x作為引數,傳入函式y。但是,函式x是在函式y體外宣告的,作用域繫結外層,因此找不到函式y的內部變數a,導致報錯。

同樣的,函式體內部宣告的函式,作用域繫結函式體內部。

function foo() {
var x = 1;
function bar() {
console.log(x);
}
return bar;
}

var x = 2;
var f = foo();
f() // 1
上面程式碼中,函式foo內部聲明瞭一個函式bar,bar的作用域繫結foo。當我們在foo外部取出bar執行時,變數x指向的是foo內部的x,而不是foo外部的x。正是這種機制,構成了下文要講解的“閉包”現象。

d 引數
概述
函式執行的時候,有時需要提供外部資料,不同的外部資料會得到不同的結果,這種外部資料就叫引數。

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

square(2) // 4
square(3) // 9
上式的x就是square函式的引數。每次執行的時候,需要提供這個值,否則得不到結果。

引數的省略
函式引數不是必需的,Javascript 允許省略引數。

function f(a, b) {
return a;
}

f(1, 2, 3) // 1
f(1) // 1
f() // undefined

f.length // 2
上面程式碼的函式f定義了兩個引數,但是執行時無論提供多少個引數(或者不提供引數),JavaScript 都不會報錯。省略的引數的值就變為undefined。需要注意的是,函式的length屬性與實際傳入的引數個數無關,只反映函式預期傳入的引數個數。

但是,沒有辦法只省略靠前的引數,而保留靠後的引數。如果一定要省略靠前的引數,只有顯式傳入undefined。

function f(a, b) {
return a;
}

f( , 1) // SyntaxError: Unexpected token ,(…)
f(undefined, 1) // undefined
上面程式碼中,如果省略第一個引數,就會報錯。

傳遞方式
函式引數如果是原始型別的值(數值、字串、布林值),傳遞方式是傳值傳遞(passes by value)。這意味著,在函式體內修改引數值,不會影響到函式外部。

var p = 2;

function f§ {
p = 3;
}
f§;

p // 2
上面程式碼中,變數p是一個原始型別的值,傳入函式f的方式是傳值傳遞。因此,在函式內部,p的值是原始值的拷貝,無論怎麼修改,都不會影響到原始值。

但是,如果函式引數是複合型別的值(陣列、物件、其他函式),傳遞方式是傳址傳遞(pass by reference)。也就是說,傳入函式的原始值的地址,因此在函式內部修改引數,將會影響到原始值。

var obj = { p: 1 };

function f(o) {
o.p = 2;
}
f(obj);

obj.p // 2
上面程式碼中,傳入函式f的是引數物件obj的地址。因此,在函式內部修改obj的屬性p,會影響到原始值。

注意,如果函式內部修改的,不是引數物件的某個屬性,而是替換掉整個引數,這時不會影響到原始值。

var obj = [1, 2, 3];

function f(o) {
o = [2, 3, 4];
}
f(obj);

obj // [1, 2, 3]
上面程式碼中,在函式f內部,引數物件obj被整個替換成另一個值。這時不會影響到原始值。這是因為,形式引數(o)的值實際是引數obj的地址,重新對o賦值導致o指向另一個地址,儲存在原地址上的值當然不受影響。

同名引數
如果有同名的引數,則取最後出現的那個值。

function f(a, a) {
console.log(a);
}

f(1, 2) // 2
上面程式碼中,函式f有兩個引數,且引數名都是a。取值的時候,以後面的a為準,即使後面的a沒有值或被省略,也是以其為準。

function f(a, a) {
console.log(a);
}

f(1) // undefined
呼叫函式f的時候,沒有提供第二個引數,a的取值就變成了undefined。這時,如果要獲得第一個a的值,可以使用arguments物件。

function f(a, a) {
console.log(arguments[0]);
}

f(1) // 1
arguments 物件
(1)定義

由於 JavaScript 允許函式有不定數目的引數,所以需要一種機制,可以在函式體內部讀取所有引數。這就是arguments物件的由來。

arguments物件包含了函式執行時的所有引數,arguments[0]就是第一個引數,arguments[1]就是第二個引數,以此類推。這個物件只有在函式體內部,才可以使用。

var f = function (one) {
console.log(arguments[0]);
console.log(arguments[1]);
console.log(arguments[2]);
}

f(1, 2, 3)
// 1
// 2
// 3
正常模式下,arguments物件可以在執行時修改。

var f = function(a, b) {
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}

f(1, 1) // 5
上面程式碼中,函式f呼叫時傳入的引數,在函式內部被修改成3和2。

嚴格模式下,arguments物件是一個只讀物件,修改它是無效的,但不會報錯。

var f = function(a, b) {
‘use strict’; // 開啟嚴格模式
arguments[0] = 3; // 無效
arguments[1] = 2; // 無效
return a + b;
}

f(1, 1) // 2
上面程式碼中,函式體內是嚴格模式,這時修改arguments物件就是無效的。

通過arguments物件的length屬性,可以判斷函式呼叫時到底帶幾個引數。

function f() {
return arguments.length;
}

f(1, 2, 3) // 3
f(1) // 1
f() // 0
(2)與陣列的關係

需要注意的是,雖然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]);
}
(3)callee 屬性

arguments物件帶有一個callee屬性,返回它所對應的原函式。

var f = function () {
console.log(arguments.callee === f);
}

f() // true
可以通過arguments.callee,達到呼叫函式自身的目的。這個屬性在嚴格模式裡面是禁用的,因此不建議使用。

e 函式的其他知識點
閉包
閉包(closure)是 Javascript 語言的一個難點,也是它的特色,很多高階應用都要依靠閉包實現。

理解閉包,首先必須理解變數作用域。前面提到,JavaScript 有兩種作用域:全域性作用域和函式作用域。函式內部可以直接讀取全域性變數。

var n = 999;

function f1() {
console.log(n);
}
f1() // 999
上面程式碼中,函式f1可以讀取全域性變數n。

但是,函式外部無法讀取函式內部宣告的變數。

function f1() {
var n = 999;
}

console.log(n)
// Uncaught ReferenceError: n is not defined(
上面程式碼中,函式f1內部宣告的變數n,函式外是無法讀取的。

如果出於種種原因,需要得到函式內的區域性變數。正常情況下,這是辦不到的,只有通過變通方法才能實現。那就是在函式的內部,再定義一個函式。

function f1() {
var n = 999;
function f2() {
  console.log(n); // 999
}
}
上面程式碼中,函式f2就在函式f1內部,這時f1內部的所有區域性變數,對f2都是可見的。但是反過來就不行,f2內部的區域性變數,對f1就是不可見的。這就是 JavaScript 語言特有的"鏈式作用域"結構(chain scope),子物件會一級一級地向上尋找所有父物件的變數。所以,父物件的所有變數,對子物件都是可見的,反之則不成立。

既然f2可以讀取f1的區域性變數,那麼只要把f2作為返回值,我們不就可以在f1外部讀取它的內部變量了嗎!

function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}

var result = f1();
result(); // 999
上面程式碼中,函式f1的返回值就是函式f2,由於f2可以讀取f1的內部變數,所以就可以在外部獲得f1的內部變量了。

閉包就是函式f2,即能夠讀取其他函式內部變數的函式。由於在 JavaScript 語言中,只有函式內部的子函式才能讀取內部變數,因此可以把閉包簡單理解成“定義在一個函式內部的函式”。閉包最大的特點,就是它可以“記住”誕生的環境,比如f2記住了它誕生的環境f1,所以從f2可以得到f1的內部變數。在本質上,閉包就是將函式內部和函式外部連線起來的一座橋樑。

閉包的最大用處有兩個,一個是可以讀取函式內部的變數,另一個就是讓這些變數始終保持在記憶體中,即閉包可以使得它誕生環境一直存在。請看下面的例子,閉包使得內部變數記住上一次呼叫時的運算結果。

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,因此也始終在記憶體中,不會在呼叫結束後,被垃圾回收機制回收。

閉包的另一個用處,是封裝物件的私有屬性和私有方法。

function Person(name) {
var _age;
function setAge(n) {
_age = n;
}
function getAge() {
return _age;
}

return {
name: name,
getAge: getAge,
setAge: setAge
};
}

var p1 = Person(‘張三’);
p1.setAge(25);
p1.getAge() // 25
上面程式碼中,函式Person的內部變數_age,通過閉包getAge和setAge,變成了返回物件p1的私有變數。

注意,外層函式每次執行,都會生成一個新的閉包,而這個閉包又會保留外層函式的內部變數,所以記憶體消耗很大。因此不能濫用閉包,否則會造成網頁的效能問題。

立即呼叫的函式表示式(IIFE)
在 Javascript 中,圓括號()是一種運算子,跟在函式名之後,表示呼叫該函式。比如,print()就表示呼叫print函式。

有時,我們需要在定義函式之後,立即呼叫該函式。這時,你不能在函式的定義之後加上圓括號,這會產生語法錯誤。

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);
}());
上面程式碼中,寫法二比寫法一更好,因為完全避免了汙染全域性變數。
f eval 命令
基本用法
eval命令接受一個字串作為引數,並將這個字串當作語句執行。

eval(‘var a = 1;’);
a // 1
上面程式碼將字串當作語句執行,生成了變數a。

如果引數字串無法當作語句執行,那麼就會報錯。

eval(‘3x’) // Uncaught SyntaxError: Invalid or unexpected token
放在eval中的字串,應該有獨自存在的意義,不能用來與eval以外的命令配合使用。舉例來說,下面的程式碼將會報錯。

eval(‘return;’); // Uncaught SyntaxError: Illegal return statement
上面程式碼會報錯,因為return不能單獨使用,必須在函式中使用。

如果eval的引數不是字串,那麼會原樣返回。

eval(123) // 123
eval沒有自己的作用域,都在當前作用域內執行,因此可能會修改當前作用域的變數的值,造成安全問題。

var a = 1;
eval(‘a = 2’);

a // 2
上面程式碼中,eval命令修改了外部變數a的值。由於這個原因,eval有安全風險。

為了防止這種風險,JavaScript 規定,如果使用嚴格模式,eval內部宣告的變數,不會影響到外部作用域。

(function f() {
‘use strict’;
eval(‘var foo = 123’);
console.log(foo); // ReferenceError: foo is not defined
})()
上面程式碼中,函式f內部是嚴格模式,這時eval內部宣告的foo變數,就不會影響到外部。

不過,即使在嚴格模式下,eval依然可以讀寫當前作用域的變數。

(function f() {
‘use strict’;
var foo = 1;
eval(‘foo = 2’);
console.log(foo); // 2
})()
上面程式碼中,嚴格模式下,eval內部還是改寫了外部變數,可見安全風險依然存在。

總之,eval的本質是在當前作用域之中,注入程式碼。由於安全風險和不利於 JavaScript 引擎優化執行速度,所以一般不推薦使用。通常情況下,eval最常見的場合是解析 JSON 資料的字串,不過正確的做法應該是使用原生的JSON.parse方法。

eval 的別名呼叫
前面說過eval不利於引擎優化執行速度。更麻煩的是,還有下面這種情況,引擎在靜態程式碼分析的階段,根本無法分辨執行的是eval。

var m = eval;
m(‘var x = 1’);
x // 1
上面程式碼中,變數m是eval的別名。靜態程式碼分析階段,引擎分辨不出m(‘var x = 1’)執行的是eval命令。

為了保證eval的別名不影響程式碼優化,JavaScript 的標準規定,凡是使用別名執行eval,eval內部一律是全域性作用域。

var a = 1;

function f() {
var a = 2;
var e = eval;
e(‘console.log(a)’);
}

f() // 1
上面程式碼中,eval是別名呼叫,所以即使它是在函式中,它的作用域還是全域性作用域,因此輸出的a為全域性變數。這樣的話,引擎就能確認e()不會對當前的函式作用域產生影響,優化的時候就可以把這一行排除掉。

eval的別名呼叫的形式五花八門,只要不是直接呼叫,都屬於別名呼叫,因為引擎只能分辨eval()這一種形式是直接呼叫。

eval.call(null, ‘…’)
window.eval(’…’)
(1, eval)(’…’)
(eval, eval)(’…’)
上面這些形式都是eval的別名呼叫,作用域都是全域性作用域。

3 陣列
a 定義
陣列(array)是按次序排列的一組值。每個值的位置都有編號(從0開始),整個陣列用方括號表示。

var arr = [‘a’, ‘b’, ‘c’];
上面程式碼中的a、b、c就構成一個數組,兩端的方括號是陣列的標誌。a是0號位置,b是1號位置,c是2號位置。

除了在定義時賦值,陣列也可以先定義後賦值。

var arr = [];

arr[0] = ‘a’;
arr[1] = ‘b’;
arr[2] = ‘c’;
任何型別的資料,都可以放入陣列。

var arr = [
{a: 1},
[1, 2, 3],
function() {return true;}
];

arr[0] // Object {a: 1}
arr[1] // [1, 2, 3]
arr[2] // function (){return true;}
上面陣列arr的3個成員依次是物件、陣列、函式。

如果陣列的元素還是陣列,就形成了多維陣列。

var a = [[1, 2], [3, 4]];
a[0][1] // 2
a[1][1] // 4
b陣列的本質
本質上,陣列屬於一種特殊的物件。typeof運算子會返回陣列的型別是object。

typeof [1, 2, 3] // “object”
上面程式碼表明,typeof運算子認為陣列的型別就是物件。

陣列的特殊性體現在,它的鍵名是按次序排列的一組整數(0,1,2…)。

var arr = [‘a’, ‘b’, ‘c’];

Object.keys(arr)
// [“0”, “1”, “2”]
上面程式碼中,Object.keys方法返回陣列的所有鍵名。可以看到陣列的鍵名就是整數0、1、2。

由於陣列成員的鍵名是固定的(預設總是0、1、2…),因此陣列不用為每個元素指定鍵名,而物件的每個成員都必須指定鍵名。JavaScript 語言規定,物件的鍵名一律為字串,所以,陣列的鍵名其實也是字串。之所以可以用數值讀取,是因為非字串的鍵名會被轉為字串。

var arr = [‘a’, ‘b’, ‘c’];

arr[‘0’] // ‘a’
arr[0] // ‘a’
上面程式碼分別用數值和字串作為鍵名,結果都能讀取陣列。原因是數值鍵名被自動轉為了字串。

注意,這點在賦值時也成立。如果一個值總是先轉成字串,再進行賦值。

var a = [];

a[1.00] = 6;
a[1] // 6
上面程式碼中,由於1.00轉成字串是1,所以通過數字鍵1可以讀取值。

上一章說過,物件有兩種讀取成員的方法:點結構(object.key)和方括號結構(object[key])。但是,對於數值的鍵名,不能使用點結構。

var arr = [1, 2, 3];
arr.0 // SyntaxError
上面程式碼中,arr.0的寫法不合法,因為單獨的數值不能作為識別符號(identifier)。所以,陣列成員只能用方括號arr[0]表示(方括號是運算子,可以接受數值)。
c length 屬性
陣列的length屬性,返回陣列的成員數量。

[‘a’, ‘b’, ‘c’].length // 3
JavaScript 使用一個32位整數,儲存陣列的元素個數。這意味著,陣列成員最多隻有 4294967295 個(232 - 1)個,也就是說length屬性的最大值就是 4294967295。

只要是陣列,就一定有length屬性。該屬性是一個動態的值,等於鍵名中的最大整數加上1。

var arr = [‘a’, ‘b’];
arr.length // 2

arr[2] = ‘c’;
arr.length // 3

arr[9] = ‘d’;
arr.length // 10

arr[1000] = ‘e’;
arr.length // 1001
上面程式碼表示,陣列的數字鍵不需要連續,length屬性的值總是比最大的那個整數鍵大1。另外,這也表明陣列是一種動態的資料結構,可以隨時增減陣列的成員。

length屬性是可寫的。如果人為設定一個小於當前成員個數的值,該陣列的成員會自動減少到length設定的值。

var arr = [ ‘a’, ‘b’, ‘c’ ];
arr.length // 3

arr.length = 2;
arr // [“a”, “b”]
上面程式碼表示,當陣列的length屬性設為2(即最大的整數鍵只能是1)那麼整數鍵2(值為c)就已經不在陣列中了,被自動刪除了。

清空陣列的一個有效方法,就是將length屬性設為0。

var arr = [ ‘a’, ‘b’, ‘c’ ];

arr.length = 0;
arr // []
如果人為設定length大於當前元素個數,則陣列的成員數量會增加到這個值,新增的位置都是空位。

var a = [‘a’];

a.length = 3;
a[1] // undefined
上面程式碼表示,當length屬性設為大於陣列個數時,讀取新增的位置都會返回undefined。

如果人為設定length為不合法的值,JavaScript 會報錯。

// 設定負值
[].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屬性沒有發生變化。這些數字鍵都變成了字串鍵名。最後兩行之所以會取到值,是因為取鍵值時,數字鍵名會預設轉為字串。
d in 運算子
檢查某個鍵名是否存在的運算子in,適用於物件,也適用於陣列。

var arr = [ ‘a’, ‘b’, ‘c’ ];
2 in arr // true
‘2’ in arr // true
4 in arr // false
上面程式碼表明,陣列存在鍵名為2的鍵。由於鍵名都是字串,所以數值2會自動轉成字串。

注意,如果陣列的某個位置是空位,in運算子返回false。

var arr = [];
arr[100] = ‘a’;

100 in arr // true
1 in arr // false
上面程式碼中,陣列arr只有一個成員arr[100],其他位置的鍵名都會返回false。
e for…in 迴圈和陣列的遍歷
for…in迴圈不僅可以遍歷物件,也可以遍歷陣列,畢竟陣列只是一種特殊物件。

var a = [1, 2, 3];

for (var i in a) {
console.log(a[i]);
}
// 1
// 2
// 3
但是,for…in不僅會遍歷陣列所有的數字鍵,還會遍歷非數字鍵。

var a = [1, 2, 3];
a.foo = true;

for (var key in a) {
console.log(key);
}
// 0
// 1
// 2
// foo
上面程式碼在遍歷陣列時,也遍歷到了非整數鍵foo。所以,不推薦使用for…in遍歷陣列。

陣列的遍歷可以考慮使用for迴圈或while迴圈。

var a = [1, 2, 3];

// for迴圈
for(var i = 0; i < a.length; i++) {
console.log(a[i]);
}

// while迴圈
var i = 0;
while (i < a.length) {
console.log(a[i]);
i++;
}

var l = a.length;
while (l–) {
console.log(a[l]);
}
上面程式碼是三種遍歷陣列的寫法。最後一種寫法是逆向遍歷,即從最後一個元素向第一個元素遍歷。

陣列的forEach方法,也可以用來遍歷陣列,詳見《標準庫》的 Array 物件一章。

var colors = [‘red’, ‘green’, ‘blue’];
colors.forEach(function (color) {
console.log(color);
});
// red
// green
// blue

f陣列的空位
當陣列的某個位置是空元素,即兩個逗號之間沒有任何值,我們稱該陣列存在空位(hole)。

var a = [1, , 1];
a.length // 3
上面程式碼表明,陣列的空位不影響length屬性。

需要注意的是,如果最後一個元素後面有逗號,並不會產生空位。也就是說,有沒有這個逗號,結果都是一樣的。

var a = [1, 2, 3,];

a.length // 3
a // [1, 2, 3]
上面程式碼中,陣列最後一個成員後面有一個逗號,這不影響length屬性的值,與沒有這個逗號時效果一樣。

陣列的空位是可以讀取的,返回undefined。

var a = [, , ,];
a[1] // undefined
使用delete命令刪除一個數組成員,會形成空位,並且不會影響length屬性。

var a = [1, 2, 3];
delete a[1];

a[1] // undefined
a.length // 3
上面程式碼用delete命令刪除了陣列的第二個元素,這個位置就形成了空位,但是對length屬性沒有影響。也就是說,length屬性不過濾空位。所以,使用length屬性進行陣列遍歷,一定要非常小心。

陣列的某個位置是空位,與某個位置是undefined,是不一樣的。如果是空位,使用陣列的forEach方法、for…in結構、以及Object.keys方法進行遍歷,空位都會被跳過。

var a = [, , ,];

a.forEach(function (x, i) {
console.log(i + '. ’ + x);
})
// 不產生任何輸出

for (var i in a) {
console.log(i);
}
// 不產生任何輸出

Object.keys(a)
// []
如果某個位置是undefined,遍歷的時候就不會被跳過。

var a = [undefined, undefined, undefined];

a.forEach(function (x, i) {
console.log(i + '. ’ + x);
});
// 0. undefined
// 1. undefined
// 2. undefined

for (var i in a) {
console.log(i);
}
// 0
// 1
// 2

Object.keys(a)
// [‘0’, ‘1’, ‘2’]
這就是說,空位就是陣列沒有這個元素,所以不會被遍歷到,而undefined則表示陣列有這個元素,值是undefined,所以遍歷不會跳過。

g類似陣列的物件
如果一個物件的所有鍵名都是正整數或零,並且有length屬性,那麼這個物件就很像陣列,語法上稱為“類似陣列的物件”(array-like object)。

var obj = {
0: ‘a’,
1: ‘b’,
2: ‘c’,
length: 3
};

obj[0] // ‘a’
obj[1] // ‘b’
obj.length // 3
obj.push(‘d’) // TypeError: obj.push is not a function
上面程式碼中,物件obj就是一個類似陣列的物件。但是,“類似陣列的物件”並不是陣列,因為它們不具備陣列特有的方法。物件obj沒有陣列的push方法,使用該方法就會報錯。

“類似陣列的物件”的根本特徵,就是具有length屬性。只要有length屬性,就可以認為這個物件類似於陣列。但是有一個問題,這種length屬性不是動態值,不會隨著成員的變化而變化。

var obj = {
length: 0
};
obj[3] = ‘d’;
obj.length // 0
上面程式碼為物件obj添加了一個數字鍵,但是length屬性沒變。這就說明了obj不是陣列。

典型的“類似陣列的物件”是函式的arguments物件,以及大多數 DOM 元素集,還有字串。

// arguments物件
function args() { return arguments }
var arrayLike = args(‘a’, ‘b’);

arrayLike[0] // ‘a’
arrayLike.length // 2
arrayLike instanceof Array // false

// DOM元素集
var elts = document.getElementsByTagName(‘h3’);
elts.length // 3
elts instanceof Array // false

// 字串
‘abc’[1] // ‘b’
‘abc’.length // 3
‘abc’ instanceof Array // false
上面程式碼包含三個例子,它們都不是陣列(instanceof運算子返回false),但是看上去都非常像陣列。

陣列的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