Почему браузер дважды обращается к Symbol.unscopables?

261
26 ноября 2016, 17:52

with(new Proxy({}, { 
  has() { return true }, 
  get(obj, key, proxy) { return console.log(String(key)) } }) 
) { 
  a-- 
}

Вывод в Chrome:

Symbol(Symbol.unscopables)
a
Symbol(Symbol.unscopables)

Вывод в Firefox:

Symbol(Symbol.unscopables)
Symbol(Symbol.unscopables)
a

Насколько я понимаю, одно обращение связано с чтением, а другое с записью.

Логично, что конструкция a-- должна записать значение в то же место, откуда прочитала. А двойное чтение из Symbol.unscopables как бы намекает, что это не так и у меня есть возможность отдать нечто для чтения, но сказать, что в мой объект этого записывать не надо?

Неужели это так и задумано? Что на эту тему говорит стандарт?

Собственно, в Хроме и FF такое почти прокатывает - чтение и запись связаны с разными объектами, однако работает по-разному:

var a, b, flag = true 
 
with (a = { x: 7 }) 
  with (b = { x: 4, get [Symbol.unscopables]() { return { x: flag=!flag } } }) 
    x++ 
 
                 // Chrome   FF 
console.log(a)   // {x:5}    {x:7} 
console.log(b)   // {x:4}    {x:8}

PS: Этот вопрос по-английски.

Answer 1

Обратимся к спецификации, в таблице 1 находится описание символа @@unscopables

An object valued property whose own property names are property names that are excluded from the with environment bindings of the associated object.

Объект, в котором названия свойств совпадают со свойствами исключенными из with связывания.

Значение этого свойства проверяется в функции HasBindings(N)

  1. Let envRec be the object Environment Record for which the method was invoked.
  2. Let bindings be the binding object for envRec.
  3. Let foundBinding be HasProperty(bindings, N)
  4. ReturnIfAbrupt(foundBinding).
  5. If foundBinding is false, return false.
  6. If the withEnvironment flag of envRec is false, return true.
  7. Let unscopables be Get(bindings, @@unscopables).
  8. ReturnIfAbrupt(unscopables).
  9. If Type(unscopables) is Object, then
    1. Let blocked be ToBoolean(Get(unscopables, N)).
    2. ReturnIfAbrupt(blocked).
  10. If blocked is true, return false.
  11. Return true.

Здесь нас интересуют пункты начиная с 6. Если проверяемое свойство N находится в блоке with, оно всегда проверяется в объекте unscopables.

Теперь перейдем к постфиксному декременту. В алгоритме есть два ключевых момента:

  1. вызов GetValue(lhs) - для получения текущего значения
  2. вызов PutValue(lhs, newValue) - для установки нового значения

Каждая из этих функций в итоге вызывает описанную выше HasBinding.

В обновленном пример добавлена функция set и в логах видно, что второй раз вызывается как раз перед ней.

var value = 10; 
with(new Proxy({}, { 
  has(o, k) { 
      console.log('has', String(k)); 
      return true; // k !== 'console'; 
    }, 
    get(obj, key, proxy) { 
      console.log('get', String(key)); 
 
      if (key === Symbol.unscopables) { 
        return { 
          a: false, 
          console: true 
        } 
      } 
 
      return value; 
    }, 
    set(o, k, v, proxy) { 
      console.log('set', k, v); 
      if (k == 'a') value = v; 
    } 
})) { 
  a--; 
}

Что характерно firefox сначала получает все связанные поля, и лишь затем получает значения свойств.

Далее можно разобрать второй пример:

var a, b, flag = true 
var valueA = 7, 
  valueB = 4; 
with(a = { 
  get x() { 
    console.log('get A', flag); 
    return valueA; 
  }, set x(v) { 
    console.log('set A', flag); 
    valueA = v; 
  } 
}) 
with(b = { 
  get x() { 
      console.log('get B', flag); 
      return valueB; 
    }, set x(v) { 
      console.log('set B', flag); 
      valueB = v; 
    }, 
    get [Symbol.unscopables]() { 
      var t = { 
        x: flag = !flag 
      }; 
      console.log('Symbol.unscopables', t); 
 
      return t; 
    } 
}) 
x++ 
 
// Chrome   FF 
console.log(valueA) // {x:5}    {x:7} 
console.log(valueB) // {x:4}    {x:8}

Что тут происходит?

  1. постфиксная операция, которая внутри себя вызывает Put и Get
  2. getter Symbol.unscopables - который определяет находится ли свойство x в текущем with или нет. Значение флага меняется на противоположное.

Как можно заметить по логам, сначала значения флага возвращается false, что в соответствии со спецификацией указывает на то, что данное имя находится в текущем скопе. Поэтому значение берется из объекта b.

Далее флаг выставляется в true и при вычслении в каком скопе находится x в котороый надо положить, получаем, что он находится не текущем скопе, поэтому значение сохраняется в объекте a.

Как видно - Chrome работает в соответствии со спецификацией.

Что касается firefox. Как можно заметить, firefox не инвертирует значение возвращенное в объекте unscopables, как это указано в спецификации, поэтому значение сначала берется из внешнего with и присваивается значению из внутреннего.

По результатам проверок, можно сделать вывод, что firefox сначала получает свойство куда записывать, и лишь затем свойство откуда брать значение. Из-за этого в примере, когда значение flag меняется на каждый вызов, происходит расхождение с работой Chrome и EDGE.

READ ALSO
Frontend фреймворк на TypeScript

Frontend фреймворк на TypeScript

Нужен фреймворк, который удовлетворяет следующим требованиям:

332
что делает метод push для js объекта?

что делает метод push для js объекта?

Метод push - ничего не делает для объекта, потому по умолчанию у объектов нет такого метода

403
Область видимости в объекте

Область видимости в объекте

Как вывестиsumOne в консоль не в объекте

241
Регулярное выражение JavaScript

Регулярное выражение JavaScript

Нужно найти все неповторяющиеся символы или по-другому - отсеять повторяющиеся символы

256