Open kuitos opened 3 years ago
强!
这就是大佬吗?
好像《你不知道的javasript》第5章 原型里有详细讲解这个诡异的特性。
细!学到了!
总结:赋值虽然不会查找(和返回)原型链上的属性,但是会继承相应的 descriptor
太强了!但代码是不是应该这么写
-console.log(boundFn.hasOwnProperty(boundFn, 'prototype'));
+console.log(boundFn.hasOwnProperty('prototype'));
太强了!但代码是不是应该这么写
-console.log(boundFn.hasOwnProperty(boundFn, 'prototype')); +console.log(boundFn.hasOwnProperty('prototype'));
typo,感谢提醒,已修改
牛
牛逼克拉斯
问题
这两天在排查一个 qiankun 的 bug 时,发现了一个我无法解释的 js 问题,这可要了我的命。
略去一切细枝末节,我们直接先来看问题。 假如有这么一段代码:
假设我们已知,函数通过 bind 调用后,返回的新的 boundFn 是一定不会有 prototype 的。
那么打印结果就应该是:
因为 boundFn 不具备自有属性 'prototype',所以在经过
boundFn.prototype = OfflineAudioContext.prototype
的赋值操作后,会为其创建一个新的自有属性 'prototype',其值为OfflineAudioContext.prototype
。一切都在情理之中。但你真的把这段代码粘到 chrome 控制台跑一下就会发现,报错了😑 从报错信息很容易判断,我们在尝试给一个 readonly 的属性做赋值,但关键是,prototype 这个属性在 boundFn 上压根不存在呀! 我们知道,对象的属性赋值操作的基本逻辑是这样的:
毫无疑问上面代码走的应该是第一个逻辑分支,完全不应该报错才对。
起初我还以为是浏览器兼容问题,然后尝试过几个浏览器之后,发现都是报错😑
排查的过程中发现,OfflineAudioContext.prototype 本身是 readonly 的 但是这跟我们 boundFn.prototype 赋值有什么关系呢,即便我们把赋值操作改成:
报错还是会照旧。 继续查,发现 boundFn 的原型链上是有 prototype 的:
而且原型链上的这个 prototype 也是 readonly 的: 但是我们一个写操作跟原型链有啥关系呢,不是读操作时才会按原型链查找吗???
ES Spec 追踪
各种尝试之后无果,这时候只能祭出 ecmascript spec,看看能不能从里面找到蛛丝马迹了😑
搜索找到赋值操作(assignment)相关的 spec 说明: 如果有过读 ecmascript spec 经验的话,会找到关键步骤在第 5 步 PutValue: 我们这个场景里,PutValue 的操作会沿着 4.a.false 的路径执行。即 put 对应的调用为
base.[[Put]](reference name, W, true)
。 找到 [[Put]] 的调用算法说明: 这里其实就能看到,如果我们走到了最后一步第6步的时候,实际上发生的事情就会是:Object.defineProperty(O, P, { writable: true, enumerable: true, configurable: true, value: V })
, 也就是我们会为对象创建一个新的属性并赋值,且这个属性是可枚举可修改的,符合我们之前的认知。那其实我们就要看看,为什么流程没有走到第6步。 先看第一步里的 [[CanPut]] 做了啥: 简单翻译下流程就是:
其实到这里我们就能发现端倪了,关键点是这几步: 这几步描述的实际就是,计算流程会一直去原型链上查找属性 P。
也就是说,即便我们是赋值操作,只要是对象属性的赋值,都会触发原型链的查找。
那么回到上面那段代码,对应的计算流程就是:
解法
那么如果我们确实想给 boundFn 加一个自身属性 prototype 该怎么做呢? 其实我们只要找到不会触发原型链查找的修改方式就可以了:
原理就是 defineProperty API 不会有 [[getProperty]] 这种触发原型链查找的调用:
结论
赋值(assignment)操作也会存在原型链查找逻辑,且是否可写也会遵循查找到的属性的 descriptor 规则。