Home > Article > Web Front-end > Functional JavaScript: .apply(), .call(), and arguments objects
We are looking for ways to tune JavaScript so that we can do some truly functional programming. In order to do this, it is necessary to understand function calls and function prototypes in detail.
Function Prototype
Now, whether you have read or ignored the article linked to above, we are ready to move on!
If we click on our favorite browser + JavaScript console, let’s take a look at the properties of the Function.prototype object:
; html-script: false ] Object.getOwnPropertyNames(Function.prototype) //=> ["length", "name", "arguments", "caller", // "constructor", "bind", "toString", "call", "apply"]
The output here depends on the browser and JavaScript version you are using. (I'm using Chrome 33)
We see a few properties that interest us. For the purpose of this article, I will discuss these:
Function.prototype.length
Function.prototype.call
Function.prototype.apply
The first one is a property, and the other two are methods. In addition to these three, I would also like to discuss this special variable arguments, which is slightly different from Function.prototype.arguments (which has been deprecated).
First, I'll define a "tester" function to help us figure out what's going on.
; html-script: false ] var tester = function (a, b, c){ console.log({ this: this, a: a, b: b, c: c }); };
This function simply records the value of the input parameter and the "context variable", which is the value of this.
Now, let’s try something:
; html-script: false ] tester("a"); //=> {this: Window, a: "a", b: (undefined), c: (undefined)} tester("this", "is", "cool"); //=> {this: Window, a: "this", b: "is", c: "cool"}
We noticed that if we don’t enter the 2nd and 3rd parameters, the program will show them as undefined. In addition, we note that the default "context" of this function is the global object window.
Use Function.prototype.call
The .call method of a function calls this function in such a way that it sets the context variable this to the value of the first input parameter, and then passes the other parameters one by one. into the function.
Syntax:
; html-script: false ] fn.call(thisArg[, arg1[, arg2[, ...]]])
So, the following two lines are equivalent:
; html-script: false ] tester("this", "is", "cool"); tester.call(window, "this", "is", "cool");
Of course, we can pass in any parameters as needed:
; html-script: false ] tester.call("this?", "is", "even", "cooler"); //=> {this: "this?", a: "is", b: "even", c: "cooler"}
The main function of this method is to set the value of this variable of the function you call .
Use Function.prototype.apply
The .apply method of the function is more practical than .call. Similar to .call, .apply is also called by setting the context variable this to the value of the first parameter in the input parameter sequence. The second and last parameter of the input parameter sequence is passed in as an array (or array-like object).
Syntax:
; html-script: false ] fun.apply(thisArg, [argsArray])
Thus, the following three lines are all equivalent:
; html-script: false ] tester("this", "is", "cool"); tester.call(window, "this", "is", "cool"); tester.apply(window, ["this", "is", "cool"]);
Being able to specify a parameter list as an array is very useful most of the time (we will find out the benefits of doing so).
For example, Math.max is a variadic function (a function can accept any number of parameters).
; html-script: false ] Math.max(1,3,2); //=> 3 Math.max(2,1); //=> 2
So, if I have an array of numbers and I need to use the Math.max function to find the largest one, how can I do this with one line of code?
; html-script: false ] var numbers = [3, 8, 7, 3, 1]; Math.max.apply(null, numbers); //=> 8
The .apply method really starts to show it’s importance when coupled with the special arguments variable: The arguments object
.
Every function expression has a special local variable available in its scope: arguments. In order to study its properties, let us create another tester function:
; html-script: false ] var tester = function(a, b, c) { console.log(Object.getOwnPropertyNames(arguments)); };
Note: In this case we have to use Object.getOwnPropertyNames like above, because arguments has some properties that are not marked as enumerable, so if Just use console.log(arguments) This way they will not be displayed.
Now we follow the old method and test by calling the tester function:
; html-script: false ] tester("a", "b", "c"); //=> ["0", "1", "2", "length", "callee"] tester.apply(null, ["a"]); //=> ["0", "length", "callee"]
The attributes of the arguments variable include attributes corresponding to each parameter passed in to the function. These are no different from the .length attribute and .callee attribute. The
.callee attribute provides a reference to the function that called the current function, but this is not supported by all browsers. For now, we ignore this property.
Let’s redefine our tester function to make it a little richer:
; html-script: false ] var tester = function() { console.log({ 'this': this, 'arguments': arguments, 'length': arguments.length }); }; tester.apply(null, ["a", "b", "c"]); //=> { this: null, arguments: { 0: "a", 1: "b", 2: "c" }, length: 3 }
Arguments: Is it an object or an array?
We can see that arguments is not an array at all, although it is more or less similar. In many cases, we'll want to treat it as an array even though it's not. There is a very nice shortcut function to convert arguments into an array:
; html-script: false ] function toArray(args) { return Array.prototype.slice.call(args); } var example = function(){ console.log(arguments); console.log(toArray(arguments)); }; example("a", "b", "c"); //=> { 0: "a", 1: "b", 2: "c" } //=> ["a", "b", "c"]
Here we use the Array.prototype.slice method to convert an array-like object into an array. Because of this, arguments objects end up being extremely useful when used in conjunction with .apply.
Some useful examples
Log Wrapper
builds the logWrapper function, but it only works correctly with unary functions.
; html-script: false ] // old version var logWrapper = function (f) { return function (a) { console.log('calling "' + f.name + '" with argument "' + a); return f(a); }; };
Of course, our existing knowledge allows us to build a logWrapper function that can serve any function:
; html-script: false ] // new version var logWrapper = function (f) { return function () { console.log('calling "' + f.name + '"', arguments); return f.apply(this, arguments); }; };
By calling
; html-script: false ] f.apply(this, arguments);
我们确定这个函数f会在和它之前完全相同的上下文中被调用。于是,如果我们愿意用新的”wrapped”版本替换掉我们的代码中的那些日志记录函数是完全理所当然没有唐突感的。 把原生的prototype方法放到公共函数库中 浏览器有大量超有用的方法我们可以“借用”到我们的代码里。方法常常把this变量作为“data”来处理。在函数式编程,我们没有this变量,但是我们无论如何要使用函数的!
; html-script: false ] var demethodize = function(fn){ return function(){ var args = [].slice.call(arguments, 1); return fn.apply(arguments[0], args); }; };
Some other examples:
; html-script: false ] // String.prototype var split = demethodize(String.prototype.split); var slice = demethodize(String.prototype.slice); var indexOfStr = demethodize(String.prototype.indexOf); var toLowerCase = demethodize(String.prototype.toLowerCase); // Array.prototype var join = demethodize(Array.prototype.join); var forEach = demethodize(Array.prototype.forEach); var map = demethodize(Array.prototype.map);
Of course, many, many more. Let’s see how these are executed:
; html-script: false ] ("abc,def").split(","); //=> ["abc","def"] split("abc,def", ","); //=> ["abc","def"] ["a","b","c"].join(" "); //=> "a b c" join(["a","b","c"], " "); // => "a b c"
Digression:
We will demonstrate later that actually a better way to use the demethodize function is parameter flipping.
在函数式编程情况下,你通常需要把“data”或“input data”参数作为函数的最右边的参数。方法通常会把this变量绑定到“data”参数上。举个例子,String.prototype方法通常操作的是实际的字符串(即”data”)。Array方法也是这样。
为什么这样可能不会马上被理解,但是一旦你使用柯里化或是组合函数来表达更丰富的逻辑的时候情况会这样。这正是我在引言部分说到UnderScore.js所存在的问题,之后在以后的文章中还会详细介绍。几乎每个Underscore.js的函数都会有“data”参数,并且作为最左参数。这最终导致非常难重用,代码也很难阅读或者是分析。:-(
管理参数顺序
; html-script: false ] // shift the parameters of a function by one var ignoreFirstArg = function (f) { return function(){ var args = [].slice.call(arguments,1); return f.apply(this, args); }; }; // reverse the order that a function accepts arguments var reverseArgs = function (f) { return function(){ return f.apply(this, toArray(arguments).reverse()); }; };
组合函数
在函数式编程世界里组合函数到一起是极其重要的。通常的想法是创建小的、可测试的函数来表现一个“单元逻辑”,这些可以组装到一个更大的可以做更复杂工作的“结构”
; html-script: false ] // compose(f1, f2, f3..., fn)(args) == f1(f2(f3(...(fn(args...))))) var compose = function (/* f1, f2, ..., fn */) { var fns = arguments, length = arguments.length; return function () { var i = length; // we need to go in reverse order while ( --i >= 0 ) { arguments = [fns[i].apply(this, arguments)]; } return arguments[0]; }; }; // sequence(f1, f2, f3..., fn)(args...) == fn(...(f3(f2(f1(args...))))) var sequence = function (/* f1, f2, ..., fn */) { var fns = arguments, length = arguments.length; return function () { var i = 0; // we need to go in normal order here while ( i++ < length ) { arguments = [fns[i].apply(this, arguments)]; } return arguments[0]; }; };
例子:
; html-script: false ] // abs(x) = Sqrt(x^2) var abs = compose(sqrt, square); abs(-2); // 2