Open shhider opened 4 years ago
这两天碰到个问题:从日志中发现一些来自 iOS 10.3 的报错Cannot read property 'xxx' of undefined,定位到代码的报错位置,发现是遍历某数组时产生的报错,该数组的元素应该全都是 Object,但实际上出现了异常的元素。排查该数组的来源,的确其元素并不保证都是 Object,问题很容易就解决了。
Cannot read property 'xxx' of undefined
但是,为什么这个报错只发生在 iOS 10.3 呢?
继续排查,发现遍历的数组,是通过 new Array(len) 实例化,之后再按索引对元素赋值,也的确会有某些元素未赋值的情况。按以往理解,未赋值的元素不就是undefined?如果元素是undefined,那取属性的操作在所有浏览器不都应该报错?
new Array(len)
undefined
我查阅了 ECMA 262 的文档:在new Array(len)这种形式的实例化逻辑中,创建 Array 对象后,执行了 set(array, 'length', len, true)就返回了这个 Array 对象。根据 set 的说明,该操作就是设置了 Array 对象的 length 属性(你得想起来「Array 就是 Object 的一种」)。
set(array, 'length', len, true)
length
7.3.3 Set ( O, P, V, Throw ) Assert: Type(O) is Object. Assert: IsPropertyKey(P) is true. Assert: Type(Throw) is Boolean. Let success be ? O.[[Set]](P, V, O). If success is false and Throw is true, throw a TypeError exception. Return success.
7.3.3 Set ( O, P, V, Throw )
看看其它形式的实例化,会通过 CreateDataProperty(array, P, item) 来设置数组元素。
CreateDataProperty(array, P, item)
7.3.4 CreateDataProperty ( O, P, V ) Assert: Type(O) is Object. Assert: IsPropertyKey(P) is true. Let newDesc be the PropertyDescriptor { [[Value]]: V, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true }. Return ? O.[[DefineOwnProperty]](P, newDesc).
7.3.4 CreateDataProperty ( O, P, V )
Assert: Type(O) is Object.
Type(O)
Assert: IsPropertyKey(P) is true.
IsPropertyKey(P)
Let newDesc be the PropertyDescriptor { [[Value]]: V, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true }.
Return ? O.[[DefineOwnProperty]](P, newDesc).
O.[[DefineOwnProperty]](P, newDesc).
根据以上,大概有个感觉就是,执行 new Array(len) 得到的数组,是只有 length 属性,真的没有元素。从 Object 角度看,也就是没有设置相应的属性(索引为 key, 元素为 value)。
如果元素是undefined,那么这个元素是「存在的」,它的值是undefined,而且是Enumerable可遍历到的。
Enumerable
继续阅读标准,可以看到 Array 的 map 方法有这么一段描述:
22.1.3.18 Array.prototype.map ( callbackfn [ , thisArg ] ) callbackfn is called only for elements of the array which actually exist; it is not called for missing elements of the array.
22.1.3.18 Array.prototype.map ( callbackfn [ , thisArg ] )
callbackfn is called only for elements of the array which actually exist; it is not called for missing elements of the array.
callbackfn
同样的说明还出现在every, filter, forEach, some 等方法中,也就是说, map, every, forEach 等遍历方法,是不会对不存在的元素执行回调函数。
every
filter
forEach
some
map
所以,数组的元素是可能「不存在」的(empty),即使是索引 index 在 length 范围内。
empty
// 在 Chrome 中试验: const arr = new Array(3) console.log(arr) // output: [empty × 3] arr.map((item) => item.prop) // output: [empty × 3] arr.forEach((item) => console.log(item.prop)) // nothing arr.reduce((res) => res += 'ha', '') // output: ''
那么,开头说的问题,应该是 iOS 10.3 的 webview 没有按照标准实现,在遍历数组遇到 empty item 时依然执行了 callbackFn(猜测,没有找到相关官方说明)。
这两天碰到个问题:从日志中发现一些来自 iOS 10.3 的报错
Cannot read property 'xxx' of undefined
,定位到代码的报错位置,发现是遍历某数组时产生的报错,该数组的元素应该全都是 Object,但实际上出现了异常的元素。排查该数组的来源,的确其元素并不保证都是 Object,问题很容易就解决了。但是,为什么这个报错只发生在 iOS 10.3 呢?
继续排查,发现遍历的数组,是通过
new Array(len)
实例化,之后再按索引对元素赋值,也的确会有某些元素未赋值的情况。按以往理解,未赋值的元素不就是undefined
?如果元素是undefined
,那取属性的操作在所有浏览器不都应该报错?我查阅了 ECMA 262 的文档:在
new Array(len)
这种形式的实例化逻辑中,创建 Array 对象后,执行了set(array, 'length', len, true)
就返回了这个 Array 对象。根据 set 的说明,该操作就是设置了 Array 对象的length
属性(你得想起来「Array 就是 Object 的一种」)。看看其它形式的实例化,会通过
CreateDataProperty(array, P, item)
来设置数组元素。根据以上,大概有个感觉就是,执行
new Array(len)
得到的数组,是只有length
属性,真的没有元素。从 Object 角度看,也就是没有设置相应的属性(索引为 key, 元素为 value)。如果元素是
undefined
,那么这个元素是「存在的」,它的值是undefined
,而且是Enumerable
可遍历到的。继续阅读标准,可以看到 Array 的 map 方法有这么一段描述:
同样的说明还出现在
every
,filter
,forEach
,some
等方法中,也就是说,map
,every
,forEach
等遍历方法,是不会对不存在的元素执行回调函数。所以,数组的元素是可能「不存在」的(
empty
),即使是索引 index 在 length 范围内。那么,开头说的问题,应该是 iOS 10.3 的 webview 没有按照标准实现,在遍历数组遇到 empty item 时依然执行了 callbackFn(猜测,没有找到相关官方说明)。
Reference