in ECMAScript

Тонкости ECMA-262-3. Часть 8. Стратегия передачи параметров в функцию.

Read this article in: English.

В этой небольшой заметке мы рассмотрим стратегии передачи параметров в функции в ECMAScript.

В общем случае данный раздел компьютерной науки называется стратегия оценки, вычисления (evaluation strategy), т.е. свод правил для оценки и определения какого-либо выражения. Стратегия передачи параметров в функцию, является её частным случаем.

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

Многие программисты уверены, что объекты в JavaScript (как и в некоторых других привычных языках) передаются в функцию по ссылке, в то время как значения примитивных типов, передаются по значению. Более того, этот “факт” можно видеть в различных статьях, обсуждениях на форумах и даже книгах по JavaScript. Однако, насколько точная это терминология и насколько верное это описание (и, что более важно, насколько верное это понимание), мы и разберём в этой статье.

Вкратце отметим, что в общей теории, стратегии обработки делятся на два вида – строгие (strict), означающие, что аргументы вычисляются до их применения и нестрогие (non-strict), означающие, как правило, вычисление аргументов по требованию (так называемые, “ленивые” вычисления).

Однако, здесь мы рассмотрим основные стратегии передачи параметров в функцию, важные для понимания этой сущности именно с точки зрения ECMAScript.

И, для начала, стоит отметить, что в ECMAScript, как и во многих других языках (например, C, Java, Python, Ruby, др.) используется строгая стратегия передачи параметров в функцию.

Так важным является порядок вычисления параметров — в ECMAScript они вычисляются слева направо. В других языках и их реализациях возможен обратный (т.е. справа налево) порядок вычисления.

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

Поскольку не все из разбираемых ниже стратегий присущи ECMAScript, чтобы не было путаницы, для примеров, описывающих поведение конкретной теоретической стратегии, будем использовать абстрактный псевдокод, схожий с Паскале-подобным синтаксисом.

Данный вид стратегии многим хорошо известен. Значением аргумента здесь является копия значения сущности, передаваемой вызывающей стороной. Изменения, производимые над параметром внутри функции, не влияют на переданный объект снаружи. В общем случае, происходит выделение новой памяти (при этом, конкретная реализация в данном обсуждении не столь существенна – это может быть стек или динамическая память), в которую копируется значение внешнего объекта, и именно значение из этой новой области памяти используется внутри функции.

bar = 10

procedure foo(barArg):
  barArg = 20;
end

foo(bar)

// изменения внутри foo не повлияли
// на bar, находящийся снаружи
print(bar) // 10

Однако, данная стратегия вызывает большое снижение производительности в случае, когда параметром функции является не примитивное значение, а комплексная структура, объект (именно так и происходит, например, в языке Си, когда структура передаётся по значению – она полностью копируется).

Опишем общий пример, на который будем опираться при описании последующих стратегий передачи. Пусть абстрактная процедура принимает два параметра: само значение объекта, а также признак, нужно ли изменить значение полностью (присвоить новое), либо же изменить лишь свойства объекта.

bar = {
  x: 10,
  y: 20
}

procedure foo(barArg, isFullChange):

  if isFullChange:
    barArg = {z: 1, q: 2}
    exit
  end

  barArg.x = 100
  barArg.y = 200

end

foo(bar)

// при передаче по значению,
// объект снаружи остался без изменений
print(bar) // {x: 10, y: 20}

// аналогично и при полном изменении объекта
// (присвоении нового значения)
foo(bar, true)

//также, без изменений
print(bar) // {x: 10, y: 20}, но не {z: 1, q: 2}

В свою очередь стратегия передачи параметра по ссылке (которая также многим известна) получает не копию значения, а неявную ссылку на объект, адрес, однозначно связанный с объектом извне. Любое изменение параметра внутри функции (присвоение нового значения или изменение свойств) отображается на объекте снаружи, поскольку с формальным параметром связан точный адрес этого объекта, т.е. аргумент является алиасом для внешнего объекта.

Псевдокод:

// описание процедуры foo см. выше

// имеем всё тот же объект
bar = {
  x: 10,
  y: 20
}

// результаты процедуры foo
// при передаче по ссылке
// будут следующими

foo(bar)

// значения свойств объекта изменились
print(bar) // {x: 100, y: 200}

// присвоение нового значения
// также изменяет внешний объект
foo(bar, true)

// bar теперь указывает на новый объект
print(bar) // {z: 1, q: 2}

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

Если первые две стратегии у большинства людей были всегда на слуху, то данная стратегия (а точнее, данная её терминология) распространена не столько широко. Но, меж тем, как мы увидим чуть ниже, именно она сыграет ключевую роль в стратегии передачи параметров в ECMAScript.

Альтернативными названиями этой стратегии являются “передача по объекту (by object)” или “передача по разделению объекта (by object-sharing)”.

Стратегия by sharing была впервые предложена и реализована Барбарой Лисков (Barbara Liskov) в языке CLU в 1974 году.

Суть её заключается в том, что в функцию передаётся копия ссылки на объект. Данная копия ссылки связывается с формальным параметром функции и является его значением.

Несмотря на то, что в данном случае фигурирует понятие ссылки, эту стратегию стоит отличать от передачи по ссылке (хотя, именно в этом моменте большинство и делает ошибку), поскольку аргумент в данном случае не является алиасом, а хранит лишь копию адреса.

Основное отличие заключается в том, что присвоение аргументу нового значения внутри функции, никак не затрагивает объект снаружи (как это было бы в случае передачи по ссылке). Однако, поскольку формальный параметр, имея копию ссылки, получает доступ к тому же объекту, что находится снаружи (т.е. объект извне полностью не копировался, как было бы в случае передачи по значению), изменения свойств локального объекта-аргумента – отражаются на внешнем объекте.

// описание процедуры foo см. выше

// вновь, та же структура объекта
bar = {
  x: 10,
  y: 20
}

// передача по разделению
// воздействует на объект
// следующим образом

foo(bar)

// значения свойств объекта изменились
print(bar) // {x: 100, y: 200}

// но при полном изменении объекта
// изменения не произошло
foo(bar, true)

// осталось от прошлого изменения
print(bar) // {x: 100, y: 200}

Данная стратегия подразумевает, что язык в большинстве своём оперирует сущностями-объектами, нежели сущностями-примитивами.

Стратегия по разделению объекта распространена во многих языках: Java, ECMAScript, Python, Ruby, Visual Basic и др.

Более того, в Python сообществе, принята именно эта терминология — by sharing. Что касается других языков, то здесь используется альтернативные терминологии, которые часто могут сбивать с толку, поскольку пересекаются в названии с другими стратегиями передачи.

Так, в большинстве случаев, например, в Java, ECMAScript или Visual Basic, данную стратегию называют так же по значению (by value), подразумевая специфичное значение — копию ссылки.

С одной стороны, это верно — присвоение аргументу лишь связывает данное имя с новым значением (адресом) и не влияет на внешний объект.

С другой же стороны, данное переплетение терминологий, действительно, если не вникать глубоко в суть, может трактоваться неверно (что и приводило к дебатам на форумах о том, как же, всё-таки, передаются в функцию объекты в JavaScript).

Общая теория, действительно, выделяет в определении передачи по значению случай специфичного значения — копии ссылки, поэтому данные технологии терминологических правил не нарушают.

В Ruby же, эту стратегию называют передачей по ссылке (by reference). Опять же, с одной стороны — передалась не копия массивной структуры (т.е., вроде бы, не by value), но с другой стороны, мы имеем дело не с оригинальной ссылкой на объект, и поэтому, вновь, это переплетение терминологий, может сбивать с толку.

Общая теория не описывает особого случая передачи по ссылке, так, как описывает специфичный случай передачи по значению.

Однако, стоит понимать, что во всех упомянутых технологиях (Java, ECMAScript, Python, Ruby, др.) используется своя локальная терминология; в действительности же — во всех них, реализована стратегия, которая в общей теории называется передача по разделению (by sharing).

Касательно С/С++, данная стратегия идеологически схожа с передачей по указателю, с той лишь разницей, что указатель, всё-таки, можно разыменовать и изменить сам объект. В целом же, присвоение значения (адреса) указателю связывает его с новым блоком памяти (т.е. блок памяти, на который он указывал до этого, остаётся нетронутым); изменение же свойств объекта при обращении через указатель — влияет на внешний объект.

Поэтому, проводя аналогию с передачей по указателю, можно явно увидеть, что это — передача по значению адреса, что и является указателем. В этом случае, by sharing — своего рода “синтаксический сахар”, который при присвоении ведёт себя как указатель (но который нельзя разыменовать), а в случае присвоения свойствам — как ссылка (которая не нуждается в разыменовании). Однако, и в С/С++ есть специальный “сахар” при обращении к свойствам объектов без явного разыменования указателя:

obj->x вместо (*obj).x

Наиболее же близко в С++ эта идеология прослеживается в одной из реализаций “умных указателей” (smart pointers), например, в boost::shared_ptr, который перегружает оператор присваивания и конструктор копирования для этих целей, а также, ведёт подсчёт ссылок на объекты, удаляя объекты по GC. Этот тип данных даже имеет схожее название — shared_ptr.

Итак, теперь нам известно, какая именно стратегия передачи используется в ECMAScript — стратегия передачи по разделению объектов (by sharing): изменения свойств объекта-аргумента видны снаружи, присвоение же нового значения — не влияют на внешний объект.

Но, как было уже упомянуто выше, среди разработчиков ES принята локальная терминология для этой стратегии — здесь она называется “передача по значению (by value), где значением является копия ссылки”.

Создатель JavaScript Brendan Eich, также отмечает, что передаётся копия ссылки. В дискуссии на одном из форумов приводятся цитаты разработчиков реализаций ECMAScript, которые называют эту передачу передачей по значению.

Более точно данное поведение можно понять и при рассмотрении обычного присваивания, где видно, что это два разных объекта, но с одинаковым значением — копией адреса.

Код ECMAScript:

var foo = {x: 10, y: 20};
var bar = foo;

alert(bar === foo); // true

bar.x = 100;
bar.y = 200;

alert([foo.x, foo.y]); // [100, 200]

Т.е. два имени связаны с одним и тем же объектом в памяти, разделяя (sharing) этот объект:

foo value: addr(0xFF) => {x: 100, y: 200} (address 0xFF) <= bar value: addr(0xFF)

Присваивание же, лишь связывает имя переменной с новым объектом (с новым адресом), но не воздействует на связанный с ней объект, как это было бы в случае ссылки:

bar = {z: 1, q: 2};

alert([foo.x, foo.y]); // [100, 200] – ничего не поменялось
alert([bar.z, bar.q]); // [1, 2] – однако bar теперь указывает на новый объект

Т.е. теперь foo и bar имеют разные значения, разные адреса:

foo value: addr(0xFF) => {x: 100, y: 200} (address 0xFF)
bar value: addr(0xFA) => {z: 1, q: 2} (address 0xFA)

Здесь всё завязано на то, что значением переменных в случае объектного типа, являются адреса, но не сами объекты. Присвоение одного объекта другому — копирует его значение-ссылку; таким образом оба объекта получают доступ к одному месту в памяти. Последующее же присваивание нового значения (нового адреса) отвязывает имя от старого адреса и связывает его с новым. И это есть важное отличие от передачи по ссылке.

Более того, если рассматривать исключительно уровень абстракции предоставляемый стандартом ECMA-262, то мы видим, что в каждом алгоритме фигурирует лишь понятие “value”. А реализация передачи этого “value” (и различные его виды — будь то примитивное значение или объект) — оставлена за кадром. С этой позиции, рассуждая в рамках абстракции ECMAScript, можно точно и однозначно говорить что есть только “value” и, соответственно, передача только by value.

Однако, чтобы не было путаницы (почему же свойства внешнего объекта могут быть изменены), здесь уже нужно рассматривать подробней сторону реализации, где мы и видим передачу by sharing, или образно — “по указателю, который нельзя разыменовать и изменить полностью сам объект, но можно изменить свойства этого объекта”.

Выделим версии допустимой для ECMAScript терминологии, относительно этого вопроса.

Итак, это может быть либо “передача по значению (by value)”, с уточнением, что имеется в виду особый случай передачи по значению, когда значением является копия адреса. С этой позиции можно говорить, что все без исключения объекты в ECMAScript передаются по значению, что, собственно, и предоставляет уровень абстракции ECMAScript.

Либо, обособленное для этого случая, определение — “передача по разделению объекта (by sharing)”, которое точно позволяет увидеть отличия от классической передачи по значению (by value) и от передачи по ссылке (by reference). В этом случае можно подразделять типы передачи: примитивы передаются по значению, объекты — по разделению.

Утверждение же “объекты передаются в функцию по ссылке” формально к ECMAScript не относится и является неверным.

Надеюсь, данная статья помогла более детально разобраться в стратегиях передачи в целом, а также, относительно данной сущности в ECMAScript. Как всегда, я буду рад ответить на ваши вопросы в комментариях.


Автор: Dmitry A. Soshnikov
Дополнения: Zeroglif

Дата публикации: 08.11.2009

Write a Comment

Comment

  1. Я очень благодарен вам за ваш ресурс и за родной русский язык.
    Благодаря этому удалось глубже понять js, возможно увидела мир книга с под вашего пера буду рад приобрести.

  2. Спасибо за курс статей!

    Невероятное совпадение то что ваш однофамилец Дмитрий Сошников был преподавателем в институте (когда я там учился) и он также занимается функциональными ЯП, вот, например,
    его страница на озоне ozon.ru/context/detail/id/6210136/

    вот такие совпадения бывают

  3. @Nemo, рад, что полезно.

    Да, я знаю того Дмитрия Сошникова, и даже встречался с ним на одной из конференций в Москве 😉

  4. Самого главного то и не хватает.
    Можно ли из функции вернуть объект в аргументе (статья то про ES)?
    в С все просто :Одна звездочка – объект RO, две – RW

  5. @arni, нет, в JS, если сравнивать с Вашей схемой, то это RO (одна звездочка-указатель), т.е. нельзя присвоить объекту-аргументу, и чтобы это отразилось на переданном снаружи объекте. Присваивание лишь свяжет аргумент с новым объектом в памяти. А вот менять свойства объекта можно — и это отразится снаружи.

  6. Здравствуйте, Дмитрий.
    Спасибо за подробную хорошую статью.
    Скажите, будет ли данный код объективным для проверки, каким способом передаются аргументы в функцию, и почему:

    V={};
    COUNTER=0;
    (function recourse(arg){
    	COUNTER++;
    	recourse(arg);
    })(V);
    

    После того, как произойдёт переполнение стека, можно глянуть, каково значение у COUNTER. После этого V можно присвоить значение простого типа, например, строку.
    У меня для разных типов одно и то же значение. Можно ли, исходя из этого, сказать, что любой аргумент передаётся по копии ссылки?
    Спасибо.

  7. @pashak

    В данном случае Ваш код свидетельствует о том, что в конкретном движке задан один и тот же “размер” стека (скорее всего — даже не сам физический размер памяти, а допустимое количество рекурсивного выделения нового стек-фрейма — т.е. то, что и содержит Ваша переменная COUNTER).

    Передача всегда идет по значению. В случае примитива — сам примитив. В случае объекта — его адрес.

    Схематично:

    V = {};
    
    CallStack = [
      frame_1: {arg: addr(V)},
      frame_2: {arg: addr(V)},
      // и т.д. до Stack Overflow
    ];

    где схематичная функция addr(...) возвращает копию адреса объекта.

    Примитив:

    V = "foo";
    
    CallStack = [
      frame_1: {arg: "foo"},
      frame_2: {arg: "foo"},
      // и т.д. до Stack Overflow
    ];

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

    Также я эту тему описывал в разделе Name Binding статьи Lexical Environments. Рекомендую к прочтению — там наглядно видно, что происходит при присвоении (rebinding) и модификации свойств (mutation).

  8. Первый цикл дочитал. Кроме слов “Вау” и “Огромное спасибо” нечего сказать. Очень доступно изложено

  9. Спасибо, огромное, Дмитрий, вы просто спасли меня от многих часов отладки) объяснив, наконец, суть передачи аргументов в функцию в JavaScript.

    В комментариях выше вы говорили, что у вас плана книга – скоро она увидит свет?