JS-对象无效属性与forEach——一个考题引起的思考

NEC前端课的JS考试出成绩了,赶紧去看了下主观题错了哪些,发现几个遗漏点:
1.forEach方法的内部实现
2.对象的未赋值属性是否有效

原题

// 以下代码执行完后,`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

当初没有在console里跑一遍,自认为对数组还算熟悉,答案出来后——“始惊次醉终狂”⊙▽⊙夸张了——不过确实刷新了对forEach和对象属性的认识。

真实的forEach

以前我模拟的forEach实现是酱紫的:

arr.myForEach = function (callback, thisArg) {
    for(var i = 0; i < this.length; i++){
        callback.call(thisArg, this[i], i, this);
    }
}

嗯,很简单易懂,但是没有一定的错误检测机制。

来看看MDN上的一段polyfill[1]

if (!Array.prototype.forEach) {
    Array.prototype.forEach = function(callback, thisArg) {
        var T, k;
        if (this == null) {//如果调用forEach方法的对象是null就抛错
            throw new TypeError(' this is null or not defined');
        }
        var O = Object(this);//获取调用forEach的对象
        var len = O.length >>> 0;//手动转为32位整数
        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) {//某个数组项是否在数组中
                kValue = O[k];
                callback.call(T, kValue, k, O);
            }
            k++;
        }
    };
}

以上代码省去了MDN上原有的注释,另外添加了几个注释以跟前面精简版模拟的forEach对比。多了几个关键点:调用对象的检查,数组长度转32位,callback检查,第二参检查,数组项有效性检查。
这里的重点是 数组项的有效性检查 ,我们从文章开始给出的原题的运行可以看出forEach对数组的未赋值项是忽略处理的。从MDN上的polyfill看出就是通过if (k in O)来判断是否忽略的,这个后面会讲到。
因此forEach的实现应该至少还要注意忽略无效项。

对象的无效属性

前面讲到了用if (k in O)来判断数组项的有效性,但是其原理是什么呢?
这里用到了in操作符来判断某个属性名k是否包含在一个对象O中,为何需要这么判断呢?
首先我们知道数组也是对象,数组中的每个项其实就是这个对象中的某个属性,只不过属性名是"0""1""2"...这样排列的。
然后回头来看,难道不是所有的数组项(无论是否已经赋值)都自动成为数组的(有效)属性/项么?
我们测试一段代码

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"]

从以上测试代码可以看出数组项未赋值或者说值为undefined的,实际上都没有被算到这个数组对象的属性中,只不过是在数组表现时,有一个“占坑”的迹象——undefined × 1
这个undefined × 1表示连续的值为undefined的数组项有1个,如果是连续n个就是× n,想想ES数组的自动更新特性,看看以下代码:

var arr =[1,2];
arr[10] = 5;
arr;//[1, 2, undefined × 8, 5]

回过头来,从这里就不难理解为何在forEach中可以通过if (k in O)判断数组项的有效性了:in操作符可以判断某个属性名是否包含在一个对象或者对象从原型继承来的属性名列表中,而属性值为undefined的属性名是不会包含在这个属性名列表中的,因此就可以判断某个数组项是否未被赋值。从对象的角度来看,数组中的未赋值项是不存在数组对象中的。

那么是否真是如此呢?反过来想,是不是一个对象所有值为undefined的属性就会从对象中“清除”呢?看看一段测试代码:

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的属性就会被从对象的属性名列表请清除出去,它们仍然是对象的属性。
那么为何数组的就不一样的呢?看看这个,仍然是对前面的数组的例子进行操作

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]

从这里也看到跟上面那段对象的例子相同的结果,被赋值为undefined的数组项,也仍然会是数组对象的属性。不过这里有一点很明显的区别,undefinedundefined × 1,主动添加或者赋值的是没有后面的× 1的。

综合来看

那么码了这么多字,还是没搞懂为何要通过if (k in O)来判断无效属性,而值为undefined的属性也并非就是无效的?
那么还有种情况,就是未声明的变量,值为undefined但是一般无法直接打印,只能通过typeof操作符来间接了解。也就是说[1,,2,,3]中的第1项和第3项(从第0项开始)都是为未声明的变量。如果要使得数组项有效(即也成为数组对象的属性),可以对其进行赋值操作(赋值undefined也可以)。

arr[3] = 123;//123
Object.getOwnPropertyNames(arr);//["0", "2", "3", "4", "length"]

因为对象中的属性,不能真正的使用var来进行变量声明,所以都是以所赋的值为判断标准的。所以你可以看到在对没有d属性一个对象obj使用obj.d来进行属性访问,结果会返回undefined,因为这个属性是未声明的。
因此MDN的polyfill那段代码要用if (k in O)来判断数组中的项是否被赋值(已声明)过,来排除那些“占坑”的数组项。

看看下面这两段的输出差异:

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);});

虽然两段代码访问arr[2]输出都是undefined,但是本质差别正如同上面的分析:前者是赋值为undefined(已声明),后者是未声明的。

bonus

要注意数组的中括号内的最后一个逗号后面如果没有值,这个最后项会被忽略,如下:

[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

  1. Array.prototype.forEach() ↩

你可能感兴趣的:(JS-对象无效属性与forEach——一个考题引起的思考)