18 августа 2019 г.

Устаревшая конструкция "with"

Материал на этой странице устарел, поэтому скрыт из оглавления сайта.

Конструкция with позволяет использовать в качестве области видимости для переменных произвольный объект.

В современном JavaScript от этой конструкции отказались. С use strict она не работает, но её ещё можно найти в старом коде, так что стоит познакомиться с ней, чтобы если что – понимать, о чём речь.

Синтаксис:

with(obj) {
  ...код...
}

Любое обращение к переменной внутри with сначала ищет её среди свойств obj, а только потом – вне with.

Пример

В примере ниже переменная будет взята не из глобальной области, а из obj:

var a = 5;

var obj = {
  a: 10
};

with(obj) {
    alert( a ); // 10, из obj
  }

Попробуем получить переменную, которой в obj нет:

var b = 1;

var obj = {
  a: 10
};

with(obj) {
    alert( b ); // 1, из window
  }

Здесь интерпретатор сначала проверяет наличие obj.b, не находит и идёт вне with.

Особенно забавно выглядит применение вложенных with:

var obj = {
  weight: 10,
  size: {
    width: 5,
    height: 7
  }
};

with(obj) {
  with(size) { // size будет взят из obj
    alert( width * height / weight ); // width,height из size, weight из obj
  }
}

Свойства из разных объектов используются как обычные переменные… Магия! Порядок поиска переменных в выделенном коде: size => obj => window.

Изменения переменной

При использовании with, как и во вложенных функциях – переменная изменяется в той области, где была найдена.

Например:

var obj = {
  a: 10
}

with(obj) {
    a = 20;
  }
alert( obj.a ); // 20, переменная была изменена в объекте

Почему отказались от with?

Есть несколько причин.

  1. В современном стандарте JavaScript отказались от with, потому что конструкция with подвержена ошибкам и непрозрачна.

    Проблемы возникают в том случае, когда в with(obj) присваивается переменная, которая по замыслу должна быть в свойствах obj, но её там нет.

    Например:

    var obj = {
      weight: 10
    };
    
    with(obj) {
      weight = 20; // (1)
      size = 35; // (2)
    }
    
    alert( obj.size );
    alert( window.size );

    В строке (2) присваивается свойство, отсутствующее в obj. В результате интерпретатор, не найдя его, создаёт новую глобальную переменную window.size.

    Такие ошибки редки, но очень сложны в отладке, особенно если size изменилась не в window, а где-нибудь во внешнем LexicalEnvironment.

  2. Ещё одна причина – алгоритмы сжатия JavaScript не любят with. Перед выкладкой на сервер JavaScript сжимают. Для этого есть много инструментов, например Closure Compiler и UglifyJS. Обычно они переименовывают локальные переменные в более короткие имена, но не свойства объектов. С конструкцией with до запуска кода непонятно – откуда будет взята переменная. Поэтому выходит, что, на всякий случай (если это свойство), лучше её не переименовывать. Таким образом, качество сжатия кода страдает.

  3. Ну и, наконец, производительность – усложнение поиска переменной из-за with влечёт дополнительные накладные расходы.

    Современные движки применяют много внутренних оптимизаций, ряд которых не может быть применён к коду, в котором есть with.

    Вот, к примеру, запустите этот код в современном браузере. Производительность функции fast существенно отличается от slow с пустым(!) with. И дело тут именно в with, т.к. наличие этой конструкции препятствует оптимизации.

    var i = 0;
    
    function fast() {
      i++;
    }
    
    function slow() {
      with(i) {}
      i++;
    }
    
    var time = performance.now();
    while (i < 1000000) fast();
    alert( "Без with: " + (performance.now() - time) );
    
    var time = performance.now();
    i = 0;
    while (i < 1000000) slow();
    alert( "С with: " + (performance.now() - time) );

Замена with

Вместо with рекомендуется использовать временную переменную, например:

/* вместо
with(elem.style) {
  top = '10px';
  left = '20px';
}
*/

var s = elem.style;

s.top = '10px';
s.left = '0';

Это не так элегантно, но убирает лишний уровень вложенности и абсолютно точно понятно, что будет происходить и куда присвоятся свойства.

Итого

  • Конструкция with(obj) { ... } использует obj как дополнительную область видимости. Все переменные, к которым идёт обращение внутри блока, сначала ищутся в obj.
  • Конструкция with устарела и не рекомендуется по ряду причин. Избегайте её.

Задачи

важность: 5

Какая из функций будет вызвана?

function f() {
  alert(1)
}

var obj = {
  f: function() {
    alert(2)
  }
};

with(obj) {
  f();
}

Вторая (2), т.к. при обращении к любой переменной внутри with – она ищется прежде всего в объекте.

Соответственно, будет выведено 2:

function f() {
  alert(1)
}

var obj = {
  f: function() {
    alert(2)
  }
};

with(obj) {
  f();
}
важность: 5

Что выведет этот код?

var a = 1;

var obj = {
  b: 2
};

with(obj) {
  var b;
  alert( a + b );
}

Выведет 3.

Конструкция with не создаёт области видимости, её создают только функции. Поэтому объявление var b внутри конструкции работает также, как если бы оно было вне её.

Код в задаче эквивалентен такому:

var a = 1;
var b;

var obj = {
  b: 2
}

with(obj) {
  alert( a + b );
}
Карта учебника