Heim >Web-Frontend >js-Tutorial >Funktionales JavaScript: .apply(), .call() und Arguments-Objekte
Wir suchen nach Möglichkeiten, JavaScript so zu optimieren, dass wir wirklich funktionale Programmierung durchführen können. Dazu ist es notwendig, Funktionsaufrufe und Funktionsprototypen im Detail zu verstehen.
Funktionsprototyp
Ob Sie nun den oben verlinkten Artikel gelesen oder ignoriert haben, wir sind bereit, weiterzumachen!
Wenn wir auf unseren Lieblingsbrowser + JavaScript-Konsole klicken, werfen wir einen Blick auf die Eigenschaften des Function.prototype-Objekts:
; html-script: false ] Object.getOwnPropertyNames(Function.prototype) //=> ["length", "name", "arguments", "caller", // "constructor", "bind", "toString", "call", "apply"]
Die Ausgabe hier hängt von der Browser- und JavaScript-Version ab, die Sie verwenden . (Ich verwende Chrome 33)
Wir sehen einige Eigenschaften, die uns interessieren. Für den Zweck dieses Artikels werde ich Folgendes besprechen:
Function.prototype.length
Function.prototype.call
Function.prototype.apply
Die erste ist eine Eigenschaft und die anderen beiden sind Methoden. Zusätzlich zu diesen drei möchte ich auch diese speziellen Variablenargumente diskutieren, die sich geringfügig von Function.prototype.arguments (das veraltet ist) unterscheiden.
Zuerst definiere ich eine „Tester“-Funktion, die uns hilft herauszufinden, was vor sich geht.
; html-script: false ] var tester = function (a, b, c){ console.log({ this: this, a: a, b: b, c: c }); };
Diese Funktion zeichnet einfach den Wert des Eingabeparameters und der „Kontextvariablen“ auf, die der Wert davon ist.
Jetzt versuchen wir etwas:
; 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"}
Uns ist aufgefallen, dass das Programm sie als undefiniert anzeigt, wenn wir den 2. und 3. Parameter nicht eingeben. Darüber hinaus stellen wir fest, dass der Standard-„Kontext“ dieser Funktion das globale Objektfenster ist.
Verwenden Sie Function.prototype.call
Die .call-Methode einer Funktion ruft die Funktion so auf, dass sie die Kontextvariable this auf den Wert des ersten Eingabeparameters setzt und dann die anderen Die Parameter werden ebenfalls einzeln an die Funktion übergeben.
Syntax:
; html-script: false ] fn.call(thisArg[, arg1[, arg2[, ...]]])
Daher sind die folgenden zwei Zeilen äquivalent:
; html-script: false ] tester("this", "is", "cool"); tester.call(window, "this", "is", "cool");
Natürlich können wir beliebige Parameter übergeben:
; html-script: false ] tester.call("this?", "is", "even", "cooler"); //=> {this: "this?", a: "is", b: "even", c: "cooler"}
Die Hauptfunktion dieser Methode besteht darin, den Wert dieser Variablen der von Ihnen aufgerufenen Funktion festzulegen.
Verwenden Sie Function.prototype.apply
Die .apply-Methode der Funktion ist praktischer als .call. Ähnlich wie .call wird auch .apply aufgerufen, indem die Kontextvariable this auf den Wert des ersten Parameters in der Eingabeparametersequenz gesetzt wird. Der zweite und letzte Parameter der Eingabeparametersequenz wird als Array (oder arrayähnliches Objekt) übergeben.
Syntax:
; html-script: false ] fun.apply(thisArg, [argsArray])
Daher sind die folgenden drei Zeilen alle gleichwertig:
; html-script: false ] tester("this", "is", "cool"); tester.call(window, "this", "is", "cool"); tester.apply(window, ["this", "is", "cool"]);
Die Möglichkeit, eine Parameterliste als Array anzugeben, ist sehr nützlich der Zeit (Wir werden die Vorteile daraus finden).
Math.max ist beispielsweise eine variadische Funktion (eine Funktion kann eine beliebige Anzahl von Parametern akzeptieren).
; html-script: false ] Math.max(1,3,2); //=> 3 Math.max(2,1); //=> 2
Wenn ich also ein Array von Werten habe und die Funktion Math.max verwenden muss, um den größten Wert zu finden, wie kann ich das mit einer Codezeile tun?
; html-script: false ] var numbers = [3, 8, 7, 3, 1]; Math.max.apply(null, numbers); //=> 8
Die .apply-Methode zeigt erst richtig ihre Bedeutung, wenn sie mit der speziellen Argumentvariablen gekoppelt wird: dem Argumentobjekt
.
Jeder Funktionsausdruck verfügt in seinem Gültigkeitsbereich über eine spezielle lokale Variable: Argumente. Um seine Eigenschaften zu untersuchen, erstellen wir eine weitere Testerfunktion:
; html-script: false ] var tester = function(a, b, c) { console.log(Object.getOwnPropertyNames(arguments)); };
Hinweis: In diesem Fall müssen wir Object.getOwnPropertyNames wie oben verwenden, da Argumente einige Eigenschaften haben, die nicht als verfügbar markiert sind. Daher werden sie nicht angezeigt, wenn Sie einfach console.log(arguments) verwenden.
Jetzt folgen wir der alten Methode und testen, indem wir die Testerfunktion aufrufen:
; html-script: false ] tester("a", "b", "c"); //=> ["0", "1", "2", "length", "callee"] tester.apply(null, ["a"]); //=> ["0", "length", "callee"]
Die Attribute der Argumentvariablen umfassen Attribute, die jedem an die Funktion übergebenen Parameter entsprechen Es gibt keinen Unterschied zwischen dem .length-Attribut und dem .callee-Attribut. Das Attribut
.callee stellt einen Verweis auf die Funktion bereit, die die aktuelle Funktion aufgerufen hat. Dies wird jedoch nicht von allen Browsern unterstützt. Diese Eigenschaft ignorieren wir vorerst.
Lassen Sie uns unsere Testerfunktion neu definieren, um sie umfangreicher zu machen:
; 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 }
Argumente: Handelt es sich um ein Objekt oder ein Array?
Wir können sehen, dass arguments überhaupt kein Array ist, obwohl es mehr oder weniger ähnlich ist. In vielen Fällen möchten wir es als Array behandeln, auch wenn dies nicht der Fall ist. Es gibt eine sehr schöne Shortcut-Funktion, um Argumente in ein Array umzuwandeln:
; 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"]
Hier verwenden wir die Methode Array.prototype.slice, um ein Array-ähnliches Objekt in ein Array umzuwandeln. Aus diesem Grund sind Argumentobjekte äußerst nützlich, wenn sie in Verbindung mit .apply verwendet werden.
Einige nützliche Beispiele
Log Wrapper
Die logWrapper-Funktion ist zwar erstellt, funktioniert aber nur mit unären Funktionen korrekt.
; html-script: false ] // old version var logWrapper = function (f) { return function (a) { console.log('calling "' + f.name + '" with argument "' + a); return f(a); }; };
Natürlich können wir aufgrund unseres vorhandenen Wissens eine LogWrapper-Funktion erstellen, die jede Funktion erfüllen kann:
; html-script: false ] // new version var logWrapper = function (f) { return function () { console.log('calling "' + f.name + '"', arguments); return f.apply(this, arguments); }; };
Durch Aufruf von
; 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); }; };
Einige weitere Beispiele:
; 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);
Natürlich noch viele, viele mehr. Sehen wir uns an, wie diese ausgeführt werden:
; 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"
Exkurs:
Wir werden später zeigen, dass die Demethodize-Funktion tatsächlich eine bessere Möglichkeit ist, Parameter umzudrehen.
在函数式编程情况下,你通常需要把“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