Узнайте, как ключевое слово this на самом деле работает в JavaScript.

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

Если вам трудно, вы не одиноки. Давайте посмотрим, как именно работает this, и устраним любую путаницу в отношении того, что это означает в различных контекстах.

Что такое «это» в JavaScript

this — это ключевое слово в JavaScript, которое относится к свойству или набору свойств в определенном контексте. Контекст, в котором мы используем this, изменяет его свойства. В глобальном контексте это относится к глобальному объекту, который в браузере является окном, но globalThis в Node.js и других реализациях JavaScript.

console.log(this); // The same as console.log(window);

Вне каких-либо функций или кода это всегда так. Однако в разных местах это означает разное.

Это в функциях в JavaScript

В функции this по-прежнему относится к глобальному объекту. Если мы ссылаемся на это в функции, по умолчанию она будет ссылаться на окно или объект globalThis:

console.log(this); // The same as console.log(window);
function myFunction() {
    console.log(this); // The same as console.log(window);
}
myFunction();

Однако в строгом режиме this внутри функции не определено.

"use strict"
console.log(this); // The same as console.log(window);
function myFunction() {
    console.log(this); // This is undefined!
}
myFunction();

Решение с вызовом()

Поначалу это немного сбивает с толку, но причина этого в том, что нам нужно добавить объект this в myFunction — JavaScript в строгом режиме не будет по умолчанию использовать его как глобальный объект. Для этого мы должны использовать call(). В приведенном ниже примере я превратил myObject в нашу переменную this:

"use strict"
console.log(this); // The same as console.log(window);
let myObject = {
    firstName: "John",
    lastName: "Doe",
    age: 76
}
function myFunction() {
    console.log(this.firstName);
}
myFunction.call(myObject); // this.firstName is defined as "John", so it will console log John
myFunction(); // this.firstName will be undefined, and this will throw an error.

call() запускает myFunction и прикрепляет myObject к ключевому слову this. Если не использовать вызов, а просто запустить myFunction(), то функция вернет ошибку, так как this.firstName будет неопределенным. Вы также можете вызвать функцию с пустым this, к которому затем можно добавить данные внутри вашей функции.

Это дает нам новое пространство для определения переменных в нашем объекте this, вместо того, чтобы загрязнять данные из глобального объекта this:

"use strict"
console.log(this); // The same as console.log(window);
function myFunction() {
    this.firstName = 'John';
    console.log(this.firstName); // This will be "John"
}
myFunction.call({});

Другое поведение в строгом режиме

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

Позвоните и подайте заявку

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

Например:

"use strict"
let otherNumbers = {
    a: 10,
    b: 4
}
function multiplyNumbers(x, y, z) {
    return this.a * this.b * x * y * z
}
// Both will return the same result, the only difference
// being that apply() uses an array for arguments.
multiplyNumbers.call(otherNumbers, 1, 2, 3);
multiplyNumbers.apply(otherNumbers, [ 1, 2, 3 ]);

Упрощение этого процесса с помощью bind()

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

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

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

"use strict"
console.log(this); // The same as console.log(window);
let myObject = {
    firstName: "John",
    lastName: "Doe",
    age: 76
}
function myFunction() {
    console.log(this.firstName);
}
let boundFunction = myFunction.bind(myObject); // this will bind this to myObject permanently.
boundFunction(); // since we used bind, this will now be set to myObject, every time we call boundFunction() - so it will return John.

Функции обозначения стрелок и это

Одной из ключевых особенностей функций нотации стрелок в JavaScript является то, что они не содержат контекста this. Это означает, что они наследуют this от своего родителя. Например, предположим, что мы находимся в строгом режиме и определяем как функцию стрелки, так и функцию «нормального» стиля. Для стрелочной функции будет унаследовано this, а для другой функции this останется неопределенным!

"use strict"
console.log(this); // The same as console.log(window);
function myFunction() {
    console.log(this.name); // This will be "John"
    let myArrowFunction = () => {
        console.log(this.name); // This will be "John"
    }
    let myNormalFunction = function() {
        console.log(this.name); // This will throw an error, since this is undefined!
    }
    myArrowFunction();
    myNormalFunction();
}
myFunction.call({
    name: "John"
});

Функции конструктора и это

Еще одна интересная вещь о this заключается в том, что при использовании в функции-конструкторе (которая является функцией, использующей ключевое слово new), возврат функции-конструктора по существу перезаписывает это. Так, например, если мы запустим следующее, хотя мы установим this.name для John, значение, возвращаемое для имени, будет Jack:

let functionA = function() {
    this.name = "John";
}
let functionB = function() {
    this.name = "John";
    return {
        name: "Jack"
    }
}
let runFunctionA = new functionA();
console.log(runFunctionA.name); // Returns "John";
let runFunctionB = new functionB();
console.log(runFunctionB.name); // Returns "Jack";

Это в контексте объекта

В контексте объекта использование this относится к объекту. Например, предположим, что мы запускаем функцию внутри объекта с именем obj, который ссылается на this.aProperty — в данном случае это ссылается на obj:

let obj = {
    aProperty: 15,
    runFunction: function() {
        console.log(this.aProperty); // Refers to 15
    }
}
obj.runFunction(); // Will console log 15, since this refers to obj

Это также верно, если вы используете нотацию get()/set():

"use strict"
let obj = {
    aProperty: 15,
    runFunction: function() {
        console.log(this.aProperty); // Refers to 15
    },
    set updateProp(division) {
        this.aProperty = this.aProperty / division; // this.aProperty refers to 15
        console.log(this.aProperty); 
    }
}
obj.updateProp = 15; // Will divide aProperty by 15, and console log the result, i.e. 1

Использование этого с прослушивателями событий

Еще одна особенность this в JavaScript заключается в том, что при использовании прослушивателя событий this относится к HTML-элементу, к которому было добавлено событие. В приведенном ниже примере мы добавляем событие клика в HTML-тег с идентификатором «hello-world»:

document.getElementById('hello-world').addEventListener('click', function(e) {
    console.log(this);
});

Если мы затем нажмем на наш HTML-элемент #hello-world, мы увидим это в нашем журнале консоли:

<div id="hello-world"></div>

Использование этого с классами

В этом разделе стоит отметить, что классы в JavaScript — это просто функции внутри. Это означает, что многие функциональные возможности, которые мы видели в функциях, применимы и к классам.

По умолчанию класс будет иметь этот набор для самого экземпляра класса. В приведенном ниже примере мы можем увидеть это в действии — и runClass.name, и runClass.whatsMyName возвращают John.

class myClass { 
    whatsMyName() {
        return this.name;
    }
    get name() {
        return "John";
    }
}
const runClass = new myClass();
console.log(runClass.name);        // Returns "John"
console.log(runClass.whatsMyName); // Returns "John"

Единственным исключением является то, что сюда не добавляются статические элементы. Поэтому, если мы определим функцию с ключевым словом static перед ней, она не будет находиться на this:

class myClass { 
    getMyAge() {
        return this.whatsMyAge();
    }
    static whatsMyAge() {
        return this.age; 
    }
    get name() {
        return "John";
    }
    get age() {
        return 143
    }
}
const runClass = new myClass();
console.log(runClass.whatsMyAge()); // Throws an error, since runClass.whatsMyAge() is undefined
console.log(runClass.getMyAge()); // Throws an error, since this.whatsMyAge() is undefined

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

Заключение

В JavaScript это может означать разные вещи. В этой статье мы рассмотрели, что это значит в разных контекстах — функциях, классах и объектах. Мы рассмотрели, как использовать bind(), call() и apply() для добавления другого контекста this к вашим функциям.

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

Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter и LinkedIn. Присоединяйтесь к нашему сообществу Discord.