從 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 標準

相關文章
相關標籤/搜索