in ECMAScript

JavaScript. Ядро.

Read this article in: English, German, French, Chinese.

Обратите внимание: доступна обновленная вторая редакция данной статьи.

Данная обзорная лекция является обобщением того, что мы изучили в курсе “Тонкости ECMA-262-3“. Каждый раздел статьи содержит ссылки на соответствующие главы цикла ES3, который вы, в случае желания и интереса, можете рассмотреть подробно, получив более глубокие и детальные описания тем.

Аудитория статьи: опытные программисты, профессионалы.

Мы начнём лекцию с рассмотрения концепции объекта, который является основной ECMAScript.

Объект

ECMAScript, будучи высоко-абстрактным объектно-ориентированным языком программирования, оперирует объектами. Существуют также и примитивы, но и они, когда требуется, также преобразуются в объекты.

Объект — это коллекция свойств, имеющая также связанный с ней объект-прототип. Прототипом является либо также объект, или же значение null.

Рассмотрим простейшую схему объекта, с которой будем работать в последующих описаниях. Прототипом объекта является внутреннее свойство [[Prototype]]. Однако на рисунках мы будем использовать нотацию __<внутреннее-свойство>__, и в частности для прототипа: __proto__.

Для кода:

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

мы имеем следующую структуру с двумя явными собственными (own) свойствами и одним неявным (внутренним) свойством __proto__, которое является ссылкой на прототип объекта foo:

Figure 1. A basic object with a prototype.

Рисунок 1. Простейший объект с прототипом.

Для чего же нужны эти прототипы? Рассмотрим понятие цепи прототипов, чтобы ответить на этот вопрос.

Цепь прототипов

Прототипы также являются обычными объектами и, соответственно, могут иметь свои прототипы. Если прототип имеет непустую ссылку (т.е. ту, которая не равна null) на свой прототип, который в свою очередь имеет ссылку на свой прототип и т.д., такая связка называется цепью прототипов (prototype chain).

Цепь прототипов (prototype chain) — это конечная цепь объектов, которая используется для организации наследования и разделяемых (shared) свойств.

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

В классовой системе данное повторное использование кода (code reuse) называется наследованием классов — мы выносим одинаковую функциональность в класс A, и описываем классы B и C, которые наследуют от A, а также имеют свои собственные дополнения.

ECMAScript не оперирует понятием «класса». Однако стилистика повторного использования кода не сильно отличается (а в некоторых аспектах является даже и более гибкой, чем классовая организация) и достигается за счет цепи прототипов. Данный вид наследования называется делегирующим наследованием (delegation based inheritance) или, ближе к ECMAScript — прототипным наследованием (prototype based inheritance).

Так же, как и в примере с классами A, B и C, в ECMAScript мы создаём объекты: a, b и c. При этом, объект a хранит общую часть объектов b и c. А объекты b и c в свою очередь хранят лишь собственные дополнительные свойства или методы.

var a = {
  x: 10,
  calculate: function (z) {
    return this.x + this.y + z;
  }
};

var b = {
  y: 20,
  __proto__: a
};

var c = {
  y: 30,
  __proto__: a
};

// вызываем унаследованный метод
b.calculate(30); // 60
c.calculate(40); // 80

Как видим, всё достаточно просто. Обратите внимание, объекты b и c имеют доступ к методу calculate, который принадлежит объекту a. И достигается это, повторим ещё раз, именно за счёт цепи прототипов.

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

Заметьте, что значение this при использовании унаследованного метода установлено в оригинальный объект, но не в (прототипный) объект, в котором метод был найден. Т.е. в примере выше this.y берётся из b и c, но не из a. Однако this.x уже берётся из a, и вновь — посредством механизма цепи прототипов.

Если прототип объекта не указан явно, тогда в качестве __proto__ используется объект-прототип по-умолчанию — Object.prototype. Объект Object.prototype также имеет свой прототип __proto__, который является конечным звеном цепи и установлен в null.

На следующей схеме представлена иерархическая структура наших объектов a, b и c:

Figure 2. A prototype chain.

Рисунок 2. Цепь прототипов.

Обратите внимание, ES5 стандартизировал альтернативный путь для прототипного наследования, используя функцию Object.create:

  var b = Object.create(a, {y: {value: 20}});
  var c = Object.create(a, {y: {value: 30}});

Вы можете найти дополнительную информацию о новых ES5 API методах в соответствующей главе.

ES6 однако стандартизирует __proto__, который может быть использован при инициализации объектов.

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

Конструктор

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

Мы можем переписать предыдущий пример с объектами b и c, используя функцию-конструктор. При этом, роль объекта a (который являлся прототипом в предыдущем примере) играет объект Foo.prototype:

// функция-конструктор
function Foo(y) {
  // которая умеет создавать объекты
  // по заданному шаблону: все эти объекты
  // будут иметь родное свойство "y"
  this.y = y;
}

// также "Foo.prototype" хранит ссылку
// на прототип вновь создаваемых объектов,
// поэтому мы можем использовать эту ссылку
// для созданиях наследуемых или разделяемых
// свойств; так же как и в предыдущем случае мы имеем:

// унаследованное свойство "x"
Foo.prototype.x = 10;

// и унаследованный метод"calculate"
Foo.prototype.calculate = function (z) {
  return this.x + this.y + z;
};

// создаём наши объекты "b" и "c"
// используя "шаблон" Foo
var b = new Foo(20);
var c = new Foo(30);

// вызываем унаследованный метод
b.calculate(30); // 60
c.calculate(40); // 80

// покажем, что мы работаем именно
// с теми свойствами, которые ожидаем

console.log(

  b.__proto__ === Foo.prototype, // true
  c.__proto__ === Foo.prototype, // true

  // также "Foo.prototype" автоматически создает
  // специальное свойство "constructor", являющееся
  // ссылкой на саму функцию-конструктор, т.е. на "Foo";
  // объекты "b" и "c" могут найти это свойство посредством
  // делегации и использовать его для проверки своего конструктора

  b.constructor === Foo, // true
  c.constructor === Foo, // true
  Foo.prototype.constructor === Foo, // true

  b.calculate === b.__proto__.calculate, // true
  b.__proto__.calculate === Foo.prototype.calculate // true

);

Данный код можно представить следующими связями:

 Рисунок 3. Связь конструктора и порожденных объектов.

Рисунок 3. Связь конструктора и порожденных объектов.

Схема выше вновь показывает, что любой объект имеет свой прототип. Функция-конструктор Foo также имеет свою ссылку __proto__, которая указывает на объект Function.prototype, и который в свою очередь ссылается посредством своего свойства __proto__ вновь на Object.prototype. При этом, повторим, Foo.prototype просто обычное свойство объекта Foo, являющееся ссылкой на прототип объектов b и c.

Формально, если рассмотреть понятие классификации (а мы только что именно классифицировали новую отдельную сущность — Foo), комбинация функции-конструктора и прототипа порождаемых объектов может быть названа “классом”. В сущности, например первоклассные (first-class) динамические классы Python’а (или CoffeeScript) имеют абсолютно аналогичную реализацию разрешения свойств и методов. С этой точки зрения, классы Python’а являются лишь синтаксическим сахаром для делегирующего наследования, используемого в ECMAScript.

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

// ES6
class Foo {
  constructor(name) {
    this._name = name;
  }

  getName() {
    return this._name;
  }
}

class Bar extends Foo {
  getName() {
    return super.getName() + ' Doe';
  }
}

var bar = new Bar('John');
console.log(bar.getName()); // John Doe

Полное и подробное описание данной темы можно найти в седьмой главе ES3 цикла. Данная глава разделена на две части: Глава 7.1. ООП. Общая теория, где вы найдете описания различных ООП парадигм и стилей, а также их сравнение с ECMAScript, и Глава 7.2. ООП. Реализация в ECMAScript , посвященная ООП именно в ECMAScript.

Теперь, когда мы разобрались с базовой концепцией объекта, давайте посмотрим, как устроен рантайм программы в ECMAScript. Это то, что называется стеком контекстов исполнения (execution context stack), каждый элемент которого абстрактно может быть представлен также в виде объекта. Да, ECMAScript практически везде оперирует понятием объекта 😉

Стек контекстов исполнения

Существует три типа кода в ECMAScript: глобальный код, код функции и eval код. Каждый тип кода выполняется в своем контексте исполнения (execution context). Существует только один глобальный контекст, и может быть множество экземпляров контекстов функции и eval. Каждый вызов функции осуществляет вход в контекст исполнения типа “функция” и исполняет код типа “функция”. Каждый вызов функции eval входит в контекст типа eval и исполняет его код.

Обратите внимание, что одна и та же функция может генерировать бесконечное множество контекстов, поскольку каждый вызов этой функции (даже, если функция вызвана рекурсивно) порождает новый контекст с новым состоянием контекста (context state):

function foo(bar) {}

// вызываем одну и ту же функцию -
// создаём три разных контекста
// при каждом вызове - с различным
// состоянием контекста (например,
// разным значением аргумента "bar")

foo(10);
foo(20);
foo(30);

Контекст исполнения может активировать другой контекст исполнения — к примеру, функция вызывает другую функцию (или глобальный код вызывает глобальную функцию), и т.д. Логически, данные вызовы организованы в виде стека, который называется стеком контекстов исполнения (execution context stack).

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

Когда caller активирует (вызывает) callee, caller приостанавливает своё выполнение и передаёт управление callee. В этот момент callee помещается в стек контекстов исполнения и становится активным (active) контекстом. После того, как callee-контекст завершает свою работу, он возвращает управление caller’у, и продолжается исполнение кода caller’а с места, где он был прерван (далее он может активировать другие контексты) до тех пор, пока не закончится его исполнение, и т.д. Контекст может просто возвратиться (return) успешно или же выйти, с исключением (exception). Брошенное, но не пойманное исключение может выйти (удалить из стека) из одного или более контекстов.

Т.е. весь рантайм (program runtime) ECMAScript представлен в виде стека контекстов исполнения, где вершина этого стека — это активный контекст. Для обозначения контекста исполнения (execution context) мы используем аббревиатуру EC:

 Рисунок 4. Стек контекстов исполнения.

Рисунок 4. Стек контекстов исполнения.

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

На схеме ниже, приняв контекст некой функции как EC1 и глобальный контекст как Global EC, мы имеем следующие модификации стека при входе и выходе из EC1:

 Рисунок 5. Изменения стека контекстов исполнения.

Рисунок 5. Изменения стека контекстов исполнения.

Именно таким образом рантайм-система ECMAScript организует выполнение её кода.

Более подробную информацию о контекстах исполнения в ECMAScript можно найти в соответствующей Главе 1. Контексты исполнения.

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

Контекст исполнения

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

 Рисунок 6. Структура контекста исполнения.

Рисунок 6. Структура контекста исполнения.

Помимо этих трех обязательных свойств: объект переменных (variable object), объект this, и цепь областей видимости (scope chain), контекст исполнения может иметь любые дополнительные свойства в зависимости от реализации.

Рассмотрим эти важные свойства контекста исполнения подробней.

Объект переменных

Объект переменных (variable object) является хранилищем данных, связанных с контекстом исполнения. Это специальный ассоциированный с контекстом объект, который хранит переменные и декларации функций, объявленные в коде контекста.

Обратите внимание, что функциональные выражения (function expressions) в отличие от деклараций функций (function declarations) не включены в объект переменных.

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

Рассмотрим следующий пример в коде глобального контекста:

var foo = 10;

function bar() {} // декларация функции, FD
(function baz() {}); // функциональное выражение, FE

console.log(
  this.foo == foo, // true
  window.bar == bar // true
);

console.log(baz); // ReferenceError, "baz" is not defined

Тогда объект переменных глобального контекста будет иметь следующие свойства (мы используем нотацию VO — сокращение от variable object для описания объекта переменных):

 Рисунок 7. Глобальный объект переменных.

Рисунок 7. Глобальный объект переменных.

И вновь обратите внимание, функция baz, являясь функциональным выражением (function expression) не включена в объект переменных. Именно поэтому мы получаем ReferenceError, когда пытаемся обратиться к ней за пределами её объявления.

Стоит отметить, что в отличие от других языков (например, С/С++), в ECMAScript только функции создают новую область видимости. Переменные и внутренние функции, объявленные внутри функции, не видны снаружи и не “загрязняют” тем самым внешний объект переменных.

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

С объектом переменных глобального контекста всё ясно. А что же насчёт функций и их объектов переменных? В контексте функции, объектом переменных является так называемый объект активации (activation object).

Объект активации

Когда функция активирована (вызвана) caller’ом, создаётся специальный объект, называемый объектом активации (activation object). При инициализации, в него в виде свойств добавляются формальные параметры функции, а также специальный объект arguments, являющийся отображением (map) формальных аргументов по числовым индексам. Далее объект активации используется в качестве объекта переменных контекста функции.

Т.е. объект переменных функции — это такой же обычный объект переменных, но помимо самих переменных и деклараций функций, он ещё хранит формальные параметры и объект arguments, и называется объектом активации.

Для примера ниже:

function foo(x, y) {
  var z = 30;
  function bar() {} // FD
  (function baz() {}); // FE
}

foo(10, 20);

мы имеем следующий объект активации контекста функции foo (сокращение AO означает activation object — объект активации):

 Рисунок 8. Объект активации.

Рисунок 8. Объект активации.

И вновь, функция-выражение baz не включена в объект переменных/активации.

Полное описание темы с уточнением различных тонкостей и нюансов (таких, например, как “поднятие” (“hoisting”) переменных и деклараций функций), можно найти в главе с одноименным названием Глава 2. Объект переменных.

Обратите внимание, в ES5 концепция объекта переменных (variable object) и объекта активации (activation object) заменена моделью лексических окружений (lexical environments), подробное описание которых можно найти в соответствующей главе.

А мы движемся дальше к следующему разделу. Как известно, в ECMAScript мы можем использовать внутренние функции, и в этих внутренних функциях мы можем использовать переменные родительских (внешних) функций или переменные глобального контекста. Поскольку мы обозначили объект переменных как хранилище данных конкретной области видимости (т.е. конкретного контекста), аналогично цепи прототипов, существует так называемая цепь областей видимости (scope chain) — цепь хранилищ данных в различных контекстах. Рассмотрим её подробней.

Цепь областей видимости

Цепь областей видимости (scope chain) — это список объектов, который используется при поиске идентификаторов (переменных), появляющихся в коде контекста.

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

Идентификаторами в контексте являются: имена переменных, деклараций функций, формальных параметров, и т.д. Когда функция в своём коде использует переменную, не являющуюся её локальной переменной (или формальным параметром, или внутренней функцией), то такая переменная называется свободной (free). И именно для поиска этих свободных переменных и используется цепочка областей видимости (scope chain).

В общем случае, scope chain представляет собой список всех тех родительских объектов переменных, плюс (спереди scope chain – первым элементом в списке) — родной объект активации функции. Однако, scope chain может также содержать и любой другой объект, к примеру, объект, динамически добавленный к scope chain во время исполнения кода контекста. Примером такого объекта может являться объект инструкции with или спец. объект инструкции catch.

При поиске (или разрешении, резолвинге — resolving) идентификатора, scope chain просматривается снизу вверх, начиная с объекта активации, и далее (если идентификатор не найден в родном объекте активации) вверх по scope-цепи — повторим, точно так же, как и поиск свойства в цепи прототипов.

var x = 10;

(function foo() {
  var y = 20;
  (function bar() {
    var z = 30;
    // "x" и "y" являются "свободными переменными"
    // и найдены в следующих (после объекта
    // активации bar) объектах scope-цепи bar
    console.log(x + y + z);
  })();
})();

Мы можем представить связь объектов в scope chain посредством внутреннего свойства __parent__, которое указывает на следующий объект в цепи. Данную реализацию можно потестировать, например, в реальном Rhino коде, и именно такой подход используется в модели лексических окружений (lexical environments) в ES5 (только там эта ссылка называется outer). Альтернативной интерпретацией scope-цепи может быть обычный массив (т.к. он тоже является списком объектов). Используя нотацию __parent__, мы можем представить пример выше следующей схемой (при этом, обратите внимание, родительские объекты переменных сохранены в свойстве функции [[Scope]]):

 Рисунок 9. Цепь областей видимости.

Рисунок 9. Цепь областей видимости.

Как мы отметили, во время исполнения кода контекста, scope-цепь может быть расширена при помощи инструкций with и catch. И, поскольку объект инструкции with является обычным объектом, он может иметь прототип (и в целом — цепь прототипов). Поэтому, поиск в scope chain получается двумерным: (1) сначала рассматривается сам текущий объект scope-цепи и далее — (2) на каждом из объектов scope chain — вглубь по цепи прототипов (если объект, конечно, имеет прототип).

Например:

Object.prototype.x = 10;

var w = 20;
var y = 30;

// в движке SpiderMonkey глобальный объект
// т.е. объект переменных глобального
// контекста наследует от "Object.prototype",
// таким образом мы можем ссылаться на
// "несуществующую переменную x", которая
// найдена в цепи прототипов

console.log(x); // 10

(function foo() {

  // локальные переменные функции "foo"
  var w = 40;
  var x = 100;

  // "x" найдена в "Object.prototype",
  // поскольку {z: 50} наследует от него

  with ({z: 50}) {
    console.log(w, x, y , z); // 40, 10, 30, 50
  }

  // после того, как "with" закончил работу, и
  // его объект удален из scope chain, "x" вновь
  // найдена в AO контекста "foo";
  // переменная "w" также локальна
  console.log(x, w); // 100, 40

  // а вот так мы можем ссылаться на
  // затененную (shadowed) глобальную
  // переменную "w" в браузерной среде
  console.log(window.w); // 20

})();

Для кода выше мы имеем слeдующую структуру (т.е. перед тем, как мы переходим по следующей __parent__ ссылки, сначала рассматриваем __proto__ цепь):

Рисунок 10. Цепь областей видимости, расширенная инструкцие "with".

Рисунок 10. Цепь областей видимости, расширенная инструкцией “with”.

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

До тех пор, пока все родительские объекты переменных существуют, нет ничего особенного в получении родительских данных из внутренних функций — мы просто обходим scope chain в поисках нужной переменной. Однако, как мы отметили выше, после того, как контекст завершает свою работу, все его свойства, да и он сам, удаляются. Но в то же время, как мы знаем, внутренняя функция может быть возвращена наружу из родительской функции. Что будет с последующей активацией такой функции, если она использует свободные переменные — при условии, что родительский контекст, где эти свободные переменные были объявлены, уже “уничтожен”? В общей теории, понятие, которое призвано решить эту проблему называется лексическим замыканием (lexical closure) или просто — замыканием. Замыкания в ECMAScript напрямую связаны со scope chain.

Замыкания

В ECMAScript функции являются объектами первого класса (first-class). Данный термин означает, что функции могут быть переданы в качестве аргумента другой функции — в подобном случае они называются “фунаргами” (“funargs”), сокращенно от функциональный аргумет (functional argument).

В свою очередь функции, которые принимают “фунарги” называются функциями более высокого порядка (higher-order functions) или, ближе к математике, операторами (operators).

Функции, которые возвращают другие функции называются функциями с функциональным значением (functions with functional value).

Существуют две концептуальные проблемы, связанные с “фунаргами” и “функциональными значениями” и эти две суб-проблемы обобщены в одну, так называемую, “Фунарг-проблему” (“Funarg problem”) или более полно — “Проблему функционального аргумента”.

И именно для решения полной “фунарг-проблемы”, и было разработано понятие замыкания. Рассмотрим эти две под-проблемы подробней (как мы увидим, в ECMAScript обе они решены с использованием упоминавшегося выше на схемах свойства функций [[Scope]]).

Первый подтип “фунарг-проблемы” называется “наружной (или восходящей) фунарг-проблемой” (“upward funarg problem”). Она проявляется, когда внутренняя функция возвращается наружу вверх (upward) из другой функции и использует уже упоминавшиеся выше свободные переменные. Для того, чтобы иметь возможность использовать переменные родительских контекстов, даже после того, как родительские контексты уже завершились, внутренняя функция в момент создания сохраняет в своём свойстве [[Scope]] родительскую scope-цепь. Далее, когда возвращенная функция будет активирована снаружи, scope chain её контекста будет сформирована как комбинация родного объекта активации и значения сохраненного свойства [[Scope]] (собственно, то, что мы видели на схемах выше):

Scope chain = Activation object + [[Scope]]

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

function foo() {
  var x = 10;
  return function bar() {
    console.log(x);
  };
}

// "foo" возвращает функцию, которая
// использует свободную переменную "x"

var returnedFunction = foo();

// глобальная переменная "x"
var x = 20;

// запуск возвращенной функции
returnedFunction(); // 10, а не 20

Данный стиль области видимости называется статическим или лексическим (static или lexical scope). Видно, что переменная x найдена в сохраненном свойстве [[Scope]] возвращенной функции bar. В общей теории существует также и динамическая область видимости (dynamic scope), где значение переменной x было бы определено как 20, а не 10. Однако, динамическая область видимости не используется в ECMAScript.

Вторым подтипом “фунарг-проблемы” является “внутренняя (или нисходящая) фунарг-проблема” (“downward funarg problem”). В данном случае (в отличие от upward-случая) родительский контекст может существовать, но может возникнуть неоднозначность при поиске идентификатора. Проблема заключается в определении, в какой области видимости должен искаться идентификатор — в статически сохраненной на момент создания функции или же динамически сформированной во время исполнения кода (т.е. в scope-цепи caller’а)? Чтобы избежать эту неоднозначность и сформировать замыкание, используется именно статически сохраненная область видимости:

// глобальная "x"
var x = 10;

// глобальная функция
function foo() {
  console.log(x);
}

(function (funArg) {

  // локальная "x"
  var x = 20;

  // при вызове ниже, неоднозначности
  // не возникает, т.к. мы используем
  // глобальную "x", которая была статически
  // сохранена в свойстве [[Scope]] функции "foo",
  // а не "x" из области видимости caller'а,
  // который активирует "funArg"

  funArg(); // 10, а не 20

})(foo); // передаем foo "вниз" в качестве "фунарга"

Итак, мы можем заключить, что статическая область видимости является обязательным требованием для присутствия в языке замыканий. Однако некоторые языки могут использовать комбинацию динамической и статической областей видимости, оставляя право выбора программисту — что “замыкать”, а что нет. И, поскольку в ECMAScript используется лишь статическая область видимости (иными словами — у нас есть решение для обоих подтипов “фунарг-проблемы”), мы можем сделать заключение, что ECMAScript имеет полную поддержку замыканий, которые технически реализованы посредством внутреннего свойства функций [[Scope]]. Теперь мы можем дать точное определение замыкания:

Замыкание (closure) — это комбинация блока кода (в ECMAScript это функция) и всех статически/лексически сохраненных родительских областей видимости. При этом, посредством этих сохраненных объектов областей видимости, функция может использовать свободные переменные.

Заметьте, поскольку каждая (нормальная) функция сохраняет [[Scope]] при создании, теоретически, все функции в ECMAScript являются замыканиями.

Другим важным моментом, связанным со [[Scope]] и замыканиями, является тот факт, что несколько функций могут иметь один и тот же родительский контекст (это вполне обычная ситуация — представьте, например, две внутренние или глобальные функции). В подобном случае, переменные, хранящиеся в [[Scope]] являются общими на все функции, поскольку все эти функции имеют одну и ту же родительскую scope-цепь. Изменения переменных, произведенные одной функцией, отражаются при чтении этих переменных из другой функции (из другого замыкания):

function baz() {
  var x = 1;
  return {
    foo: function foo() { return ++x; },
    bar: function bar() { return --x; }
  };
}

var closures = baz();

console.log(
  closures.foo(), // 2
  closures.bar()  // 1
);

Данный код может быть представлен следующей схемой:

Рисунок 11. Разделяемое свойство [[Scope]].

Рисунок 11. Разделяемое свойство [[Scope]].

Именно с этой особенностью связана частая ошибка начинающих программистов при создании функций в цикле. Используя переменную-счетчик цикла внутри функции, программисты часто получают неожиданный результат, когда все функции имеют одно и то же значение счетчика внутри функции. Теперь должно быть очевидно, почему так происходит — потому что все эти функции созданы в одном и том же контексте и имеют одно и то же значение свойства [[Scope]], где переменная-счетчик имеет последнее присвоенное (после того, как цикл завершился) значение.

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = function () {
    alert(k);
  };
}

data[0](); // 3, а не 0
data[1](); // 3, а не 1
data[2](); // 3, а не 2

Существует несколько способов исправить эту ситуацию, один из которых — это создать дополнительный промежуточный контекст (дополнительный объект активации) в scope-цепи, к примеру, используя дополнительную функцию:

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = (function (x) {
    return function () {
      alert(x);
    };
  })(k); // передаем значение "k"
}

// теперь всё верно
data[0](); // 0
data[1](); // 1
data[2](); // 2

Те, кто заинтересуется глубже, теоретическое описание замыканий, а также описания их практических применений, можно найти в Главе 6. Замыкания. Для более подробного описания scope-цепи предлагается обратиться к Главе 4. Цепь областей видимости.

А мы переходим к следующему разделу, где рассмотрим последнее свойство контекста исполнения — значение this.

This

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

Любой объект может быть использован в качестве this. Я хотел бы вновь напомнить и прояснить часто неверную трактовку this относительно контекста исполнения. Часто, this неверно описывается, как свойство объекта переменных. Недавняя подобная ошибка была, например, в этой книге (хотя, отмеченная глава данной книги технически вполне хорошая). Запомните ещё раз:

this является свойством контекста исполнения, но не свойством объекта переменных.

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

Кстати, например в Python’е (в отличие от ECMAScript) объект self является обычным аргументом метода и не отличается от других переменных — т.е. так же ищется в лексическом окружении, и даже может быть изменен на другой объект во время исполнения кода метода. В ECMAScript нельзя присвоить this новое значение, поскольку, повторим, этот объект не является переменной и не расположен в объекте переменных.

В глобальном контексте, значением this является сам глобальный объект (что означает, this равен здесь объекту переменных):

var x = 10;

console.log(
  x, // 10
  this.x, // 10
  window.x // 10
);

В случае контекста функции, значение this может меняться при каждом новом вызове одной и той же функции. Здесь уже значение this передаётся caller’ом, и определяется по форме выражения вызова (т.е. по форме того, как функция вызвана).

Например, функция foo ниже является вызванным контекстом (callee) из глобального контекста, который является вызывающим контекстом (caller). Продемонстрируем на примере, как для одной и той же функции, значение this изменяется при каждом новом вызове:

// код функции "foo" никогда
// не меняется, но значение "this"
// контекста меняется от вызова к вызову

function foo() {
  alert(this);
}

// caller активирует "foo" (callee) и
// передает "this" для callee-контекста

foo(); // глобальный объект
foo.prototype.constructor(); // foo.prototype

var bar = {
  baz: foo
};

bar.baz(); // bar

(bar.baz)(); // тоже bar
(bar.baz = bar.baz)(); // а здесь уже глобальный объект
(bar.baz, bar.baz)(); // и здесь глобальный
(false || bar.baz)(); // и тут тоже глобальный

var otherFoo = bar.baz;
otherFoo(); // вновь глобальный объект

Для подробного рассмотрения работы this и механизмов того, как и почему это значение определяется, можно обратиться к одноименной Глава 3. This, где все эти вопросы обсуждаются подробно и в деталях.

Заключение

На этом мы завершаем данный краткий обзор. Хотя, он получился не таким уж и “кратким” 😉 Однако полное подробное описание данных тем потребовало бы целой книги. Мы, тем не менее, не затронули две важные темы: функции (типы функций — декларация функции и функциональное выражение, и их различия), а также стратегию передачи параметров, используемую в ECMAScript. Обе темы можно найти в соответствующих главах ES3 серии: Глава 5. Функции и Глава 8. Стратегия передачи параметров в функцию.

Если у вас есть комментарии, вопросы или дополнения, я буду рад обсудить и ответить на них в комментариях.

Удачи в изучении ECMAScript!

Авторы перевода: Дмитрий Сошников и Антон Бырна
Дата перевода: 18.01.2011

Автор оригинала: Дмитрий Сошников [англ., читать »]
Дата публикации: 02.09.2010

Write a Comment

Comment

41 Comments

  1. Отлично! Спасибо за статью!

  2. Таки непонятно, “scope chain” является свойством VO, или отдельным свойством EC существующим только для AO?
    или AO и VO различаются только названием?

    помимо самих переменных и деклараций функций, он ещё хранит формальные параметры и объект arguments, и называется объектом активации.

    помимо каких переменных? из VO контекста, где определена функция?
    они копируются в AO функции, или VO контекста делается родительским для AO в цепочке?

  3. Правильно ли я понял, что в ECMA заменой динамической области видимости является EVAL?

  4. Спасибо за перевод!

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

    Объект переменных (variable object) – ????
    объект (кого? чего?) – переменных ?

    1. переменная(variable) (какая?) – объектная(- какой-то двойной смысл возникает в русском языке, ассоциативность)

    2. переменная(variable) (какая?) – ассоциированная с контекстом объект (взято ниже из вашего текста)

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

    Еще один нерешенный момент перевода(в главе “Стек контекстов исполнения”):
    Callers и callee – стали именами собственными, что не совсем логично.

    В то же время, контекст может быть как вызываемым(callee) так и вызывающим(caller). (например, функция, вызванная из глобального кода, затем вызывает другую функцию).

    Т.е. выполнение(runtime) программы на ECMAScript представлен в виде стека контекстов исполнения,

    Вместо словосочетания “scope chain” всегда должно быть употреблено словосочетание “Цепь областей видимости”, хоть это и громоздко. В контексте главы это есть своего рода именем собственным.

    “фунаргами” = “фунзначи” = “фунаргуи” 😀

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

    С глубоким уважением к вашему труду
    Ч.М.А. AK Tracker

  5. @qMax

    Таки непонятно, “scope chain” является свойством VO, или отдельным свойством EC существующим только для AO?

    Да, “scope chain” это свойство контекста исполнения (EC) — о чем свидетельствует схема 6 в соответствующем разделе выше.

    или AO и VO различаются только названием?

    С точки зрения хранения данных контекста — да, по сути только названием. А вообще, как было отмечено, VO — это лишь абстрактное понятие. Т.е., если не важно, о каком типе контекста идет речь, можно везде называть VO. Если же мы разделяем тип контекста, то — в глобальном контексте VO — это сам глобальный объект, а в контексте функции, VO — это AO.

    помимо каких переменных? из VO контекста, где определена функция?

    Из родного контекста функции:

    function foo(c) {
      var a = 10;
      var b = 20;
      alert(arguments);
    }

    Вот помимо переменных a и b, AO (т.е. VO в контексте функции) хранит ещё и параметр c, а также объект arguments.

    или VO контекста делается родительским для AO в цепочке?

    VO порождающего контекста, т.е. контекста, в котором функция была создана, становится следующим в scope-цепи контекста этой функции.

  6. @Echo

    Правильно ли я понял, что в ECMA заменой динамической области видимости является EVAL?

    Да, eval в данном случае является особенностью, привносящей динамику в статическую область видимости. Но — лишь в том отношении, что нельзя на этапе парсинга определить, в какой области видимости будет разрешена переменная. Аналогичной инструкцией является with. Более подробно можно почитать в статье, посвященной общей теории лексических окружений — ECMA-262-5 in detail. Chapter 3.1. Lexical environments: Common Theory.

  7. @Tracker

    Объект переменных (variable object) – ????

    Да — т.е. “объект, хранящий переменные”. Данный русский перевод использовался с самого начала в одноимённой статье.

    Callers и callee – стали именами собственными, что не совсем логично.

    Да, возможно; но это использовано лишь для того, чтобы не сбивать главную суть объяснения. А вообще, да, caller и callee — это “имена собственные” (лишь абстракция для вызывающей и вызываемой сторон) в этой отношении. И в ECMAScript они используются именно с этой позиции.

    Вместо словосочетания “scope chain” всегда должно быть употреблено словосочетание “Цепь областей видимости”, хоть это и громоздко.

    Да, логично; но я думаю, можно меня простить за, опять же, желание сконцентрировать именно на сути предмета, делая более короткими “имена нюансов” — при этом, не меняя и не упрощая саму суть, естественно 😉

    “фунаргами” = “фунзначи” = “фунаргуи” 😀

    Нет; все-таки, “фунаргами” 🙂

    Спасибо за подробный комментарий, рад видеть все больше людей, заинтересованных в глубоком JavaScript.

  8. Спасибо огромное, очень люблю такого рода “глубокие” статьи.

  9. А с помощью какой программы такие схемы связей нарисовать можно?

  10. Отличный материал!

    Заметил опечатку:
    “называется вызывающим контекстов” – несогласовано

  11. Дмитрий.

    А у Вас не было желания написать что-нить вроде “JavaScript. The bad Parts”? Как недавно признался Брендан, язык создавался в спешке, буквально за неделю и если бы у него было больше времени, многое можно было бы сделать по другому (think prototype inheritance 😉 ).


    Dmitry Sychov

  12. @Dmitry Sychov

    “Bad parts” — они, порой, субъективны. У Крокфорда есть такой раздел в его “Good parts”, но и там тоже не все может быть отнесено к bad part’ами.

    Вообще, о хорошем писать приятней все-таки 😀 Плохие стороны касаются недочетов в дизайне, либо же старых решений, которые на сегодня устарели и требуют обновления — да, об этом можно рассказать. Но вообще, критикуя, нужно предлагать. И такие предложения периодически поступают (и от меня в том числе) в главной дискуссии по дизайну ECMAScript — мэйл-листе es-discuss. В целом, мои статьи аналитические, а не с критической позиции.

    В прототипном наследовании нет ничего сложного, и в целом оно не является напрямую плохой стороной языка. JS позволяет создать сахар для работы с классами. Комбинация “конструктор + прототип объекта” является классом. Как было отмечено, например классы Python’a или CoffeeScript являются всего лишь синтаксическим сахаром для делегирующего (прототипного) наследования, используемого в JS.

  13. Отлично все «разжевали», спасибо!
    У меня вопрос, статьи на русском — это полные аналоги тех что на английском?

  14. @Ruzzz

    статьи на русском — это полные аналоги тех что на английском?

    Да, к тому же изначальные оригиналы статей (ES3) были именно русские. Сейчас за основу выбран английский, но русские переводы периодически тоже появляются.

  15. Большое спасибо! Очень интересно и полезно!

  16. Дмитрий , как Вы считаете, может точнее будет так:

    Прототипом объекта является внутреннее свойство [[Prototype]] ?
    Внутреннее свойство [[Prototype]] содержит ссылку на прототип объекта

    объект a хранит ту общую часть объектов b и c ?
    объект a хранит общую часть объектов b и c

  17. @Stanislav, спасибо, рад, что полезно.

    @arbeiter

    Дмитрий , как Вы считаете, может точнее будет так:

    Прототипом объекта является внутреннее свойство [[Prototype]] ?
    Внутреннее свойство [[Prototype]] содержит ссылку на прототип объекта

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

    объект a хранит ту общую часть объектов b и c ?
    объект a хранит общую часть объектов b и c

    Да, можно заменить.

  18. По Рисунку 1.

    var foo = {
      x: 10,
      y: 20
    };
    alert(foo.__proto__); // Object
    

    Создав объект foo , с неявным (внутренним) свойством __proto__ , интерпретатор поместил в него (в свойство __proto__) ссылку на объект Object.

    И это естественно, ибо никаких прямых указаний на объект-прототип ему не было дано и он выполнил действия по умолчанию.

    Поэтому в заголовке правого блоке вместо foo prototype должно стоять Object.

    А вот саму стрелку можно подписать foo prototype. Тогда рисунок получает логическую законченность. Слева объект foo , а справа его прототип объект Object.

    Или я не прав ?

  19. @arbeiter

    Создав объект foo , с неявным (внутренним) свойством __proto__ , интерпретатор поместил в него (в свойство __proto__) ссылку на объект Object.

    На Object.prototype.

    Поэтому в заголовке правого блоке вместо foo prototype должно стоять Object.

    “foo prototype” — это абстрактное описание, что у объекта foo есть некий прототип. В данном моменте еще речи не идет, что именно является прототипом (т.е. Object.prototype). Поэтому, всё правильно там стоит на рисунке 😉

    Слева объект foo , а справа его прототип объект Object.

    Или я не прав ?

    Неправы в плане Object. Еще раз — Object.prototype. В общем случае, прототипом объекта является ФункцияКонструктор.prototype.

  20. Два уточняющих вопроса.

    Вы пишите – В общем случае, прототипом объекта является ФункцияКонструктор.prototype.

    Как известно prototype является свойством объекта Function , поэтому ФункцияКонструктор.prototype – это свойство (переменная) , которое хранит ссылку на объект прототипа, т.е.это не сам объект.

    Если Вы оперируете конструкцией Object.prototype для подчеркивания , что это именно объект-прототип то мне это понятно. Если в это вкладывается другой смысл, то нет.

  21. @arbeiter

    Как известно prototype является свойством объекта Function , поэтому ФункцияКонструктор.prototype – это свойство (переменная) , которое хранит ссылку на объект прототипа, т.е.это не сам объект.

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

    Общее правило простое, которое можно запомнить: если говорят, что “значением свойства является объект“, то автоматом подразумевается, что “значением свойства является ссылка на объект“. По-другому быть не может в JavaScript.

    На схемах, стрелка указателя уже показывает, что значением свойства является ссылка.

    Вот здесь еще можно почитать: Name binding.

    Если Вы оперируете конструкцией Object.prototype для подчеркивания , что это именно объект-прототип то мне это понятно. Если в это вкладывается другой смысл, то нет.

    Нет никакого другого смысла, именно это и имеется в виду. Просто Вы написали “прототипом является Object, я поправил: “не сам Object, а Object.prototype.

    Вот здесь тоже описана схема: Explicit prototype and implicit [[Prototype]] properties.

  22. Спасибо, с принятой системой обозначений и умолчаний разобрались.

  23. Если в Вашем примере , демонстрирующем работу конструктора , убрать операторы Foo.prototype.x = 10; и Foo.prototype.calculate = function … , то какое значение примет внутреннее свойство __proto__ объектов b и c.

  24. @arbeiter

    Еще раз, в объект.__proto__ всегда записывается ссылка на ФункцияКонструктор.prototype — независимо от того, будет (или не будет) что-то добавлено в ФункцияКонструктор.prototype.

    (есть, правда, исключение — если ФункцияКонструктор.prototype — не объект, то берется дефолтное значение Object.prototype)

    Поэтому ответ — будет так же Foo.prototype. Перечитайте еще раз эту часть и полностью часть 7.2; в частности алгоритм создания объектов — там все это подробно расписано.

  25. Спасибо Дмитрий я понял , отдельное спасибо за терпение. Последний вопрос по теме конструктор.

    Почему в примере Вы предпочли использовать Foo.prototype.x = 10; вместо альтернативы – указать в теле конструктора this.x = 10;

  26. @arbeiter

    Почему в примере Вы предпочли использовать Foo.prototype.x = 10; вместо альтернативы – указать в теле конструктора this.x = 10;

    В данном случае, это сделано, чтобы показать, как работает наследование. Свойство x — наследуемое для a и b, и потому помещено в прототип.

    Если б нужно было, чтобы каждый из объектов имел свое свойство x, тогда бы было this.x = 10.

  27. Статья супер. Спасибо!
    Вопрос.
    “Для того, чтобы иметь возможность использовать переменные родительских контекстов, даже после того, как родительские контексты уже завершились, внутренняя функция в момент создания сохраняет в своём свойстве [[Scope]] родительскую scope-цепь.” Когда происходит момент создания функции? Покажите место в коде, когда сохраняется [[scope]]. 2 примера проблем в замыкании. Огромное спасибо, не могу уснуть:))

    // КОД РАЗ
    
    // глобальная "x"
    var x = 10;
     
    // глобальная функция
    function foo() {
      console.log(x);
    }
     
    (function (funArg) {
     
      // локальная "x"
      var x = 20;
     
      // при вызове ниже, неоднозначности
      // не возникает, т.к. мы используем
      // глобальную "x", которая была статически
      // сохранена в свойстве [[Scope]] функции "foo",
      // а не "x" из области видимости caller'а,
      // который активирует "funArg"
     
      funArg(); // 10, а не 20
     
    })(foo); // передаем foo "вниз" в качестве "фунарга"
    // КОД ДВА
    // глобальная "x"
    var x = 10;
     
    // глобальная функция
    function foo() {
      console.log(x);
    }
     
    (function (funArg) {
     
      // локальная "x"
      var x = 20;
     
      // при вызове ниже, неоднозначности
      // не возникает, т.к. мы используем
      // глобальную "x", которая была статически
      // сохранена в свойстве [[Scope]] функции "foo",
      // а не "x" из области видимости caller'а,
      // который активирует "funArg"
     
      funArg(); // 10, а не 20
     
    })(foo); // передаем foo "вниз" в качестве "фунарга"
  28. @Рома

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

  29. Дмитрий.
    вы ответили “декларации функций создаются при входе в контекст”.
    т.е.

    var x = 10;
    // тут происходит создание функции, foo.[[scope]] = activeContext.scope.
    function foo() {
       return x;
    }
    
    // тут уже есть sсope?
    foo();

    Спасибо большое за ответ)

  30. @Рома

    тут уже есть sсope?

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

    // Вызываем функцию "до" объявления
    foo(); // 10, scope есть
    
    var x = 10;
    
    function foo() {
      console.log(x);
    }
    

    Функциональные выражения обрабатываются при запуске кода, и поэтому их можно вызывать только после определения:

    // Вызываем функцию "до" объявления
    // foo(); // Error
    
    var x = 10;
    
    var foo = function() {
      console.log(x);
    };
    
    foo(); // 10, OK, scope есть, как и сама функция
    

    Еще раз прочитайте часть 2 и часть 5.

  31. Дмитрий, проясните, пожалуйста одну вещь. Вот Вы пишете

    ECMAScript, будучи высоко-абстрактным объектно-ориентированным языком программирования, оперирует объектами. Существуют также и примитивы, но и они, когда требуется, также преобразуются в объекты.

    Д.Флэнаган в своей книге пишет следующее:

    Any value in JavaScript that is not a string, a number, true, false, null, or undefined is an object. And even though strings, numbers, and booleans are not objects, they behave like immutable objects.

    Мне всегда казалось, что строка, например, это объект. Точнее массив. Так как строка в JavaScript ведет себя как массив символов:

    var str = "Hello!";
    str[5]; // "!"
    

    Сегодня я был немного удивлен, узнав, что с числами можно творить вещи и поинтереснее:

    2..toString();
    2 .toString();
    (2).toString();
    

    Этот пример из JavaScript Garden
    Там же говорится что:

    Everything in JavaScript acts like an object, with the only two exceptions being null and undefined.

    Я так и не понял, являются ли простые типы объектами или нет. Поясните, пожалуйста.

  32. @Scripter

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

    var s = "hello";
    console.log(s[0], s.charAt(0)); // "h", "h", якобы объект
    
    s.bar = 100; // и даже присвоить новое свойство "можно"
    
    console.log(s.bar); // undefined, но потом его нет
    

    Поэтому фраза “behave like immutable objects” верна, примитивы могут вести себя как объекты (и то, только на чтение), но все же, они не являются объектами.

    Сегодня я был немного удивлен, узнав, что с числами можно творить вещи и поинтереснее:

    2..toString();
    2 .toString();
    (2).toString();
    

    Да, это пример был описан в разделе наследования (и еще раньше Zeroglif‘ом), т.к. позволяет наиболее удачно одной строкой показать суть сразу нескольких тем — наследование через делегацию, “автобоксинг” примитивов и особенности парсера в JS.

  33. Большая вам благодарность за статью.

    Все собранное воедино наконец-то выстраивается в единую картину как таки работает этот JavaScript.

    Описанное выше касается так же ActionScript (кроме типизации) ?

    И еще один вопрос. Каким плагином реализованы у вас в начале каждой статьи ссылки на версии на других языках?

  34. Аудитория статьи: опытные программисты, профессионалы.

    Позвольте Дмитрий, полюбопытствовать. Значит ли, что если данная статья понятна читателю, то читатель – опытный программист, профессионал(в контексте этой статьи)? Спасибо.

  35. @Timur, я думаю, это значит, что багаж накопленного профессионального опыта (возможно, не связанного напрямую со статьей), сделает восприятие тем статьи более легким.

  36. запятую забыли в примере с Конструктором “Foo.prototype”
    в строке 48

  37. Огромное спасибо. Все гениально просто изложено!

  38. why (0,''.charAt)() gives string expected in ie6?

    more specifically

    try{  (0,''.charAt)() } catch(e){for(k in e) alert(e[k])}