It ultimately makes sense not to reinvent the wheel, but it's also of a way to Improve yourself by (re)creating things that were already existing. I'm writing this series to mainly improving my understanding of how things work in JavaScript's Standard built-in objects like call
, apply
, bind
.
Function.prototype.call()
The call() method calls a function with a given this value and arguments provided individually. -- MDN
Initially, the method call
invokes the function and allows you to pass comma-separated arguments.
Example from MDN
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.call(this, name, price);
this.category = 'food';
}
function Toy(name, price) {
Product.call(this, name, price);
this.category = 'toy';
}
const food = new Food('cheese', 5)
console.log(food.name) // cheese
console.log(food) // {name: 'chees', price: 5, category: 'food'}
const fun = new Toy('robot', 40);
console.log(fun.name) // robot
Custom example
const add = (a, b) => a + b
console.log(add.call(null, 3, 8, 10)) // 11
Above examples, we can understand the basic functionality of the call method.
- Call changes the
this
context of the caller, In the above exampleProduct.call
replaces thethis
from its original function body with the first argument ofcall
, That isFood
. > Using the call to chain constructors for an object -- MDN
-
If call called with more than one arguments then in left to right order, starting with the second argument, pass each argument to the original function.
- in our case
name
andprice
.
- in our case
The
call
should not make any side effect on thethis
object.
The thisArg value is passed without modification as
this
value. This is a change from Edition 3, where an undefined or null thisArg is replaced with the global object and ToObject is applied to all other values and that result is passed as thethis
value. Even though the thisArg is passed without modification, non-strict functions still perform these transformations upon entry to the function. -- Ecma
Lets re-implement the call method.
if(!Function.prototype.fauxCall){
Function.prototype.fauxCall = function(context){
context.fn = this;
return context.fn();
}
}
const food = new Food('cheese', 5)
console.log(food) // expected {name: 'chees', price: 5, category: 'food'}
If we run the above code, we'll get
instead of
{name: 'chees', price: 5, category: 'food'}
Ok, we need to pass original arguments when we call fn()
. Seems easy, but 🤔 how do we know how many arguments are coming from the original call?
Here we can use arguments
it is Array-like object accessible inside the function, but still, we have a problem; remember arguments
is not an array
its an object
that's why Array-like
We can convert this object
to array
with Array.from
(more ways), then ignore the first argument by Array.slice
from the second element.
if(!Function.prototype.fauxCall){
Function.prototype.fauxCall = function(context){
const args = Array.from(arguments).slice(1);
context.fn = this;
return context.fn(...args);
}
}
const food = new Food('cheese', 5)
console.log(food) // expected {name: 'chees', price: 5, category: 'food'}
If we run the above code, we'll get
Ok looks good, but still, we can see the side effect. Get rid of the side effect we can use delete
operator, however, even if we can delete this side effect fn
property that we created we have one more problem; if context
already has a property with the same name fn
. In this case, should form the random key then assign it to context then we have to delete it.
if(!Function.prototype.fauxCall){
Function.prototype.fauxCall = function(context){
const fnName =
[...Array(10)].map(_ => ((Math.random() * 36) | 0).toString(36)).join`` ||
{};
const args = Array.from(arguments).slice(1);
context[fnName]= this;
const result = obj[fnName](...args);
delete obj[fnName];
return result;
}
}
const food = new Food('cheese', 5)
console.log(food) // expected {name: 'chees', price: 5, category: 'food'}
If we run the above code, we'll get
Almost success, but if we call with null instead of the object we'll get an error.
Remember our add
function? if we want to fauxCall
add function without this
argument we'll get error
const add = (a, b) => a + b;
console.log(add.fauxCall(null, 5, 6, 7)); // 11 :: 7 will ignore by add method
It's because o we're trying to set a property to a null
object, and we can fix it by guard function.
Also, add two more methods to check the existing property and assign new property instead of static fnName
variable.
-
getRandomKey
: this function generates and returns a random string each time. -
checkRandomKey
: this function takes two arguments; key and context (object) and checks this object already has the same key as property if-then recurse it with the new key, until finding a unique new property for the property name.
Complete implementation
const isOBject = obj => {
const type = typeof obj;
return type === "function" || (type === "object" && !!obj);
};
const getRandomKey = () => {
return (
[...Array(10)].map(_ => ((Math.random() * 36) | 0).toString(36)).join`` ||
{}
);
};
const checkRandomKey = (key, obj) => (obj[key] === undefined) ? key : checkRandomKey(getRandomKey(), obj);
if(!Function.prototype.fauxCall){
Function.prototype.fauxCall = function(_context) {
const context = isOBject(_context) ? _context : {};
const fnName = checkRandomKey(getRandomKey(), context);
const args = Array.from(arguments).slice(1);
context[fnName] = this;
const result = context[fnName](...args);
delete context[fnName];
return result;
};
}
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.fauxCall(this, name, price);
this.category = "food";
}
const add = (a, b) => a + b;
console.log(new Food("cheese", 5)); // {name: 'chees', price: 5, category: 'food'}
console.log(add.fauxCall(null, 5, 6, 7)); // 11 :: 7 will ignore by add method
Function.prototype.apply()
The apply() method calls a function with a given this value, and arguments provided as an array (or an array-like object). -- MDN
Initially, the method apply
invokes the function and allows you to pass an array or array-like arguments. Sound familiar? yes because call
and apply
almost doing the same thing only different is call accept comma-separated arguments while apply accepts array or array-like object as the argument.
In this case, everything that we did for the call
is valid for apply
except args
part, now we know exactly which argument should go with the function call.
//... all call helper codes
if(!Function.prototype.fauxApply){
Function.prototype.fauxApply = function(_context, _args) {
const context = isOBject(_context) ? _context : {};
const fnName = checkRandomKey(getRandomKey(), context);
const args = _args.length ? _args : []
context[fnName] = this;
const result = context[fnName](...args);
delete context[fnName];
return result;
};
}
const numbers = [5, 6, 7];
console.log(new Food("cheese", 5)); // {name: 'chees', price: 5, category: 'food'}
console.log(add.fauxApply(null, 5, 6, 7)); // 11 :: 7 will ignore by add method
Function.prototype.bind()
The bind() method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.
Only different between call and bind is call invoke the function and returns the value but bind returns a new function with updated context.
So we can simply return a new function that calls call
with arguments and context.
//... all call helper codes
Function.prototype.fauxBind = function(_contetxt){
const args = Array.from(arguments).slice(1);
const self = this;
return function(){
//return self.fauxApply(_contetxt, args)
return self.fauxCall(_contetxt, ...args) // either call or apply
}
}
console.log(add.fauxBind(null, 4,7)());
CodeSandbox
♥
This implementation here is one of many ways. The purpose of this simulation is only to get how call
works under the hood. If you find any issue or typo please let me know.
Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.