Тонкости ECMA-262-3. Часть 3. This.

Read this article in: English, Chinese (version1, version2).

В данной небольшой заметке, мы рассмотрим ещё одну сущность, связанную с контекстами исполнения. Речь пойдёт о ключевом слове this.

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

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

Давайте подробней разберём, что же такое this в ECMAScript.

Итак, this является свойством контекста исполнения:

activeExecutionContext = {
  VO: {...},
  this: thisValue
};

где VO — это объект переменных (variable object), который мы подробно разбирали в предыдущей статье.

Значение this напрямую связанно с типом исполняемого кода контекста. Определяется оно при входе в контекст и на протяжении исполнения кода контекста, является неизменным.

Рассмотрим эти случаи подробней.

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

// явное объявление свойства
// глобального объекта
this.a = 10; // global.a = 10
alert(a); // 10

// косвенное, посредством присваивания
// неопределённому до этого идентификатору
b = 20;
alert(this.b); // 20

// также косвенное, посредством объявления
// переменной, поскольку объектом переменных
// в глобальном контексте является сам глобальный объект
var c = 30;
alert(this.c); // 30

Со значением this в коде функции – интереснее. Именно в этом моменте возникает большинство затруднений.

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

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

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

var foo = {x: 10};

var bar = {
  x: 20,
  test: function () {

    alert(this === bar); // true
    alert(this.x); // 20

    this = foo; // ошибка, нельзя менять this

    alert(this.x); // если бы не было ошибки, было бы 10, а не 20

  }

};

// при входе в контекст, значение this
// определёно как объект "bar"; почему – будет
// подробно разобрано ниже

bar.test(); // true, 20

foo.test = bar.test;

// однако, здесь уже this указывает
// на "foo" - при вызове той же функции

foo.test(); // false, 10

Итак, от чего же зависит меняющееся значение this в коде функции? Здесь есть несколько факторов.

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

Это очень важный момент, его нужно понять, запомнить и чётко осознавать, тогда, с определением значения this в любом контексте, никаких проблем не будет. Именно форма выражения вызова влияет на значение this вызываемого контекста; и ничто другое, как можно видеть в некоторых статьях и даже книгах по JavaScript, где могут писать, что “this зависит от того, как описана функция: если это глобальная функция, то this будет указывать на глобальный объект, если же функция является методом объекта, то this всегда будет объектом, которому принадлежит вызываемый метод” – это абсолютно неверно. Забегая вперёд, покажем, что уже обычную глобальную функцию можно активировать разными формами вызова, которые и влияют на разное значение this:

function foo() {
  alert(this);
}

foo(); // global

alert(foo === foo.prototype.constructor); // true

// но при иной форме вызова той же
// функции, this будет иметь уже другое значение

foo.prototype.constructor(); // foo.prototype

Аналогично можно вызвать функцию, описанную как метод объекта, но значение this не будет являться этим объектом:

var foo = {
  bar: function () {
    alert(this);
    alert(this === foo);
  }
};

foo.bar(); // foo, true

var exampleFunc = foo.bar;

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

// опять же, при иной форме вызова той же
// функции, this уже установлен в другое значение

exampleFunc(); // global, false

Каким же образом форма выражения вызова влияет на значение this? Здесь всё завязано на один из внутренних типов реализации – тип Reference, который, для полного понимания определения значения this, нужно рассмотреть подробней.

Псевдокодом, значение типа Reference можно представить в виде обычного объекта с двумя свойствами — база (base) (т.е. объект, которому принадлежит свойство) и имя свойства (property name) внутри базы:

var valueOfReferenceType = {
  base: <base object>,
  propertyName: <property name>
};

Значение типа Reference может быть получено только в двух случаях:

  1. когда мы имеем дело с идентификатором (identifier);
  2. либо же с выражением доступа к свойству (property accessor);

Процесс разрешения имён идентификаторов (identifier resolution) подробно рассматривается в четвёртой части (Цепь областей видимости, Scope chain); здесь же, отметим, что на выходе данного алгоритма всегда будет значение типа Reference (это важно для значения this).

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

var foo = 10;
function bar() {}

в промежуточных операциях, будут получены соответствующие значения типа Reference:

var fooReference = {
  base: global,
  propertyName: 'foo'
};

var barReference = {
  base: global
  propertyName: 'bar'
};

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

function GetValue(value) {

  if (Type(value) != Reference) {
    return value;
  }

  var base = GetBase(value);

  if (base === null) {
    throw new ReferenceError;
  }

  return base.[[Get]](GetPropertyName(value));

}

где внутренний метод [[Get]] получает значение свойства объекта, учитывая также и наследуемые свойства из цепи прототипов:

GetValue(fooReference); // 10
GetValue(barReference); // function object "bar"

Выражение доступа к свойству (property accessor), так же многим известно; осуществляется оно либо через точечную нотацию (когда имя свойства является правильным идентификатором и заранее известно), либо же — через скобочную:

foo.bar();
foo['bar']();

На выходе промежуточного вычисления также будет получено значение типа Reference:

var fooBarReference = {
  base: foo,
  propertyName: 'bar'
};

GetValue(fooBarReference); // function object "bar"

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

Значение this в контексте функции определяется вызывающей стороной (caller-ом) по форме вызова. Если слева от скобок вызова ( ), находится выражение типа Reference, то значением this будет являться базовый объект этого значения типа Reference. Во всех остальных случаях (т.е. при любом другом типе значения, отличном от типа Reference), значением this будет всегда являться null. Но, т.к. null особого смысла для значения this не несёт, автоматом подставляется глобальный объект.

Покажем на примерах:

function foo() {
  return this;
}

foo(); // global

Видим, что слева от скобок вызова стоит выражение типа Reference (поскольку foo — это идентификатор):

var fooReference = {
  base: global,
  propertyName: 'foo'
};

Соответственно, значение this определенно, как база этого значения типа Reference, т.е. глобальный объект. Аналогично с выражением доступа к свойству:

var foo = {
  bar: function () {
    return this;
  }
};

foo.bar(); // foo

Имеем, опять же, значение типа Reference, где базой является объект foo, который и будет использован в качестве значения this при активации функции bar:

var fooBarReference = {
  base: foo,
  propertyName: 'bar
};

Однако, активируя ту же самую функцию, но с другой формой вызова, мы имеем уже другое значение this:

var test = foo.bar;
test(); // global

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

var testReference = {
  base: global,
  propertyName: 'test'
};

Теперь мы можем точно сказать, почему одна и та же функция, но активированная разными формами вызова, имеет и разные значения this — ответ заключён в разных промежуточных значения типа Reference:

function foo() {
  alert(this);
}

foo(); // global, т.к.

var fooReference = {
  base: global,
  propertyName: 'foo'
};

alert(foo === foo.prototype.constructor); // true

// другая форма вызова

foo.prototype.constructor(); // foo.prototype, т.к.

var fooPrototypeConstructorReference = {
  base: foo.prototype,
  propertyName: 'constructor'
};

Ещё (классический) пример динамического определения this по форме выражения вызова:

function foo() {
  alert(this.bar);
}

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

x.test = foo;
y.test = foo;

x.test(); // 10
y.test(); // 20

Итак, как мы уже отметили, в случае, когда слева от скобок вызова находится значение не типа Reference, а любого другого типа, значение this будет автоматически определенно как null, и, как следствие, global.

Рассмотрим примеры таких выражений:

(function () {
  alert(this); // null => global
})();

В данном случае, мы имеем объект Function, но не объект типа Reference (это не идентификатор и не выражение доступа к свойству), соответственно, значение this в конечном итоге будет определенно, как глобальный объект.

Примеры сложнее:

var foo = {
  bar: function () {
    alert(this);
  }
};

foo.bar(); // Reference, OK => foo
(foo.bar)(); // Reference, OK => foo

(foo.bar = foo.bar)(); // global?
(false || foo.bar)(); // global?
(foo.bar, foo.bar)(); // global?

Почему же, используя выражение доступа к свойству (property accessor), промежуточным результатом которого должно являться значение типа Reference, мы, в определённых вызовах, получаем в качестве this не базовый объект (т.е. foo), а global?

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

С первым случаем всё понятно – там однозначно тип Reference и, как следствие, this – это база, т.е. foo.

Во втором случае применяется оператор группировки, который не вызывает, рассмотренный выше, метод получения реального значения объекта из значения типа Reference, т.е. GetValue (см. примечание к 11.1.6). Соответственно, на выходе оператора группировки — всё ещё значение типа Reference, а потому, значение this снова определено, как база, т.е. foo.

В третьем случае, оператор присваивания, в отличие от оператора группировки, вызывает метод GetValue (см. шаг 3 11.13.1). В итоге на выходе уже будет значения типа Function, означающее, что в качестве this будет использован null и, как следствие, global.

Аналогично с четвертым и пятым случаями – оператор запятая и логическое ИЛИ вызывают GetValue, соответственно, мы теряем значение типа Reference и получаем значение типа Function; вновь, this определён как global.

Существует ситуация, когда выражение вызова определит слева от скобок вызова значение типа Reference, однако значение this будет определено как null и, как следствие, global. Это относится к случаю, когда базовым объектом значения типа Reference, является объект активации.

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

function foo() {
  function bar() {
    alert(this); // global
  }
  bar(); // равносильно AO.bar()
}

Объект активации всегда возвращает в качестве значения thisnull (т.е. схематичная запись AO.b() равносильна null.b()). И здесь, мы снова возвращаемся к вышеописанному случаю, и снова, в качестве this подставляется глобальный объект.

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

var x = 10;

with ({

  foo: function () {
    alert(this.x);
  },
  x: 20

}) {

  foo(); // 20

}

// поскольку

var  fooReference = {
  base: __withObject,
  propertyName: 'foo'
};

Аналогичная ситуация должна быть с активацией функции, являющейся параметром выражения catch: в данном случае объект catch также добавляется перед объектом активации, либо глобальным объектом. Однако, данное поведение было определенно как баг ECMA-262-3 и исправлено в новой версии стандарта ECMA-262-5; таким образом, значение this в данной активации должно быть global, но не объект catch:

try {
  throw function () {
    alert(this);
  };
} catch (e) {
  e(); // __catchObject - в ES3, global - исправлено в ES5
}

// по идее

var eReference = {
  base: __catchObject,
  propertyName: 'e'
};

// однако, это баг, и base
// принудительно определяется как
// null => global

var eReference = {
  base: global,
  propertyName: 'e'
};

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

(function foo(bar) {

  alert(this);

  !bar && foo(1); // "должен" быть спец.объект, но всегда global

})(); // global

Есть ещё одна ситуация, связанная со значением this в контексте функции — это вызов функции в качестве конструктора:

function A() {
  alert(this); // вновь сознанный объект, ниже - объект "a"
  this.x = 10;
}

var a = new A();
alert(a.x); // 10

В данном случае, оператор new вызовет внутренний метод [[Construct]] функции “A”, который, в свою очередь, после создания объекта, вызовет внутренний метод [[Call]], всё той же функции “А”, передав в качестве значения this вновь созданный объект.

Существуют два метода, описанные в Function.prototype (а, соответственно, они доступны всем функциям), позволяющие явно указать значение this при вызове функции. Это методы .apply и .call; оба они принимают в качестве первого параметра значение this, которое будет использовано в контексте вызова. Разница между этими методами несущественная: для первого из них вторым параметром обязательно должен быть массив (либо, массиво-подобный объект, например, arguments), в свою очередь, .call может принимать любые параметры; обязательным параметром для обоих методов является лишь первый – значение this.

Примеры:

var b = 10;

function a(c) {
  alert(this.b);
  alert(c);
}

a(20); // this === Global, this.b == 10, c == 20

a.call({b: 20}, 30); // this === {b: 20}, this.b == 20, c == 30
a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40

В данной заметке мы разобрали особенности ключевого слова this в ECMAScript (и они, действительно, – особенности, в отличие, скажем, от C++ или Java). Надеюсь, статья помогла более чётко представить, как работает эта сущность в JavaScript. Как всегда, буду рад ответить на ваши вопросы в комментариях.

10.1.7 – This;
11.1.1 – Ключевое слово this;
11.2.2 – Оператор new;
11.2.3 – Вызовы функций.


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

Дата публикации: 29.06.2009; обновление: 07.03.2010;


Tags: , ,

 
 
 

Leave a Reply

Code: For code you can use tags [js], [text], [ruby] and other.

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>