最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • ES 拾遗之赋值操作与原型链查找

    正文概述 掘金(Kuitos45071)   2021-04-29   616

    问题

    这两天在排查一个 qiankun 的 bug 时,发现了一个我无法解释的 js 问题,这可要了我的命。

    略去一切细枝末节,我们直接先来看问题。
    假如有这么一段代码:

    (() => {
      'use strict';
      
      const boundFn = Function.prototype.bind.call(OfflineAudioContext, window);
      console.log(boundFn.hasOwnProperty(boundFn, 'prototype'));
      boundFn.prototype = OfflineAudioContext.prototype;
      console.log(boundFn.hasOwnProperty(boundFn, 'prototype'));
    })();
    

    假设我们已知,函数通过 bind 调用后,返回的新的 boundFn 是一定不会有 prototype 的。

    那么打印结果就应该是:

    false
    true
    

    因为 boundFn 不具备自有属性 'prototype',所以在经过 boundFn.prototype = OfflineAudioContext.prototype 的赋值操作后,会为其创建一个新的自有属性 'prototype',其值为 OfflineAudioContext.prototype。一切都在情理之中。

    但你真的把这段代码粘到 chrome 控制台跑一下就会发现,报错了?
    ES 拾遗之赋值操作与原型链查找
    从报错信息很容易判断,我们在尝试给一个 readonly 的属性做赋值,但关键是,prototype 这个属性在 boundFn 上压根不存在呀!
    我们知道,对象的属性赋值操作的基本逻辑是这样的:

    1. 如果对象上该属性不存在,则创建一个自有属性并赋值
    2. 如果对象上该属性已存在,则修改该属性的值,修改过程会触发该属性上的 data descriptor(writable 配置)检测或 accessor descriptor (setter 配置) 的调用。

    毫无疑问上面代码走的应该是第一个逻辑分支,完全不应该报错才对。

    起初我还以为是浏览器兼容问题,然后尝试过几个浏览器之后,发现都是报错?

    排查的过程中发现,OfflineAudioContext.prototype 本身是 readonly 的
    ES 拾遗之赋值操作与原型链查找
    但是这跟我们 boundFn.prototype 赋值有什么关系呢,即便我们把赋值操作改成:

    boundFn.prototype = 123;
    

    报错还是会照旧。
    继续查,发现 boundFn 的原型链上是有 prototype 的:
    ES 拾遗之赋值操作与原型链查找
    而且原型链上的这个 prototype 也是 readonly 的:ES 拾遗之赋值操作与原型链查找
    但是我们一个写操作跟原型链有啥关系呢,不是读操作时才会按原型链查找吗???

    ES Spec 追踪

    各种尝试之后无果,这时候只能祭出 ecmascript spec,看看能不能从里面找到蛛丝马迹了?

    搜索找到赋值操作(assignment)相关的 spec 说明:
    ES 拾遗之赋值操作与原型链查找
    如果有过读 ecmascript spec 经验的话,会找到关键步骤在第 5 步 PutValue:
    ES 拾遗之赋值操作与原型链查找
    我们这个场景里,PutValue 的操作会沿着 4.a.false 的路径执行。即 put 对应的调用为 base.[[Put]](reference name, W, true)
    找到 [[Put]] 的调用算法说明:
    ES 拾遗之赋值操作与原型链查找
    这里其实就能看到,如果我们走到了最后一步第6步的时候,实际上发生的事情就会是:
    Object.defineProperty(O, P, { writable: true, enumerable: true, configurable: true, value: V }), 也就是我们会为对象创建一个新的属性并赋值,且这个属性是可枚举可修改的,符合我们之前的认知。

    那其实我们就要看看,为什么流程没有走到第6步。
    先看第一步里的 [[CanPut]] 做了啥:
    ES 拾遗之赋值操作与原型链查找
    简单翻译下流程就是:

    1. 查找自身属性的 descriptor
    2. 如果有则按照 descriptor 的规则判断
    3. 如果没有则看对象是否有原型
    4. 如果原型是 null 则直接根据对象是否可拓展返回结果
    5. 否则去原型链上查找属性
    6. 如果原型链上找不到,则直接根据对象是否可拓展返回结果
    7. 如果原型链上能找到,则记录查找后的值对应的 descriptor
    8. 如果记录的值是 accessor descriptor,那么就根据 setter 配置决定返回值
    9. 如果记录的值是 data descriptor,那么就根据是否和拓展或者是否 writable 来给出返回值


    其实到这里我们就能发现端倪了,关键点是这几步:
    ES 拾遗之赋值操作与原型链查找
    这几步描述的实际就是,计算流程会一直去原型链上查找属性 P。

    也就是说,即便我们是赋值操作,只要是对象属性的赋值,都会触发原型链的查找。

    那么回到上面那段代码,对应的计算流程就是:

    1. 先触发了 boundFn 自身属性里查找 prototype 的操作
    2. 发现不存在 prototype,则去原型链上找
    3. 由于 boundFn 的原型指向了 BaseAudioContext,所以返回的实际是 BaseAudioContext.prototype
    4. 而 BaseAudioContext.prototype 的 writable 配置为 false
    5. 故 [[CanPut]] 操作返回了 false
    6. 返回 false 后就直接 throw 了一个 TypeError

    解法

    那么如果我们确实想给 boundFn 加一个自身属性 prototype 该怎么做呢?
    其实我们只要找到不会触发原型链查找的修改方式就可以了:

    - boundFn.prototype = OfflineAudioContext.prototype;
    + Object.defineProperty(boundFn, 'prototype', { value: OfflineAudioContext.prototype, enumerable: false, writable: true })
    

    原理就是 defineProperty API 不会有 [[getProperty]] 这种触发原型链查找的调用:
    ES 拾遗之赋值操作与原型链查找

    结论

    赋值(assignment)操作也会存在原型链查找逻辑,且是否可写也会遵循查找到的属性的 descriptor 规则。


    起源地下载网 » ES 拾遗之赋值操作与原型链查找

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元