Skip to content

Instantly share code, notes, and snippets.

@Nathan-Wall
Created November 18, 2012 17:56
Show Gist options
  • Save Nathan-Wall/4106515 to your computer and use it in GitHub Desktop.
Save Nathan-Wall/4106515 to your computer and use it in GitHub Desktop.
An implementation of call, apply, and bind without the use of call, apply, or bind. Additionally includes a lazyBind implementation.

The following script FN.js is an experiment in implementing call, apply, and bind without the use of Function.prototype.call, Function.prototype.apply, or Function.prototype.bind. This implementation is for ECMAScript 5. It accurately emulates call, apply, and bind in the vast majority of cases with these known limitations:

  • Call stack is increased over what would be witnessed for built-in call, apply, and bind. This will show up in an Error's stack property and in function.caller in non-strict mode. Otherwise, besides a performance loss, this should be an unobservable limitation.
  • In strict mode, it is not possible to call a function in the context of null. Due to limitations imposed by the language, the function will be called in the context of undefined instead.
  • If an object is frozen/sealed/made unextensible in another frame, it breaks its ability to be used as the context object in emulated call, apply, or bind. This is a small limitation because it only applies to objects frozen in another frame; objects frozen in the same frame where FN is defined will work fine.
  • That's it! Every other aspect of using the emulated call, apply, or bind should be identical to using the built-in methods.

Use:

This script provides the FN object with members call, apply, bind, lazyBind, and instance. The functions call, apply, and bind take a function as their first argument and an object to use as the "thisArg"/context as their second argument. lazyBind similarly takes a function as its first argument, but it generates a function which allows you to pass in the context later.

var obj = { foo: 'bar' };
function logThis(a, b, c) {
    console.log(this, a, b, c);
}

FN.call(logThis, obj); // => { foo: 'bar' }, undefined, undefined, undefined
FN.call(logThis, obj, 1, 2); // => { foo: 'bar' }, 1, 2, undefined
FN.apply(logThis, obj, [ 3, 4, 5 ]); // => { foo: 'bar' }, 3, 4, 5
    
var bound = FN.bind(logThis, obj);
bound('x', 'y', 'z'); // => { foo: 'bar' }, 'x', 'y', 'z'

var lazyBound = FN.lazyBind(logThis);
lazyBound(obj, 'x', 'y', 'z'); // => { foo: 'bar' }, 'x', 'y', 'z'

The instance property allows you to use the functions as methods, which work exactly the same as the built-ins:

Function.prototype.fnCall = FN.instance.call;
Function.prototype.fnApply = FN.instance.apply;
Function.prototype.fnBind = FN.instance.bind;
Function.prototype.fnLazyBind = FN.instance.lazyBind;
   
var obj = { foo: 'bar' };
function logThis(a, b, c) {
    console.log(this, a, b, c);
}

logThis.fnCall(obj); // => { foo: 'bar' }, undefined, undefined, undefined
logThis.fnCall(obj, 1, 2); // => { foo: 'bar' }, 1, 2, undefined
logThis.fnApply(obj, [ 3, 4, 5 ]); // => { foo: 'bar' }, 3, 4, 5
    
var bound = logThis.fnBind(obj);
bound('x', 'y', 'z'); // => { foo: 'bar' }, 'x', 'y', 'z'

var lazyBound = logThis.fnLazyBind();
lazyBound(obj, 'x', 'y', 'z'); // => { foo: 'bar' }, 'x', 'y', 'z'
var FN = (function(Object, String) {
'use strict';
var undefined,
create = Object.create,
keys = Object.keys,
getOwnPropertyNames = Object.getOwnPropertyNames,
getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor,
isExtensible = Object.isExtensible,
defineProperty = Object.defineProperty,
fromCharCode = String.fromCharCode,
// A property name can be prepped to be exposed when object[SECRET_KEY] is accessed.
preppedName,
// Determines whether object[SECRET_KEY] should expose the secret map.
locked = true,
random = getRandomGenerator(),
// idNum will ensure identifiers are unique.
idNum = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ],
preIdentifier = randStr(7) + '0',
SECRET_KEY = '!S:' + getIdentifier();
// Override Object.create
Object.create = function create_(proto, props) {
var obj = create(proto, props);
Secrets(obj);
return obj;
};
// Override getOwnPropertyNames
Object.getOwnPropertyNames = (function() {
var original = Object.getOwnPropertyNames;
return function getOwnPropertyNames(obj) {
var ret = [ ],
names = original(obj);
for (var i = 0, j = 0; i < names.length; i++)
if (names[i] != SECRET_KEY)
ret[j++] = names[i];
return ret;
};
})();
// Override functions which prevent extensions on objects to go ahead and add a secret map first.
[ 'preventExtensions', 'seal', 'freeze' ].forEach(function(u) {
var original = Object[u];
Object[u] = function(obj) {
// Define the secret map.
Secrets(obj);
return original(obj);
};
});
var methods = {
set: function setSecretProperty(O, name, value) {
locked = false;
O[SECRET_KEY][name] = value;
return value;
},
getOwn: function getOwnSecretProperty(O, name) {
locked = false;
return O[SECRET_KEY][name];
},
delete: function deleteSecretProperty(O, name) {
locked = false;
return delete O[SECRET_KEY][name];
}
};
function Symbol(/* params */) {
Secrets(this).set('id', '!Y:' + getIdentifier());
}
Object.defineProperties(Symbol.prototype, {
toString: {
value: function() {
preppedName = Secrets(this).getOwn('id');
return SECRET_KEY;
},
enumerable: false,
writable: true,
configurable: true
},
// We can't simulate "delete obj[symbol]" in ES5. So we'll have to resort to
// "symbol.deleteFrom(obj)" in this situation.
deleteFrom: {
value: function(obj) {
var S = Secrets(obj), T = Secrets(this);
if (!S || !T) return false;
return S.delete(T.getOwn('id'));
},
enumerable: false,
writable: true,
configurable: true
}
});
var contextualCall = contextualize(emulateCall),
contextualApply = contextualize(emulateApply),
contextualBind = contextualize(emulateBind);
return {
call: emulateCall,
apply: emulateApply,
bind: emulateBind,
lazyBind: emulateBind(contextualBind, contextualCall),
instance: {
call: contextualCall,
apply: contextualApply,
bind: contextualBind,
lazyBind: contextualize(emulateBind(contextualBind, contextualCall))
}
};
function emulateApply(f, context, args) {
var argsClass = getClassOf(args);
if (argsClass != 'Array' && argsClass != 'Arguments')
throw new TypeError('Arguments list has wrong type.');
var $method = new Symbol(),
argStrs = [ ];
for (var i = 0; i < args.length; i++)
argStrs[argStrs.length] = 'args[' + i + ']';
// If context is null or undefined, evaluate without a context.
if (context == null)
return eval('f(' + join(argStrs, ', ') + ');');
// Coerce context to an object.
context = Object(context);
// Make sure the context object has a Secrets map defined on it.
if (!Secrets(context))
throw new Error('Cannot use object as a context object.');
context[$method] = f;
// Use eval to call the method with the right number of arguments without using the built-in apply.
var ret = eval('context[$method](' + join(argStrs, ', ') + ');');
$method.deleteFrom(context);
return ret;
}
function emulateCall(f, context/*, ...args */) {
// Although emulateCall could in a sense be implemented using emulateApply, we need an independent
// implementation here because emulateApply relies on getClassOf which relies on emulateCall.
var $method = new Symbol(),
args = arguments,
argStrs = [ ];
for (var i = 2; i < arguments.length; i++)
argStrs[argStrs.length] = 'args[' + i + ']';
// If context is null or undefined, evaluate without a context.
if (context == null)
return eval('f(' + join(argStrs, ', ') + ');');
// Coerce context to an object.
context = Object(context);
// Make sure the context object has a Secrets map defined on it.
if (!Secrets(context))
throw new Error('Cannot use object as a context object.');
context[$method] = f;
// Use eval to call the method with the right number of arguments without using the built-in apply.
var ret = eval('context[$method](' + join(argStrs, ', ') + ');');
$method.deleteFrom(context);
return ret;
}
function emulateBind(f, context/*, ...preArgs */) {
var preArgs = [ ];
for (var i = 2; i < arguments.length; i++)
preArgs[preArgs.length] = arguments[i];
return function bound(/* ...args */) {
// Copy the preArgs array with concat.
var args = preArgs.concat([ ]);
for (var i = 0; i < arguments.length; i++)
args[args.length] = arguments[i];
return emulateApply(f, context, args);
};
}
function Secrets(O, name) {
if(O === Object.prototype) return;
if (O !== Object(O))
throw new TypeError('Not an object: ' + O);
if (!(SECRET_KEY in O)) {
if (!isExtensible(O))
throw new Error('Object is not extensible.');
defineProperty(O, SECRET_KEY, own({
get: (function() {
var secretMap = create(
// Prevent the secret map from having a prototype chain.
null,
own({
Secrets: { value: preloadMethods(methods, O) }
})
);
return function getSecret() {
var value;
// The lock protects against retrieval in the event that the SECRET_KEY is discovered.
if (locked) {
if (!preppedName) return;
var name = preppedName;
preppedName = undefined;
value = secretMap.Secrets.getOwn(name);
return value;
}
locked = true;
return secretMap;
};
})(),
set: function setSecret(value) {
// Weird Chrome behavior where getOwnPropertyNames seems to call object[key] = true...
// Let's ignore it.
if(preppedName === undefined) return;
var ret;
locked = false;
var name = preppedName;
preppedName = undefined;
ret = this[SECRET_KEY].Secrets.set(name, value);
return ret;
},
enumerable: false,
configurable: false
}));
}
locked = false;
if (name) return O[SECRET_KEY].Secrets.getOwn(name);
return O[SECRET_KEY].Secrets;
}
function getIdentifier() {
var range = 125 - 65, idS = '';
idNum[0]++;
for(var i = 0; i < idNum.length; i++) {
if (idNum[i] > range) {
idNum[i] = 0;
if (i < idNum.length) idNum[i + 1]++;
else idNum = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
}
idS += encodeStr(idNum[i]);
}
return preIdentifier + ':' + join(getRandStrs(8, 11), '/') + ':' + idS;
}
function encodeStr(num) {
return fromCharCode(num + 65);
}
function getRandStrs(count, length) {
var r = [ ];
for(var i = 0; i < count; i++)
r[i] = randStr(length);
return r;
}
function randStr(length) {
var s = '';
for (var i = 0; i < length; i++)
s += encodeStr(random() * (125 - 65 + 1));
return s;
}
function getRandomGenerator() {
var getRandomValues
= typeof crypto != 'undefined' && crypto != null
? (function() {
var f = crypto.random || crypto.getRandomValues;
if (f) return f;
return undefined;
})()
: undefined;
if (getRandomValues) {
// Firefox (15 & 16) seems to be throwing a weird "not implemented" error on getRandomValues.
// Not sure why?
try { getRandomValues(new Uint8Array(4)); }
catch(x) { getRandomValues = undefined }
}
if (typeof getRandomValues == 'function' && typeof Uint8Array == 'function') {
return (function() {
var values = new Uint8Array(4), index = 4;
return function random() {
if (index >= values.length) {
getRandomValues(values);
index = 0;
}
return values[index++] / 256;
};
})();
} else return Math.random;
}
function join(array, glue) {
var s = String(array[0]);
for (var i = 1; i < array.length; i++) {
s += glue + array[i];
}
return s;
}
function preloadMethods(methods, arg) {
var loaded = create(null),
ks = keys(methods);
for (var i = 0, method; i < ks.length; i++) {
method = ks[i];
loaded[method] = (function(method) {
return function loadedMethod($0, $1) {
return methods[method](arg, $0, $1);
};
})(method);
}
return loaded;
}
function getClassOf(obj) {
return emulateCall(Object.prototype.toString, obj).slice(8, -1);
}
function contextualize(f) {
return function contextualized() {
var args = arguments,
argStrs = [ 'this' ];
for (var i = 0; i < arguments.length; i++)
argStrs[argStrs.length] = 'args[' + i + ']';
return eval('f(' + join(argStrs, ', ') + ');');
};
}
function own(obj) {
var O = create(null);
var keys = getOwnPropertyNames(obj);
for (var i = 0, key; i < keys.length; i++) {
key = keys[i];
defineProperty(O, key,
getOwnPropertyDescriptor(obj, key));
}
return O;
}
})(Object, String);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment