Связывание/применение конструкторов в JavaScript

Я искал решения для вызова конструкторов Javascript с произвольным числом аргументов и нашел несколько хороших сообщений SO, что заставило меня поверить, что эти три вызова должны работать одинаково. Однако, по крайней мере, в носороге и node.js, они не выполняют:

1. f = Date.bind(Date, 2000,0,1)
2. g = Date.bind.call(Date, 2000, 0, 1)
3. h = Date.bind.apply(Date, [2000, 0, 1])

Первый имеет желаемый результат:

print(new f()) //=> Sat Jan 01 2000 00:00:00 GMT-0500 (EST)

Но другие два не делают:

print(new g()) //=> Thu Feb 01 1900 00:00:00 GMT-0500 (EST)
print(new h()) //=> Wed Jun 01 1904 00:00:00 GMT-0400 (EST)

Так что что-то ушло где-то. Мысли о чем? Неплохо ли смешать такие вещи, как apply, bind и/или call с помощью new?

Ответ 1

Ранее принятый ответ был неверным. Вы можете использовать bind, call и apply with constructors для создания новых конструкторов просто отлично - единственная проблема в вашем тесте заключается в том, что вы забыли, что bind.apply и bind.call применяются и вызывают bind, а не сам конструктор, поэтому вы указали неверные аргументы.

f = Date.bind(null, 2000,0,1)
g = Function.bind.call(Date, null, 2000, 0, 1)
h = Function.bind.apply(Date, [ null, 2000, 0, 1 ])

new f() //=> Sat Jan 01 2000 00:00:00 GMT-0500 (EST)
new g() //=> Sat Jan 01 2000 00:00:00 GMT-0500 (EST)
new h() //=> Sat Jan 01 2000 00:00:00 GMT-0500 (EST)

Все три: instanceof Дата.

Аргументы вызова - это контекст выполнения, за которым следуют применяемые аргументы. Применить аргументы - это контекст выполнения и массив аргументов. Привязка аргументов - это контекст выполнения, за которым следуют аргументы для привязки.

Таким образом, аргументы для применения, например, являются контекстом для применения bind (Date), за которым следует массив, который является аргументом для bind (поэтому первый член массива является аргументом контекста привязки). Вот почему это запутывает вызов или применение bind; странно задавать аргументы контекста для обоих.

Обратите внимание, что при использовании связывания с конструкторами аргумент контекста всегда игнорируется, потому что "новый" явно создает новый контекст. Я использую null, когда аргумент контекста не имеет значения, чтобы это было ясным, но это может быть что угодно.

Между тем, применять и вызывать в этих примерах нужно знать, что контекст, в котором они должны применяться /call bind, - это функция Date. Я переключил "Date" на "Function", где это возможно, чтобы осветить то, что на самом деле обеспечивает контекст где. Когда мы вызываем заявку или вызываем Date.bind, мы действительно вызываем apply или вызываем метод привязки, не привязанный к объекту Date. Метод связывания в таком случае может исходить из любой функции вообще. Это может быть Number.bind.call(Date, null, 2000, 0, 1), и результат будет точно таким же.

Если не понятно, почему, рассмотрите разницу между следующими примерами:

context.method();

и

var noLongerAMethod = context.method;
noLongerAMethod();

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

Лично я не думаю, что мне когда-либо приходилось звонить или применять bind, и трудно представить себе ситуацию, для которой это было бы хорошим решением, но конструкторы связывания для создания новых конструкторов - это то, что я нашел очень полезно по случаю. В любом случае это забавная головоломка.

Ответ 2

bind и apply/call работает только при работе с функцией, но не конструктором, поэтому в основном с помощью собственных методов вы не можете этого сделать, заключается в написании метода bindConstruct, но это может быть связано с дополнительной сложностью:

function bindConstruct(fn) {
    // since constructor always accepts a static this value
    // so bindConstruct cannot specify this
    var extraArgs = [].slice.call(arguments, 1);

    // create a 'subclass' of fn
    function sub() {
        var args = extraArgs.concat([].slice.call(arguments));
        fn.apply(this, args);
    }
    sub.prototype = fn.prototype;
    sub.prototype.constructor = sub;

    return sub;
}

Это, фактически, создает подкласс для вашего конструктора.

Затем ваш код:

var MyClass = function(x, y) {
    console.log(arguments);
    console.log(x + y);
}
var BindedMyClass = bindConstruct(MyClass, 1, 2, 3);
var c = new BindedMyClass(4, 5);
console.log(c instanceof MyClass);
console.log(c instanceof BindedMyClass);

Вы также можете записать эту функцию в Function.prototype или в качестве расширения для встроенной функции bind.