Простое объяснение применения JavaScript, вызова и привязки
🤔 После прочтения этой статьи, если вы еще не использовали apply, call and bind, вы взорвали мне голову!
Во-первых, предположим, что у нас есть такой пример:
- Кошки едят рыбу
- Собаки едят мясо
Что мы хотим получить в итоге:
- Кошки едят мясо
- Собаки едят рыбу
Предисловие
Сначала подготовьте два объекта: cat
, dog
:
Когда мы будем готовы, давайте сначала реализуем call
.
вызов
Если собака ест рыбу, ее нужно использовать так: cat.eatFish.call(dog)
, видно, что call
вызывается методом eating fish
на cat
, а параметры dog
и fish
, поэтому надо использовать так:
cat.eatFish.call(dog);
Для метода вызова the
примерная логика следующая:
- Первый переданный параметр используется как контекст; вот
dog
. dog
добавляет методeatFish
, указывающий наeatFish
кота, который являетсяthis
кота.- Конечно,
dog
тоже может есть все видыfish
- После еды
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
:
bind
также используется для преобразования указателяthis
.bind
не выполняется немедленно, как эти два, а возвращает новую функцию, связанную сthis
, которую необходимо вызвать снова для выполнения.bind
поддерживает каррирование функций.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
самостоятельно!