Винтажный JS — bind, call и apply своими руками
Один из самых часто задаваемых на собеседовании вопросов — «напишите свою реализацию метода bind». Не знаю, что таким образом собеседующие хотят проверить, но, думаю, если вы только начинаете свой путь в волшебном мире JS, вам будет интересно посмотреть как ответить на этот вопрос.
Итак, для начала давайте вспомним, что вообще делают методы bind, call и apply. Все три метода делают, собственно, одну и ту же вещь — позволяют вызвать функцию, указав ей объект, который она должна использовать в качестве контекста. Контекст, если забыли, это то, к чему мы обращаемся с помощью ключевого слова this. Например, у нас есть объект:
const user = {
fullName: 'Иван Человеков'
}
И у нас есть абстрактная функция, возвращающая имя:
function getName() {
return this.fullName;
}
Если мы просто вызовем эту функцию, то в ответ получим undefined. Потому что при вызове контекстом для неё будет глобальный объект window, у которого нет свойства fullName. А вот если бы мы могли сказать ей, что контекстом должен быть объект user, то мы бы получили в ответ Иван Человеков. Именно это мы и можем сделать с помощью методов bind, call и apply.
const user = {
fullName: 'Иван Человеков'
}
function getName() {
return this.fullName;
}
getName.bind(person)() // Иван Человеков
У метода bind есть особенность. Он возвращает функцию с новым контекстом. Т.е нам нужно её вызывать самостоятельно. Именно поэтому в примере после bind стоят скобки, чтобы сразу же вызвать возвращаемую функцию. Методы call и apply самостоятельно вызывают функцию, к которой применяются. Между собой они отличаются тем, как они работают с аргументами, которые мы передаём в вызываемую функцию.
const user = {
firstName: '',
lastName: '',
fullName() {
return `${this.firstName} ${this.lastName}`
}
}
function getFullName(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
return this.fullName();
}
getFullName.call(user, 'Иван', 'Человеков');
getFullName.apply(user, ['Иван', 'Человеков']);
Как видно из примера, метод call принимает аргументы просто в виде списка, е метод apply — в виде массива. Метод bind, в силу своих особенностей (помните же, что он возвращает функцию, которую нужно ещё вызвать), может принимать аргументы двумя способами.
getFullName.bind(user, 'Иван', 'Человеков')();
getFullName.bind(user, 'Иван')('Человеков');
getFullName.bind(user)('Иван', 'Человеков');
Т.е мы можем передавать аргументы как в сам метод bind, так и непосредственно в возвращаемую функцию.
Теперь, когда мы вспомнили как это всё работает, можем приступить к написанию собственной функции bind. Для начала определимся с логикой:
- функция принимает на вход функцию, контекст которой нужно поменять, сам объект контекста и дополнительные аргументы
- функция возвращает полученную функцию, контекст которой заменён на полученный объект
Тут всё понятно. А как сделать так, чтобы нужный нам объект стал контекстом функции? Самый простой способ — сделать эту функцию методом объекта.
function bind(fn, context) {
return function() {
const uuid = Date.now().toString();
context[uuid] = fn;
const res = context[uuid]();
delete context[uuid];
return res;
}
}
Ну вот, собственно, логика готова. Разберём, что тут у нас происходит. Чтобы сделать функцию методом объекта контекста нам нужно создать у него новое свойство. И свойство это должно быть уникальным, чтобы не получилось так, что мы случайно изменим существующее поле. Самый простой способ получить уникальное значение — использовать время. Оно вполне уникально и никогда не повторяется. Формируем уникальную строку const uuid = Date.now().toString();, создаём новое поле у объекта и кладём туда нашу функцию context[uuid] = fn;. Затем мы помещаем вызов функции в новую переменную const res = context[uuid](); и возвращаем объекту контекста изначальное состояние delete context[uuid];. И в самом конце возвращаем переменную с функцией return res.
Теперь нужно разобраться с аргументами. Как помните, аргументы могут быть переданы как в саму функцию bind, так и в возвращаемую функцию. И нам нужно обработать оба варианта. Это достаточно просто, нам поможет синтаксис rest parameters.
function bind(fn, context, ...rest) {
return function(...args) {
const uuid = Date.now().toString();
context[uuid] = fn;
const res = context[uuid](...rest, ...args);
delete context[uuid];
return res;
}
}
Готово. Давайте проверим работоспособность нашей крафтовой функции bind.
const user = {
firstName: '',
lastName: '',
fullName() {
return `${this.firstName} ${this.lastName}`
}
}
function getFullName(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
return this.fullName();
}
bind(getFullName, user, 'Иван', 'Человеков')() // => Иван Человеков
bind(getFullName, user, 'Иван')('Человеков') // => Иван Человеков
bind(getFullName, user)('Иван', 'Человеков') // => Иван Человеков
Да, мы сделали это, написали свой собственный bind 🥇. Написать call и apply теперь проще простого.
call(fn, context, ...args) {
const uuid = Date.now().toString();
context[uuid] = fn;
const res = context[uuid](...args);
delete context[uuid];
return res;
}
apply(fn, context, args) {
const uuid = Date.now().toString();
context[uuid] = fn;
const res = context[uuid](...args);
delete context[uuid];
return res;
}
Тут нам не нужно возвращать функцию, поэтому кода немного меньше, а логика та же самая. Немного лишь отличается работа с аргументами, Как помните функция call принимает список аргументов. Поэтому мы используем синтаксис rest arguments. В случае с apply это не нужно, так как тут мы точно знаем, что принимаем массив. Вот и всё. Это оказалось не так уж и сложно 🎉.