简体   繁体   中英

How are input parameters filled in javascript method chains?

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:

Using functions as method parameters

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:

  1. The browser starts to parse the concat method, but needs to figure out what parameter to give it.
  2. That parameter includes a method call, so the toUpperCase() method of b is executed, returning the string "WORLD!".
  3. The returned string becomes the parameter for 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.


Method Chaining

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 I understand correctly

what does this data refer to? How is it filled?

You mean how it works? It depends on how the callback gets called. For example:

function Lib() {}

Lib.prototype.text = function(callback) {
  var data = 'hello world';
  callback(data); // `data` is the first parameter
  return this; // chain
};

var lib = new Lib();

lib.text(function(data){
  console.log(data); //=> "hello world"
});

The d corresponds to a single element in the set of data first passed in by .data(data).enter() in the earlier method call. What's happening is that d3 is implicitly looping over the corresponding dom elements that map to the data, hence the whole notion of data driven documents or d3.

That is, for each element in your data, when you call a selectAll() you should expect to see the same number of elements appended to the dom as there are in your data set. So when you hit a .attr() call, the single datum which corresponds to an element in the dom is passed in to the function. When you're reading

.append("rect").attr("width",function(datum){})...

you should read that as a single iteration over one element in your whole set of data that's being bound to your selection.

I'm a bit unsure I've answered your question as it seems to be both a mixture of questions about declarative/functional programming as well as method chaining. The above answer is with respect to the declarative/functional manner of d3. The below answer is in reference to method chaining.

Here's a great example that offers more insight into method chaining outlined by Mike Bostock's in his wonderful article http://bost.ocks.org/mike/chart/ .

I recommend that you read through the d3 api found here ( https://github.com/mbostock/d3/wiki/API-Reference ) to further understand what each function is operating on. For example selection.filter Furthermore I highly recommend that you step in to a toy example such as the one that you've given to further understand what's going on.

Method chaining with respect to d3 reusable charts.

function SomeChart()
{
    var someProperty = "default value of some sort";

    function chartObject()
    {
    }

    chartObject.setSomeProperty(propertyValue)
    { 
     if (!arguments.length) return someProperty;
     someProperty = property;
     return chartObject;
    }

    return chartObject;
}  



var chart = SomehChart();

What is chart at this point?

chart = chart.setSomeProperty("some special value");

What is chart at this point?

chart = chart.setSomeProperty();

What is chart at this point?

What's the difference between the following?

var chart = SomeChart().setSomeProperty("some special value");

and

var chart = SomeChart();
chart.setSomeProperty();

The answer is that they're all the same except for when chart = chart.setSomeProperty(); .

in d3 the d3.select("body") will return something {search_element:document.body,selectAll:function,...} so with the dot notation you call the previous object's available function. It maybe just returns the class itself. So all the methods are available in whatever order with the next dot. But in ajax done , some functions have to be called so they fill the object's important parameters the done function uses.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM