從me.name = 'forceddd' 開始

forceddd發表於2021-12-16

me.name = 'forceddd' 的賦值語句在JavaScript中是隨處可見的。但是,我們真的瞭解這句程式碼做了什麼事情嗎?是建立了一個新屬性嗎?是修改了原有屬性的值嗎?這次操作是成功還是失敗了呢?這只是一行簡單的賦值語句,但如果我們認真思考的話,就會發現這種細節其實也沒有那麼簡單。

萬物皆可分類討論。首先分為兩類情況:物件 me 上已有 name 屬性和沒有 name 屬性。

當然,我們都知道,JavaScript中存在著原型鏈機制,所以當物件 me 中不存在 name 屬性時,也可以分為兩種情況: me 的原型鏈上存在 name 屬性與不存在 name 屬性。也就是可以分為三類來討論。

  1. me中已存在name屬性

    const me = {
        name: 'me',
    };
    me.name = 'forceddd';
    
    console.log(me.name); //forceddd

    在這種情況下顯然我們的目的是重新設定 name 屬性的值,結果似乎也是顯而易見的,me.name 被修改成了 'forceddd'

    但是不要忘記了,一個物件的屬性也是擁有它自己的屬性的,其中就包括是否是隻讀的(writable),(通常用於描述一個物件的某個屬性的各種特性的物件被稱為屬性描述符或者是屬性描述物件),所以當name屬性的可寫性(writable)為 false 時,結果會是什麼樣的呢?不僅如此, name 屬性定義了 getter 或者 setter 函式,成為一個訪問描述符的時候,結果又會是怎麼樣的呢?

    • name 屬性的 writablefalse

      const me = {
          name: 'me',
      };
      Object.defineProperty(me, 'name', {
          writable: false,
          value: me.name,
      });
      
      me.name = 'forceddd';
      // name的屬性值仍是 'me'
      console.log(me.name); //me

      因為 name 屬性是隻讀的的,所以賦值操作 me.name = 'forceddd' 失敗了,這恆河狸。但是要注意的是,這種操作失敗是靜默的,如果我們在操作之後沒有校驗 name 值的話,是很難發現的。使用嚴格模式可以把這種靜默失敗變成顯式的TypeError

      'use strict';//使用嚴格模式
      const me = {
          name: 'me',
      };
      
      Object.defineProperty(me, 'name', {
          writable: false,
          value: me.name,
      });
      me.name = 'forceddd';
      //TypeError: Cannot assign to read only property 'name' of object '#<Object>'
    • name 屬性是訪問描述符(定義了 getter 或者 setter )時

      const me = {
          _name: 'me',
          get name() {
              return this._name;
          },
          set name(v) {
              console.log('呼叫setter,設定name');
              this._name = v;
          },
      };
      
      me.name = 'forceddd';//呼叫setter,設定name
      
      console.log(me.name);//forceddd

      此時,name 屬性存在 setter 函式,在進行賦值操作時,就會呼叫 setter 函式。一般來說,我們在定義訪問描述符時,getter setter 都是成對出現的,但是隻定義其中一個也是沒有任何問題的,只是這樣的話可能會出現一些我們不期望的情況,比如,當 name 只定義了一個 getter 函式時,不存在 setter 函式,賦值操作便沒有意義了。

      非嚴格模式下,因為沒有 setter 函式,所以賦值靜默失敗了。

      const me = {
          _name: 'me',
          get name() {
              return this._name;
          },
      };
      
      me.name = 'forceddd';
      
      console.log(me.name); //me

      嚴格模式下,對沒有 setter 函式的訪問描述符進行賦值操作,會出現一個TypeError,也是非常河狸的。

      'use strict';
      const me = {
          _name: 'me',
          get name() {
              return this._name;
          },
      };
      
      me.name = 'forceddd';
      //TypeError: Cannot set property name of #<Object> which has only a getter

    總結一下,當 me 中存在 name 屬性時,進行賦值操作時,存在三種情況:

    1. 當屬性是一個訪問描述符時,如果存在 setter,則呼叫 setter ;如果不存在 setter ,在非嚴格模式下會靜默失敗,在嚴格模式下會產生一個TypeError
    2. 當屬性的屬性描述符中可寫性(writable)為 false 時,在非嚴格模式下會靜默失敗,在嚴格模式下會產生一個TypeError
    3. 當屬性不屬於上面兩種情況時,將值設為該屬性的值,賦值成功。
  2. me及其原型鏈上均不存在name屬性

    此時,會在物件 me 上新建一個屬性 name ,值為 'forceddd' 。這是也是我們經常使用的一種方式。

    const human = {};
    const me = {};
    Object.setPrototypeOf(me, human);
    me.name = 'forceddd';
    
    console.log({ me, human }); //{ me: { name: 'forceddd' }, human: {} }

    我們使用這種方式建立的屬性當然是可以通過賦值的方式修改的,這也就是代表著此時這個屬性的屬性描述符中的 writabletrue 。事實上,此時屬性的可配置性可列舉性也都為 true ,這和通過 defineProperty 新增屬性的預設值是不同的, defineProperty 方法預設新增的屬性描述符中 writableconfigurableenumerable 預設值都為 false 。這也是值得注意的一點。

    console.log(Object.getOwnPropertyDescriptor(me, 'name'));
    // {
    //   value: 'forceddd',
    //   writable: true,
    //   enumerable: true,
    //   configurable: true
    // }

    通過defineProperty方法新增屬性

    Object.defineProperty(me, 'prop', { value: 'forceddd' });
    console.log(Object.getOwnPropertyDescriptor(me, 'prop'));
    // {
    //   value: 'forceddd',
    //   writable: false,
    //   enumerable: false,
    //   configurable: false
    // }
  3. me中不存在name屬性,而其原型鏈上存在name屬性

    此時, me 的原型物件 human 上存在 name 屬性,很多時候,我們使用 me.name = 'forceddd' 之類的賦值語句,只是為了修改物件 me ,而不想牽涉到其他物件。但是在JS中,訪問物件屬性時,如果該屬性在物件上不存在,便會去原型鏈上查詢,所以物件的原型鏈是很難繞開的。

    const human = {};
    const me = {};
    Object.setPrototypeOf(me, human);

    如前文所述,name 屬性在 human 物件上也是有三種情況:

    • name 屬性的屬性描述符中 writablefalse

      //設定human物件的name屬性
      Object.defineProperty(human, 'name', {
          writable: false,
          value: 'human',
      });
      me.name = 'forceddd';
      console.log(me);//{}

      此時,當我們進行了賦值操作後,檢查 me 物件,會發現它並沒有 name 屬性。WTF?很難理解是吧,因為 human 中的 name 屬性是隻讀的,所以將 human 物件為原型的物件就不能通過 = 賦值操作新增 name 屬性了。

      其實這是為了模仿類的繼承行為,如果父類中的 name 屬性是隻讀的,那麼繼承它的子類中的 name 屬性自然應該也是隻讀的。又因為在JS中時通過原型鏈來模擬的類的繼承行為,所以就導致了這一看起來很奇怪的現象。

      同樣地,如果是在嚴格模式下,不會是靜默失敗了,而是會產生一個TypeError

    • name 屬性是一個訪問描述符

      Object.defineProperty(human, 'name', {
          set() {
              console.log('呼叫了human的setter');
          }
      });
      me.name = 'forceddd';//'呼叫了human的setter'
      console.log(me);//{}

      此時,同樣不會在 me 物件上建立 name 屬性,而是會呼叫 humanname 屬性的 setter 函式。類似地,當只存在getter不存在 setter 時,嚴格模式下會產生TypeError非嚴格模式下會靜默失敗。

    • name 屬性不是之前兩種情況

      這種情況最簡單,也是最符合我們預想的,會在 me 物件上建立一個 name 屬性。

    總結一下:當 me 中不存在 name 屬性,而其原型鏈上存在 name 屬性時,有三種情況:

    1. 如果該屬性在原型鏈上是一個訪問描述符並且存在 setter,則會呼叫這個 setter;如果只存在 getter ,則非嚴格模式下會靜默失敗,嚴格模式下出現TypeError
    2. 如果該屬性在原型鏈是是一個 writablefalse 的只讀屬性,在非嚴格模式下會靜默失敗,嚴格模式下出現TypeError
    3. 如果該屬性在原型鏈上不是前兩種情況,只是一個 writabletrue 的普通屬性,則會在 me 物件上建立該屬性。

如果你一定、必須要修改或者設定 mename 屬性的話,可以使用 defineProperty 方法來繞過 = 賦值的這些限制。

Object.defineProperty(human, 'name', {
    set() {
        console.log('呼叫了human的setter');
        // me.name = 'forceddd';
    },
});
Object.defineProperty(me, 'name', {
    value: 'forceddd',
    enumerable: true,
    writable: true,
    configurable: true,
});
console.log(me); //{ name: 'forceddd' }

文字上的描述總是沒有那麼直觀,最後總結為一張圖,便於理解。

相關文章