Немного поговорим об 'этом'. (this in js)

07/07/2018
Konstantin Ostrovsky

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

Знание Объекто Ореинтированного Программирования (ООП) будет большим плюсом и поможет Вам в понимании материала.

1. Ключевое слово this

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

Попробуем на практике. Допустим, у нас есть функция расчета расстояния между двумя футболистами на поле. Для реализации воспользуемся "школьной" формулой расчета расстояния между двумя точками (AB = √(xb - xa)*2 + (yb - ya)*2).

В качестве аргументов мы будем использовать два объекта "футболистов".
Объекты будут иметь три свойства:

  1. x - позиция по оси Х;
  2. y - позиция по оси Y;
  3. name - имя футболиста.
var footboal1 = 
{
    x: 110,
    y: 50,
    name: 'John'
};

Функция будет хранить в контексте копии этих объектов, расстояние между ними и метод для пересчета расстояния.


function distance(man1, man2) {
	this.man1 = man1; // Первый футболист
	this.man2 = man2; // Второй футболит
	this.duration = 0; // Расстояние между футболистами
	this.calc = function(){
		this.duration = Math.sqrt( Math.pow( this.man1.x - this.man2.x, 2 ) + Math.pow( this.man1.y - this.man2.y, 2 ) );
		// Math.pow - возведение в степень
		// Math.sqrt - извлечение корня
		// В этой функции контекстом является функция distance, так как эта функция выполняется в ее контексте.
	}

	// Сразу же после инициализации вызываем функцию для расчета расстояния
	this.calc();
	
	// Возвращаем контекст функции. Делать это не обязательно, но, если после выполнения функции, Вы хотите использовать контекст функции для дополнительных расчетов или получения свойств контекста, необходимо вернуть контекст. Подробнее ниже по коду...
	return this;
}

var footboal1 = {  x: 110, y: 50, name: 'John' },
footboal2 = {  x: 150, y: 95, name: 'Piter' };

var distanceFootboals = distance(footboal1, footboal2); // Поосле выполнения функции 'distance', в переменную 'distanceFootboals' запишется контекст выполнения функции, так как мы вернули его в функции
console.log(`Расстояние между ${distanceFootboals.man1.name} и ${distanceFootboals.man2.name} составляет ${distanceFootboals.duration}`);
// Расстояние между John и Piter составляет 60.207972893961475

// Можно изменить свойства контекста и повторно вызвать методы
distanceFootboals.man1.x = 90;
distanceFootboals.man2.y = 180;
distanceFootboals.calc(); // Запускаем пересчет
console.log(`Расстояние между ${distanceFootboals.man1.name} и ${distanceFootboals.man2.name} составляет ${distanceFootboals.duration}`);
// Расстояние между John и Piter составляет 143.17821063276352

Важно заметить, что при каждом вызове функции 'distance', будет создаваться новый контекст.

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

2. This in ES6

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

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

Пример стрелочной функции и сранение с обычной:

    
function loadImage() {
	let img1 = new Image(); // Создаем объект изображения
	img1.onload = function(){ // После загрузки изображения будет вызвана эта функция
		console.log('Контекст выполнения обычной функции:')
		console.log(this);
	}

	let img2 = new Image(); // Создаем объект изображения
	img2.onload = () => { // После загрузки изображения будет вызвана эта функция
		console.log('Контекст выполнения стрелочной функции:');
		console.log(this);
	}

	img1.src = img2.src = "https://web-panda.ru/public/images/this_in_js.jpg";
}

var loader = loadImage(),
// Контекст выполнения стрелочной функции:
// window{...}
// Контекст выполнения обычной функции:
// <img src=​"https:​/​/​web-panda.ru/​public/​images/​this_in_js.jpg">​

loader = new loadImage();
// Контекст выполнения стрелочной функции:
// loadImage {}
// Контекст выполнения обычной функции:
// <img src=​"https:​/​/​web-panda.ru/​public/​images/​this_in_js.jpg">​
    

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

Контекстом стрелочной функции при создании экземляра объекта является loadImage{}, но при обычном вызове контекстом будет window{...}. Контекст window - это самый верхний уровень, это контекст окна браузера, в котором выполняется код.

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

Использовать стрелочные функции следует тогда, когда Вам не требуется контекст. Чаще всего стрелочные функции используют в Promise и callback, а так же в методах объекта.

 // Примеры использования
   
    // Callback
    function foo(callback){
        callback = (typeof(callback) == 'function') ? callback : () => {};
        
        setTimeout(() => {
            callback()
        }, 150);
        
    }
    
    foo( () => console.log('Callback time!') );
    
    // Promise
    function foo2(){
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve()
            }, 150);
        });
    }
    
    foo2().then( ()=>{
        console.log('Promise time!')
    } );

Помимо стрелочных функций в ES6 был добавлен новый способ объявления переменных let. В чем отличие от привычного var?

Переменная, созданная при помощи let, ограничивается областью видимости {...}. Такое объявление переменных упрощает жизнь сборщику "мусора", так как переменная хранится в оперативной памяти до тех пор, пока не выполнится {...}.

    //// VAR ////
var age = 20;

if (true) {
  var age = 25;
  console.log(age); // 25 (внутри блока)
}

console.log(age); // 25 (снаружи блока то же самое)


//// LET ////
var age = 20;

if (true) {
  let age = 25;
  console.log(age); // 25 (внутри блока)
}

console.log(age); // 20 (снаружи блока переменная не изменила значение)

console.log(window.age); // Переменная age записана в объект window

Хочу обратить Ваше внимание, что все переменные, созданные вне функции, будут записаны в объект window, это плохая практика, так как переменные созданные в объекте window хранятся в оперативной памяти до обновления или закрытия вкладки браузера.

3. Методы явного указания контекста call и apply

Метод call предназначен для вызова функций с указанием контекста. Что это значит? С помощью этого метода можно вызывать функцию в произвольном контексте.

В качестве аргументов в функцию передается контекст (объект) и произвольное количество аргументов.

func.call(context, arg1, arg2, ...)

Давайте вызовим функцию, которая выводит свойство name из своего контекста. При этом свойство name мы явно укажем через метод call.


function getName(){
	console.log(this.name);
}

getName.call({ name: 'Vasya' }); // Vasya

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


function getUser(lastname, age){
	console.log(`Всем привет! Я ${this.name} ${lastname} и мне ${age} лет.`);
}

getUser.call({ name: 'Vasya' }, 'Petrov', 20); // Всем привет! Я Vasya Petrov и мне 20 лет.

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


    getUser.call({ name: 'Vasya' }, 'Petrov', 20); // Всем привет! Я Vasya Petrov и мне 20 лет.
    getUser.apply({ name: 'Vasya' }, ['Petrov', 20] ); // Всем привет! Я Vasya Petrov и мне 20 лет.

К примеру, функция рассчета суммы всех переданных аргументов, вызванная при помощи apply:


function sum(){
	let result = 0;
	for(var arg of arguments) result += arg;
	console.log(`Сумма всех аргументов: ${result}`);
}

sum.apply({}, [20, 5, 15, 25, 17, 18]); // Сумма всех аргументов: 100