Каким образом свойство в прототипе участвует в записи

В интернете много где, например, тут, утверждается, что при записи свойства в объект, даже если такого свойства в объекте еще нет, но есть в прототипе, свойство просто создастся в объекте без участия прототипа. То есть предполагается, что если есть

const animal = {
  name: 'somename',
};

const cat = {
  __proto__: animal,
};

и я сделаю cat.name = 'Васька', то name из animal вообще никак не будет задействован. Однако если свойству name у animal поставить дескриптор writable = false, то при записи свойства у cat возникнет ошибка.

'use strict';

const animal = {
  name: 'somename',
};

const cat = {
  __proto__: animal,
};

Object.defineProperty(animal, 'name', { writable: false });

cat.name = 'Васька';
console.log(cat);

Объясните, пожалуйста, какая все-таки очередность действий происходит при такой записи.


Ответы (1 шт):

Автор решения: Grundy

Рассмотрим, что происходит при выполнении оператора присваивания

В случае обычного присваивания вызывается внутренний метод PutValue, в который параметрами передается новое значение, и куда его нужно установить.

Если погрузиться в алгоритм PutValue станет интересен пункт 6

b. Пусть succeeded будет результатом выполнения ? base.[[Set]](GetReferencedName(V), W, GetThisValue(V)).
c. Если succeeded равно false и строгий режим, тогда кинуть TypeError исключение.

Как можно заметить, если внутренний метод [[Set]] вернет false, будет кинуто исключение, что и происходит в случае кода в вопросе.

Чтобы разобраться, почему возвращается false нужно погрузиться немного глубже, а именно в метод OrdinarySet, который получает текущий дескриптор свойства в объекте, если он есть и передает в OrdinarySetWithOwnDescriptor

В случае, если переданный дескриптор свойства - undefined, и у объекта есть prototype - вернется результат вызова [[Set]] у прототипа, с переданным Receiver - который ссылается на текущий объект.

В случае если у объекта нет прототипа просто создается PropertyDescriptor со значением по умолчанию

{ [[Value]]: undefined, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true }

Важно обратить внимание на значение [[Writable]]: true.

Далее проверяем, что полученный PropertyDescriptor является DataProperty, то есть у него есть хотя бы одно из свойств [[Value]], [[Writable]], в этом случае происходит следующее:

  1. если [[Writable]]: false - вернуть false
  2. если Type(Receiver) не Object, вернуть false
  3. проверяет наличие нужного свойства у Receiver и если оно есть
    1. проверяется значение [[Writable]]: если [[Writable]]: false - вернуть false
    2. устанавливается новое значение свойства
  4. если данного свойства нет:
    1. просто создается данное свойство у Receiver.

Рассмотрим что происходит в коде в вопросе:

  1. попадаем в метод OrdinarySetWithOwnDescriptor, в котором параметр ownDesc принимает значение undefined, так как такого свойства нет в объекте cat. В качестве Receiver передается сам объект cat

  2. так как ownDesc == undefined, но у cat есть прототип animal берем его и вызываем [[Set]] уже для него и

  3. попадаем в метод OrdinarySetWithOwnDescriptor, в котором параметр ownDesc принимает значение

     {value: 'somename', writable: false, enumerable: true, configurable: true}`
    

    так как нужное свойство есть в объекте animal. В качестве Receiver все еще передается объект cat.

  4. ownDesc является DataProperty, а значит происходит переход к следующей проверке

  5. [[Writable]]: false - в данном случае поле уже writable: false - поэтому происходит возврат false, как результат - ошибка в строгом режиме.

→ Ссылка