从 ECMA 规范解析 JavaScript 默认的取值和赋值行为

前言

若是你是一个经验丰富的 Vue 开发者,那么你必定知道 Vue 的响应式原理是经过拦截对象的 get 和 set 实现的javascript

// src/core/observer/index.js
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
        //...
    },
    set: function reactiveSetter (newVal) {
        //...
    }
  })
复制代码

因此当给响应式变量赋值的时候就会触发其中的 set 函数,从而更新视图java

<template>
    <div >{{message}}</div>
</template>

<script>
    export default {
        data() {
            return {
                message:'hello world'
            }
        },
       mounted() {
            this.message = 'hello Vue'
       }
    }
</script>

复制代码

本文和 Vue 框架其实并无什么关系,可是咱们来思考一个问题react

为何给响应式变量赋值会触发 set 函数,而不是直接赋值?bash

你给对象的属性定义了 set 函数就不会执行默认的赋值逻辑了啊,这不是弟弟问题么框架

事实上 JavaScript 在访问对象属性或者给对象属性赋值的时候会分别执行 [[Get]] 和 [[Put]] 操做,它们是对象内置的 2 个默认行为,没法修改函数

接下来咱们经过 ECMA 规范来分析 JavaScript 在对象取值和赋值的时候内部究竟作了什么ui

[[Get]]

当从对象中获取某个执行值时,会执行 [[Get]] 操做,它在标准中是这么定义的this

凭本人的渣渣英语水平大体翻译的结果是这样的spa

  1. 首先先会执行 [[GetProperty]] 操做,它的做用是判断对象属性是否存在于当前对象,若是存在,则直接返回这个属性,不然会递归向对象的原型链上找,找到后返回该属性,直到原型链尽头则返回 undefined
  2. 拿到第一步的结果后若是是 undefined,则 [[Get]] 的结果就是 undefined,即这个对象中没有这个属性
  3. 若是不是 undefined,会判断这个属性是否被定义了数据描述符,若是是,则返回数据描述符的 value 属性
  4. 若是这个属性被定义了访问器描述符,即 get 函数,则会触发 get 函数,并返回执行后的结果

经过标准就能很明显的看出 JavaScript 在访问对象属性时执行的逻辑,当这个属性不存在于当前对象会沿着原型链查找,这就是为何空对象也能够调用 toString,valueOf 等方法,由于这些方法都存在于对象的原型链上,同时若是属性定义了 get 函数也会直接返回执行的结果prototype

[[CanPut]]

[[Put]] 比 [[Get]] 的行为要复杂一点,规范原文是这么写的

[[Put]] 方法依赖一个叫 [[CanPut]] 的内部行为,咱们来看它的定义

首先会判断当前属性是否存在于当前对象中,若是存在则继续判断属性是否有访问器描述符,即 set 函数,若是 set 函数存在 [[CanPut]] 的结果为 true,不然若是访问器描述符为 undefined 或者不合法则返回 false。或者当属性存在于当前对象可是没有定义访问器描述符,那该属性必定被定义了数据描述符, [[CanPut]] 的结果为数据描述符的 writable 值,最后当属性不存在与当前对象,和 [[Get]] 相同会往上遍历原型链,直到终点,反复执行以前的逻辑

通俗的来讲 [[CanPut]] 返回的是一个布尔值,表示当前属性是否可被赋值

[[Put]]

回到 [[Put]] 中,当 [[CanPut]] 的值是 false 时会直接退出赋值的逻辑,而且根据 Throw 这个参数,当 Throw 为 true 时,抛出异常,反之静默,而这个 Throw 对应的是否开启严格模式,同时也验证了严格模式下赋值失败会抛出错误的行为

当 [[CanPut]] 的值是 true 时,表明当前属性能够被赋值,执行如下逻辑

  1. 若是属性在当前对象上,且拥有数据描述符,则直接返回数据描述符的 value 属性,同时触发 [[DefineOwnProperty]] 这个内部方法

通常状况下,对象属性赋值通常都是执行这个逻辑并返回 value 属性做为赋值语句的结果值,举个例子

给 obj 对象的 a 属性赋值数字123,那么 123 就是 a 属性数据描述符中 value 的值,[[Put]] 操做最终返回的值就是 123,对应最后一行赋值语句的结果值

触发 [[DefineOwnProperty]] 这个内部方法 这句话又怎么理解呢?规范中 [[DefineOwnProperty]] 的行为很是复杂,这里我再举个小例子

经过拦截 defineProperty 和 getOwnPropertyDescriptor 能够发现,默认的赋值行为会触发这个两个拦截器,更多的行为有兴趣的朋友能够根据底部连接自行查看

  1. 不然若是属性在当前对象或者原型链上,且拥有访问器描述符,则让赋值表达式右边的值做为惟一参数传入 set 函数并返回结果

  2. 不然若是属性在当前对象原型链上,且拥有数据描述符,则在当前对象建立一个新的属性,并让其数据描述符的值为 {[[Value]]: V, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true}. ,并抛弃原来的数据描述符,同时触发 [[DefineOwnProperty]] 内部方法并返回

什么意思呢,考虑如下状况

let obj = {}
Object.defineProperty(Object.prototype, 'a', {
    configurable: false,
    enumerable: false,
    value: "",
    writable: true
})

obj.a = 1
console.log(Object.getOwnPropertyDescriptor(obj,'a'))

// {value: 1, writable: true, enumerable: true, configurable: true}
复制代码

obj 对象并无属性 a,而在 Object 的原型对象中定义了一个 a 属性,其数据描述符的 configurable,enumerable 都为 false,但最终赋值的时候 obj 对象上会存在一个 a 属性,同时 configurable,enumerable 都为 true

总结

结合《你不知道的 JavaScript 上卷》中对 [[Get]] 和 [[Put]] 的定义,能够得出如下结论

当给对象取值时,会触发 [[Get]] 操做,若是当前对象上有该属性,则判断

  • 含有 get 函数时,执行 get 函数,返回执行结果,
  • 没有 get 函数时,返回数据描述符的 value 属性

若是当前对象上没有该属性,会向上查找原型链,直到尽头,查找过程当中会反复执行上面两步

当给对象赋值时,会触发 [[Put]] ( 不是理想中的 [[Set]] ),若是当前对象上有该属性,则判断

  • writable 为 true 时,执行赋值操做
  • writable 为 false 时,严格模式会抛出错误,非严格模式下静默失败
  • 含有 set 函数时,执行 set 函数

若是当前对象没有该属性,会向上查找原型链,若是在原型链上层找到该属性,则判断

  • writable 为 true 时,会在当前对象(非原型链)建立属性,且设置数据描述符 configurable,enumerable,writable 为 true,value 为赋值的值
  • writable 为 false 时,严格模式会抛出错误,非严格模式下静默失败
  • 含有 set 函数时,执行 set 函数

若是属性是数据描述符的话还会触发内部的 [[DefineOwnProperty]] 操做,若是定义了 defineProperty 和 getOwnPropertyDescriptor 会触发这两个拦截器

参考资料

你不知道的 JavaScript 上卷

ECMA-262 标准

相关文章
相关标签/搜索