es5--數(shù)據(jù)類型筆記

數(shù)值

  • 判斷NaN更可靠的方法是,利用NaN為唯一不等于自身的值的這個特點,進(jìn)行判斷。
function myIsNaN(value) {
  return value !== value;
}
  • isFinite方法返回一個布爾值,表示某個值是否為正常的數(shù)值。除了Infinity、-InfinityNaNundefined這幾個值會返回false,isFinite對于其他的數(shù)值都會返回true。
isFinite(Infinity) // false
isFinite(-Infinity) // false
isFinite(NaN) // false
isFinite(undefined) // false
isFinite(null) // true
isFinite(-1) // true

字符串

  • 如果要在單引號字符串的內(nèi)部,使用單引號,就必須在內(nèi)部的單引號前面加上反斜杠,用來轉(zhuǎn)義。雙引號字符串內(nèi)部使用雙引號,也是如此。
'Did she say \'Hello\'?'
// "Did she say 'Hello'?"

"Did she say \"Hello\"?"
// "Did she say "Hello"?"
  • 字符串默認(rèn)只能寫在一行內(nèi),分成多行將會報錯。如果長字符串必須分成多行,可以在每一行的尾部使用反斜杠。
    下面代碼表示,加了反斜杠以后,原來寫在一行的字符串,可以分成多行書寫。但是,輸出的時候還是單行,效果與寫在同一行完全一樣。注意,反斜杠的后面必須是換行符,而不能有其他字符(比如空格),否則會報錯。
var longString = 'Long \
long \
long \
string';

longString
// "Long long long string"
  • 轉(zhuǎn)義

反斜杠(\)在字符串內(nèi)有特殊含義,用來表示一些特殊字符,所以又稱為轉(zhuǎn)義符。
需要用反斜杠轉(zhuǎn)義的特殊字符,主要有下面這些。

  • \0 :null(\u0000
  • \b :后退鍵(\u0008
  • \f :換頁符(\u000C
  • \n :換行符(\u000A
  • \r :回車鍵(\u000D
  • \t:制表符(\u0009
  • \v:垂直制表符(\u000B
  • \':單引號(\u0027
  • \" :雙引號(\u0022
  • \\ :反斜杠(\u005C
反斜杠還有三種特殊用法。
  • \HHH
    反斜杠后面緊跟三個八進(jìn)制數(shù)(000377),代表一個字符。HHH對應(yīng)該字符的 Unicode 碼點,比如\251表示版權(quán)符號。顯然,這種方法只能輸出256種字符。
  • \xHH
    \x后面緊跟兩個十六進(jìn)制數(shù)(00FF),代表一個字符。HH對應(yīng)該字符的 Unicode 碼點,比如\xA9表示版權(quán)符號。這種方法也只能輸出256種字符。
  • \uXXXX
    \u后面緊跟四個十六進(jìn)制數(shù)(0000FFFF),代表一個字符。HHH對應(yīng)該字符的 Unicode 碼點,比如\u00A9表示版權(quán)符號。
'\251' // "?"
'\xA9' // "?"
'\u00A9' // "?"

'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true

如果在非特殊字符前面使用反斜杠,則反斜杠會被省略。如果字符串的正常內(nèi)容之中,需要包含反斜杠,則反斜杠前面需要再加一個反斜杠,用來對自身轉(zhuǎn)義。

'\a'   (a是一個正常字符,前面加反斜杠沒有特殊含義,反斜杠會被自動省略)
// "a"

"Prev \\ Next"
// "Prev \ Next"
  • Base64轉(zhuǎn)碼

有時,文本里面包含一些不可打印的符號,比如 ASCII 碼0到31的符號都無法打印出來,這時可以使用 Base64 編碼,將它們轉(zhuǎn)成可以打印的字符。另一個場景是,有時需要以文本格式傳遞二進(jìn)制數(shù)據(jù),那么也可以使用 Base64 編碼。

所謂 Base64 就是一種編碼方法,可以將任意值轉(zhuǎn)成 0~9、A~Z、a-z、+和/這64個字符組成的可打印字符。使用它的主要目的,不是為了加密,而是為了不出現(xiàn)特殊字符,簡化程序的處理。

JavaScript 原生提供兩個 Base64 相關(guān)的方法。
btoa():任意值轉(zhuǎn)為 Base64 編碼
atob():Base64 編碼轉(zhuǎn)為原來的值

var string = 'Hello World!';
btoa(string) // "SGVsbG8gV29ybGQh"
atob('SGVsbG8gV29ybGQh') // "Hello World!"

注意,這兩個方法不適合非 ASCII 碼的字符,會報錯。

btoa('你好') //報錯

要將非 ASCII 碼字符轉(zhuǎn)為 Base64 編碼,必須中間插入一個轉(zhuǎn)碼環(huán)節(jié),再使用這兩個方法。

function b64Encode(str) {
  return btoa(encodeURIComponent(str));
}

function b64Decode(str) {
  return decodeURIComponent(atob(str));
}

b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE"
b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好"

encodeURIComponent() 函數(shù)可把字符串作為 URI 組件進(jìn)行編碼。
該方法不會對 ASCII 字母和數(shù)字進(jìn)行編碼,也不會對這些 ASCII 標(biāo)點符號進(jìn)行編碼:- _ . ! ~ * ' ( )
其他字符(比如 :;/?:@&=+$,# 這些用于分隔 URI 組件的標(biāo)點符號),都是由一個或多個十六進(jìn)制的轉(zhuǎn)義序列替換的。
decodeURIComponent()函數(shù)可對encodeURIComponent() 函數(shù)編碼的 URI 進(jìn)行解碼。

對象

對象(object)是 JavaScript 語言的核心概念,也是最重要的數(shù)據(jù)類型。
什么是對象?簡單說,對象就是一組“鍵值對”(key-value)的集合,是一種無序的復(fù)合數(shù)據(jù)集合。
如果鍵名不符合標(biāo)識名的條件(比如第一個字符為數(shù)字,或者含有空格或運(yùn)算符),且也不是數(shù)字,則必須加上引號,否則會報錯。
對象的每一個鍵名又稱為“屬性”(property),它的“鍵值”可以是任何數(shù)據(jù)類型。如果一個屬性的值為函數(shù),通常把這個屬性稱為“方法”,它可以像函數(shù)那樣調(diào)用。

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

obj.p(1) // 2

上面代碼中,對象obj的屬性p,就指向一個函數(shù)。
如果屬性的值還是一個對象,就形成了鏈?zhǔn)揭谩?/p>

var o1 = {};
var o2 = { bar: 'hello' };

o1.foo = o2;
o1.foo.bar // "hello"

上面代碼中,對象o1的屬性foo指向?qū)ο?code>o2,就可以鏈?zhǔn)揭?code>o2的屬性。
對象的屬性之間用逗號分隔,最后一個屬性后面可以加逗號(trailing comma),也可以不加。
屬性可以動態(tài)創(chuàng)建,不必在對象聲明時就指定。

var obj = {};
obj.foo = 123;
obj.foo // 123

上面代碼中,直接對obj對象的foo屬性賦值,結(jié)果就在運(yùn)行時創(chuàng)建了foo屬性。

  • 對象的引用

如果不同的變量名指向同一個對象,那么它們都是這個對象的引用,也就是說指向同一個內(nèi)存地址。修改其中一個變量,會影響到其他所有變量。

var o1 = {};
var o2 = o1;

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

o2.b = 2;
o1.b // 2

上面代碼中,o1o2指向同一個對象,因此為其中任何一個變量添加屬性,另一個變量都可以讀寫該屬性。
此時,如果取消某一個變量對于原對象的引用,不會影響到另一個變量。

var o1 = {};
var o2 = o1;

o1 = 1;
o2 // {}

上面代碼中,o1o2指向同一個對象,然后o1的值變?yōu)?code>1,這時不會對o2產(chǎn)生影響,o2還是指向原來的那個對象。
但是,這種引用只局限于對象,如果兩個變量指向同一個原始類型的值。那么,變量這時都是值的拷貝。

var x = 1;
var y = x;

x = 2;
y // 1

上面的代碼中,當(dāng)x的值發(fā)生變化后,y的值并不變,這就表示yx并不是指向同一個內(nèi)存地址。
對象采用大括號表示,這導(dǎo)致了一個問題:如果行首是一個大括號,它到底是表達(dá)式還是語句?

{foo:123}

JavaScript 引擎讀到上面這行代碼,會發(fā)現(xiàn)可能有兩種含義。第一種可能是,這是一個表達(dá)式,表示一個包含foo屬性的對象;第二種可能是,這是一個語句,表示一個代碼區(qū)塊,里面有一個標(biāo)簽foo,指向表達(dá)式123
為了避免這種歧義,V8 引擎規(guī)定,如果行首是大括號,一律解釋為對象。不過,為了避免歧義,最好在大括號前加上圓括號。這種差異在eval語句(作用是對字符串求值)中反映得最明顯。

eval('{foo: 123}') // 123
eval('({foo: 123})') // {foo: 123}

上面代碼中,如果沒有圓括號,eval將其理解為一個代碼塊;加上圓括號以后,就理解成一個對象。

  • 對象屬性的讀取

讀取對象的屬性,有兩種方法,一種是使用點運(yùn)算符,還有一種是使用方括號運(yùn)算符。

var obj = {
  p: 'Hello World'
};

obj.p // "Hello World"
obj['p'] // "Hello World"

上面代碼分別采用點運(yùn)算符和方括號運(yùn)算符,讀取屬性p。
請注意,如果使用方括號運(yùn)算符,鍵名必須放在引號里面,否則會被當(dāng)作變量處理。

var foo = 'bar';

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

obj.foo  // 1
obj[foo]  // 2

上面代碼中,引用對象objfoo屬性時,如果使用點運(yùn)算符,foo就是字符串;如果使用方括號運(yùn)算符,但是不使用引號,那么foo就是一個變量,指向字符串bar。

方括號運(yùn)算符內(nèi)部還可以使用表達(dá)式。數(shù)字鍵可以不加引號,因為會自動轉(zhuǎn)成字符串。注意,數(shù)值鍵名不能使用點運(yùn)算符(因為會被當(dāng)成小數(shù)點),只能使用方括號運(yùn)算符。

obj['hello' + ' world']
obj[3 + 3]

var obj = {
  0.7: 'Hello World'
};
obj['0.7'] // "Hello World"
obj[0.7] // "Hello World"

var obj = {
  123: 'hello world'
};
obj.123 // 報錯
obj[123] // "hello world"

點運(yùn)算符和方括號運(yùn)算符,不僅可以用來讀取值,還可以用來賦值。
JavaScript 允許屬性的“后綁定”,也就是說,你可以在任意時刻新增屬性,沒必要在定義對象的時候,就定義好屬性。

var obj = { p: 1 };
// 等價于
var obj = {};
obj.p = 1;
obj['p'] = 1;
  • 查看一個對象本身的所有屬性,可以使用Object.keys方法。
var obj = {
  key1: 1,
  key2: 2
};
Object.keys(obj);
// ['key1', 'key2']
  • delete命令用于刪除對象的屬性,刪除成功后返回true。
var obj = { p: 1 };
Object.keys(obj) // ["p"]
delete obj.p // true
obj.p // undefined
Object.keys(obj) // []

注意,刪除一個不存在的屬性,delete不報錯,而且返回true。因此,不能根據(jù)delete命令的結(jié)果,認(rèn)定某個屬性是存在的。只有一種情況,delete命令會返回false,那就是該屬性存在,且不得刪除。

var obj = Object.defineProperty({}, 'p', {
  value: 123,
  configurable: false
});
obj.p // 123
delete obj.p // false

上面代碼之中,對象obj的p屬性是不能刪除的,所以delete命令返回false
另外,需要注意的是,delete命令只能刪除對象本身的屬性,無法刪除繼承的屬性

var obj = {};
delete obj.toString // true
obj.toString // function toString() { [native code] }

上面代碼中,toString是對象obj繼承的屬性,雖然delete命令返回true,但該屬性并沒有被刪除,依然存在。這個例子還說明,即使delete返回true,該屬性依然可能讀取到值。

  • 屬性是否存在:in 運(yùn)算符

in運(yùn)算符用于檢查對象是否包含某個屬性(注意,檢查的是鍵名,不是鍵值),如果包含就返回true,否則返回false。它的左邊是一個字符串,表示屬性名,右邊是一個對象。

in運(yùn)算符的一個問題是,它不能識別哪些屬性是對象自身的,哪些屬性是繼承的。就像下面代碼中,對象obj本身并沒有toString屬性,但是in運(yùn)算符會返回true,因為這個屬性是繼承的。
這時,可以使用對象的hasOwnProperty方法判斷一下,是否為對象自身的屬性,返回值為booleam。

所有繼承了 [Object] 的對象都會繼承到 hasOwnProperty 方法。這個方法可以用來檢測一個對象是否含有特定的自身屬性;和 [in] 運(yùn)算符不同,該方法會忽略掉那些從原型鏈上繼承到的屬性。

var obj = { p: 1 };
'p' in obj // true
'toString' in obj // true

var obj = {};
if ('toString' in obj) {
  console.log(obj.hasOwnProperty('toString')) // false
}
  • 屬性的遍歷:for...in 循環(huán)

for...in循環(huán)用來遍歷一個對象的全部屬性。使用是注意點:

  • 它遍歷的是對象所有可遍歷(enumerable)的屬性,會跳過不可遍歷的屬性。
  • 它不僅遍歷對象自身的屬性,還遍歷繼承的屬性。
    舉例來說,對象都繼承了toString屬性,但是for...in循環(huán)不會遍歷到這個屬性。
var obj = {a: 1, b: 2, c: 3};
for (var i in obj) {
  console.log('鍵名:', i);
  console.log('鍵值:', obj[i]);
}
// 鍵名: a
// 鍵值: 1
// 鍵名: b
// 鍵值: 2
// 鍵名: c
// 鍵值: 3

var obj = {};

// toString 屬性是存在的
obj.toString // toString() { [native code] }
for (var p in obj) {
  console.log(p);
} // 沒有任何輸出

上面代碼中,對象obj繼承了toString屬性,該屬性不會被for...in循環(huán)遍歷到,因為它默認(rèn)是“不可遍歷”的。

如果繼承的屬性是可遍歷的,那么就會被for...in循環(huán)遍歷到。但是,一般情況下,都是只想遍歷對象自身的屬性,所以使用for...in的時候,應(yīng)該結(jié)合使用hasOwnProperty方法,在循環(huán)內(nèi)部判斷一下,某個屬性是否為對象自身的屬性。

var person = { name: '老張' };
for (var key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key);
  }
}
// name
  • with語句

它的作用是操作同一個對象的多個屬性時,提供一些書寫的方便。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區(qū)塊內(nèi)部有變量的賦值操作,必須是當(dāng)前對象已經(jīng)存在的屬性,否則會創(chuàng)造一個當(dāng)前作用域的全局變量。

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

上面代碼中,對象obj并沒有p1屬性,對p1賦值等于創(chuàng)造了一個全局變量p1。正確的寫法應(yīng)該是,先定義對象obj的屬性p1,然后在with區(qū)塊內(nèi)操作它。
這是因為with區(qū)塊沒有改變作用域,它的內(nèi)部依然是當(dāng)前作用域。這造成了with語句的一個很大的弊病,就是綁定對象不明確。

with(obj){
console.log(x)
}

單純從上面的代碼塊,根本無法判斷x到底是全局變量,還是對象obj的一個屬性。這非常不利于代碼的除錯和模塊化,編譯器也無法對這段代碼進(jìn)行優(yōu)化,只能留到運(yùn)行時判斷,這就拖慢了運(yùn)行速度。因此,建議不要使用with語句,可以考慮用一個臨時變量代替with。

with(obj1.obj2.obj3) {
  console.log(p1 + p2);
}
// 可以寫成
var temp = obj1.obj2.obj3;
console.log(temp.p1 + temp.p2);

函數(shù)

JavaScript 有三種聲明函數(shù)的方法。

  • function命令

function命令聲明的代碼區(qū)塊,就是一個函數(shù)。function命令后面是函數(shù)名,函數(shù)名后面是一對圓括號,里面是傳入函數(shù)的參數(shù)。函數(shù)體放在大括號里面。

function print(s) {
  console.log(s);
}
  • 函數(shù)表達(dá)式,采用變量賦值的寫法。
var print = function(s) {
  console.log(s);
};

這種寫法將一個匿名函數(shù)賦值給變量。這時,這個匿名函數(shù)又稱函數(shù)表達(dá)式(Function Expression),因為賦值語句的等號右側(cè)只能放表達(dá)式。

采用函數(shù)表達(dá)式聲明函數(shù)時,function命令后面不帶有函數(shù)名。如果加上函數(shù)名,該函數(shù)名只在函數(shù)體內(nèi)部有效,在函數(shù)體外部無效。

var print = function x(){
  console.log(typeof x);
};
x
// ReferenceError: x is not defined
print()
// function

上面代碼在函數(shù)表達(dá)式中,加入了函數(shù)名x。這個x只在函數(shù)體內(nèi)部可用,指代函數(shù)表達(dá)式本身,其他地方都不可用。這種寫法的用處有兩個,一是可以在函數(shù)體內(nèi)部調(diào)用自身,二是方便除錯(除錯工具顯示函數(shù)調(diào)用棧時,將顯示函數(shù)名,而不再顯示這里是一個匿名函數(shù))。因此,這種形式聲明函數(shù)也非常常見。

需要注意的是,函數(shù)的表達(dá)式需要在語句的結(jié)尾加上分號,表示語句結(jié)束。而函數(shù)的聲明在結(jié)尾的大括號后面不用加分號??偟膩碚f,這兩種聲明函數(shù)的方式,差別很細(xì)微,可以近似認(rèn)為是等價的。

  • Function 構(gòu)造函數(shù)
var add = new Function(
  'x',
  'y',
  'return x + y'
);
// 等同于
function add(x, y) {
  return x + y;
}

上面代碼中,Function構(gòu)造函數(shù)接受三個參數(shù),除了最后一個參數(shù)是add函數(shù)的“函數(shù)體”,其他參數(shù)都是add函數(shù)的參數(shù)。

你可以傳遞任意數(shù)量的參數(shù)給Function構(gòu)造函數(shù),只有最后一個參數(shù)會被當(dāng)做函數(shù)體,如果只有一個參數(shù),該參數(shù)就是函數(shù)體。

Function構(gòu)造函數(shù)可以不使用new命令,返回結(jié)果完全一樣??偟膩碚f,這種聲明函數(shù)的方式非常不直觀,幾乎無人使用。

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

函數(shù)可以調(diào)用自身,這就是遞歸(recursion)。上面就是通過遞歸,計算斐波那契數(shù)列的代碼。

javascript里面執(zhí)行過程(純屬個人這樣子認(rèn)為方便自己理解)
第1次進(jìn)入函數(shù)fib (6-2)+(6-1)
第2次進(jìn)入函數(shù)fib (4-2)+(4-1)+(5-2)+(5-1)
第3次進(jìn)入函數(shù)fib (2-2)+(2-1)+(3-2)+(3-1)+(3-2)+(3-1)+(4-2)+(4-1)
第4次進(jìn)入函數(shù)fib (0)0+(1)1+(1)1+(2-2)+(2-1)+(1)+(2-2)+(2-1)+(2-2)+(2-1)+(3-2)+(3-1)
第5次進(jìn)入函數(shù)fib (0)+(1)+(1)+(0)+(1)+(1)+(0)+(1)+(0)+(1)+(1)+(2-2)+(2-1)
第6次進(jìn)入函數(shù)fib (0)+(1)+(1)+(0)+(1)+(1)+(0)+(1)+(0)+(1)+(1)+(0)+(1)
最后結(jié)果就為最后一次進(jìn)入函數(shù)值的和為8

在js里函數(shù)看作一種值,與其他值一樣的用法。采用function命令聲明函數(shù)時,這個函數(shù)和變量一樣會提升,也就是js預(yù)解析或者js引擎運(yùn)作。但是如果采用賦值語句定義函數(shù),就不會整個函數(shù)提升會報錯。由于function聲明會被提升,賦值聲明不會,所以當(dāng)function命令和賦值語句同時聲明一個函數(shù),最后總是采用賦值語句的定義。

var f = function () {
  console.log('1');
}
function f() {
  console.log('2');
}
f() // 1
  • 函數(shù)的屬性和方法

fn.name 屬性 獲取函數(shù)名(注意賦值語句定義時的匿名函數(shù)和具名函數(shù)區(qū)別)
fn.length 屬性 返回函數(shù)預(yù)期傳入的參數(shù)個數(shù),即函數(shù)定義之中的參數(shù)個數(shù)
fn.toString() 方法 返回一個字符串,內(nèi)容是函數(shù)的源碼,函數(shù)內(nèi)部的注釋也可以返回。

  • 函數(shù)作用域

作用域(scope)指的是變量存在的范圍。在 ES5 的規(guī)范中,Javascript 只有兩種作用域:一種是全局作用域,變量在整個程序中一直存在,所有地方都可以讀??;另一種是函數(shù)作用域,變量只在函數(shù)內(nèi)部存在。(ES6 又新增了塊級作用域)
函數(shù)內(nèi)部定義的變量,會在該作用域內(nèi)覆蓋同名全局變量。

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

函數(shù)內(nèi)部的變量提升和全局變量提升一樣,函數(shù)內(nèi)部也會發(fā)生預(yù)解析(js引擎運(yùn)作)
函數(shù)本身也是一個值,也有自己的作用域。它的作用域與變量一樣,就是其聲明時所在的作用域,與其運(yùn)行時所在的作用域無關(guān)

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

上面代碼中,函數(shù)x是在函數(shù)f的外部聲明的,所以它的作用域綁定外層,內(nèi)部變量a不會到函數(shù)f體內(nèi)取值,所以輸出1,而不是2
總之,函數(shù)執(zhí)行時所在的作用域,是定義時的作用域,而不是調(diào)用時所在的作用域。

很容易犯錯的一點是,如果函數(shù)A調(diào)用函數(shù)B,卻沒考慮到函數(shù)B不會引用函數(shù)A的內(nèi)部變量。同樣的,函數(shù)體內(nèi)部聲明的函數(shù),作用域綁定函數(shù)體內(nèi)部。

例一:
var x = function () {
  console.log(a);
};
function y(f) {
  var a = 2;
  f();
}
y(x)   // ReferenceError: a is not defined

例二:
function foo() {
  var x = 1;
  function bar() {
    console.log(x);
  }
  return bar;
}
var x = 2;
var f = foo();
f() // 1

上面代碼例一中將函數(shù)x作為參數(shù),傳入函數(shù)y。但是,函數(shù)x是在函數(shù)y體外聲明的,作用域綁定外層,因此找不到函數(shù)y的內(nèi)部變量a,導(dǎo)致報錯。例二代碼中,函數(shù)foo內(nèi)部聲明了一個函數(shù)bar,bar的作用域綁定foo。當(dāng)我們在foo外部取出bar執(zhí)行時,變量x指向的是foo內(nèi)部的x,而不是foo外部的x。正是這種機(jī)制,構(gòu)成了下文要講解的“閉包”現(xiàn)象。

  • 參數(shù)

函數(shù)參數(shù)不是必需的,Javascript 允許省略參數(shù)。但是,沒有辦法只省略靠前的參數(shù),而保留靠后的參數(shù)。如果一定要省略靠前的參數(shù),只有顯式傳入undefined。

函數(shù)參數(shù)如果是原始類型的值(數(shù)值、字符串、布爾值)傳遞方式是傳值傳遞,就是原始值的拷貝傳遞。如果是復(fù)合類型的值(數(shù)組、對象、其他函數(shù))傳遞方式就是傳址傳遞,在函數(shù)內(nèi)部修改參數(shù)會影響到原始值。(注意:如果函數(shù)內(nèi)部修改的不是參數(shù)對象的某個屬性,而是替換整個參數(shù),這是不會影響到原始值)

//傳值傳遞
var p = 2;
function f(p) {
  p = 3;
}
f(p);
p // 2

//傳址傳遞
var obj = { p: 1 };
function f(o) {
  o.p = 2;
}
f(obj);
obj.p // 2

//注意事項
var obj = [1, 2, 3];
function f(o) {
  o = [2, 3, 4];
}
f(obj);
obj // [1, 2, 3]

上面注意事項代碼中,在函數(shù)f內(nèi)部,參數(shù)對象obj被整個替換成另一個值。這時不會影響到原始值。這是因為,形式參數(shù)(o)的值實際是參數(shù)obj的地址,重新對o賦值導(dǎo)致o指向另一個地址,保存在原地址上的值當(dāng)然不受影響。
如果有同名的參數(shù),則取最后出現(xiàn)的那個值。調(diào)用是沒有提供最后的參數(shù),就會返回undefined,這時,如果要獲取第一個參數(shù)的值,可以使用arguments對象

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

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

function f(a, a) {
  console.log(arguments[0]);
}
f(1) // 1
  • arguments對象

arguments對象包含了函數(shù)運(yùn)行時的所有參數(shù),arguments[0]就是第一個參數(shù),arguments[1]就是第二個參數(shù),以此類推。這個對象只有在函數(shù)體內(nèi)部,才可以使用。

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

非嚴(yán)格模式下,arguments對象可以在運(yùn)行時修改。嚴(yán)格模式下,arguments對象是一個只讀對象,修改它是無效的,但不會報錯。

//非嚴(yán)格模式
var f = function(a,b){
  arguments[0]=2;
  arguments[1]=3;
  return a+b;
}
f(1,1)  //5

//嚴(yán)格模式
var f=function(a,b){
  'use strict'; //開啟嚴(yán)格模式
  arguments[0]=2;
  arguments[1]=3;
  return a+b;
}
f(1,1) //2

通過arguments對象的length屬性,可以判斷函數(shù)調(diào)用時到底帶幾個參數(shù)。

function f() {
  return arguments.length;
}
f(1, 2, 3) // 3
f(1) // 1
f() // 0

需要注意的是,雖然arguments很像數(shù)組,但它是一個對象。數(shù)組專有的方法(比如sliceforEach),不能在arguments對象上直接使用。

如果要讓arguments對象使用數(shù)組方法,真正的解決方法是將arguments轉(zhuǎn)為真正的數(shù)組。下面是兩種常用的轉(zhuǎn)換方法:slice方法和逐一填入新數(shù)組。

var args = Array.prototype.slice.call(arguments); //因為Array是一個類,不能直接引用,需要獲取原型后才能使用。

// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {
  args.push(arguments[i]);
}

arguments對象帶有一個callee屬性,返回它所對應(yīng)的原函數(shù)??梢酝ㄟ^arguments.callee,達(dá)到調(diào)用函數(shù)自身的目的。這個屬性在嚴(yán)格模式里面是禁用的,因此不建議使用。

var f = function () {
  console.log(arguments.callee === f);
}
f() // true
  • 閉包

閉包(closure)是 Javascript 語言的一個難點,也是它的特色,很多高級應(yīng)用都要依靠閉包實現(xiàn)。

理解閉包,首先必須理解變量作用域。前面提到,JavaScript 有兩種作用域:全局作用域和函數(shù)作用域。函數(shù)內(nèi)部可以直接讀取全局變量,但是,函數(shù)外部無法讀取函數(shù)內(nèi)部聲明的變量。

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

function f1(){
  var n = 999;
}
console.log(n); // uncaught ReferenceError: n is not defined

上面代碼中,函數(shù)f2就在函數(shù)f1內(nèi)部,這時f1內(nèi)部的所有局部變量,對f2都是可見的。但是反過來就不行,f2內(nèi)部的局部變量,對f1就是不可見的。這就是 JavaScript 語言特有的"鏈?zhǔn)阶饔糜?結(jié)構(gòu)(chain scope),子對象會一級一級地向上尋找所有父對象的變量。所以,父對象的所有變量,對子對象都是可見的,反之則不成立。

既然f2可以讀取f1的局部變量,那么只要把f2作為返回值,我們不就可以在f1外部讀取它的內(nèi)部變量了嗎!

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

上面代碼中,函數(shù)f1的返回值就是函數(shù)f2,由于f2可以讀取f1的內(nèi)部變量,所以就可以在外部獲得f1的內(nèi)部變量了。

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

閉包的最大用處有兩個,一個是可以讀取函數(shù)內(nèi)部的變量,另一個就是讓這些變量始終保持在內(nèi)存中,即閉包可以使得它誕生環(huán)境一直存在。請看下面的例子,閉包使得內(nèi)部變量記住上一次調(diào)用時的運(yùn)算結(jié)果。

function createIncrementor(start) {
  return function () {
    return start++;
  };
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7

上面代碼中,start是函數(shù)createIncrementor的內(nèi)部變量。通過閉包,start的狀態(tài)被保留了,每一次調(diào)用都是在上一次調(diào)用的基礎(chǔ)上進(jìn)行計算。從中可以看到,閉包inc使得函數(shù)createIncrementor的內(nèi)部環(huán)境,一直存在。所以,閉包可以看作是函數(shù)內(nèi)部作用域的一個接口。

為什么會這樣呢?原因就在于inc始終在內(nèi)存中,而inc的存在依賴于createIncrementor,因此也始終在內(nèi)存中,不會在調(diào)用結(jié)束后,被垃圾回收機(jī)制回收。

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

function Person(name) {
  var _age;
  function setAge(n) {
    _age = n;
  }
  function getAge() {
    return _age;
  }
  return {
    name: name,
    getAge: getAge,
    setAge: setAge
  };
}
var p1 = Person('張三');
p1.setAge(25);
p1.getAge() // 25

上面代碼中,函數(shù)Person的內(nèi)部變量_age,通過閉包getAgesetAge,變成了返回對象p1的私有變量。

注意,外層函數(shù)每次運(yùn)行,都會生成一個新的閉包,而這個閉包又會保留外層函數(shù)的內(nèi)部變量,所以內(nèi)存消耗很大。因此不能濫用閉包,否則會造成網(wǎng)頁的性能問題。

  • 立即調(diào)用的函數(shù)表達(dá)式(IIFE)
    在 Javascript 中,圓括號()是一種運(yùn)算符,跟在函數(shù)名之后,表示調(diào)用該函數(shù)。比如,print()就表示調(diào)用print函數(shù)。有時,我們需要在定義函數(shù)之后,立即調(diào)用該函數(shù)。這時,你不能在函數(shù)的定義之后加上圓括號,這會產(chǎn)生語法錯誤。產(chǎn)生這個錯誤的原因是,function這個關(guān)鍵字即可以當(dāng)作語句,也可以當(dāng)作表達(dá)式。
    為了避免解析上的歧義,JavaScript 引擎規(guī)定,如果 function 關(guān)鍵字出現(xiàn)在行首,一律解釋成語句。因此,JavaScript 引擎看到行首是 function 關(guān)鍵字之后,認(rèn)為這一段都是函數(shù)的定義,不應(yīng)該以圓括號結(jié)尾,所以就報錯了。解決方法就是不要讓 function 出現(xiàn)在行首,讓引擎將其理解成一個表達(dá)式。最簡單的處理,就是將其放在一個圓括號里面。
// 語句
function f() {}
// 表達(dá)式
var f = function f() {}

(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();

上面兩種寫法都是以圓括號開頭,引擎就會認(rèn)為后面跟的是一個表示式,而不是函數(shù)定義語句,所以就避免了錯誤。這就叫做“立即調(diào)用的函數(shù)表達(dá)式”(Immediately-Invoked Function Expression),簡稱 IIFE。
注意,上面兩種寫法最后的分號都是必須的。如果省略分號,遇到連著兩個 IIFE,可能就會報錯。兩行之間沒有分號,JavaScript 會將它們連在一起解釋,將第二行解釋為第一行的參數(shù)。

推而廣之,任何讓解釋器以表達(dá)式來處理函數(shù)定義的方法,都能產(chǎn)生同樣的效果,比如下面的寫法。

var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();

通常情況下,只對匿名函數(shù)使用這種“立即執(zhí)行的函數(shù)表達(dá)式”。它的目的有兩個:一是不必為函數(shù)命名,避免了污染全局變量;二是 IIFE 內(nèi)部形成了一個單獨的作用域,可以封裝一些外部無法讀取的私有變量。

// 寫法一
var tmp = newData;
processData(tmp);
storeData(tmp);

// 寫法二
(function () {
  var tmp = newData;
  processData(tmp);
  storeData(tmp);
}());

上面代碼中,寫法二比寫法一更好,因為完全避免了污染全局變量。

  • eval命令

eval命令接受一個字符串作為參數(shù),并將這個字符串當(dāng)作語句執(zhí)行。

eval("var a=1;")
console.log(a) //1

上面代碼將字符串當(dāng)作語句運(yùn)行,生成了變量a。如果參數(shù)字符串無法當(dāng)作語句運(yùn)行,那么就會報錯。放在eval中的字符串,應(yīng)該有獨自存在的意義,不能用來與eval以外的命令配合使用。舉例來說,下面的代碼將會報錯。

eval("3x"); // Uncaught SyntaxError: Invalid or unexpected token
eval("return;"); // Uncaught SyntaxError: Invalid or unexpected token

上面代碼會報錯,因為字符串3x無法當(dāng)作語句運(yùn)行、return不能單獨使用,必須在函數(shù)中使用。
如果eval的參數(shù)不是字符串,那么會原樣返回。eval沒有自己的作用域,都在當(dāng)前作用域內(nèi)執(zhí)行,因此可能會修改當(dāng)前作用域的變量的值,造成安全問題。

eval(123) //123

var a = 1;
eval(" a = 2 ");
console.log(a) //2

上面代碼中,eval命令修改了外部變量a的值。由于這個原因,eval有安全風(fēng)險。為了防止這種風(fēng)險,JavaScript 規(guī)定,如果使用嚴(yán)格模式,eval內(nèi)部聲明的變量,不會影響到外部作用域。

(function f() {
  'use strict';
  eval('var foo = 123');
  console.log(foo);  // ReferenceError: foo is not defined
})()

上面代碼中,函數(shù)f內(nèi)部是嚴(yán)格模式,這時eval內(nèi)部聲明的foo變量,就不會影響到外部。不過,即使在嚴(yán)格模式下,eval依然可以讀寫當(dāng)前作用域的變量。

(function f() {
  'use strict';
  var foo = 1;
  eval('foo = 2');
  console.log(foo);  // 2
})()

上面代碼中,嚴(yán)格模式下,eval內(nèi)部還是改寫了外部變量,可見安全風(fēng)險依然存在。

總之,eval的本質(zhì)是在當(dāng)前作用域之中,注入代碼。由于安全風(fēng)險和不利于 JavaScript 引擎優(yōu)化執(zhí)行速度,所以一般不推薦使用。通常情況下,eval最常見的場合是解析 JSON數(shù)據(jù)的字符串,不過正確的做法應(yīng)該是使用原生的 JSON.parse方法。

前面說過eval不利于引擎優(yōu)化執(zhí)行速度。更麻煩的是,還有下面這種情況,引擎在靜態(tài)代碼分析的階段,根本無法分辨執(zhí)行的是eval

var m = eval;
m('var x = 1');
x // 1

上面代碼中,變量meval的別名。靜態(tài)代碼分析階段,引擎分辨不出m('var x = 1')執(zhí)行的是eval命令。

為了保證eval的別名不影響代碼優(yōu)化,JavaScript 的標(biāo)準(zhǔn)規(guī)定,凡是使用別名執(zhí)行eval,eval內(nèi)部一律是全局作用域。

var a = 1;
function f() {
  var a = 2;
  var e = eval;
  e('console.log(a)');
}
f() // 1

上面代碼中,eval是別名調(diào)用,所以即使它是在函數(shù)中,它的作用域還是全局作用域,因此輸出的a為全局變量。這樣的話,引擎就能確認(rèn)e()不會對當(dāng)前的函數(shù)作用域產(chǎn)生影響,優(yōu)化的時候就可以把這一行排除掉。
eval的別名調(diào)用的形式五花八門,只要不是直接調(diào)用,都屬于別名調(diào)用,因為引擎只能分辨eval()這一種形式是直接調(diào)用。下面這些形式都是eval的別名調(diào)用,作用域都是全局作用域。

eval.call(null, '...')
window.eval('...')
(1, eval)('...')
(eval, eval)('...')

數(shù)組

數(shù)組(array)是按次序排列的一組值。每個值的位置都有編號(從0開始),整個數(shù)組用方括號表示。除了在定義時賦值,數(shù)組也可以先定義后賦值。任何類型的數(shù)據(jù),都可以放入數(shù)組。如果數(shù)組的元素還是數(shù)組,就形成了多維數(shù)組。

var arr = [
  {a: 1},
  [1, 2, 3],
  function() {return true;}
];
arr[0] // Object {a: 1} 對象
arr[1] // [1, 2, 3] 數(shù)組
arr[2] // function (){return true;} 函數(shù)

本質(zhì)上,數(shù)組屬于一種特殊的對象。typeof運(yùn)算符會返回數(shù)組的類型是object。數(shù)組的特殊性體現(xiàn)在,它的鍵名是按次序排列的一組整數(shù)(0,1,2...)。

console.log(typeof [1,2,3]); //object

var arr = ['a','b','c'];
Object.keys(arr) //['0','1','2']

上面代碼中,Object.keys方法返回數(shù)組的所有鍵名。可以看到數(shù)組的鍵名就是整數(shù)0、1、2。

由于數(shù)組成員的鍵名是固定的(默認(rèn)總是0、1、2...),因此數(shù)組不用為每個元素指定鍵名,而對象的每個成員都必須指定鍵名。JavaScript 語言規(guī)定,對象的鍵名一律為字符串,所以,數(shù)組的鍵名其實也是字符串。之所以可以用數(shù)值讀取,是因為非字符串的鍵名會被轉(zhuǎn)為字符串。

對象有兩種讀取成員的方法:點結(jié)構(gòu)(object.key)和方括號結(jié)構(gòu)(object[key])。但是,對于數(shù)值的鍵名,不能使用點結(jié)構(gòu)。因為單獨的數(shù)值不能作為標(biāo)識符(identifier),所以,數(shù)組成員只能用方括號arr[0]表示(方括號是運(yùn)算符,可以接受數(shù)值)。

var arr = [1,2,3];
console.log(arr.0) //syntaxError

數(shù)組的屬性length,返回數(shù)組成員的數(shù)量。只要是數(shù)組就一定有length屬性,該屬性是一個動態(tài)的值,等于鍵名中的最大整數(shù)+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屬性是可寫的。如果人為設(shè)置一個小于當(dāng)前成員個數(shù)的值,該數(shù)組的成員會自動減少到length設(shè)置的值。

var arr = [ 'a', 'b', 'c' ];
arr.length // 3
arr.length = 2;
arr // ["a", "b"]

上面代碼表示,當(dāng)數(shù)組的length屬性設(shè)為2(即最大的整數(shù)鍵只能是1)那么整數(shù)鍵2(值為c)就已經(jīng)不在數(shù)組中了,被自動刪除了。
清空數(shù)組的一個有效方法,就是將length屬性設(shè)為0。如果人為設(shè)置length大于當(dāng)前元素個數(shù),則數(shù)組的成員數(shù)量會增加到這個值,新增的位置都是空位。如果人為設(shè)置length為不合法的值,JavaScript 會報錯。

var arr = [ 'a', 'b', 'c' ];
arr.length = 0;
console.log(arr) // []

var arr = [ 'a' ];
arr.length = 2;
arr[1] //undefined

// 設(shè)置負(fù)值
[].length = -1
// RangeError: Invalid array length

// 數(shù)組元素個數(shù)大于等于2的32次方
[].length = Math.pow(2, 32)
// RangeError: Invalid array length

// 設(shè)置字符串
[].length = 'abc'
// RangeError: Invalid array length

值得注意的是,由于數(shù)組本質(zhì)上是一種對象,所以可以為數(shù)組添加屬性,但是這不影響length屬性的值。

var a = [];
a['p'] = 'abc';
a.length // 0
a[2.1] = 'abc';
a.length // 0

上面代碼將數(shù)組的鍵分別設(shè)為字符串和小數(shù),結(jié)果都不影響length屬性。因為,length屬性的值就是等于最大的數(shù)字鍵加1,而這個數(shù)組沒有整數(shù)鍵,所以length屬性保持為0。
如果數(shù)組的鍵名是添加超出范圍的數(shù)值,該鍵名會自動轉(zhuǎn)為字符串。

var arr = [];
arr[-1] = 'a';
arr[Math.pow(2, 32)] = 'b';
arr.length // 0
arr[-1] // "a"
arr[4294967296] // "b"

上面代碼中,我們?yōu)閿?shù)組arr添加了兩個不合法的數(shù)字鍵,結(jié)果length屬性沒有發(fā)生變化。這些數(shù)字鍵都變成了字符串鍵名。最后兩行之所以會取到值,是因為取鍵值時,數(shù)字鍵名會默認(rèn)轉(zhuǎn)為字符串。

  • in運(yùn)算符

檢查某個鍵名是否存在的運(yùn)算符in,適用于對象,也適用于數(shù)組。注意,如果數(shù)組的某個位置是空位,in運(yùn)算符返回false。

//例一
var arr = [ 'a', 'b', 'c' ];
2 in arr  // true
'2' in arr // true
4 in arr // false
//例二
var arr = [];
arr[100] = 'a';
100 in arr // true
1 in arr // false

上面代碼例一表明,數(shù)組存在鍵名為2的鍵。由于鍵名都是字符串,所以數(shù)值2會自動轉(zhuǎn)成字符串。上面代碼例二中,數(shù)組arr只有一個成員arr[100],其他位置的鍵名都會返回false

  • for...in 循環(huán)和數(shù)組的遍歷

for...in循環(huán)不僅可以遍歷對象,也可以遍歷數(shù)組,畢竟數(shù)組只是一種特殊對象。但是,for...in不僅會遍歷數(shù)組所有的數(shù)字鍵,還會遍歷非數(shù)字鍵。所以,不推薦使用for...in遍歷數(shù)組。

var a = [1, 2, 3];
for (var i in a) {
  console.log(a[i]);
}
// 1
// 2
// 3

var a = [1, 2, 3];
a.foo = true;
for (var key in a) {
  console.log(key);
}
// 0
// 1
// 2
// foo

數(shù)組的遍歷可以考慮使用for循環(huán)或while循環(huán)。下面代碼是三種遍歷數(shù)組的寫法。最后一種寫法是逆向遍歷,即從最后一個元素向第一個元素遍歷。

var a = [1, 2, 3];

// for循環(huán)
for(var i = 0; i < a.length; i++) {
  console.log(a[i]);
}

// while循環(huán)
var i = 0;
while (i < a.length) {
  console.log(a[i]);
  i++;
}

var l = a.length;
while (l--) {
  console.log(a[l]);
}

數(shù)組的forEach方法,也可以用來遍歷數(shù)組

var colors = ['red', 'green', 'blue'];
colors.forEach(function (color) {
  console.log(color);
});
// red
// green
// blue
  • 數(shù)組空位

當(dāng)數(shù)組的某個位置是空元素,即兩個逗號之間沒有任何值,我們稱該數(shù)組存在空位(hole)。數(shù)組的空位不影響length屬性。需要注意的是,如果最后一個元素后面有逗號,并不會產(chǎn)生空位。也就是說,有沒有這個逗號,結(jié)果都是一樣的。數(shù)組的空位是可以讀取的,返回undefined。使用delete命令刪除一個數(shù)組成員,會形成空位,并且不會影響length屬性。

var a = [1, , 1];
a.length // 3

var a = [1, 2, 3,];
a.length // 3
a // [1, 2, 3]

var a = [, , ,];
a[1] // undefined

var a = [1, 2, 3];
delete a[1];
a[1] // undefined
a.length // 3

上面代碼用delete命令刪除了數(shù)組的第二個元素,這個位置就形成了空位,但是對length屬性沒有影響。也就是說,length屬性不過濾空位。所以,使用length屬性進(jìn)行數(shù)組遍歷,一定要非常小心。

數(shù)組的某個位置是空位,與某個位置是undefined,是不一樣的。如果是空位,使用數(shù)組的forEach方法、for...in結(jié)構(gòu)、以及Object.keys方法進(jìn)行遍歷,空位都會被跳過。不過使用for循環(huán)和while循環(huán)遍歷空位會返回undefined不會跳過。如果某個位置是undefined,遍歷的時候就不會被跳過。

var a = [, , ,];

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

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

Object.keys(a)
// []

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']
  • 類似數(shù)組的對象

如果一個對象的所有鍵名都是正整數(shù)或零,并且有·length·屬性,那么這個對象就很像數(shù)組,語法上稱為“類似數(shù)組的對象”(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就是一個類似數(shù)組的對象。但是,“類似數(shù)組的對象”并不是數(shù)組,因為它們不具備數(shù)組特有的方法。對象obj沒有數(shù)組的push方法,使用該方法就會報錯。

“類似數(shù)組的對象”的根本特征,就是具有length屬性。只要有length屬性,就可以認(rèn)為這個對象類似于數(shù)組。但是有一個問題,這種length屬性不是動態(tài)值,不會隨著成員的變化而變化。

var obj = {
  length: 0
};
obj[3] = 'd';
obj.length // 0

上面代碼為對象obj添加了一個數(shù)字鍵,但是length屬性沒變。這就說明了obj不是數(shù)組。

典型的“類似數(shù)組的對象”是函數(shù)的arguments對象,以及大多數(shù) 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

上面代碼包含三個例子,它們都不是數(shù)組(instanceof運(yùn)算符返回false),但是看上去都非常像數(shù)組。數(shù)組的slice方法可以將“類似數(shù)組的對象”變成真正的數(shù)組。

var arr = Array.prototype.slice.call(arrayLike);

除了轉(zhuǎn)為真正的數(shù)組,“類似數(shù)組的對象”還有一個辦法可以使用數(shù)組的方法,就是通過call()把數(shù)組的方法放到對象上面。

function print(value, index) {
  console.log(index + ' : ' + value);
}
Array.prototype.forEach.call(arrayLike, print);

上面代碼中,arrayLike代表一個類似數(shù)組的對象,本來是不可以使用數(shù)組的forEach()方法的,但是通過call(),可以把forEach()嫁接到arrayLike上面調(diào)用。

下面的例子就是通過這種方法,在arguments對象上面調(diào)用forEach方法。

// forEach 方法
function logArgs() {
  Array.prototype.forEach.call(arguments, function (elem, i) {
    console.log(i + '. ' + elem);
  });
}

// 等同于 for 循環(huán)
function logArgs() {
  for (var i = 0; i < arguments.length; i++) {
    console.log(i + '. ' + arguments[i]);
  }
}

字符串也是類似數(shù)組的對象,所以也可以用Array.prototype.forEach.call遍歷。

Array.prototype.forEach.call('abc', function (chr) {
  console.log(chr);
});
// a
// b
// c

注意,這種方法比直接使用數(shù)組原生的forEach要慢,所以最好還是先將“類似數(shù)組的對象”轉(zhuǎn)為真正的數(shù)組,然后再直接調(diào)用數(shù)組的forEach方法。

var arr = Array.prototype.slice.call('abc');
arr.forEach(function (chr) {
  console.log(chr);
});
// a
// b
// c
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容