Контекст (This) — JS: Введение в ООП
Для полноценного изучения ООП в JavaScript нужно разобраться с таким понятием как контекст this . На базе контекста строится почти все остальное, включая то, как работают методы и классы.
Немного усложняет ситуацию то, что контекст по-разному работает для обычных и стрелочных функций. И так как стрелочные функции появились в языке позже, то для понимания темы нужно начать изучение контекста с обычных функций. Небольшое напоминание про то, как выглядят такие функции:
// Определение стрелочной функции и присваивание константе const f = () => 'i am an arrow function'; // Определение обычной анонимной функции function() return 'i am a regular function without name'; > // Определение обычной именованной функции function f() return 'i am a regular function with name'; >
В этом уроке мы рассматриваем только обычные функции, стрелочные будут в одном из следующих уроков. Обычные функции работают с контекстом одинаково, независимо от того, именованные они или нет.
В JavaScript функции ведут себя как данные: их можно записывать в переменные, константы и даже в свойства объектов. Функции, записанные внутрь свойств объектов, называют методами:
const company = name: 'Hexlet' >; // Создание функции, которая сразу же присваивается свойству getName и становится методом company.getName = function() return 'Hexlet'; >; // Вызов метода company.getName(); // "Hexlet"
Это всего лишь один из множества возможных вариантов добавления функции в объект. Ниже еще несколько примеров:
// При создании объекта const obj = getName: function() return 'Hexlet'; >, >; // Через присваивание константы const company = name: 'Hexlet' >; function getHexlet() return 'Hexlet'; >; // Имя не принципиально company.getName = getHexlet; company.getName(); // "Hexlet"
Все варианты выше эквивалентны. Они приводят к одному и тому же результату, но есть одна загвоздка. Метод возвращает строку и никак не использует данные объекта. Если поменяется имя, то метод продолжит возвращать «зашитое» в него значение, а не текущее имя компании внутри объекта.
company.getName(); // "Hexlet" company.name = 'Hexlet Plus'; // Имя поменяли, но очевидно, что возврат остался прежний company.getName(); // "Hexlet"
Для решения этой задачи, нам нужно получить доступ к данным объекта внутри метода. Делается это через специальное ключевое слово this , называемое контекстом. Внутри методов оно ссылается на текущий объект, к которому привязан метод.
const company = name: 'Hexlet', employees: [] >; company.getName = function getName() return this.name; >; company.getName(); // "Hexlet" company.name = 'Hexlet Plus'; company.getName(); // "Hexlet Plus"
this дает возможность не только читать данные, но и менять их:
company.setName = function setName(name) this.name = name; >; company.getName(); // "Hexlet" company.setName('Hexlet Plus'); company.getName(); // "Hexlet Plus"
Другой пример — изменение внутреннего массива в объекте:
// Добавление нового сотрудника company.addEmployee = function addEmployee(user) // Важно, что на момент вызова, employees уже добавлен в company this.employees.push(user); >; const user = name: 'Petya' >; company.addEmployee(user); company.employees; // [< name: 'Petya' >] // Или через метод company.getEmployees = function() return this.employees; >; company.getEmployees(); // [< name: 'Petya' >]
Как видно из примеров выше, свойства можно менять как напрямую, так и из методов. Какой способ предпочесть – зависит от ситуации. С дальнейшим прохождением курсов и опытом вы начнете лучше понимать, какой способ предпочесть.
Контекст
Выше, когда давалось определение this , говорилось, что this ссылается на текущий объект, к которому привязан метод. И здесь кроется ключевое отличие this в JavaScript от this в других языках. В JavaScript this у метода может измениться:
const company1 = name: 'Hexlet', getName: function getName() return this.name > >; const company2 = name: 'Hexlet Plus' >; company1.getName(); // "Hexlet" company2.getName = company1.getName; // В обоих случаях одна и та же функция company2.getName(); // "Hexlet Plus" company1.getName(); // "Hexlet"
Что здесь произошло? Вызов той же самой функции из другого объекта привел к смене объекта, на который ссылается this . Эта особенность называется поздним связыванием. Значение this ссылается на тот объект, из которого происходит вызов метода.
Лучше всего можно понять эту особенность, познакомившись с тем, как вызываются функции внутри самого JavaScript и откуда там берется this . Так как в JavaScript функции — это тоже объекты, то у них есть свои методы. Среди них есть метод call() , который и используется для вызова:
const sayHi = () => 'Hi!'; sayHi.call(); // "Hi!"
Зачем так сделано? Дело в том, что первым параметром call() принимает контекст — объект, на который и будет ссылаться this внутри функции. Функции для этого не обязательно быть методом:
const getName = function getName() return this.name; >; const company1 = name: 'Hexlet' >; // Функция вызывается напрямую, она не является методом getName.call(company1); // "Hexlet" const company2 = name: 'Hexlet Plus' >; getName.call(company2); // "Hexlet Plus"
В примере выше мы заменили контекст функции getName() с помощью call() , передав в него новый контекст.
В этом и заключается весь секрет this . Это ссылка на контекст, который мы можем заменить в функции, как показано выше. Также JavaScript прокидывает контекст автоматически в метод. В этом случае можно точно сказать, на какой объект ссылается контекст. Например, в коде ниже метод getName() принадлежит объекту company и контекст ссылается на этот же объект:
const company = name: 'Hexlet', getName: function getName() return this.name > >;
Теперь, когда вы знаете как работает this , попробуйте ответить на вопрос, что будет выведено на экран?
const company = name: 'Hexlet', country: name: 'Finland', getName: function getName() return this.name; > >, >; console.log(company.country.getName()); // => ?
Правильный ответ: «Finland» . Почему? Потому что контекстом для метода getName() является объект country , а не company . Если немного модифицировать код, то понять эту идею станет проще:
const country > = company; console.log(country.getName()); // "Finland"
Сокращенное определение методов
Из-за необходимости использовать обычные функции при создании объектов в JavaScript был введен специальный сокращенный синтаксис создания методов при определении объектов:
const company = name: 'Hexlet', getName() return this.name; >, // То же самое что // getName: function getName() // return this.name; // >, >;
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях:
this : контекст выполнения функций
Про контекст и this часто спрашивают на собеседованиях. Ответим подробно и разберёмся в нюансах.
Время чтения: 8 мин
Открыть/закрыть навигацию по статье
- Кратко
- Функция
- Глобальный объект
- Строгий режим
- Значение this
- Как не забыть о new
- Непрямой вызов
- Связывание функций
- Стрелочные функции
- Саша Беспоясов советует
- Пусть у нас объявлена функция function Animal () < >.
Обновлено 17 марта 2023
Кратко
Скопировать ссылку «Кратко» Скопировано
Грубо говоря, this — это ссылка на некий объект, к свойствам которого можно получить доступ внутри вызова функции. Этот this — и есть контекст выполнения.
Но чтобы лучше понять, что такое this и контекст выполнения в JavaScript, нам потребуется зайти издалека.
Сперва вспомним, как мы в принципе можем выполнить какую-то инструкцию в коде.
Выполнить что-то в JS можно 4 способами:
- вызвав функцию;
- вызвав метод объекта;
- использовав функцию-конструктор;
- непрямым вызовом функции.
Функция
Скопировать ссылку «Функция» Скопировано
Первый и самый простой способ выполнить что-то — вызвать функцию.
function hello(whom) console.log(`Hello, $!`)> hello('World')// Hello, World!
function hello(whom) console.log(`Hello, $whom>!`) > hello('World') // Hello, World!
Чтобы выполнить функцию, мы используем выражение hello и скобки с аргументами.
Когда мы вызываем функцию, значением this может быть лишь глобальный объект или undefined при использовании ‘use strict’ .
Глобальный объект
Скопировать ссылку «Глобальный объект» Скопировано
Глобальный объект — это, так скажем, корневой объект в программе.
Если мы запускаем JS-код в браузере, то глобальным объектом будет window . Если мы запускаем код в Node-окружении, то global .
Строгий режим
Скопировать ссылку «Строгий режим» Скопировано
Можно сказать, что строгий режим — неказистый способ борьбы с легаси.
Включается строгий режим с помощью директивы ‘use strict’ в начале блока, который должен выполняться в строгом режиме:
function nonStrict() // Будет выполняться в нестрогом режиме.> function strict() 'use strict' // Будет выполняться в строгом режиме.>
function nonStrict() // Будет выполняться в нестрогом режиме. > function strict() 'use strict' // Будет выполняться в строгом режиме. >
Также можно настроить строгий режим для всего файла, если указать ‘use strict’ в начале.
Значение this
Скопировать ссылку «Значение this» Скопировано
Вернёмся к this . В нестрогом режиме при выполнении в браузере this при вызове функции будет равен window :
function whatsThis() console.log(this === window)> whatsThis()// true
function whatsThis() console.log(this === window) > whatsThis() // true
То же — если функция объявлена внутри функции:
function whatsThis() function whatInside() console.log(this === window) > whatInside()> whatsThis()// true
function whatsThis() function whatInside() console.log(this === window) > whatInside() > whatsThis() // true
И то же — если функция будет анонимной и, например, вызвана немедленно:
;(function () console.log(this === window)>)()// true
;(function () console.log(this === window) >)() // true
В приведённом выше примере вы можете заметить ; перед анонимной функцией. Дело в том, что существующий механизм автоподстановки точек с запятой (ASI) срабатывает лишь в определённых случаях, в то время как строка, начинающаяся с ( , не входит в перечень этих случаев. Поэтому опытные разработчики зачастую добавляют ; в тех случаях, когда их код может быть скопирован и добавлен в существующий.
В строгом режиме — значение будет равно undefined :
'use strict' function whatsThis() console.log(this === undefined)> whatsThis()// true
'use strict' function whatsThis() console.log(this === undefined) > whatsThis() // true
Метод объекта
Скопировать ссылку «Метод объекта» Скопировано
Если функция хранится в объекте — это метод этого объекта.
const user = name: 'Alex', greet() console.log('Hello, my name is Alex') >,> user.greet()// Hello, my name is Alex
const user = name: 'Alex', greet() console.log('Hello, my name is Alex') >, > user.greet() // Hello, my name is Alex
user . greet ( ) — это метод объекта user .
В этом случае значение this — этот объект.
const user = name: 'Alex', greet() console.log(`Hello, my name is $`) >,> user.greet()// Hello, my name is Alex
const user = name: 'Alex', greet() console.log(`Hello, my name is $this.name>`) >, > user.greet() // Hello, my name is Alex
Обратите внимание, что this определяется в момент вызова функции. Если записать метод объекта в переменную и вызвать её, значение this изменится.
const user = name: 'Alex', greet() console.log(`Hello, my name is $`) >,> const greet = user.greetgreet()// Hello, my name is
const user = name: 'Alex', greet() console.log(`Hello, my name is $this.name>`) >, > const greet = user.greet greet() // Hello, my name is
При вызове через точку user . greet ( ) значение this равняется объекту до точки ( user ). Без этого объекта this равняется глобальному объекту (в обычном режиме). В строгом режиме мы бы получили ошибку «Cannot read properties of undefined».
Чтобы такого не происходило, следует использовать bind ( ) , о котором мы поговорим чуть позже.
Вызов конструктора
Скопировать ссылку «Вызов конструктора» Скопировано
Конструктор — это функция, которую мы используем, чтобы создавать однотипные объекты. Такие функции похожи на печатный станок, который создаёт детали LEGO. Однотипные объекты — детальки, а конструктор — станок. Он как бы конструирует эти объекты, отсюда название.
По соглашениям конструкторы вызывают с помощью ключевого слова new , а также называют с большой буквы, причём обычно не глаголом, а существительным. Существительное — это та сущность, которую создаёт конструктор.
Например, если конструктор будет создавать объекты пользователей, мы можем назвать его User , а использовать вот так:
function User() this.name = 'Alex'> const firstUser = new User()firstUser.name === 'Alex'// true
function User() this.name = 'Alex' > const firstUser = new User() firstUser.name === 'Alex' // true
При вызове конструктора this равен свежесозданному объекту.
В примере с User значением this будет объект, который конструктор создаёт:
function User() console.log(this instanceof User) // true this.name = 'Alex'> const firstUser = new User()firstUser instanceof User// true
function User() console.log(this instanceof User) // true this.name = 'Alex' > const firstUser = new User() firstUser instanceof User // true
На самом деле, многое происходит «за кулисами»:
- При вызове сперва создаётся новый пустой объект, и он присваивается this .
- Выполняется код функции. (Обычно он модифицирует this , добавляет туда новые свойства.)
- Возвращается значение this .
Если расписать все неявные шаги, то:
function User() // Происходит неявно: // this = <>; this.name = 'Alex' // Происходит неявно: // return this;>
function User() // Происходит неявно: // this = <>; this.name = 'Alex' // Происходит неявно: // return this; >
То же происходит и в ES6-классах, узнать о них больше можно в статье про объектно-ориентированное программирование.
class User constructor() this.name = 'Alex' > greet() /*. */ >> const firstUser = new User()
class User constructor() this.name = 'Alex' > greet() /*. */ > > const firstUser = new User()
Как не забыть о new
Скопировать ссылку «Как не забыть о new» Скопировано
При работе с функциями-конструкторами легко забыть о new и вызвать их неправильно:
const firstUser = new User() // ✅const secondUser = User() // ❌
const firstUser = new User() // ✅ const secondUser = User() // ❌
Хотя на первый взгляд разницы нет, и работает будто бы правильно. Но на деле разница есть:
console.log(firstUser)// User console.log(secondUser)// undefined
console.log(firstUser) // User console.log(secondUser) // undefined
Чтобы не попадаться в такую ловушку, в конструкторе можно прописать проверку на то, что новый объект создан:
function User() if (!(this instanceof User)) throw Error('Error: Incorrect invocation!') > this.name = 'Alex'> // или function User() if (!new.target) throw Error('Error: Incorrect invocation!') > this.name = 'Alex'> const secondUser = User()// Error: Incorrect invocation!
function User() if (!(this instanceof User)) throw Error('Error: Incorrect invocation!') > this.name = 'Alex' > // или function User() if (!new.target) throw Error('Error: Incorrect invocation!') > this.name = 'Alex' > const secondUser = User() // Error: Incorrect invocation!
Непрямой вызов
Скопировать ссылку «Непрямой вызов» Скопировано
Непрямым вызовом называют вызов функций через call ( ) или apply ( ) .
Оба первым аргументом принимают this . То есть они позволяют настроить контекст снаружи, к тому же — явно.
function greet() console.log(`Hello, $`)> const user1 = const user2 = greet.call(user1)// Hello, Alexgreet.call(user2)// Hello, Ivan greet.apply(user1)// Hello, Alexgreet.apply(user2)// Hello, Ivan
function greet() console.log(`Hello, $this.name>`) > const user1 = name: 'Alex' > const user2 = name: 'Ivan' > greet.call(user1) // Hello, Alex greet.call(user2) // Hello, Ivan greet.apply(user1) // Hello, Alex greet.apply(user2) // Hello, Ivan
В обоих случаях в первом вызове this === user1 , во втором — user2 .
Разница между call ( ) и apply ( ) — в том, как они принимают аргументы для самой функции после this .
call ( ) принимает аргументы списком через запятую, apply ( ) же — принимает массив аргументов. В остальном они идентичны:
function greet(greetWord, emoticon) console.log(`$ $ $`)> const user1 = const user2 = greet.call(user1, 'Hello,', ':-)')// Hello, Alex :-)greet.call(user2, 'Good morning,', ':-D')// Good morning, Ivan :-Dgreet.apply(user1, ['Hello,', ':-)'])// Hello, Alex :-)greet.apply(user2, ['Good morning,', ':-D'])// Good morning, Ivan :-D
function greet(greetWord, emoticon) console.log(`$greetWord> $this.name> $emoticon>`) > const user1 = name: 'Alex' > const user2 = name: 'Ivan' > greet.call(user1, 'Hello,', ':-)') // Hello, Alex :-) greet.call(user2, 'Good morning,', ':-D') // Good morning, Ivan :-D greet.apply(user1, ['Hello,', ':-)']) // Hello, Alex :-) greet.apply(user2, ['Good morning,', ':-D']) // Good morning, Ivan :-D
Связывание функций
Скопировать ссылку «Связывание функций» Скопировано
Особняком стоит bind ( ) . Это метод, который позволяет связывать контекст выполнения с функцией, чтобы «заранее и точно» определить, какое именно значение будет у this .
function greet() console.log(`Hello, $`)> const user1 = const greetAlex = greet.bind(user1)greetAlex()// Hello, Alex
function greet() console.log(`Hello, $this.name>`) > const user1 = name: 'Alex' > const greetAlex = greet.bind(user1) greetAlex() // Hello, Alex
Обратите внимание, что bind ( ) , в отличие от call ( ) и apply ( ) , не вызывает функцию сразу. Вместо этого он возвращает другую функцию — связанную с указанным контекстом навсегда. Контекст у этой функции изменить невозможно.
function getAge() console.log(this.age);> const howOldAmI = getAge.bind().bind() howOldAmI();//20
function getAge() console.log(this.age); > const howOldAmI = getAge.bind(age: 20>).bind(age: 30>) howOldAmI(); //20
Стрелочные функции
Скопировать ссылку «Стрелочные функции» Скопировано
У стрелочных функций собственного контекста выполнения нет. Они связываются с ближайшим по иерархии контекстом, в котором они определены.
Это удобно, когда нам нужно передать в стрелочную функцию, например, родительский контекст без использования bind ( ) .
function greetWaitAndAgain() console.log(`Hello, $!`) setTimeout(() => console.log(`Hello again, $!`) >)> const user = user.greetWaitAndAgain = greetWaitAndAgain;user.greetWaitAndAgain() // Hello, Alex!// Hello again, Alex!
function greetWaitAndAgain() console.log(`Hello, $this.name>!`) setTimeout(() => console.log(`Hello again, $this.name>!`) >) > const user = name: 'Alex' > user.greetWaitAndAgain = greetWaitAndAgain; user.greetWaitAndAgain() // Hello, Alex! // Hello again, Alex!
При использовании обычной функции внутри контекст бы потерялся, и чтобы добиться того же результата, нам бы пришлось использовать call ( ) , apply ( ) или bind ( ) .
На практике
Скопировать ссылку «На практике» Скопировано
Саша Беспоясов советует
Скопировать ссылку «Саша Беспоясов советует» Скопировано
Гибкий, нефиксированный контекст в JS — это одновременно и удобно, и опасно.
Удобно это тем, что мы можем писать очень абстрактные функции, которые будут использовать контекст выполнения, для своей работы. Так мы можем добиться полиморфизма.
Однако в то же время гибкий this может быть и причиной ошибки, например, если мы используем конструктор без new или просто спутаем контекст выполнения.
Всегда используйте ‘use strict’ .
Это относится скорее не конкретно к контексту, а в целом рекомендация для написания кода
Однако и с контекстом строгий режим позволит раньше обнаружить закравшуюся ошибку. Например:
В нестрогом режиме, если мы забудем new , name станет полем на глобальном объекте.
function User() this.name = 'Alex'> const user = User()// window.name === 'Alex';// user === window
function User() this.name = 'Alex' > const user = User() // window.name === 'Alex'; // user === window
В строгом мы получим ошибку, потому что изначально контекст внутри функции в строгом режиме — undefined :
function User() 'use strict' this.name = 'Alex'> const user = User()// Uncaught TypeError: Cannot set property 'name' of undefined.
function User() 'use strict' this.name = 'Alex' > const user = User() // Uncaught TypeError: Cannot set property 'name' of undefined.
Всегда используйте new и ставьте проверки в конструкторе.
При использовании конструкторов всегда используйте new . Это обезопасит вас от ошибок и не будет вводить в заблуждение разработчиков, которые будут читать код после.
А для защиты «от дурака» желательно ставить проверки внутри конструктора:
function User() if (!(this instanceof User)) throw Error('Error: Incorrect invocation!') > this.name = 'Alex'> const secondUser = User() // Error: Incorrect invocation!
function User() if (!(this instanceof User)) throw Error('Error: Incorrect invocation!') > this.name = 'Alex' > const secondUser = User() // Error: Incorrect invocation!
Авто-байнд для методов класса.
В ES6 появились классы, но они не работают в старых браузерах. Обычно разработчики транспилируют код — то есть переводят его с помощью разных инструментов в ES5.
Может случиться так, что при транспиляции, если она настроена неправильно, методы класса не будут распознавать this , как экземпляр класса.
class User name: 'Alex' greet() console.log(`Hello $`) >> // this.name может быть undefined;// this может быть undefined.
class User name: 'Alex' greet() console.log(`Hello $this.name>`) > > // this.name может быть undefined; // this может быть undefined.
Чтобы от этого защититься, можно использовать стрелочные функции, чтобы создать поля классов.
На собеседовании
Скопировать ссылку «На собеседовании» Скопировано
Что такое контекст в js
Контекст выполнения функции — это одно из фундаментальных понятий в JavaScript. Контекстом еще часто называют значение переменной this внутри функции. Также иногда путают понятия «контекст выполнения» и «область видимости» — это не одно и то же. Давайте разберемся с этими понятиями.
Каждый вызов функции имеет и область видимости, и переменную this, и контекст выполнения. Область видимости определяет доступ к переменным при вызове функции и является уникальной для каждого вызова. Значение переменной this — это ссылка на объект, который «вызывает» код в данный момент. Контекст выполнения содержит и область видимости, и аргументы функции, и переменную this.
Переменная this
Значение переменной this чаще всего определяется тем, как вызывается функция. Когда функция вызывается как метод объекта, переменная this приобретает значение ссылки на объект, который вызывает этот метод:
var user = < name: 'John Smith', getName: function() < console.log(this.name); >>; user.getName(); // John Smith
Тот же принцип применяется при вызове функции с оператором new, чтобы создать экземпляр объекта. При вызове таким образом, в качестве значения this в рамках функции будет установлена ссылка на вновь созданный объект, например:
function test() < alert(this); >test(); // window new test(); // test
Когда мы вызываем функцию как функцию (не как метод объекта), эта функция будет выполнена в глобальном контексте. Значением переменной this в данном случае будет ссылка на глобальный объект. Однако, если функция вызывается как функция в строгом режиме (strict mode) — значением this будет undefined.
Контекст выполнения
Код в JavaScript может быть одного из следующих типов:
- eval-код — код, выполняющийся внутри функции eval();
- код функции — код, выполняющийся в теле функции;
- глобальный код — код, не выполняющийся в рамках какой-либо функции.
Когда интерпретатор JavaScript выполняет код, по умолчанию контекстом выполнения является глобальный контекст. Каждый вызов функции приводит к созданию нового контекста выполнения.
var hello = 'Hello'; var user = function() < // контекст выполнения функции var name = 'John Smith'; var getName = function() < // контекст выполнения функции return name; >var sayHello = function() < // контекст выполнения функции console.log(hello + ', ' + getName()); >sayHello(); > user();
В данном примере мы имеем один глобальный контекст выполнения и 3 контекста выполнения функции.
Каждый раз, когда создается новый контекст выполнения, он добавляется в верхнюю часть стека выполнения. Браузер всегда будет выполнять код в текущем контексте выполнения, который находится на вершине стека выполнения. После завершения, контекст будет удален из верхней части стека и управление вернется к контексту выполнения ниже.
Основные вещи, которые необходимо помнить и понимать о контексте выполнения:
- Однопоточность — JavaScript работает в однопоточном режиме, т.е. только одна операция может быть выполнена в определенный момент времени.
- Синхронное выполнение кода — код выполняется синхронно, т.е. следующая операция не выполняется до завершения предыдущей.
- Один глобальный контекст выполнения.
- Бесконечное количество контекстов выполнения функции.
- Каждый вызов функции создает новый контекст выполнения, даже если функция рекурсивно вызывает сама себя.
В интерпретаторе JavaScript каждое создание контекста выполнения происходит в два этапа: этап создания (когда функция только вызвана, но код внутри нее еще не выполняется) и этап выполнения. На этапе создания интерпретатор сначала создает объект переменных (также называемый объект активации), который состоит из всех переменных, объявлений функций и аргументов, определенных внутри контекста выполнения. Затем инициализируется область видимости, и в последнюю очередь определяется значение переменной this. На этапе выполнения внутренним переменным присваивается значение, код интерпретируется и выполняется.
Таким образом, контекст выполнения функции можно представить в виде следующего объекта:
executionContextObj = < variableObject: < /* объект активации - состоит из параметров функции, внутренних переменных и объявлений функций */ >, scopeChain: < /* цепочка областей видимости - объект активации + все объекты активации родительских контекстов выполнения */ >, this: <> >
Для каждого контекста выполнения существует своя цепочка областей видимости. Цепочка областей видимости контекста выполнения включает области видимости из предыдущих контекстов в стеке выполнения.
Т.е. каждый раз, когда мы пытаемся получить доступ к переменной в контексте выполнения функции, процесс поиска этой переменной начинается с собственной области видимости функции. Если переменная с таким именем в текущей области видимости не найдена, поиск продолжается в иерархии областей видимости.
Понятия области видимости и контекста выполнения очень важны и играют значительную роль в языке JavaScript. Их хорошее понимание важно для изучения ряда шаблонов проектирования, понимания работы замыканий, функций обратного вызова, частичного применения функций и других важных концепций JavaScript.
Привязка контекста к функции
При передаче методов объекта в качестве колбэков, например для setTimeout , возникает известная проблема – потеря this .
В этой главе мы посмотрим, как её можно решить.
Потеря «this»
Мы уже видели примеры потери this . Как только метод передаётся отдельно от объекта – this теряется.
Вот как это может произойти в случае с setTimeout :
let user = < firstName: "Вася", sayHi() < alert(`Привет, $!`); > >; setTimeout(user.sayHi, 1000); // Привет, undefined!
При запуске этого кода мы видим, что вызов this.firstName возвращает не «Вася», а undefined !
Это произошло потому, что setTimeout получил функцию sayHi отдельно от объекта user (именно здесь функция и потеряла контекст). То есть последняя строка может быть переписана как:
let f = user.sayHi; setTimeout(f, 1000); // контекст user потеряли
Метод setTimeout в браузере имеет особенность: он устанавливает this=window для вызова функции (в Node.js this становится объектом таймера, но здесь это не имеет значения). Таким образом, для this.firstName он пытается получить window.firstName , которого не существует. В других подобных случаях this обычно просто становится undefined .
Задача довольно типичная – мы хотим передать метод объекта куда-то ещё (в этом конкретном случае – в планировщик), где он будет вызван. Как бы сделать так, чтобы он вызывался в правильном контексте?
Решение 1: сделать функцию-обёртку
Самый простой вариант решения – это обернуть вызов в анонимную функцию, создав замыкание:
let user = < firstName: "Вася", sayHi() < alert(`Привет, $!`); > >; setTimeout(function() < user.sayHi(); // Привет, Вася! >, 1000);
Теперь код работает корректно, так как объект user достаётся из замыкания, а затем вызывается его метод sayHi .
То же самое, только короче:
setTimeout(() => user.sayHi(), 1000); // Привет, Вася!
Выглядит хорошо, но теперь в нашем коде появилась небольшая уязвимость.
Что произойдёт, если до момента срабатывания setTimeout (ведь задержка составляет целую секунду!) в переменную user будет записано другое значение? Тогда вызов неожиданно будет совсем не тот!
let user = < firstName: "Вася", sayHi() < alert(`Привет, $!`); > >; setTimeout(() => user.sayHi(), 1000); // . в течение 1 секунды user = < sayHi() < alert("Другой пользователь в 'setTimeout'!"); >>; // Другой пользователь в 'setTimeout'!
Следующее решение гарантирует, что такого не случится.
Решение 2: привязать контекст с помощью bind
В современном JavaScript у функций есть встроенный метод bind, который позволяет зафиксировать this .
Базовый синтаксис bind :
// полный синтаксис будет представлен немного позже let boundFunc = func.bind(context);
Результатом вызова func.bind(context) является особый «экзотический объект» (термин взят из спецификации), который вызывается как функция и прозрачно передаёт вызов в func , при этом устанавливая this=context .
Другими словами, вызов boundFunc подобен вызову func с фиксированным this .
Например, здесь funcUser передаёт вызов в func , фиксируя this=user :
let user = < firstName: "Вася" >; function func() < alert(this.firstName); >let funcUser = func.bind(user); funcUser(); // Вася
Здесь func.bind(user) – это «связанный вариант» func , с фиксированным this=user .
Все аргументы передаются исходному методу func как есть, например:
let user = < firstName: "Вася" >; function func(phrase) < alert(phrase + ', ' + this.firstName); >// привязка this к user let funcUser = func.bind(user); funcUser("Привет"); // Привет, Вася (аргумент "Привет" передан, при этом this = user)
Теперь давайте попробуем с методом объекта:
let user = < firstName: "Вася", sayHi() < alert(`Привет, $!`); > >; let sayHi = user.sayHi.bind(user); // (*) sayHi(); // Привет, Вася! setTimeout(sayHi, 1000); // Привет, Вася!
В строке (*) мы берём метод user.sayHi и привязываем его к user . Теперь sayHi – это «связанная» функция, которая может быть вызвана отдельно или передана в setTimeout (контекст всегда будет правильным).
Здесь мы можем увидеть, что bind исправляет только this , а аргументы передаются как есть:
let user = < firstName: "Вася", say(phrase) < alert(`$, $!`); > >; let say = user.say.bind(user); say("Привет"); // Привет, Вася (аргумент "Привет" передан в функцию "say") say("Пока"); // Пока, Вася (аргумент "Пока" передан в функцию "say")
Удобный метод: bindAll
Если у объекта много методов и мы планируем их активно передавать, то можно привязать контекст для них всех в цикле:
for (let key in user) < if (typeof user[key] == 'function') < user[key] = user[key].bind(user); >>
Некоторые JS-библиотеки предоставляют встроенные функции для удобной массовой привязки контекста, например _.bindAll(obj) в lodash.
Частичное применение
До сих пор мы говорили только о привязывании this . Давайте шагнём дальше.
Мы можем привязать не только this , но и аргументы. Это делается редко, но иногда может быть полезно.
Полный синтаксис bind :
let bound = func.bind(context, [arg1], [arg2], . );
Это позволяет привязать контекст this и начальные аргументы функции.
Например, у нас есть функция умножения mul(a, b) :
function mul(a, b)
Давайте воспользуемся bind , чтобы создать функцию double на её основе:
function mul(a, b) < return a * b; >let double = mul.bind(null, 2); alert( double(3) ); // = mul(2, 3) = 6 alert( double(4) ); // = mul(2, 4) = 8 alert( double(5) ); // = mul(2, 5) = 10
Вызов mul.bind(null, 2) создаёт новую функцию double , которая передаёт вызов mul , фиксируя null как контекст, и 2 – как первый аргумент. Следующие аргументы передаются как есть.
Это называется частичное применение – мы создаём новую функцию, фиксируя некоторые из существующих параметров.
Обратите внимание, что в данном случае мы на самом деле не используем this . Но для bind это обязательный параметр, так что мы должны передать туда что-нибудь вроде null .
В следующем коде функция triple умножает значение на три:
function mul(a, b) < return a * b; >let triple = mul.bind(null, 3); alert( triple(3) ); // = mul(3, 3) = 9 alert( triple(4) ); // = mul(3, 4) = 12 alert( triple(5) ); // = mul(3, 5) = 15
Для чего мы обычно создаём частично применённую функцию?
Польза от этого в том, что возможно создать независимую функцию с понятным названием ( double , triple ). Мы можем использовать её и не передавать каждый раз первый аргумент, т.к. он зафиксирован с помощью bind .
В других случаях частичное применение полезно, когда у нас есть очень общая функция и для удобства мы хотим создать её более специализированный вариант.
Например, у нас есть функция send(from, to, text) . Потом внутри объекта user мы можем захотеть использовать её частный вариант: sendTo(to, text) , который отправляет текст от имени текущего пользователя.
Частичное применение без контекста
Что если мы хотим зафиксировать некоторые аргументы, но не контекст this ? Например, для метода объекта.
Встроенный bind не позволяет этого. Мы не можем просто опустить контекст и перейти к аргументам.
К счастью, легко создать вспомогательную функцию partial , которая привязывает только аргументы.
function partial(func, . argsBound) < return function(. args) < // (*) return func.call(this, . argsBound, . args); >> // использование: let user = < firstName: "John", say(time, phrase) < alert(`[$] $: $!`); > >; // добавляем частично применённый метод с фиксированным временем user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); user.sayNow("Hello"); // Что-то вроде этого: // [10:00] John: Hello!
Результатом вызова partial(func[, arg1, arg2. ]) будет обёртка (*) , которая вызывает func с:
- Тем же this , который она получает (для вызова user.sayNow – это будет user )
- Затем передаёт ей . argsBound – аргументы из вызова partial ( «10:00» )
- Затем передаёт ей . args – аргументы, полученные обёрткой ( «Hello» )
Благодаря оператору расширения . реализовать это очень легко, не правда ли?
Также есть готовый вариант _.partial из библиотеки lodash.
Итого
Метод bind возвращает «привязанный вариант» функции func , фиксируя контекст this и первые аргументы arg1 , arg2 …, если они заданы.
Обычно bind применяется для фиксации this в методе объекта, чтобы передать его в качестве колбэка. Например, для setTimeout .
Когда мы привязываем аргументы, такая функция называется «частично применённой» или «частичной».
Частичное применение удобно, когда мы не хотим повторять один и тот же аргумент много раз. Например, если у нас есть функция send(from, to) и from всё время будет одинаков для нашей задачи, то мы можем создать частично применённую функцию и дальше работать с ней.