结论
FYI: 以下说的 api-monitor 是一个库,引入这个库,可自动上报服务端接口的耗时、接口错误信息、错误码、HTTP 状态码等信息,用于监控接口质量。
在开始前,先看一段代码,问能不能进入 for 循环?
var origin = {};
Object.defineProperty(origin, 'username', {
get() {
return 'mbj';
},
set() {},
configurable: true,
enumerable: true,
});
var obj = Object.create(origin);
Object.defineProperty(obj, 'username', {
get: function () {
return 'wmy';
},
set: function () {},
});
console.log('obj', obj);
for (var attr in obj) {
console.log('attr', attr);
console.log(
`obj.${attr}属性描述符`,
Object.getOwnPropertyDescriptor(obj, attr)
);
console.log(
`obj.__proto__.${attr}属性描述符`,
Object.getOwnPropertyDescriptor(obj.__proto__, attr)
);
}
结果
| 移动端 ios14.5beta 版本 | 移动端 ios 非 14.5 版本 | Android | PC Chrome | |
|---|---|---|---|---|
| 是否能进入 for 循环 | 不能 | 能 | 能 | 不能 |
从上面结果可以看出,核心的问题在于:如果一个对象的上有一个属性,不可枚举,并且 prototype 上也有个属性,可枚举,那这属性用 for in 可枚举吗?
结果是:
| 移动端 ios14.5beta 版本和 PC Chrome | 移动端 ios 非 14.5 版本 和 Android | |
|---|---|---|
| for in 是否可枚举 | 否 | 是 |
回到对 XMLHttprequest 的影响,首先在 api-monitor 代理 XMLHttprequest 之前,就有库对 XMLHttprequest 进行过代理了,并且有
window. __real=window.XMLHttpRequest
window.XMLHttpRequest= function {
this._xhr = new window.__real();
for (var attr in this._xhr) {
var type = '';
try {
type = typeof this._xhr[attr];
} catch (e) {}
if (type === 'function') {
this[attr] = hookfun(attr);
} else {
Object.defineProperty(this, attr, {
get: genGetter(attr),
set: genSetter(attr),
});
}
}
}
window.XMLHttpRequest.prototype = window.__real.prototype
注意这里用 Object.defineProperty 定义了属性并且 enumerable 和 configurable 为 false,到 api-monitor 再代理 XMLHttpRequest 对象时,其实是别人已经代理过的。
此时
var xml=new window.XMLHttpRequest()
就符合上述那段代码了,比如 xml 上的一个 onreadystatechange 属性,其实
| 描述符 | |
|---|---|
| xml.onreadystatechange | enumerable 和 configurable 为 false |
| xml.__proto__.onreadystatechange | enumerable 和 configurable 为 true |
于是 api-monitor 在通过 for in 遍历这个 xml 实现代理功能的时候,在 ios 14.5 上就遍历不到 onreadystatechange,responseText 这些属性,导致获取不到数据响应。
总结
如果一个对象上有一个属性,不可枚举,并且对象的 prototype 上也有个属性,可枚举,则
- 移动端 ios14.5beta 版本和 PC Chrome,不可枚举
- 移动端 ios 非 14.5 版本 和 Android,可枚举
从这次事件看出,xml 的不确定性和历史遗留问题比较多,通过遍历 xml 实例代理真正的 xml 不可靠。
下面为定位这个问题的具体过程分析,有兴趣的同学可以看看
api-monitor 拦截 XMLHttprequest 方式
api-monitor 用自己实现的 XMLHttprequest 覆盖了 window.XMLHttprequest,当使用者在使用 xml 的时候,其实是在使用 api-monitor 提供的 xml,xml 实例代理了真正的 xml 实例,如调用 xml.open,会调用真正 xml 实例的 open 方法,如果给 xml.onreadystatechange 赋值,会给真正的 xml 实例赋值。
实现代码主要是基于ajax-hook
非 IOS 14.5 beta 版本
可以从 XMLHttprequest 的实例上通过 for in 遍历出所有的属性和方法,这就没问题,api-monitor 提供的 xml 能代理到真行 xml 实例的所有方法和属性。
代码如下:
// 遍历真正的xml实例
var xhr = new window.XMLHttpRequest();
console.log('enter????', Object.keys(xhr));
for (var attr in xhr) {
console.log('attr ', attr);
var type = '';
if (attr === 'xhr') continue;
try {
type = typeof xhr[attr]; // May cause exception on some browser
} catch (e) {}
if (type === 'function') {
// 代理真正xml的方法
this[attr] = hookFunction(attr);
} else {
// 代理真正xml的属性
Object.defineProperty(this, attr, {
get: getterFactory(attr),
set: setterFactory(attr),
enumerable: true,
});
}
}
下图所示日志是在用 for in 遍历真正的 xml 实例的输出:
ios 14.5 beta
在通过 for in 遍历 xhr 实例的时候,ios 14.5 不能遍历出 onreadystatechange,ontimeout,timeout 等属性,因此,在给 xml 的 onreadystatechange 赋值时,就不能给真正的 xml 实例挂载 onreadystatechange 方法,导致没法响应数据。
代码同上,下图所示日志是在用 for in 遍历真正的 xml 实例的输出:
遍历对比
下面通过三种方式来遍历真正的 xml 对象,对比在 ios14.5beta 和非 14.5 的结果区别
| 遍历方式 | 移动端 ios14.5beta 版本 | 移动端 ios 非 14.5 版本 | PC Chrome |
|---|---|---|---|
| Object.keys | 所有的方法 | 所有的方法 | 空 |
| for in | 所有的方法 | 所有的方法和属性 | 所有的方法和属性 |
| Object.getOwnPropertyNames() | 所有的方法和属性 | 所有的方法和属性 | 空 |
由上述结果,我们可以猜测在移动端属性和方法是直接挂载在 xml 实例上的,只不过属性是不可遍历的,我通过如下代码来获取下属性的描述符
console.log(
'descriptor ',
Object.getOwnPropertyDescriptor(xhr, 'responseType')
);
console.log(
'descriptor ',
Object.getOwnPropertyDescriptor(xhr, 'send')
);
在移动端 ios 得到的结果都是
// responseType
{
configurable:false,
enumerable:false,
get(){},
set(){},
}
// send
{
configurable: true,
enumerable: true,
value:function(){}
writable: true
}
这个结果验证了我们的猜想,那就矛盾了,为什么移动端 ios 非 14.5 版本能遍历出不可枚举的属性,猜测是苹果爸爸在 14.5 beta 修复了这个 bug,我们继续往下看。
柳暗花明又一村
打印了在 api-monitor 里面获取的 xmlhttprequest,发现这个 xmlhttprequest 也是被其他业务覆盖的,也就是上面我说的真正的 xml 实例其实也是别人代理的一个 xml 实例,并不是原生的 xml 实例。从下述打印出来的实现方式来看,方法挂载在实例上,属性通过如下方式定义在实例上
Object.defineProperty(this, attr, {
get: genGetter(attr),
set: genSetter(attr),
});
configurable、enumerable 的默认值都是 false,因此理论上,这个实例应该是不能通过 for in 遍历出来的,但是移动端 ios 非 14.5 版本和 Android 能遍历出来,PC Chrome 和 ios 14.5 版本不能。 又发现
window.__real.prototype === window.XMLHttpRequest.prototype // true
因此猜测跟原型上的属性有关,于是打印出所有 window.__real.prototype 属性的属性描述符,发现 configurable,enumerable 都为 true,结合上面 xml 实例的属性描述符 configurable,enumerable 都为 false,得出最开始的结论。
我的打印代码和别人实现的 XMLHttpRequest:
if (typeof window !== 'undefined') {
console.log('fake', window.XMLHttpRequest);
const foo = new window.XMLHttpRequest();
console.log('open', foo.open);
console.log('send', foo.send);
console.log('real ', window.__real);
console.log(
'isEqual',
window.__real,
window.__real.prototype === window.XMLHttpRequest.prototype
); // 这里打印出来是true
}
// 打印出来的window.XMLHttpRequest
window.__real=window.XMLHttpRequest
window.XMLHttpRequest= function {
this._xhr = new window.__real();
for (var attr in this._xhr) {
var type = '';
try {
type = typeof this._xhr[attr];
} catch (e) {}
if (type === 'function') {
this[attr] = hookfun(attr);
} else {
Object.defineProperty(this, attr, {
get: genGetter(attr),
set: genSetter(attr),
});
}
}
}
window.XMLHttpRequest.prototype = window.__real.prototype
❤️ 谢谢支持
以上便是本次分享的全部内容,希望对你有所帮助^_^
喜欢的话别忘了 分享、点赞、收藏 三连哦~。
欢迎关注公众号 ELab团队 收货大厂一手好文章~