en.javascript.info/01-js/06-objects-more/04-call-apply/article.md
Ilya Kantor f301cb744d init
2014-10-26 22:10:13 +03:00

13 KiB
Raw Blame History

Явное указание this: "call", "apply"

Итак, мы знаем, что в this -- это текущий объект при вызове "через точку" и новый объект при конструировании через new.

В этой главе наша цель получить окончательное и полное понимание this в JavaScript. Для этого не хватает всего одного элемента: способа явно указать this при помощи методов call и apply.

[cut]

Метод call

Синтаксис метода call:

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

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

Вызов func.call(context, a, b...) -- то же, что обычный вызов func(a, b...), но с явно указанным this(=context).

Например, у нас есть функция showFullName, которая работает с this:

function showFullName() { 
  alert( this.firstName + " " + this.lastName );
}

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

Вызов showFullName.call(user) запустит функцию, установив this = user, вот так:

//+ run
function showFullName() { 
  alert( this.firstName + " " + this.lastName );
}

var user = { 
  firstName: "Василий",
  lastName: "Петров"
};

*!*
// функция вызовется с this=user
showFullName.call(user)  // "Василий Петров"
*/!*

После контекста в call можно передать аргументы для функции. Вот пример с более сложным вариантом showFullName, который конструирует ответ из указанных свойств объекта:

//+ run
var user = { 
  firstName: "Василий",
  surname: "Петров",
  patronym: "Иванович"
};

function showFullName(firstPart, lastPart) { 
  alert( this[firstPart] + " " + this[lastPart] );
}

*!*
// f.call(контекст, аргумент1, аргумент2, ...)
showFullName.call(user, 'firstName', 'surname')  // "Василий Петров"
showFullName.call(user, 'firstName', 'patronym')  // "Василий Иванович"
*/!*

"Одалживание метода"

При помощи call можно легко взять метод одного объекта, в том числе встроенного, и вызвать в контексте другого.

Это называется "одалживание метода" (на англ. method borrowing).

Используем эту технику для упрощения манипуляций с arguments.

Как мы знаем, arguments не массив, а обычный объект, поэтому таких полезных методов как push, pop, join и других у него нет. Но иногда так хочется, чтобы были...

Нет ничего проще! Давайте скопируем метод join из обычного массива:

//+ run
function printArgs() {
  arguments.join = [].join; // одолжили метод (1)

  var argStr = arguments.join(':');  // (2)

  alert(argStr);  // сработает и выведет 1:2:3
}

printArgs(1, 2, 3);
  1. В строке `(1)` объявлен пустой массив `[]` и скопирован его метод `[].join`. Обратим внимание, мы не вызываем его, а просто копируем. Функция, в том числе встроенная -- обычное значение, мы можем скопировать любое свойство любого объекта, и `[].join` здесь не исключение.
  2. В строке `(2)` запустили `join` в контексте `arguments`, как будто он всегда там был.
  3. [smart header="Почему вызов сработает?"]

    Здесь метод join массива скопирован и вызван в контексте arguments. Не произойдёт ли что-то плохое от того, что arguments -- не массив? Почему он, вообще, сработал?

    Ответ на эти вопросы простой. В соответствии со спецификацией, внутри join реализован примерно так:

    function join(separator) {
      if (!this.length) return '';
    
      var str = this[0]; 
    
      for (var i = 1; i<this.length; i++) {
        str += separator + this[i]; 
      }
      
      return str;
    }
    

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

    В качестве this подойдёт даже обычный объект:

    //+ run
    var obj = {  // обычный объект с числовыми индексами и length
      0: "А", 
      1: "Б", 
      2: "В",
      length: 3
    };
    
    *!*
    obj.join = [].join;
    alert( obj.join(';') ); // "A;Б;В"
    */!*
    

    [/smart]

    ...Однако, копирование метода из одного объекта в другой не всегда приемлемо!

    Представим на минуту, что вместо arguments у нас -- произвольный объект. У него тоже есть числовые индексы, length и мы хотим вызвать в его контексте метод [].join. То есть, ситуация похожа на arguments, но (!) вполне возможно, что у объекта есть свой метод join.

    Поэтому копировать [].join, как сделано выше, нельзя: если он перезапишет собственный join объекта, то будет страшный бардак и путаница.

    Безопасно вызвать метод нам поможет call:

    //+ run
    function printArgs() {
      var join = [].join; // скопируем ссылку на функцию в переменную
    
    *!*
      // вызовем join с this=arguments,
      // этот вызов эквивалентен arguments.join(':') из примера выше
      var argStr = join.call(arguments, ':'); 
    */!*
    
      alert(argStr);  // сработает и выведет 1:2:3
    }
    
    printArgs(1, 2, 3);
    

    Мы вызвали метод без копирования. Чисто, безопасно.

    Ещё пример: [].slice.call(arguments)

    В JavaScript есть очень простой способ сделать из arguments настоящий массив. Для этого возьмём метод массива: slice.

    По стандарту вызов arr.slice(start, end) создаёт новый массив и копирует в него элементы массива arr от start до end. А если start и end не указаны, то копирует весь массив.

    Вызовем его в контексте arguments:

    //+ run
    function printArgs() {
      // вызов arr.slice() скопирует все элементы из this в новый массив
    *!*
      var args = [].slice.call(arguments);
    */!*
      alert( args.join(', ') ); // args - полноценный массив из аргументов
    }
    
    printArgs('Привет', 'мой', 'мир'); // Привет, мой, мир
    

    Как и в случае с join, такой вызов технически возможен потому, что slice для работы требует только нумерованные свойства и length. Всё это в arguments есть.

    Метод apply

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

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

    func.call(context, arg1, arg2)
    // идентичен вызову
    func.apply(context, [arg1, arg2]);
    

    В частности, эти две строчки cработают одинаково:

    showFullName.call(user, 'firstName', 'surname'); 
    
    showFullName.apply(user, ['firstName', 'surname']);
    

    Преимущество apply перед call отчётливо видно в следующем примере, когда мы формируем массив аргументов динамически:

    //+ run
    var arr = [];
    arr.push(1);
    arr.push(5);
    arr.push(2);
    
    // получить максимум из элементов arr
    alert( Math.max.apply(null, arr) ); // 5
    

    Обратим внимание, в примере выше вызывается метод Math.max. Его стандартное применение -- это выбор максимального аргумента из переданных, которых может быть сколько угодно:

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

    В примере выше мы передали аргументы через массив -- второй параметр apply... Но вы, наверное, заметили небольшую странность? В качестве контекста this был передан null.

    Строго говоря, полным эквивалентом вызову Math.max(1,2,3) был бы вызов Math.max.apply(Math, [1,2,3]). В обоих этих вызовах контекстом будет объект Math.

    Но в данном случае в качестве контекста можно передавать что угодно, поскольку в своей внутренней реализации метод Math.maxне использует this. Действительно, зачем this, если нужно всего лишь выбрать максимальный из аргументов?

    Вот так, при помощи apply мы получили короткий и элегантный способ вычислить максимальное значение в массиве!

    [smart header="Вызов call/apply с null или undefined"]

    В старом стандарте при указании первого аргумента null или undefined в call/apply, функция получала this = window, например:

    //+ run
    function f() { 
      alert(this);
    }
    
    f.call(null); // window
    

    Это поведение исправлено в современном стандарте (15.3).

    Если функция работает в строгом режиме, то this передаётся "как есть":

    //+ run
    function f() { 
      "use strict";
    *!*
      alert(this); // null, а не window
    */!*
    }
    
    f.call(null);
    

    [/smart]

    Итого про this

    Значение this устанавливается в зависимости от того, как вызвана функция:

    При вызове функции как метода
    obj.func(...)    // this = obj
    obj["func"](...)
    
    При обычном вызове
    func(...)        // this = window (ES3) /undefined (ES5)
    
    В `new`
    new func()       // this = {} (новый объект)
    
    Явное указание
    func.apply(context, args) // this = context (явная передача)
    func.call(context, arg1, arg2, ...)