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()

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,205评论 0 4
  • 第三章 类型、值和变量 1、存取字符串、数字或布尔值的属性时创建的临时对象称做包装对象,它只是偶尔用来区分字符串值...
    坤少卡卡阅读 626评论 0 1
  • 一、必做作业:修改第一次练习的A1 片段 一: 选自《坚持,一种可以养成的习惯》 【R:阅读原文】 尽量找出不被侵...
    阳光语录阅读 244评论 0 0
  • 如何确认一家公司是否合规? 我想这是我们在考察一个全新的项目时,最重要的一件事情。 没有人希望奋斗打拼了一段时间后...
    弄月人阅读 701评论 0 0
  • 2017年7月12日 18:00-20:00 膝旋转与弯曲练习 直角式坐姿,两腿向前伸直。 弯曲右膝,脚掌平贴垫...
    悠悠12321阅读 383评论 0 0