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