Замыкания и объекты JavaScript. Переизобретаем интерпретатор

  • Tutorial
Обычно концепции или парадигмы программирования объясняют либо описательно — «разжёвывая» новые идеи простыми словами, либо метафорически — уподобляя их хорошо знакомым аудитории предметам и понятиям. Но ни первый, ни второй способ не дает такого точного и полного представления о предмете, как взгляд с точки зрения низкоуровневой реализации.

Когда в изучении языка доходишь до нетривиальных вещей, бывает полезно сместить уровень абстракции, чтобы понять, как на самом деле всё устроено. Ведь, по большому счету, любые конструкции языков сколь угодно высокого уровня сводятся к старому доброму машинному коду. Писать в объектно-ориентированном или функциональном стиле можно и на чистом C, и даже на ассемблере. Грубо говоря, любой высокоуровневый язык — это зафиксированный на уровне компилятора или интерпретатора набор синтаксических карамелек и шоколадок. Повышение уровня абстракции позволяет писать более сложные программы с меньшими усилиями, но вот понять в начале пути, что конкретно имеется в виду под наследованием или замыканием, как это всё работает и почему, гораздо легче, разобравшись, каким образом всё это реализовано.

JavaScript, как никакой другой язык, нуждается в именно таком объяснении. Функциональная природа, скрытая за Си-подобным синтаксисом, и непривычная прототипная модель наследования поначалу сильно сбивают с толку. Давайте мысленно понизим уровень JavaScript до простого процедурного, наподобие Си. Отталкиваясь от этого «недоязыка», переизобретем функциональное и объектно-ориентированное программирование.

Первоклассные функции


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

Рассмотрим следующий код:
//  Листинг 1
var x = 10;

var foo = function()  {
  var y = 20;
  var bar = function() {
    var z = 30;
    return x+y+z; 	
  }
  return bar;
}

var baz = foo();
console.log(baz());  // 60


Переменные x, foo и baz — глобальные, и поэтому доступны везде, независимо от глубины стека. В момент вызова foo, на вершине стека оказывается кадр с локальными переменными y и bar, затем, при выходе из foo, этот кадр теряется, а в момент вызова baz — в кадре активации только z. Откуда интерпретатору взять y? В языке Си (и всех его потомках) эта проблема решается весьма сурово — объявление вложенных функций запрещено. В Паскале — наоборот, вложенные функции есть, но возможность вернуть функцию на выходе отсутствует. Когда говорят, что функции в императивных языках не являются объектами первого класса, имеют в виду именно эту ситуацию. Функциональные же языки позволяют делать с функциями всё, что угодно (пример выше на настоящем JavaScript вполне работоспособен). Как им это удается?

При возврате вложенной функции bar из внешней функции foo, стековый кадр, созданный при вызове foo, сохраняется в переменной baz в составе контекста исполнения функции bar. Таким образом формируется цепочка областей видимости, совершенно не зависящая от стека вызовов. Конкретная реализация механизма формирования этой цепочки, конечно, может сильно отличаться от этого упрощенного описания, но главное то, что переменная y существует, пока существует baz, несмотря на то, что при выходе из foo ссылка на неё исчезает из стека.

Совокупность всех существующих в данный момент цепочек видимости (на каждую определенную в текущем контексте исполнения функцию — по одной цепочке) образует нечто вроде трехмерного дерева. Если мы возвращаем несколько вложенных функций, ветви дерева расходятся в стороны, совместно используя переменные внешней функции, а если вызываем внешнюю функцию несколько раз, ответвление идет вверх, создавая независимые копии свободных переменных:
//  Листинг 2
var x = 'I am global!';

var foo = function (y)  {
  var z = 'unchanged';
  var getXYZ = function ()  {
    return 'x: '+x+'  y: '+y+'  z: '+z;
  }
  var setZ = function(newZ)  {
    z = newZ;
  }
  return [getXYZ, setZ];  // Мы ведь договорились, что не знаем ничего про объекты, правда?
}

var a = foo('Alice');        // *1*
var b = foo('Bob');          // *2*

console.log(a[0]());         // x: I am global!  y: Alice  z: unchanged
a[1]('changed');             //                                          *3*
console.log(a[0]());         // x: I am global!  y: Alice  z: changed    *4*
console.log(b[0]());         // x: I am global!  y: Bob  z: unchanged 
x = 'Everybody can see me!'; //                                          *5*
console.log(a[0]());         // x: Everybody can see me!  y: Alice  z: changed 
console.log(b[0]());         // x: Everybody can see me!  y: Bob  z: unchanged


В точке *1* «дерево видимости» разветвляется в узле foo, так как мы возвратили две вложенные функции. Они могут сообщаться друг с другом через переменные y и z, что видно в точке *3*. В точке *2* мы входим в функцию foo во второй раз, и дерево растет вверх — создаются копии всех локальных переменных foo. «Серые» getXYZ и setZ так же сообщаются через y и z в составе «серого» узла foo, но они ничего не знают об y и z из «черного» узла foo, что хорошо видно в точке *4*. В то же время переменная x уровнем выше видна всем листьям дерева видимости (*5*).

Таким образом, в момент написания программы мы определяем структуру будущих деревьев видимости, в момент фактического создания ветвей дерева можем повлиять на некоторые переменные внутри него и оставить их зафиксированными (после вызва foo('Alice') или foo('Bob') нет никакого способа изменить значение переменной y снаружи) и, пока ветви дерева существует, можем управлять его состоянием лишь постольку, поскольку это позволяют листья.

Необходимость в этих дополнительных структурах данных интерпретатора — одно из главных отличий внутреннего устройства функциональных языков от императивных. Так как такие структуры создаются неявно, без непосредственного участия программиста, то и освобождать память от них может только сам интерпретатор. Поэтому функциональные языки не могут существовать без сборщиков мусора, а императивные — могут. Между прочим, первый сборщик мусора был написан аж в 1959 году для языка Лисп. Ах да, чуть не забыл — ссылка на функцию вместе с её цепочкой областей видимости называется замыканием.

Инкапсуляция и наследование


Итак, слегка доработав интерпретатор, мы превратили примитивный процедурный язык в полноценный функциональный и поняли, как устроены замыкания. Теперь неплохо бы научить его работать с объектами. Хотя само по себе ООП знакомо большинству лучше, чем замыкания, проблем с ним возникает гораздо больше. Дело в том, что если в остальных языках существует жестко зафиксированный набор конструкций, однозначно задающий стиль и оттенки реализации объектной парадигмы в этом конкретном языке, то в JavaScript можно городить что угодно и как угодно. И городят… Любой уважающий себя автор книги о JavaScript считает своим долгом привести не меньше четырех разных способов организовать иерархии объектов, чтобы продемонстрировать читателю “мощь и выразительность языка”. Это конечно круто, но привычный к Java, Ruby или C# мозг закипает от такой анархии. Пока я не столкнулся с JavaScript, я не испытывал никакой потребности разобраться, как именно работают все эти объектные штуки — они просто работали, как написано в книжке. С JavaScript такой номер не проходит.

«Забудем» о том, что в JavaScript объекты уже есть, и будем называть их структурами, как в Си, а свойства объектов — членами структур. Так же пока откажемся от точечной нотации и будем везде обращаться к членам структуры через квадратные скобки. Так как у нас уже есть функции первого класса, с которыми можно обращаться так же вольно, как с любыми переменными, сконструировать структуру, содержащую как данные, так и функцию для их обработки, проще простого:
//  Листинг 3
var obj = {
  x: 10,
  y: 20,
  foo: function () {return x + y;}
};

console.log(obj['foo']());  //  Ошибка!


На самом деле всё немножко сложней. Этот пример неработоспособен, так как в объекте активации (так принято называть стековый кадр в JavaScript) нашей функции foo() нет никаких x и y. Нет их и выше по цепочке, там есть только переменная obj. Чтобы таки до них добраться, нам придется обращаться к ним, как к obj['x'] и obj['y']:
//  Листинг 4
var obj = {
  x: 10,
  y: 20,
  foo: function () {return obj['x'] + obj['y'];}
}

console.log(obj['foo']());  //  30


Заработало! Мы поместили данные и функцию, которая их обрабатывает, в одну структуру. Но очень часто нам бывает нужно несколько объектов с таким же устройством, теми же функциями, но разными значениями переменных. Создадим функцию, порождающую такие структуры:
//  Листинг 5
function createObj(x, y)  {
  var obj = {};
  obj['x'] = x,
  obj['y'] = y,
  obj['foo'] = function () {return obj['x'] + obj['y'];}
  return obj;
}  

var obj1 = createObj(1, 2);
var obj2 = createObj(3, 4);

console.log(obj1['foo']());  //  3
console.log(obj2['foo']());  //  7


Так как функция createObj() возвращает вложенную функцию в составе объекта obj, при каждом её вызове создается замыкание, содержащее независимые друг от друга копии x, y и foo (дерево областей видимости растет вверх).То, что у нас получилось, уже очень похоже на полноценный объект. Чтобы это дело отметить, в последующих листингах перейдем на более лаконичную точечную нотацию. Но ООП — не ООП без наследования. Как его организовать? Мы могли бы написать функцию, которая бы копировала все свойства объекта-родителя в объект-потомок. Такое наследование называют каскадным, но, строго говоря, это скорее клонирование, чем наследование. Изменения в реализации родителя никак не скажутся на потомке, кроме того, если каждый потомок будет содержать копии методов родителя, то это приведет к лишнему расходу памяти. Пожалуй, лучше просто хранить в одном из свойств потомка ссылку на родителя. Ещё нам понадобится функция для поиска свойств вверх по цепочке наследования:
//  Листинг 6
function createObj(x, y)  {
  var obj = {};
  obj.x = x,
  obj.y = y,
  obj.foo = function () {return obj.x + obj.y;}
  return obj;
}  

function createChild (parent)  {
  var child = {};
  child.__parent__ = parent;
  return child; 
}

function lookupProperty (obj, prop)  {
  if (prop in obj)
    return obj[prop];
  else if (obj.__parent__)
    return lookupProperty (obj.__parent__, prop);
 }
 
var a = createObj(1, 2);
var b = createChild (a);

console.log(lookupProperty(b, 'y'));      //  2
console.log(lookupProperty(b, 'foo')());  //  3


Вроде бы порядок, но если мы изменим объект b, например так: b.x = 10, то увидим, что на самом деле ничего не работает. Метод foo() по-прежнему обращается к свойствам своего объекта, а не объекта-потомка. Если мы хотим повторно использовать методы при наследовании, необходимо научить их работать со свойствами чужих объектов. Можно передавать методу аргумент, который указывает на текущий объект. Также необходимо использовать функцию lookupProperty() внутри метода, потому что мы не знаем заранее, определены ли в текущем объекте свойства x и y, или их придется искать вверх по цепочке наследования. Сами функции createChild() и lookupProperty() остаются без изменений:
//  Листинг 7
function createObj(x, y)  {
  var obj = {};
  obj.x = x,
  obj.y = y,
  obj.foo = function (currentObj) {
    return lookupProperty(currentObj, 'x') + lookupProperty(currentObj, 'y');
  }
  return obj;
}  

function createChild (parent)  {
  var child = {};
  child.__parent__ = parent;
  return child; 
}

function lookupProperty (obj, prop)  {
  if (prop in obj)
    return obj[prop];
  else if (obj.__parent__)
    return lookupProperty (obj.__parent__, prop);
 }
 
var a = createObj(1, 2);
var b = createChild (a);
b.x = 10;

console.log(lookupProperty(b, 'y'));      // 2
console.log(lookupProperty(b, 'foo')(b)); // 12


Итак, мы только что реализовали делегирующее наследование, совершенно не используя встроенные возможности JavaScript. Наш пример весьма либерален — можно наследовать объект, а можно создать с нуля, объект-потомок может в любой момент разорвать цепочку наследования и прицепиться к другому объекту.

Давайте внесем в наш интерпретатор изменения для поддержки ООП. Так как наследование — это хорошо, и почти всегда имеет смысл хоть что-нибудь от чего-нибудь наследовать, lookupProperty() стоит сделать полностью прозрачной, убрав внутрь интерпретатора. Больше мы её не увидим, но будем помнить, что она есть.

Затем, объединим функции createObj() и createChild() — они довольно похожи. Обе они создают временный объект при входе и возвращают его при выходе. Включим объединенную функцию в состав корневого объекта Object. Она будет принимать два аргумента — объект-родитель и объект, описывающий отличия потомка от родителя (такой подход ещё называют дифференциальным или разностным наследованием).

Наконец, чтобы при каждом вызове метода не передавать через аргументы текущий объект, будем автоматически предоставлять ссылку на объект, которому принадлежит метод. Назовем её this, в соответствии с традицией ООП:
//  Листинг 8
var a = Object.create(null, {
  x: {value: 1},
  y: {value: 2},
  foo: {value: function() {
    return this.x + this.y;
    }
  }
});    

var b = Object.create(a, {x: {value: 10}});

console.log(b.x+', '+b.y+', '+b.foo());  //  10, 2, 12


Мы привели наследование в соответствие со стандартом EcmaScript 5. К сожалению, новый стандарт работает ещё не везде и не очень быстро. Кроме того, уже написаны миллионы строк кода, в котором наследование сделано по-старинке, через new. Эта схема предполагает принудительное использование конструкторов и прототипов. Говорят, Брендан Айк ввёл её в язык, чтобы не шокировать привычных к классическому наследованию программистов простотой и прямолинейностью описанной выше схемы. Пожалуй, из Айка вышел бы неплохой контрабандист — ему удалось протащить функциональный язык в мейнстрим программирования, где до этого правили бал императивные языки, замаскировав его Си-подобным синтаксисом, и прототипное наследование, запутав его и сделав похожим на классическое.

Для классического ООП характерно наличие жесткой границы между классами и экземплярами. В прототипном ООП её нет вообще, так как нет классов и любой объект может служить прототипом. Чтобы смягчить это различие, был создан особый тип функций — конструкторы. Иерархия конструкторов существует параллельно иерархии объектов, каждый конструктор «висит» над своим объектом, как класс над экземпляром. Конструктор не является прототипом объекта, а прототип конструктора не имеет никакого отношения к прототипу создаваемых этим конструкторам объектов.

Вернемся к листингу 7. Чтобы вписать конструкторы в схему наследования, вспомним, что в JavaScript всё, в том числе и функции, является объектом. То есть мы можем добавлять свойства к функциям, как к обычным объектам. Добавим свойство prototype, которое будет указывать на прототип создаваемого объекта в случае вызова функции в качестве конструктора. Затем, как и при переходе к листингу 8, переименуем временные переменные obj, child и currentObj в this, спрячем их объявление и возврат внутрь интерпретатора, туда же уберем lookupProperty(). Так как каждый конструктор создает один определенный тип объектов, использование универсального метода вроде Object.create() лишено смысла, поэтому будем называть конструкторы по типу объектов, которые они создают, но с прописной буквы, чтобы не путать с обычными функциями. Чтобы интерпретатор знал, что мы хотим вызвать функцию в качестве конструктора, добавим ключевое слово new перед её именем. Вот что у нас получится:
//  Листинг 9
function A(x, y)  {
  this.x = x,
  this.y = y,
  this.foo = function () {
    return this.x + this.y;
  }
}; 

function B () {};
B.prototype = new A(1, 2);

var b = new B();

console.log(b.x+', '+b.y+', '+b.foo());  //  1, 2, 3


Пара конструктор+прототип (в этом примере: B()+B.prototype) играет ту же роль, что класс в классическом ООП. Обратите внимание, что в листинге 8 объект a служит исключительно для того, чтобы унаследовать от него b, а в листинге 9 мы вообще избавились от переменной a, значит и конструктор A() нам не нужен. Свойства x и y, разные для каждого объекта, можно задать в конструкторе B, а общий для всех метод foo() — в прототипе:
//  Листинг 10
function B (x, y) {
  this.x = x,
  this.y = y
};

B.prototype.foo = function () {
  return this.x + this.y;
}

var b = new B(1, 2);

console.log(b.x+', '+b.y+', '+b.foo());  //  1, 2, 3


То же самое для Object.create():
// Листинг 11
var B = {
foo: function() {
  return this.x + this.y;
  }
};

var b = Object.create(B, {x: {value: 10}, y: {value: 20}});

console.log(b.x+', '+b.y+', '+b.foo());  //  10, 20, 30


Заключение


1. Выполнение программы на JavaScript обеспечивают три основные структуры данных интерпретатора — стек вызовов, цепочка областей видимости и цепочка наследования. Стек — самая древняя и примитивная структура. Он строго линеен (хотя возможны варианты...), зато быстр и прост. Две последние структуры скорее похожи на деревья, чем на цепочки, но так как в каждый момент времени выполняется только одна функция, и она может перемещаться по этим деревьям только вверх, с её точки зрения это именно цепочки.

2. Структура дерева областей видимости статична и задается во время написания программы. Корнем дерева является глобальный объект. Рост цепочки областей видимости происходит так же, как и рост стека, при вызове очередной функции. А вот при возврате в вызывающую функцию стек уменьшается всегда, а цепочка областей видимости может продолжать расти, вернув ссылку на вложенную функцию и даже начать ветвиться, если таких функций несколько или если внешняя функция выполняется несколько раз. Такие долгоживущие узлы и образуют замыкания.

3. Дерево наследования растет из объекта Object (UPD: на самом деле оно растет из null. Так как этот объект, мягко говоря, не слишком содержательный, обычно его не принимают во внимание. Спасибо azproduction за поправку). Основное назначение дерева наследования — поиск свойств вверх по цепочке прототипов, если свойство отсутствует в самом объекте — то, что делала функция lookupProperty() в наших примерах. В стандарте EcmaScript это метод [[get]]. Ссылка на родительский объект (__parent__ в листингах 6 и 7) во многих реализациях называется __proto__ и доступна программисту. Но её использование считается плохим тоном. Стандарт языка не предусматривает возможности менять родителей, как в нашей самодельной реализации наследования. Object.prototype и Object.__proto__ — совершенно разные вещи. Object.prototype используется только при вызове функции в качестве конструктора и задает прототип возвращаемого объекта.

4. В JavaScript нет модификаторов private, protected или public для сокрытия реализации объекта. Тем не менее такое сокрытие можно реализовать с помощью замыканий. Вот так или даже так. Однако это довольно сомнительная практика — читать и тестировать такой код сложнее. В большинстве современных динамических языков private — это всего лишь соглашение, и к частным свойствам при желании можно обращаться извне. В JavaScript принято обозначать частные свойства подчеркиванием: _private. Кроме того, часто используются модули. Это очень удобная и уже практически стандартная альтернатива частным методам и свойствам.

5. Ключевое слово this указывает на текущий объект, что довольно очевидно в случае конструкторов и методов объектов. В случае вызова функции не как метода объекта, this по-умолчанию указывает на глобальный объект. Хотя создание, передача и возврат объектов, на которые указывает this, спрятаны внутрь интерпретатора, в методах call() и apply() остались торчать уши переменной currentObj из листинга 7: первый аргумент этих методов будет виден внутри вызываемой функции, как this.

Список дополнительной литературы


  1. Javascript Closures
  2. JavaScript. Ядро
  3. Тонкости ECMA-262-3. Замыкания.
  4. Тонкости использования this
  5. ECMA-262-5 in detail. Lexical environments: Common Theory
  6. Learning Javascript with Object Graphs: часть 1, часть2, часть3
  7. Объектно-ориентированный Си (pdf)
  8. Основы и заблуждения насчет JavaScript
Share post

Comments 30

    –2
    Хорошая статья, но, мне кажется, называть javascript ФП языком несколько неверно. Скорее всего — язык с элементами ФП. Впрочем тут все расходятся в толковании термина — так что это имхо.
      0
      Спасибо! Но всё-таки я бы скорее назвал JavaScript функциональным языком с элементами ООП. Уж очень много завязано на замыкания и функции. Может быть, нет функциональной чистоты, но это же не Haskell — цели у создателей языка были совсем другие.
      +4
      Спасибо, весьма интересный подход, прям учитался с головой, с эффектом —«блин дочитал :(».
        +5
        Уф! Ну наконец-то! Спасибо за комментарии, а то я впервые с таким столкнулся — за 6 часов топик набрал 62 добавления в избранное, 42 плюса в рейтинг 6 плюсов в сами-знаете-что и 1 (ОДИН) комментарий. Я прям не знал, что и думать… Так и хотелось сказать с возмущённой интонацией: «Если плюсуете, так хоть пишите, за что!» :)
          0
          ну а что вы хотели — отличная статья, ни добавить, ни прибавить :) пишите ещё, с удовольствием почитаю
        +1
        Шедевральная статья. Распечатаю, буду читать каждый день с фонариком под одеялом.
        Спасибо!
          0
          И вам спасибо!
          0
          Хорошая статья. Спасибо.
          Одно но: так как подавляющее число людей визуалы — я бы добавил побольше иллюстраций, особенно при объяснении как растет стек и растет цепочка замыканий. Я сам когда пытаюсь чтото понять — рисую на листке карандашом объекты и их взаимосвязи
            +1
            Я тоже визуал, но мне кажется, что стек очень легко представить без всяких картинок — он же линейный. А замыкания я вроде нарисовал. Впрочем, у меня взгляд уже замылился, так что если будут ещё пожелания насчет иллюстраций — может быть завтра на свежую голову добавлю чего-нибудь.
              +1
              Просто стэк рисовать, конечно, не надо, а вот эту фразу: «При возврате вложенной функции bar из внешней функции foo, стековый кадр, созданный при вызове foo, сохраняется в переменной baz в составе контекста исполнения функции bar. Таким образом формируется цепочка областей видимости, совершенно не зависящая от стека вызовов.» наверное можно было пояснить. Засыпаю и так и не разобрался кадр в стэке остаётся или нет. Копируется в цепочку или переносится. По идее переносится, но с JS я ни в чём не уверен :)
                +2
                Ну, я не могу со стопроцентной уверенностью сказать, что и как происходит в каждом конкретном движке JavaScript- для этого нужно смотреть исходники. Под словом «интерпретатор» я понимаю некий условный, игрушечный движок, придуманный для иллюстрации. Давайте договоримся, что у нас в стеке всегда хранятся только ссылки на переменные, а не значения. Тогда всё становится яснее — при вызове функции в куче создается объект активации (он же стековый кадр), а на стеке сохраняется ссылка на него. Ссылки на тот же самый объект хранятся, кроме стека, еще и в каждой вложенной функции. Если мы не возвращаем вложенную функцию, при уменьшении стека единственная ссылка на объект активации теряется, а если возвращаем — она уже не единственная, и объект продолжает жить.

                Надеюсь, стало понятнее. Ещё раз подчеркну — если вы прочитаете исходники, например, V8, возможно, что там всё реализовано не так — что-то хранится непосредственно на стеке, наверняка есть множество вспомогательных структур, всякие оптимизации. Но если вы не собираетесь писать свой интерпретатор, это не существенно.
                  0
                  Да, вот так понятней :) А как на самом деле реализовано — не важно. Хотя я ваш метод использовал для окончательного понимания многих фишек в ООП C++ читая ассемблерные листинги, сгенерированные VC++ (с отключенными оптимизациями).
            +3
            Шикарная статья! Читал взахлеб :) теперь прототипы перестали быть чем-то странным и не понятным)
              +1
              Все-таки я всегда говорил что JavaScript — мощный язык (может я просто не знаю о подобных).

              Как ни странно — вы меня не удивили (изучал его досконально), но написано шикарно, все-таки немного нового поймал.
                0
                Немного не в тему — но есть ли способ JS (в ачстности листинги из поста) запустить в консоли, в браузере не так как-то, а гугл что-то не то, что хочется выдавать, пишет про объект console а не линуксовую
                  +2
                  Я иногда балуюсь Rhino Shell, хотя браузерными объектами там и не пахнет.
                  Скрин листинга
                    0
                    О, кажется то, что нужно. А на DOM я и не рассчитывал, особых проблем нет. Я язык понять хочу лучше.
                    +3
                    Есть — Node.js, я под ним и писал эти скрипты. А что не так с браузерами? Я проверял в Chrome и Firefox — всё работает. Вроде как и в IE8+, и в Опере и в Сафари consol.log() тоже есть.
                      0
                      Удобство, вернее его отсутствие, непривычное. Под консолью я имел в виду терминал с bash например, а не объект consol :)
                        +3
                        Вы про это?
                          0
                          Ага, спасибо :)
                    –12
                    > Переменные x, foo и baz — глобальные,
                    Дальше не читал
                      –6
                      Хотя конечно придираюсь, все правильно дальше описано. Хотел просто сказать про это:
                      var myValue;
                      function setValue()
                      {
                      myValue = «test»;
                      }

                      function getValue()
                      {
                      alert(window.myValue); // yup, it's «test»
                      }
                        +5
                        вам вообще ничего читать не надо.
                        +3
                        Приятно когда человек пытается разобраться в вопросе, и делится с нами этим опытом в доступной форме. В случае JS нужно именно уметь думать и понимать происходящее, тут не получится следовать каким-то строгим шаблонным/справочным установкам и при этом считать себя JS экспертом. Многие работают с JS годами, но вникать не пытаются.

                        Подобные статьи по JS очень полезны, в отличие от других статей/переводов «справочного» характера.

                        Иногда читаю блоги (в основном англоязычные), с анализом работы разных JS конструкций/приемов, их интересно читать, не жалея о потраченном времени потому что узнаешь действительно что-то новое. На хабре вот тоже иногда бывает что-то похожее.

                        В общем просто хотел сказать спасибо :)
                          +11
                          Хорошая статья, поздравляю (Хабр снова начинает становится «тортом»! ;))

                          Ниже пара дополнений.

                          P.S.:

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


                          Кстати, у GCC есть расширение — он все-таки позволяет создавать вложенные функции и их даже можно вернуть наружу (т.е. upward funarg). Достигается это за счет техники трамплинов (trampoline).

                          Также, если нет upward funarg'a (иными словами, если вложенная функция используется только локально внутри родительской функции, «не убегая» наружу вверх), то техника лямбда-поднятия (lambda lifting) также решает эту проблему. Именно она используется в Паскеле для вложенных функций.

                          Помимо этого, часть «первоклассности» (без вложенных функций) все же есть и в Си. Например, можно свободно передать указатель на функцию (к примеру, параметр функции сортировки). Поскольку Си использует также статическую область видимости, то функция правильно будет использовать переменных того контекста (глобального, т.к. нет вложенных функций), где она была порождена, а не контекста, где будет запускаться.

                          если каждый потомок будет содержать копии методов родителя, то это приведет к лишнему расходу памяти


                          Здесь стоит отметить, что «копии метода» не будет, но будет, действительно, копия слота (т.е. создастся родное свойство). Слот же этот будет ссылаться на ту же функцию. Примитивов, конечно же, это не касается.

                          Некоторые прототипные языки, например Io, используют так же делегирующее наследование, но метод, который осуществляет «сцепление» предка с потомком назван почему-то clone ;)

                          каждый конструктор «висит» над своим объектом


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

                          Пара конструктор+прототип (в этом примере: B()+B.prototype) играет ту же роль, что класс в классическом ООП


                          Собственно, это и есть «класс» в JavaScript (только без сахара), поскольку реализует классовый code reuse. Как эта пара превращается в класс при добавлении сахара можно хорошо видеть, например, на классах CoffeeScript. Или Python. Да-да, Питон — это такой же делегирующий «прототипный» язык, как и JavaScript, только в нем добавлен сахар. Именно это я отмечал, говоря, что разницы «прототип vs. класс» недостаточно, всегда нужно задействовать все сопутствующие механизмы (первоклассные сущности, динамика и т.д. — здесь сводная таблицы — "классификация классов").

                          Этот пример с Python'ом достаточно интересен для анализа. Как только появляется «сахар» — всё, язык сразу же классовый и сразу же понятный. Но стоит убрать из Питона сахар — мы получим тот же JavaScript в этом аспекте. Таким образом, «сахар решает». А «класс» — это не какое-то ключевое слово class в языке, и даже не возможность создавать классы с помощью этого слова. Класс — это возможность программировать в классифицирующем стиле. А уж как это делается — с ключевым ли словом class или ключевым словом function — это по большому счету дело десятое.

                          наследование сделано по-старинке, через new


                          Такой же сахар над делегацией для классов (как в Python, Ruby или CoffeeScript), кстати, довольно активно обсуждался в es-discuss (возможно мы даже увидим классы в JS — такие же как в Coffee). Так что, «по-старинке» скорей всего здесь некорректно. Разница ведь в данном случае не в «старинках», а в методах code reuse'a. Если нам нужен классовый реюз (т.е. (1) возможность генерировать объекты по шаблону и (2) наследовать строгую иерархию), то мы используем классовый реюз. Если нет — пожалуйста, прототипный (т.е. (1) не нужно генерировать много однотипных объектов и (2) «наследую от кого хочу»). Это два разных код реюза и оба применимы. Здесь на Хабре был подробный тред в комментах на эту тему.

                          Ссылка на родительский объект (__parent__ в листингах 6 и 7) во многих реализациях называется __proto__


                          Не совсем удачно выбранное псевдо-имя, т.к. __parent__ — это реальное (хоть и нестандартное) свойство как раз, чтобы связываться объекты активации в scope chain. Больше подошло бы __delegate__ — оно отражает суть механизма и показывает, что по идее, может быть несколько прототипов (делегатов), если это свойство, например, будет списком. Так, к примеру, Io и Self поддерживают множественное наследование, как раз расширения список делегатов. Если, вдруг, станут интересны эксперименты, для JS тоже такое реализуется на прокси-объектах.

                          В случае вызова функции не как метода объекта, this по-умолчанию указывает на глобальный объект


                          Да, там есть сложные заморочки, когда, и вроде кажется, что функция вызвана как метод, но все равно this становится глобальным (или undefined в strict mode):

                          var o = {m: function() {this;}};
                          
                          o.m(); // o
                          (o.m)(); // o
                          
                          (o.m = o.m)(); // global
                          (o.m || o.n)(); // global
                          


                          В целом же, еще раз — статья отличная ;)
                            +3
                            Вот именно о таком комментарии я мечтал вчера вечером :) Спасибо!
                              0
                              Не очень понимаю вот этот пример, могли бы вы объяснить?
                              (o.m || o.n)(); // Почему global?
                              0
                              Гениальная статья, спасибо огромное. Именно такого экскурса в ecma мне не хватало. Попробую поковырять в данном ключен классовый и поддерживающий строгую типизацию ActionScript 3. Вот там весёлости должны получиться.
                                0
                                На сколько разный может быть подход к изучению вещей :)
                                Мне, например, для того чтобы использовать замыкания, не пришлось изучать все до уровня стека и кадров. Тем более реализация тех или иных вещей может зависеть от движка. Ценность статьи однако несомненна, для тех кто любит покопаться в деталях и через это понять, как все работает.

                                Only users with full accounts can post comments. Log in, please.