Винтажный 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
это не нужно, так как тут мы точно знаем, что принимаем массив. Вот и всё. Это оказалось не так уж и сложно 🎉.