NEC前端課的JS考試出成績(jī)了,趕緊去看了下主觀題錯(cuò)了哪些,發(fā)現(xiàn)幾個(gè)遺漏點(diǎn):
1.forEach
方法的內(nèi)部實(shí)現(xiàn)
2.對(duì)象的未賦值屬性是否有效
原題
// 以下代碼執(zhí)行完后,`obj`和`count`的值分別是
var obj = {}, count = 0;
function logArray(value, index, array) {
count++;
obj[count] = value;
}
[1, 2, , 4].forEach(logArray);
得分/總分 | |
---|---|
A.{1: 1, 2: 2, 3: 4} ,3
|
○ |
B.{} ,0
|
|
C.{1: 1, 2: 2, 3: , 4:4} ,3
|
|
D.{1: 1, 2: 2, 3: , 4:4} ,4
|
×0.00/2.00 |
當(dāng)初沒(méi)有在console里跑一遍,自認(rèn)為對(duì)數(shù)組還算熟悉,答案出來(lái)后——“始驚次醉終狂”⊙▽⊙夸張了——不過(guò)確實(shí)刷新了對(duì)forEach
和對(duì)象屬性的認(rèn)識(shí)。
真實(shí)的forEach
以前我模擬的forEach
實(shí)現(xiàn)是醬紫的:
arr.myForEach = function (callback, thisArg) {
for(var i = 0; i < this.length; i++){
callback.call(thisArg, this[i], i, this);
}
}
嗯,很簡(jiǎn)單易懂,但是沒(méi)有一定的錯(cuò)誤檢測(cè)機(jī)制。
來(lái)看看MDN上的一段polyfill[1]
if (!Array.prototype.forEach) {
Array.prototype.forEach = function(callback, thisArg) {
var T, k;
if (this == null) {//如果調(diào)用forEach方法的對(duì)象是null就拋錯(cuò)
throw new TypeError(' this is null or not defined');
}
var O = Object(this);//獲取調(diào)用forEach的對(duì)象
var len = O.length >>> 0;//手動(dòng)轉(zhuǎn)為32位整數(shù)
if(typeof callback !== "function") {//callback檢查
throw new TypeError(callback + ' is not a function');
}
if (arguments.length > 1) {//第二參檢查
T = thisArg;
}
k = 0;
while (k < len) {
var kValue;
if (k in O) {//某個(gè)數(shù)組項(xiàng)是否在數(shù)組中
kValue = O[k];
callback.call(T, kValue, k, O);
}
k++;
}
};
}
以上代碼省去了MDN上原有的注釋,另外添加了幾個(gè)注釋以跟前面精簡(jiǎn)版模擬的forEach
對(duì)比。多了幾個(gè)關(guān)鍵點(diǎn):調(diào)用對(duì)象的檢查,數(shù)組長(zhǎng)度轉(zhuǎn)32位,callback檢查,第二參檢查,數(shù)組項(xiàng)有效性檢查。
這里的重點(diǎn)是 數(shù)組項(xiàng)的有效性檢查 ,我們從文章開(kāi)始給出的原題的運(yùn)行可以看出forEach
對(duì)數(shù)組的未賦值項(xiàng)是忽略處理的。從MDN上的polyfill看出就是通過(guò)if (k in O)
來(lái)判斷是否忽略的,這個(gè)后面會(huì)講到。
因此forEach
的實(shí)現(xiàn)應(yīng)該至少還要注意忽略無(wú)效項(xiàng)。
對(duì)象的無(wú)效屬性
前面講到了用if (k in O)
來(lái)判斷數(shù)組項(xiàng)的有效性,但是其原理是什么呢?
這里用到了in
操作符來(lái)判斷某個(gè)屬性名k
是否包含在一個(gè)對(duì)象O
中,為何需要這么判斷呢?
首先我們知道數(shù)組也是對(duì)象,數(shù)組中的每個(gè)項(xiàng)其實(shí)就是這個(gè)對(duì)象中的某個(gè)屬性,只不過(guò)屬性名是"0"
、"1"
、"2"
...這樣排列的。
然后回頭來(lái)看,難道不是所有的數(shù)組項(xiàng)(無(wú)論是否已經(jīng)賦值)都自動(dòng)成為數(shù)組的(有效)屬性/項(xiàng)么?
我們測(cè)試一段代碼
var arr = [1,,2,,3];
arr;//[1, undefined × 1, 2, undefined × 1, 3]
arr[1];//undefined
'0' in arr;//true
'1' in arr;//false
'3' in arr;//false
Object.getOwnPropertyNames(arr);//["0", "2", "4", "length"]
從以上測(cè)試代碼可以看出數(shù)組項(xiàng)未賦值或者說(shuō)值為undefined
的,實(shí)際上都沒(méi)有被算到這個(gè)數(shù)組對(duì)象的屬性中,只不過(guò)是在數(shù)組表現(xiàn)時(shí),有一個(gè)“占坑”的跡象——undefined × 1
。
這個(gè)undefined × 1
表示連續(xù)的值為undefined
的數(shù)組項(xiàng)有1個(gè),如果是連續(xù)n個(gè)就是× n
,想想ES數(shù)組的自動(dòng)更新特性,看看以下代碼:
var arr =[1,2];
arr[10] = 5;
arr;//[1, 2, undefined × 8, 5]
回過(guò)頭來(lái),從這里就不難理解為何在forEach
中可以通過(guò)if (k in O)
判斷數(shù)組項(xiàng)的有效性了:in
操作符可以判斷某個(gè)屬性名是否包含在一個(gè)對(duì)象或者對(duì)象從原型繼承來(lái)的屬性名列表中,而屬性值為undefined
的屬性名是不會(huì)包含在這個(gè)屬性名列表中的,因此就可以判斷某個(gè)數(shù)組項(xiàng)是否未被賦值。從對(duì)象的角度來(lái)看,數(shù)組中的未賦值項(xiàng)是不存在數(shù)組對(duì)象中的。
那么是否真是如此呢?反過(guò)來(lái)想,是不是一個(gè)對(duì)象所有值為undefined
的屬性就會(huì)從對(duì)象中“清除”呢?看看一段測(cè)試代碼:
var obj = {a:'tom',b:'jerry'};
obj.a = undefined;
'a' in obj;//true
obj.c = undefined
'c' in obj;//true
Object.getOwnPropertyNames(obj);//["a", "b", "c"]
從上面可以看出,并不是值為undefined
的屬性就會(huì)被從對(duì)象的屬性名列表請(qǐng)清除出去,它們?nèi)匀皇菍?duì)象的屬性。
那么為何數(shù)組的就不一樣的呢?看看這個(gè),仍然是對(duì)前面的數(shù)組的例子進(jìn)行操作
arr[0] = undefined;
'0' in arr;//true
arr.push(undefined);//6
arr[5];//undefined
'5' in arr;//true
arr;//[undefined, undefined × 1, 2, undefined × 1, 3, undefined]
從這里也看到跟上面那段對(duì)象的例子相同的結(jié)果,被賦值為undefined
的數(shù)組項(xiàng),也仍然會(huì)是數(shù)組對(duì)象的屬性。不過(guò)這里有一點(diǎn)很明顯的區(qū)別,undefined
和undefined × 1
,主動(dòng)添加或者賦值的是沒(méi)有后面的× 1
的。
綜合來(lái)看
那么碼了這么多字,還是沒(méi)搞懂為何要通過(guò)if (k in O)
來(lái)判斷無(wú)效屬性,而值為undefined
的屬性也并非就是無(wú)效的?
那么還有種情況,就是未聲明的變量,值為undefined
但是一般無(wú)法直接打印,只能通過(guò)typeof
操作符來(lái)間接了解。也就是說(shuō)[1,,2,,3]
中的第1項(xiàng)和第3項(xiàng)(從第0項(xiàng)開(kāi)始)都是為未聲明的變量。如果要使得數(shù)組項(xiàng)有效(即也成為數(shù)組對(duì)象的屬性),可以對(duì)其進(jìn)行賦值操作(賦值undefined
也可以)。
arr[3] = 123;//123
Object.getOwnPropertyNames(arr);//["0", "2", "3", "4", "length"]
因?yàn)閷?duì)象中的屬性,不能真正的使用var
來(lái)進(jìn)行變量聲明,所以都是以所賦的值為判斷標(biāo)準(zhǔn)的。所以你可以看到在對(duì)沒(méi)有d
屬性一個(gè)對(duì)象obj
使用obj.d
來(lái)進(jìn)行屬性訪問(wèn),結(jié)果會(huì)返回undefined
,因?yàn)檫@個(gè)屬性是未聲明的。
因此MDN的polyfill那段代碼要用if (k in O)
來(lái)判斷數(shù)組中的項(xiàng)是否被賦值(已聲明)過(guò),來(lái)排除那些“占坑”的數(shù)組項(xiàng)。
看看下面這兩段的輸出差異:
var arr = [1,2,undefined,4];
arr.forEach(function(item,index,array){console.log(index,item);});
var arr = [1,2,,4];
arr.forEach(function(item,index,array){console.log(index,item);});
雖然兩段代碼訪問(wèn)arr[2]
輸出都是undefined
,但是本質(zhì)差別正如同上面的分析:前者是賦值為undefined
(已聲明),后者是未聲明的。
bonus
要注意數(shù)組的中括號(hào)內(nèi)的最后一個(gè)逗號(hào)后面如果沒(méi)有值,這個(gè)最后項(xiàng)會(huì)被忽略,如下:
[1,2,];//[1, 2]
[1,2,].length;//2
[1,2,,];//[1, 2, undefined × 1]
[1,2,,].length;//3
[1,2,,,];//[1, 2, undefined × 2]
[1,2,,,].length;//4