// 'this' arg gets dropped because console.log doesn't log it's own context
test = bindArguments(console.log, 'this', 'one', undefined, 'three', undefined, 'REST-After-This:', Symbol.split, 'END-2', 'END-Argument')
> test('TWO-filled-undefslot')
>> one TWO-filled-undefslot three undefined REST-After-This: END-2 END-Argument
> test(null, undefined, 'rest#1', 'rest#2')
>> one null three undefined REST-After-This: rest#1 rest#2 END-2 END-Argument
> test('TWO-filled-undefslot', 'FOUR-filled-undefslot', 'rest#1', 'rest#2')
>> one 2-filled-undefslot three 4-filled-undefslot REST-After-This: rest#1 rest#2 END-2 END-Argument
- The first arg following the function is always the
this
arg
- I used
Symbol.split
to differentiate between leading and trailing args. It'd probably be better if leading & trailing arrays containing the arguments to bind were instead directly passed.
undefined
values in the leading bound arguments get substituted with passed in arguments
- (bonus) The bound function inherits all properties/fields of the function it's passed.
- Remove
copyProperties
& REMOVE_DESCRIPTORS to omit behavior
UsingSymbol.split
:
const bindArguments = (()=>{
const { defineProperty, getOwnPropertyDescriptors, defineProperties } = Object;
const defineValue = (target, key, value) => defineProperty(target, key, { value: value });
const $targetFn = Symbol.for('bindArguments::[[TargetFunction]]');
const $thisArgV = Symbol.for('bindArguments::[[BoundThis]]');
const $headArgs = Symbol.for('bindArguments::[[BoundArgs]].Leading');
const $tailArgs = Symbol.for('bindArguments::[[BoundArgs]].Trailing');
const EMPTY_ARRAY = Object.freeze([]);
const REMOVE_DESCRIPTORS = [$targetFn, $thisArgV, $headArgs, $tailArgs, 'name', 'toString', 'length'];
const copyProperties = (target, source) => {
const descriptors = getOwnPropertyDescriptors(source);
for(let key in descriptors) {
if (REMOVE_DESCRIPTORS.includes(key)) { continue; }
defineProperty(target, key, descriptors[key]);
}
}
const fillUndefinedPositions = (boundArgs, args) => {
let idxUndefined = boundArgs.indexOf(undefined);
if (idxUndefined >= 0) {
boundArgs = [...boundArgs];
while (idxUndefined >= 0 && args.length > 0) {
boundArgs[idxUndefined] = args.shift();
idxUndefined = boundArgs.indexOf(undefined, idxUndefined+1);
}
}
return boundArgs;
};
const boundFuncProps = {
name: { configurable: true, get() { return 'bound ' + this[$targetFn].name; } },
toString:{ configurable: true, get() { return this[$targetFn].toString; } },
length: { configurable: true, get() { return Math.max(0, this[$targetFn].length - this[$headArgs].length); } }
}
const createBoundFunction = (targetFunction, boundThis, leadingArgs, trailingArgs) => {
defineValue(boundFunction, $targetFn, targetFunction);
defineValue(boundFunction, $thisArgV, boundThis);
defineValue(boundFunction, $headArgs, leadingArgs.length > 0 ? leadingArgs : EMPTY_ARRAY);
defineValue(boundFunction, $tailArgs, trailingArgs.length > 0 ? trailingArgs : EMPTY_ARRAY);
defineProperties(boundFunction, boundFuncProps)
copyProperties(boundFunction, targetFunction);
return boundFunction;
function boundFunction(...args) {
const targetFn = boundFunction[$targetFn];
const thisArgV = boundFunction[$thisArgV];
const headArgsV = boundFunction[$headArgs];
const tailArgs = boundFunction[$tailArgs];
const headArgs = headArgsV.length > 0 && args.length > 0 ? fillUndefinedPositions(headArgsV, args) : headArgsV;
const thisArg = thisArgV === undefined || this instanceof boundFunction ? this : thisArgV;
const result = targetFn.call(thisArg, ...headArgs, ...args, ...tailArgs);
return result;
};
};
return (fn, ...bindArgs) => {
const argsLength = bindArgs.length;
if (argsLength === 0) { return fn; }
Function.prototype.bind
const idxSplit = bindArgs.indexOf(Symbol.split);
switch(idxSplit) {
case -1:
return createBoundFunction(fn, bindArgs[0], bindArgs.slice(1), EMPTY_ARRAY);
case 0:
return createBoundFunction(fn, undefined, EMPTY_ARRAY, bindArgs.slice(1));
case 1:
return createBoundFunction(fn, bindArgs[0], EMPTY_ARRAY, bindArgs.slice(2));
default:
return createBoundFunction(fn, bindArgs[0], bindArgs.slice(1, idxSplit), bindArgs.slice(idxSplit+1));
}
};
})();
Pass leading and trailing arguments directly.
test2 = bindArguments2(console.log, null, ['this', 'one', undefined, 'three', undefined, 'REST-After-This:'], ['END-2', 'END-Argument'])
> test2('TWO-filled-undefslot', 'FOUR-filled-undefslot', 'rest#1', 'rest#2')
>> this one 2-filled-undefslot three 4-filled-undefslot REST-After-This: rest#1 rest#2 END-2 END-Argument
const bindArguments2 = (()=>{
const isArray = Array.isArray;
const { defineProperty, getOwnPropertyDescriptors, defineProperties } = Object;
const defineValue = (target, key, value) => defineProperty(target, key, { value: value });
const $targetFn = Symbol.for('bindArguments::[[TargetFunction]]');
const $thisArgV = Symbol.for('bindArguments::[[BoundThis]]');
const $headArgs = Symbol.for('bindArguments::[[BoundArgs]].Leading');
const $tailArgs = Symbol.for('bindArguments::[[BoundArgs]].Trailing');
const EMPTY_ARRAY = Object.freeze([]);
const REMOVE_DESCRIPTORS = [$targetFn, $thisArgV, $headArgs, $tailArgs, 'name', 'toString', 'length'];
const copyProperties = (target, source) => {
const descriptors = getOwnPropertyDescriptors(source);
for(let key in descriptors) {
if (REMOVE_DESCRIPTORS.includes(key)) { continue; }
defineProperty(target, key, descriptors[key]);
}
}
const fillUndefinedPositions = (boundArgs, args) => {
let idxUndefined = boundArgs.indexOf(undefined);
if (idxUndefined >= 0) {
boundArgs = [...boundArgs];
while (idxUndefined >= 0 && args.length > 0) {
boundArgs[idxUndefined] = args.shift();
idxUndefined = boundArgs.indexOf(undefined, idxUndefined+1);
}
}
return boundArgs;
};
const boundFuncProps = {
name: { configurable: true, get() { return 'bound ' + this[$targetFn].name; } },
toString:{ configurable: true, get() { return this[$targetFn].toString; } },
length: { configurable: true, get() { return Math.max(0, this[$targetFn].length - this[$headArgs].length); } }
}
return bindArguments;
function bindArguments(
/**@type {(...args: any[])=>any}*/fn,
/**@type {ThisParameterType<typeof fn>}*/thisArg,
/**@type {Parameters<typeof fn>}*/leadingArgs,
/**@type {DropEndWith<Parameters<typeof fn>, typeof leadingArgs>}*/trailingArgs
) {
defineValue(boundFunction, $targetFn, fn);
defineValue(boundFunction, $thisArgV, thisArg);
defineValue(boundFunction, $headArgs, isArray(leadingArgs) && leadingArgs.length > 0 ? leadingArgs : EMPTY_ARRAY);
defineValue(boundFunction, $tailArgs, isArray(trailingArgs) && trailingArgs.length > 0 ? trailingArgs : EMPTY_ARRAY);
defineProperties(boundFunction, boundFuncProps)
copyProperties(boundFunction, fn);
return boundFunction;
/**@type {(...args: DropBetween<Parameters<typeof fn>, typeof leadingArgs, typeof trailingArgs>>)=>ReturnType<typeof fn>}*/
function boundFunction(...args) {
const targetFn = boundFunction[$targetFn];
const thisArgV = boundFunction[$thisArgV];
const headArgsV = boundFunction[$headArgs];
const tailArgs = boundFunction[$tailArgs];
const headArgs = headArgsV.length > 0 && args.length > 0 ? fillUndefinedPositions(headArgsV, args) : headArgsV;
const thisArg = thisArgV === undefined || this instanceof boundFunction ? this : thisArgV;
const result = targetFn.call(thisArg, ...headArgs, ...args, ...tailArgs);
return result;
};
};
})();