Я заметил, что thisна сайте Stack Overflow нет четкого объяснения того, что такое ключевое слово и как оно правильно (и неправильно) используется в JavaScript.
Я был свидетелем очень странного поведения с ним и не мог понять, почему это произошло.
Как thisработает и когда его следует использовать?
Решение проблемы
thisэто ключевое слово в JavaScript, которое является свойством контекста выполнения. Его основное использование в функциях и конструкторах. Правила thisдовольно просты (если придерживаться лучших практик).
Техническое описание thisв спецификации
Стандарт ECMAScript определяет thisс помощью абстрактной операции (сокращенно AO ) ResolveThisBinding:
[AO] ResolveThisBinding […] определяет привязку ключевого слова this, используя LexicalEnvironment текущего контекста выполнения. [Шаги]:
Глобальные записи среды, записи среды модуля и записи среды функции имеют собственный метод GetThisBinding.
АО GetThisEnvironment находит LexicalEnvironment текущего исполняемого контекста и ближайшую восходящую запись среды (путем итеративного доступа к их свойствам [[OuterEnv]]), которая имеет привязку this (т. е. HasThisBinding возвращает true ). Этот процесс заканчивается одним из трех типов записей среды.
Значение thisчасто зависит от того, находится ли код в строгом режиме.
Возвращаемое значение GetThisBinding отражает значение thisтекущего контекста выполнения, поэтому всякий раз, когда устанавливается новый контекст выполнения, thisразрешается в отдельное значение. Это также может произойти при изменении текущего контекста выполнения. В следующих подразделах перечислены пять случаев, когда это может произойти.
Вы можете поместить образцы кода в проводник AST, чтобы следить за деталями спецификации.
1. Глобальный контекст выполнения в скриптах
Это код сценария, оцениваемый на верхнем уровне, например, непосредственно внутри <script>:
<script>
// Global context
console.log(this); // Logs global object.
setTimeout(function(){
console.log("Not global context");
});
</script>
В начальном глобальном контексте выполнения скрипта оценка thisприводит к тому, что GetThisBinding выполняет следующие шаги:
Конкретный метод GetThisBinding глобальной записи среды envRec […] [делает это]:
Свойству [[GlobalThisValue]] глобальной записи среды всегда присваивается определяемый хостом глобальный объект, который доступен через globalThis( windowв Интернете, globalна Node.js; Документы на MDN ). Следуйте инструкциям InitializeHostDefinedRealm, чтобы узнать, как возникает свойство [[GlobalThisValue]].
2. Глобальный контекст выполнения в модулях
Модули были представлены в ECMAScript 2015.
Это относится к модулям, например, когда они находятся непосредственно внутри файла <script type="module">, в отличие от простого файла <script>.
В начальном глобальном контексте выполнения модуля оценка thisприводит к тому, что GetThisBinding выполняет следующие шаги:
Конкретный метод GetThisBinding модуля Environment Record […] [делает это]:
В модулях значение thisвсегда находится undefinedв глобальном контексте. Модули неявно находятся в строгом режиме.
3. Ввод оценочного кода
Есть два вида evalзвонков: прямые и непрямые. Это различие существует с 5-го издания ECMAScript.
- Прямой
evalвызов обычно выглядит какeval(…);или(eval)(…);(или((eval))(…);и т. д.). 1 Это только в том случае, если выражение вызова соответствует узкому шаблону. 2 - Косвенный
evalвызов включает в себя вызов ссылки на функциюevalлюбым другим способом. Это может бытьeval?.(…),(…, eval)(…),window.eval(…),eval.call(…,…)и т. д. Учитываяconst aliasEval1 = eval; window.aliasEval2 = eval;, это также будетaliasEval1(…),aliasEval2(…). Отдельно, учитываяconst originalEval = eval; window.eval = (x) => originalEval(x);, вызовeval(…)также был бы косвенным.
См. ответ chuckj на «(1, eval)('this') vs eval('this') в JavaScript?» и подробный ECMA-262-5 Дмитрия Сошникова - Глава 2: Строгий режим ( в архиве ), когда вы можете использовать косвенный eval()вызов.
PerformEval выполняет evalкод. Он создает новую декларативную запись среды в качестве своей LexicalEnvironment, из которой GetThisEnvironment получает thisзначение.
Затем, если он thisпоявляется в evalкоде, вызывается метод GetThisBinding записи среды, найденной GetThisEnvironment, и возвращается его значение.
И созданная декларативная Environment Record зависит от того, evalбыл ли вызов прямым или косвенным:
- В прямой оценке это будет основано на LexicalEnvironment текущего исполняемого контекста.
- При косвенной оценке оно будет основано на свойстве [[GlobalEnv]] ( глобальная запись среды ) записи области, которая выполнила косвенную оценку.
Что значит:
- При прямой оценке
thisзначение не меняется; это взято из лексической области, которая вызвалаeval. - При косвенной оценке
thisзначением является глобальный объект (globalThis).
Что насчет new Function? — new Functionпохож на eval, но не вызывает код сразу; он создает функцию. Эта привязка нигде здесь не применяется, за исключением случаев, когда вызывается функция, которая работает нормально, как описано в следующем подразделе.
4. Ввод функционального кода
Ввод кода функции происходит при вызове функции.
Существует четыре категории синтаксиса для вызова функции.
- АО EvaluateCall выполняется для этих трех: 3
- Нормальные вызовы функций
- Необязательные вызовы цепочки
- Шаблоны с тегами
- И EvaluateNew выполняется для этого: 3
- Вызов конструктора
Фактический вызов функции происходит в Call AO, который вызывается с thisValue, определенным из контекста; этот аргумент передается в длинной цепочке вызовов, связанных с вызовом. Call вызывает внутренний слот [[Call]] функции. Это вызывает PrepareForOrdinaryCall, где создается новая функция Environment Record:
Запись среды функции — это декларативная запись среды, которая используется для представления области действия функции верхнего уровня и, если функция не является ArrowFunction, обеспечивает thisпривязку. Если функция не является функцией ArrowFunction и ссылается на super, ее функция Environment Record также содержит состояние, которое используется для выполнения superвызовов метода из функции.
Кроме того, в записи среды функции есть поле [[ThisValue]]:
Это thisзначение используется для этого вызова функции.
Вызов NewFunctionEnvironment также устанавливает свойство [[ThisBindingStatus]] функциональной среды.
[[Call]] также вызывает OrdinaryCallBindThis, где соответствующий thisArgument определяется на основе:
- оригинальная ссылка,
- вид функции и
- находится ли код в строгом режиме.
После определения последний вызов метода BindThisValue только что созданной функции Environment Record фактически устанавливает для поля [[ThisValue]] значение thisArgument.
Наконец, именно в этом поле находится запись функции Environment Record. GetThisBinding AO gets the value for this from:
Конкретный метод GetThisBinding функции Environment Record envRec […] [делает это]:
[…]
3. Вернуть envRec.[[ThisValue]].
Опять же, как именно определяется это значение, зависит от многих факторов; это был просто общий обзор. На этом техническом фоне давайте рассмотрим все конкретные примеры.
Стрелочные функции
Когда функция стрелки оценивается, внутренний слот [[ThisMode]] объекта функции устанавливается в «лексический» в OrdinaryFunctionCreate.
At OrdinaryCallBindThis, который принимает функцию F:
undefined). […]что просто означает, что остальная часть алгоритма, который связывает это, пропускается. Стрелочная функция не привязывает собственное значение this.
Итак, что же thisвнутри стрелочной функции? Оглядываясь назад на ResolveThisBinding и GetThisEnvironment, метод HasThisBinding явно возвращает false.
Конкретный метод HasThisBinding функции Environment Record envRec […] [делает это]:
Таким образом, внешняя среда просматривается итеративно. Процесс завершится в одной из трех сред, имеющих привязку this.
Это просто означает, что в телах стрелочных функций thisисходит из лексической области действия стрелочной функции или, другими словами (из функции стрелок против объявления/выражений функции: они эквивалентны/заменяемы? ):
Стрелочные функции не имеют собственной thisпривязки […]. Вместо этого [этот идентификатор] разрешается в лексической области видимости, как и любая другая переменная. Это означает, что внутри стрелочной функции this[относится] к [значению this] в среде, в которой определена стрелочная функция (т. е. «вне» стрелочной функции).
Свойства функции
В обычных функциях ( function, методах ) thisопределяется тем, как вызывается функция.
Вот где эти «варианты синтаксиса» пригодятся.
Рассмотрим этот объект, содержащий функцию:
const refObj = {
func: function(){
console.log(this);
}
};
В качестве альтернативы:
const refObj = {
func(){
console.log(this);
}
};
В любом из следующих вызовов функций thisзначение внутриfunc will be refObj.1
refObj.func()refObj["func"]()refObj?.func()refObj.func?.()refObj.func``
Если вызываемая функция синтаксически является свойством базового объекта, то эта база будет «ссылкой» вызова, которой в обычных случаях будет значение this. Это объясняется этапами оценки, указанными выше; например, в refObj.func()(или refObj["func"]()) CallMemberExpression — это полное выражение refObj.func(), состоящее из MemberExpression refObj.func и Arguments ().
Но также refObj.funcи refObjиграть три роли, каждая:
- они оба выражения,
- они оба ссылки, и
- они обе ценности.
refObj.funcв качестве значения является вызываемым функциональным объектом; соответствующая ссылка используется для определения thisпривязки.
Примеры необязательных цепочек и шаблонов с тегами работают очень похоже: в основном ссылка — это все до ?.(), до ``или до ().
EvaluateCall использует IsPropertyReference этой ссылки, чтобы синтаксически определить, является ли она свойством объекта. Он пытается получить свойство [[Base]] ссылки (например refObj, при применении к refObj.func; или foo.barпри применении к foo.bar.baz). Если оно записано как свойство, то GetThisValue получит это свойство [[Base]] и будет использовать его как значение this.
Примечание. Геттеры/сеттеры работают так же, как и методы в отношении this. Простые свойства не влияют на контекст выполнения, например здесь, thisнаходятся в глобальной области видимости:
const o = {
a: 1,
b: this.a, // Is `globalThis.a`.
[this.a]: 2 // Refers to `globalThis.a`.
};
Вызовы без базовой ссылки, строгий режим иwith
Вызов без базовой ссылки обычно является функцией, которая не вызывается как свойство. Например:
func(); // As opposed to `refObj.func();`.
Это также происходит при передаче или назначении методов или использовании оператора запятой. Именно здесь важна разница между эталонной записью и значением.
Примечание. Функция j: следуя спецификации, вы заметите, что она jможет возвращать только сам объект функции (значение), но не справочную запись. Поэтому базовая ссылка refObjтеряется.
const g = (f) => f(); // No base ref.
const h = refObj.func;
const j = () => refObj.func;
g(refObj.func);
h(); // No base ref.
j()(); // No base ref.
(0, refObj.func)(); // Another common pattern to remove the base ref.
EvaluateCall вызывает Call с неопределенным значением thisValue. Это имеет значение в OrdinaryCallBindThis ( F: объект функции; thisArgument: thisValue, переданный Call ):
[…]
[…]
Примечание: шаг 5 устанавливает фактическое значениеthis to the supplied thisArgument in strict mode — undefined in this case. In "sloppy mode", an undefined or null thisArgument results in this being the global this value.
Если IsPropertyReference возвращает false, EvaluateCall выполняет следующие действия:
Отсюда может появиться неопределенное thisValue: refEnv. WithBaseObject () всегда undefined, за исключениемwith операторов. В этом случае thisValue будет объектом привязки.
Там также Symbol.unscopables( Документы на MDN ) для управления withповедением привязки.
Подводя итог, пока:
function f1(){
console.log(this);
}
function f2(){
console.log(this);
}
function f3(){
console.log(this);
}
const o = {
f1,
f2,
[Symbol.unscopables]: {
f2: true
}
};
f1(); // Logs `globalThis`.
with(o){
f1(); // Logs `o`.
f2(); // `f2` is unscopable, so this logs `globalThis`.
f3(); // `f3` is not on `o`, so this logs `globalThis`.
}
и:
"use strict";
function f(){
console.log(this);
}
f(); // Logs `undefined`.
// `with` statements are not allowed in strict-mode code.
thisОбратите внимание, что при вычислении не имеет значения, где определена нормальная функция.
.call,,, thisArg .applyи примитивы _.bind
Другим следствием шага 5 OrdinaryCallBindThis в сочетании с шагом 6.2 (6.b в спецификации) является то, что примитивное значение this приводится к объекту только в «неаккуратном» режиме.
Чтобы изучить это, давайте представим другой источник значения this: три метода, которые переопределяют привязку this: 4
Function.prototype.apply(thisArg, argArray)Function.prototype.{call,bind}(thisArg,...args)
.bindсоздает связанную функцию, привязка которой имеет значение thisArg и не может измениться снова. .callи .applyнемедленно вызовите функцию с привязкой this, установленной в thisArg.
.callи .applyсопоставьте непосредственно с Call, используя указанный thisArg. .bindсоздает связанную функцию с BoundFunctionCreate. У них есть собственный метод [[Call]], который ищет внутренний слот [[BoundThis]] функционального объекта.
Примеры установки пользовательского значения this:
function f(){
console.log(this);
}
const myObj = {},
g = f.bind(myObj),
h = (m) => m();
// All of these log `myObj`.
g();
f.bind(myObj)();
f.call(myObj);
h(g);
Для объектов это одинаково в строгом и нестрогом режимах.
Теперь попробуйте указать примитивное значение:
function f(){
console.log(this);
}
const myString = "s",
g = f.bind(myString);
g(); // Logs `String { "s" }`.
f.call(myString); // Logs `String { "s" }`.
В нестрогом режиме примитивы принудительно преобразуются в объектную оболочку. Это тот же объект, который вы получаете при вызове Object("s")или new String("s"). В строгом режиме вы можете использовать примитивы:
"use strict";
function f(){
console.log(this);
}
const myString = "s",
g = f.bind(myString);
g(); // Logs `"s"`.
f.call(myString); // Logs `"s"`.
Библиотеки используют эти методы, например, jQuery устанавливает thisэлемент DOM, выбранный здесь:
$("button").click(function(){
console.log(this); // Logs the clicked button.
});
Конструкторы, классы иnew
При вызове функции как конструктора с помощью newоператора EvaluateNew вызывает Construct, который вызывает метод [[Construct]]. Если функция является базовым конструктором (т.е. не class extends… {… }), она устанавливает thisArgument в новый объект, созданный из прототипа конструктора. Свойства, установленные thisв конструкторе, в конечном итоге будут применены к результирующему объекту экземпляра. thisнеявно возвращается, если только вы явно не вернете свое собственное непримитивное значение.
A class— это новый способ создания функций-конструкторов, представленный в ECMAScript 2015.
function Old(a){
this.p = a;
}
const o = new Old(1);
console.log(o); // Logs `Old { p: 1 }`.
class New{
constructor(a){
this.p = a;
}
}
const n = new New(1);
console.log(n); // Logs `New { p: 1 }`.
Определения классов неявно находятся в строгом режиме:
class A{
m1(){
return this;
}
m2(){
const m1 = this.m1;
console.log(m1());
}
}
new A().m2(); // Logs `undefined`.
super
Исключением для поведения с newявляется class extends… {… }, как упоминалось выше. Производные классы не сразу устанавливают свое значение this при вызове; они делают это только после того, как базовый класс будет достигнут через серию superвызовов (происходит неявно без собственного constructor). Использование thisперед вызовом superне допускается.
Вызов superвызывает суперконструктор со значением this лексической области (функция Environment Record) вызова. GetThisValue имеет специальное правило для superвызовов. Он использует BindThisValue для установки thisэтой записи среды.
class DerivedNew extends New{
constructor(a, a2){
// Using `this` before `super` results in a ReferenceError.
super(a);
this.p2 = a2;
}
}
const n2 = new DerivedNew(1, 2);
console.log(n2); // Logs `DerivedNew { p: 1, p2: 2 }`.
5. Оценка полей класса
Поля экземпляра и статические поля были представлены в ECMAScript 2022.
Когда a classоценивается, выполняется ClassDefinitionEvaluation, изменяя текущий контекст выполнения. Для каждого ClassElement:
- если поле статическое, то
thisотносится к самому классу, - если поле не является статическим, то
thisссылается на экземпляр.
Частные поля (например #x) и методы добавляются в PrivateEnvironment.
Статические блоки в настоящее время являются предложением этапа 3 TC39. Статические блоки работают так же, как и статические поля и методы: thisвнутри них имеется ссылка на сам класс.
Обратите внимание, что в методах и геттерах/сеттерах thisработает так же, как и в обычных свойствах функций.
class Demo{
a = this;
b(){
return this;
}
static c = this;
static d(){
return this;
}
// Getters, setters, private modifiers are also possible.
}
const demo = new Demo;
console.log(demo.a, demo.b()); // Both log `demo`.
console.log(Demo.c, Demo.d()); // Both log `Demo`.
1: (o.f)()эквивалентно o.f(); (f)()эквивалентно f(). Это объясняется в этой статье 2ality ( в архиве ). В частности, посмотрите, как оценивается ParenthesizedExpression.
2: Это должно быть MemberExpression, не должно быть свойством, должно иметь [[ReferencedName]] точно "eval" и должно быть встроенным объектом %eval%.
3: Всякий раз, когда в спецификации говорится: «Пусть ref будет результатом вычисления X», тогда X is some expression that you need to find the evaluation steps for. For example, evaluating a MemberExpression or CallExpression is the result of one of these algorithms. Some of them result in a Reference Record.
4: Есть также несколько других собственных и хост-методов, которые позволяют предоставлять значение this, в частности Array.prototype.map, Array.prototype.forEach, и т. д., которые принимают thisArg в качестве второго аргумента. Любой может создавать свои собственные методы для изменения, thisтакие как (func, thisArg) => func.bind(thisArg), и т. д (func, thisArg) => func.call(thisArg). Как всегда, MDN предлагает отличную документацию.
Просто для удовольствия, проверьте свое понимание на нескольких примерах
Для каждого фрагмента кода ответьте на вопрос: «Каково значение thisв отмеченной строке? Почему?".
Чтобы открыть ответы, щелкните серые поля.
if(true){
console.log(this); // What is `this` here?
}
globalThis. Отмеченная строка оценивается в начальном глобальном контексте выполнения.
const obj = {};
function myFun(){
return { // What is `this` here?
"is obj": this === obj,
"is globalThis": this === globalThis
};
}
obj.method = myFun;
console.log(obj.method());
Комментариев нет:
Отправить комментарий