Простое объяснение применения JavaScript, вызова и привязки

🤔 После прочтения этой статьи, если вы еще не использовали apply, call and bind, вы взорвали мне голову!

Во-первых, предположим, что у нас есть такой пример:

  • Кошки едят рыбу
  • Собаки едят мясо

Что мы хотим получить в итоге:

  • Кошки едят мясо
  • Собаки едят рыбу

Предисловие

Сначала подготовьте два объекта: cat, dog:

Когда мы будем готовы, давайте сначала реализуем call.

вызов

Если собака ест рыбу, ее нужно использовать так: cat.eatFish.call(dog), видно, что call вызывается методом eating fish на cat, а параметры dog и fish, поэтому надо использовать так:

cat.eatFish.call(dog);

Для метода вызова the примерная логика следующая:

  1. Первый переданный параметр используется как контекст; вот dog.
  2. dog добавляет метод eatFish, указывающий на eatFish кота, который является this кота.
  3. Конечно, dog тоже может есть все виды fish
  4. После еды dog удаляет метод eatFish, потому что он ему не принадлежит, он просто заимствован.

Согласно приведенной выше логике, мы можем написать:

Таким образом, пользовательское call завершено! Давайте проверим это сейчас:

cat.eatFish.defineCall(dog);
dog.eatMeat.defineCall(cat);
// output:
// 🐶 eat fish!
// 🐱 eat meal!

🎉 Теперь собака может есть рыбу, а кошка мясо!

Теперь давайте dog съедим больше видов fish, давайте ненадолго изменим кошачий eatFish(Эта собака немного жадная):

let cat = {
  name: "🐱",
  eatFish(...args) {
    console.log(`${this.name} eat fish!what it eats:${args}`);
  },
};

Затем мы снова используем его следующим образом:

cat.eatFish.defineCall(dog, "salmon", "tuna", "shark");
// output:
// 🐶 eat fish!what it eats:salmon,tuna,shark

Таким образом, dog может есть все виды fish. Конечно, также можно использовать arguments для управления параметрами.

применять

Использование apply и call аналогично. Разница в том, что второй параметр — это array, мы можем записать его так:

Теперь используйте его снова и посмотрите, правильно ли он написан:

cat.eatFish.defineApply(dog, ["salmon", "tuna", "shark"]);
// output:
// 🐶 eat fish!what it eats:salmon,tuna,shark

🎉 Сработало!

связывать

Теперь, когда реализованы call и apply, можно реализовать и немного более сложный bind. Ведь они друзья.

Давайте сначала посмотрим, что есть у bind:

  1. bind также используется для преобразования указателя this.
  2. bind не выполняется немедленно, как эти два, а возвращает новую функцию, связанную с this, которую необходимо вызвать снова для выполнения.
  3. bind поддерживает каррирование функций.
  4. this новой функции, возвращаемой bind, не может быть изменено, равно как и call и apply.

Напишем пошагово, начиная с самого простого:

Function.prototype.defineBind = function (obj) {
  // If this does not exist, this may point to window during execution
  let fn = this;
  return function () {
    fn.apply(obj);
  };
};

Затем добавляем в него функцию передачи параметров, которая становится такой:

Затем добавьте к нему карри:

Сейчас defineBind почти оформился, пусть модернизируют до настоящего бинда, и есть еще одна деталь:

Возвращенная функция обратного вызова также может быть сконструирована в форме new, но в процессе построения ее this будет игнорироваться, а возвращаемый экземпляр по-прежнему может наследовать свойства конструктора и свойства прототипа конструктора, а также может получать свойства нормально (тоже просто this пропало, все остальное в норме).

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

function Fn(){};
let o = new Fn();
console.log(o.constructor === Fn); // true

И когда конструктор запущен, внутренний this указывает на экземпляр (кто звонит, на него указывает this), поэтому this.constructor указывает на конструктор:

function Fn() {
  console.log(this.constructor === Fn); // true
};
let o = new Fn();
console.log(o.constructor === Fn); // true

Можно ли изменить прототипное наследование, изменив точку this.contructor?

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

Таким образом, bind завершается, и экземпляр, созданный возвращаемым конструктором, не влияет на конструктор.

😱 но! Изменение прототипа экземпляра напрямую влияет на конструктор!

🤔 Что насчет этого? Было бы неплохо, если бы в прототипе конструктора не было ничего, чтобы они не влияли друг на друга… бла-бла-бла-бла-бла…

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

К Fn2 добавляется слой __proto__, чтобы прототип Fn2 указывал на экземпляр, а прототип экземпляра был Fn, чтобы изменения в Fn2 не повлияли на Fn (конечно, его еще можно модифицировать через __proto__.__proto__)!

Затем используйте отчет об ошибках, чтобы отшлифовать его:

🎉 Рукописная привязка завершена!

Наконец, используйте собаку, чтобы съесть рыбу, чтобы проверить:

Напоследок прилагается рукописный бинд es6 версии, можете его пройти, он пока относительно понятен:

🎉 Поздравляем, вы выучили и написали call, apply и bind самостоятельно!

Узнать больше