Front-end course

Контекст вызова. Рекурсия

Контекст вызова this, call, apply…

Доступ к свойствам объекта

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

let user = {
  firstName: "John",
  lastName: "Doe",
  getName: function() {
    return this.firstName;
  }
}

alert( user.getName() ); // John
let user = {
  firstName: "John",
  lastName: "Doe",
}

user.getFullName = function() {
  return this.firstName + " " + this.lastName;
}

alert( user.getFullName() ); // "John Doe"

ECMAScript 2015 позволяет определять методы объекта в сокращенном формате

let user = {
  firstName: "John",
  lastName: "Doe",
  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

alert( user.getFullName() ); // "John Doe"

Указатель this

Значение this является контекстом вызова и определяется в момент вызова функции. Указатель this содержит ссылку на объект, являющийся текущим контекстом (объект, чья функция вызывается)

При вызове функции без контекста в non-strict режиме this получает значение window, в строгом режиме – undefined

function f() {
  "use strict";
  
  return this.name;
}

let user1 = { name: "John Doe", getName: f };
let user2 = { name: "Harry Potter", getName: f };

alert( user1.getName() ); // "John Doe"
alert( user1.getName() ); // "Harry Potter"

Передача контекста

неявно, через вызов метода

При обращении к методу объекта значением this является сам объект:

let user = {
  name: "John",
  getName: function() {
    console.log(this);
    alert(this.name);
  }
};

user.getName();

явно, через функцию call

Синтаксис метода: func.call(context, arg1, arg2, …) , где

  • func – вызываемый метод
  • context – контекст вызова, то есть значение, которое принимает this
  • arg1, arg2 и т.д. – список параметров функции func
let getFullName = function() {
  alert(this.firstName + " " + this.lastName);
};

let user = {
  firstName: "John",
  lastName: "Doe"
};

getFullName.call(user); // "John Doe"

Вариант с добавлением аргументов

let getFullName = function(param1, param2) {
  alert(this[param1] + " " + this[param2]);
};

let user = {
  firstName: "John",
  lastName: "Doe"
};

getFullName.call(user, "firstName", "lastName"); // "John Doe"

С помощью метода call можно вызывать методы одного объекта в контексте другого (method borrowing)

let user = {
  firstName: "John",
  sayHello() {
    return "Hi " + this.firstName;
  }
};

let user2 = {
  firstName: "Kate",
};

console.log(user.sayHello.call(user2));
function joinArgs() {
  // вызов [].slice() скопирует все элементы из this в новый массив
  let args = [].slice.call(arguments);
  
  return args.join(" "); // args - полноценный массив из аргументов
}

alert( joinArgs("What's up", "Mr.", "President") ); // "What's up Mr.President"

явно, через функцию apply

Вызов функции при помощи func.apply работает аналогично func.call, но принимает массив аргументов вместо списка

Синтаксис метода: func.apply(context, [args, arg2, …]) , где

  • func – вызываемый метод
  • context – контекст вызова, то есть значение, которое принимает this
  • [arg1, arg2 и т.д.] – массив параметров функции func
let getFullName = function(param1, param2) {
  alert(this[param1] + " " + this[param2]);
};

let user = {
  firstName: "John",
  lastName: "Doe"
};

getFullName.apply(user, ["firstName", "lastName"]); // "John Doe"

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

let arr = [1, 5, 2];

let max = arr[0];

for ( let i = 1; i < arr.length; i++ ) {
  if (arr[i] > max) max = arr[i]; 
}

alert(max); // 5
let arr = [1, 5, 2];

alert( Math.max(1, 5, 2) ); // 5

alert( Math.max(arr) ); // NaN

alert( Math.max.apply(Math, arr) ); // 5
alert( Math.max.apply(null, arr) ); // 5

При указании первым аргументом методов call/apply значений null или undefined контекстом будет глобальный объект

неявно, через конструктор

Если функция вызывается через оператор new как конструктор объекта, то this указывает на создаваемый объект:

function User(name) {
  // this = {};
  
  // модификация this, добавление свойств или методов
  this.name = name;
  
  // return this;
}

let john = new User("John");
console.log(john); // { name: "John" }

Потеря контекста

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

let bomb = {
  delay: 1000,
  sound: "BOOOM!",
  explode: function() {
    // this === bomb
    
   setTimeout(function() {
     // this === window
     
     alert(this.sound);
   }, this.delay);
  }
}

bomb.explode();

Стрелочные функции не имеют собственного this, они исполняются в том контексте, в котором были созданы. Таким образом, ситуация потери контекста не происходит, что иногда удобно

let bomb = {
  delay: 1000,
  sound: "BOOOM!",
  explode: function() {
   setTimeout(() => alert(this.sound), this.delay);
  }
}

bomb.explode();

Привязка контекста методом bind

Встроенный метод bind создает новую функцию (обертку) с зафиксированным контекстом, переданным в качестве параметра функции

Синтаксис метода: func.bind(context, args, arg2, …) , где

  • func – произвольная функция
  • context – контекст, который должен быть привязан к функции func
  • arg1, arg2 и т.д. – список параметров функции func, которые будут зафиксированы в созданном методе
let bomb = {
  delay: 1000,
  sound: "BOOOM!",
  explode: function() {
    // this === bomb
    
   setTimeout(function() {
     // this === window
     
     alert(this.sound);
   }.bind(this), this.delay);
  }
}

bomb.explode();

Карринг

С помощью метода bind можно зафиксировать не только контекст, но и параметры функции – такое преобразование называется карринг (currying), или каррирование

Фиксировать можно любое количество параметров исходной функции, новая функция (partial function) будет использовать оставшиеся

function multi(x, y) {
  return x * y;
}

let byTwo = multi.bind(null, 2);

for ( let i = 1; i <= 10; i++ ) {
  console.log( i + " x 2 = " + byTwo(i));
}

Формирование цепочки вызовов (chaining)

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

let counter = {
  count: 0,
  up: function() {
    this.count++;
    return this;
  }
}

counter.up().up().up();
alert(counter.count);

Рекурсия

rekursiy

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

Частный случай подвызова - когда функция вызывает сама себя. Это называется рекурсией.

let arr = [0, 1];

function fibo(count) {
  arr[arr.length] = arr[arr.length - 2] + arr[arr.length - 1];
  count -= 1;
  
  if (count !== 0) {
    return fibo(count);
  }
  
  return arr;
}

console.log(fibo(20));

Базис рекурсии - значение на котором рекурсия заканчивается.

Глубина рекурсии - общее кол-во вложенных вызовов.

У каждого вызова функции есть свой “контекст выполнения” (execution context).

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

При любом вложенном вызове JavaScript запоминает текущий контекст выполнения в специальной внутренней структуре данных - “стеке контекстов”.

Для нового вызова создается свой контекст выполнения, и управление переходит в него, а когда он завершен - старый контекст достается из стека и выполнение внешней функции возобновляется.

Любая рекурсия может быть переделана в цикл. Как правило, вариант с циклом будет быстрее и эффективнее.

Материалы для прочтения


Written by Vadim Goloviychuk