I am trying to really understand the details of how javascript works. During method chaining, sometimes one method returns to another method that has a named input parameter.
For instance, in D3, the pattern looks like this:
d3.select("body").selectAll("p")
.data(dataset)
.enter()
.append("p")
.text(function(d) { return d; }); //what does this d refer to? How is it filled?
In jquery, the pattern looks like this:
$.ajax({
...
})
.done(function( data ) { //what does this data refer to? How is it filled?
I know from practical coding that the name of these input parameter can be anything. But where does the data that files the input parameter come from? Does it just refer to the data returned from the prior method in the chain?
Two different topics, so I'll explain them separately:
First, a correction: The examples you are giving are not examples where "one method returns to another method that has a named input parameter". They are examples where a function is given as the input parameter to another method.
To clarify, I'll give you an example where the return value of one function is used as the input to another.
var a = "Hello ",
b = "World!";
var c = a.concat( b.toUpperCase() ); //c = "Hello WORLD!"
In order to create c
, the following happens, in order:
concat
method, but needs to figure out what parameter to give it.toUpperCase()
method of b
is executed, returning the string "WORLD!".a
's concat()
method which can now be executed. As far as the concat()
method is concerned, the result is the same as if you wrote c = a.concat("WORLD!")
-- it doesn't care that the string "WORLD!" was created by another function.
You can tell that the returned value of b.toUpperCase()
is being passed as the parameter, and not the function itself, because of the parentheses at the end of the function name. Parentheses after a function name tell the browser to execute that function, just as soon as it has figured out the values of any parameters to that function. Without the parentheses, the function is treated as any other object, and can be passed around as a parameter or variable without actually doing anything.
When a function object, unexecuted, is used as the parameter for another function, what happens is entirely dependent on the instructions inside that second function. If you pass that function object to console.log()
, the string representation of the function will be printed to the console without ever executing the function you passed in. However, most methods that accept another function as input are designed to call that function with specified parameters.
One example is the map()
method of arrays. The map
method creates a new array in which every element is the result of running the mapping function on the corresponding element of the original array.
var stringArray = ["1", "2!", "3.0", "?"];
var numberArray = stringArray.map(parseFloat); //numberArray = [1, 2, 3, NaN]
The function parseFloat()
is a built-in function that takes a string and tries to figure out a number from it. Note that when I pass it in to the map function, I'm just passing it in as a variable name, not executing it with parentheses at the end. It is executed by the map function, and it is the map function that decides what parameters it gets. The results of each call to parseFloat
are assigned by the map
function to their place in the result array.
Specifically, the map function executes parseFloat
with three parameters: the element from the array, the index of that element in the array, and the array as a whole. The parseFloat
function only uses one parameter, however, so the second and third parameters are ignored. (If you try to do the same thing with parseInt
, however, you'll get unexpected results because parseInt
does use a second parameter -- which it treats as the radix ("base") of the integer.)
The map function doesn't care how many parameters the passed-in function is expecting, and it certainly doesn't care which variable names are used inside that function. If you were writing the map function yourself, it would look something like this:
Array.prototype.myMap = function(f) {
var result = [];
for (var i = 0, n=this.length; i<n; i++) {
result[i] = f( this[i], i, this);
//call the passed-in function with three parameters
}
return result;
};
The f
function is called, and given parameters, without knowing anything about what it is or what it does. The parameters are given in a specific order -- element, index, array -- but are not linked to any particular parameter name.
Now, a limitation of something like the map
method is that there are very few Javascript functions which can be called directly, just passing a value as a parameter. Most functions are methods of a specific object. For example, we couldn't use the toUpperCase
method as a parameter to map
, because toUpperCase
only exists as a method of a string object, and only acts on that particular string object, not on any parameter that the map function might give it. In order to map an array of strings to uppercase, you need to create your own function that works in the way the map function will use it.
var stringArray = ["hello", "world", "again"];
function myUpperCase(s, i, array) {
return s.toUpperCase(); //call the passed-in string's method
}
var uppercaseStrings = stringArray.map( myUpperCase );
//uppercaseStrings = ["HELLO", "WORLD", "AGAIN"]
However, if you're only ever going to use the function myUpperCase
this once, you don't need to declare it separately and give it a name. You can use it directly as an anonymous function.
var stringArray = ["hello", "world", "again"];
var uppercaseStrings = stringArray.map(
function(s,i,array) {
return s.toUpperCase();
}
);
//uppercaseStrings still = ["HELLO", "WORLD", "AGAIN"]
Starting to look familiar? This is the structure used by so many d3 and JQuery functions -- you pass-in a function, either as a function name or as an anonymous function, and the d3/JQuery method calls your function on each element of a selection, passing in specified values as the first, second and maybe third parameter.
So what about the parameter names? As you mentioned, they can be anything you want. I could have used very long and descriptive parameter names in my function:
function myUpperCase(stringElementFromArray, indexOfStringElementInArray, ArrayContainingStrings) {
return stringElementFromArray.toUpperCase();
}
The values that get passed in to the function will be the same, based purely on the order in which the map function passes them in. In fact, since I never use the index parameter or the array parameter, I can leave them out of my function declaration and just use the first parameter. The other values still get passed in by map
, but they are ignored. However, if I wanted to use the second or third parameter passed in by map
, I would have to declare a name for the first parameter, just to keep the numbering straight:
function indirectUpperCase (stringIDontUse, index, array) {
return array[index].toUpperCase();
}
That's why, if you want to use the index number in d3, you have to write function(d,i){return i;}
. If you just did function(i){return i;}
the value of i
would be the data object, because that's what the d3 functions always pass as the first parameter, regardless of what you call it. It's the outside function that passes in the values of the parameters. The parameter names only exist inside the inner function.
Requisite caveats and exceptions:
I said that the map function doesn't care how many parameters a passed-in function expects. That's true, but other outer functions could use the passed-in function's .length
property to figure out how many parameters are expected and pass different parameter values accordingly.
You don't have to name arguments for a function in order to access passed-in parameter values. You can also access them using the arguments
list inside that function. So another way of writing the uppercase mapping function would be:
function noParamUpperCase() {
return arguments[0].toUpperCase();
}
However, note that if an outer function is using the number of parameters to determine what values to pass to the inner function, this function will appear not to accept any arguments.
You'll notice that nowhere above did I mention method chaining. That's because it's a completely separate code pattern, that just happens to also be used a lot in d3 and JQuery.
Let's go back to the first example, which created "Hello WORLD!" out of "Hello " and "World!". What if you wanted to create "HELLO World!" instead? You could do
var a = "Hello ",
b = "World!";
var c = a.toUpperCase().concat( b ); //c = "HELLO World!"
The first thing that happens in the creation of c
is the a.toUpperCase()
method gets called. That method returns a string ("HELLO "), which like all other strings has a .concat()
method. So .concat(b)
is now getting called as a method of that returned string, not of the original string a
. The result is that b
gets concatenated to the end of the uppercase version of a
: "HELLO World!".
In that case, the returned value was a new object of the same type as the starting object. In other cases, it could be a completely different type of data.
var numberArray = [5, 15];
var stringArray = numberArray.toString().split(","); //stringArray = ["5", "15"]
We start with an array of numbers, [5,15]
. We call the array's toString()
method, which produces a nicely formatted string version of the array: "5,15". This string now has all it's string methods available, including .split()
, which splits a string into an array of substrings around a specified split character, in this case the comma.
You could call this a type of method chaining, calling a method of a value returned by another method. However, when method chaining is used to describe a feature of a Javascript library, the key aspect is that the returned value of the method is the same object that called the method.
So when you do
d3.select("body").style("background", "green");
The d3.select("body")
method creates a d3 selection object. That object has a style()
method. If you use the style method to set a style, you don't really need any information back from it. It could have been designed not to return any value at all. Instead, it returns the object that the method belongs to (the this
object). So you can now call another method of that object. Or you could assign it to a variable. Or both.
var body = d3.select("body").style("background", "green")
.style("max-width", "20em");
However, you always have to be aware of the methods which don't return the same object. For example, in a lot of d3 code examples you see
var svg = d3.select("svg").attr("height", "200")
.attr("width", "200")
.append("g")
.attr("transform", "translate(20,20)");
Now, the method append("g")
doesn't return the same selection of the <svg>
element. It returns a new selection consisting of the <g>
element, which is then given a transform attribute. The value that gets assigned to the variable svg
is the last return value from the chain. So in later code, you would have to remember that the variable svg
doesn't actually refer to a selection of the <svg>
element, but to the <g>
. Which I find confusing, so I try to avoid ever using the variable name svg
for a selection that isn't actually an <svg>
element.
Of course, any of those d3 .attr()
or .style()
methods could have taken a function as the second parameter instead of a string. But that wouldn't have changed how method chaining works.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With