이 기사의 목차:
JavaScript에서 함수는 내장 클래스 객체입니다. 즉, 다른 String, Array, Number 및 Object 클래스 객체와 같은 내장 객체를 관리하는 데 사용할 수 있는 객체 유형입니다. 함수는 실제로 객체이기 때문에 "변수에 저장되고, 매개변수를 통해 (다른) 함수에 전달되고, 함수 내에서 생성되고, 함수의 결과 값으로 반환"될 수 있습니다.
함수는 내장 객체이기 때문에 이를 다른 함수에 매개변수로 전달할 수도 있고, 함수 내에서 실행을 지연할 수도 있고, 실행 후 반환할 수도 있습니다. 이것이 JavaScript에서 콜백 함수를 사용하는 핵심입니다. 이 기사의 나머지 부분은 JavaScript 콜백 함수에 대한 포괄적인 연구입니다. 콜백 함수는 JavaScript에서 가장 널리 사용되는 함수형 프로그래밍 기술일 수 있습니다. 아마도 JavaScript 또는 jQuery 코드의 작은 조각만으로도 이 기사를 읽은 후에는 이러한 미스터리함을 없애는 데 도움이 될 것입니다.
콜백 함수는 잘 알려진 프로그래밍 패러다임인 함수형 프로그래밍 에서 유래되었습니다. 기본적으로 함수형 프로그래밍은 함수의 매개변수를 지정합니다. 함수형 프로그래밍은 이제 더 작은 범위에서 사용되지만 "전문적인 스마트" 프로그래머들에게는 항상 어려운 기술로 간주되어 왔으며 이는 과거에도 그랬고 앞으로도 그럴 것입니다.
다행히 함수형 프로그래밍은 여러분이나 저 같은 일반인도 이해하고 사용할 수 있도록 설명되어 있습니다. 함수형 프로그래밍의 가장 중요한 기술 중 하나는 콜백 함수입니다. 곧 읽어보겠지만 콜백 함수 구현은 일반 매개변수 변수를 전달하는 것만큼 간단합니다. 이 기술은 너무 간단해서 왜 고급 JavaScript 주제에 자주 포함되는지 궁금합니다.
1. 콜백이나 고급기능이란 무엇인가요?
콜백 함수는 다른 함수(여기에서는 "otherFunction"이라고 함)에 매개변수로 전달되는 상위 수준 함수인 상위 수준 함수로 간주됩니다. 콜백 함수는 otherFunction 내에서 호출(또는 실행)됩니다. 콜백 함수의 본질은 패턴(일반적인 문제를 해결하기 위한 패턴)이므로 콜백 함수를 콜백 패턴이라고도 합니다.
jQuery에서 일반적으로 사용되는 다음 콜백 함수에 대해 생각해 보세요.
//Note that the item in the click method's parameter is a function, not a variable. //The item is a callback function $("#btn_1").click(function() { alert("Btn 1 Clicked"); });
이전 예에서 볼 수 있듯이 click 메소드의 형식 매개변수에 함수를 전달했으며, click 메소드는 전달한 콜백 함수를 호출(또는 실행)합니다. 이 예제는 JavaScript에서 콜백 함수를 사용하는 일반적인 방법을 제공하며 jQuery에서 널리 사용됩니다.
기본 JavaScript의 또 다른 전형적인 예를 자세히 살펴보겠습니다.
var friends = ["Mike", "Stacy", "Andy", "Rick"]; friends.forEach(function (eachName, index){ console.log(index + 1 + ". " + eachName); // 1. Mike, 2. Stacy, 3. Andy, 4. Rick });
forEach의 매개변수와 마찬가지로 익명 함수(함수 이름이 없는 함수)를 다시 forEach 메서드에 전달합니다.
지금까지는 익명 함수를 다른 함수나 메소드에 매개변수로 전달했습니다. 다른 더 복잡한 콜백 함수를 살펴보기 전에 콜백이 어떻게 작동하는지 이해하고 우리만의 콜백을 구현해 봅시다.
2. 콜백 기능은 어떻게 구현되나요?
함수를 변수처럼 사용하거나, 다른 함수의 매개변수로, 다른 함수의 반환 결과로 사용하거나, 다른 함수에서 호출할 수 있습니다. 콜백 함수를 다른 함수에 매개변수로 전달할 때, 함수의 정의만 전달하고 매개변수에서 실행하지는 않습니다.
포함(호출) 함수의 매개변수에 콜백 함수가 정의되어 있으면 언제든지 이를 호출(즉, 콜백)할 수 있습니다.
이는 콜백 함수가 즉시 실행되지 않고 (이름에서 알 수 있듯이) 함수가 포함된 함수 본문의 지정된 위치에서 "콜백"함을 나타냅니다. 따라서 첫 번째 jQuery 예제도 다음과 같습니다.
//The anonymous function is not being executed there in the parameter. //The item is a callback function $("#btn_1").click(function() { alert("Btn 1 Clicked"); });
匿名函数将延迟在click函数的函数体内被调用,即使没有名称,也可以被包含函数通过 arguments对象访问。
回调函数是闭包的
当作为参数传递一个回调函数给另一个函数时,回调函数将在包含函数函数体内的某个位置被执行,就像回调函数在包含函数的函数体内定义一样。这意味着回调函数是闭包的,想更多地了解闭包,请参考作者另一个贴子Understand JavaScript Closures With Ease。从所周知,闭包函数可以访问包含函数的作用域,所以,回调函数可以访问包含函数的变量,甚至是全局变量。
三、实现回调函数的基本原则
简单地说,自己实现回调函数的时候需要遵循几条原则。
1、使用命名函数或匿名函数作为回调
在前面的jQuery和forEach的例子中,我们在包含函数的参数中定义匿名函数,这是使用回调函数的通用形式之一,另一个经常被使用的形式是定义一个带名称的函数,并将函数名作为参数传递给另一个函数,例如:

// global variable var allUserData = []; // generic logStuff function that prints to console function logStuff (userData) { if ( typeof userData === "string") { console.log(userData); } else if ( typeof userData === "object") { for (var item in userData) { console.log(item + ": " + userData[item]); } } } // A function that takes two parameters, the last one a callback function function getInput (options, callback) { allUserData.push (options); callback (options); } // When we call the getInput function, we pass logStuff as a parameter. // So logStuff will be the function that will called back (or executed) inside the getInput function getInput ({name:"Rich", speciality:"JavaScript"}, logStuff); // name: Rich // speciality: JavaScript
2、传递参数给回调函数
因为回调函数在执行的时候就和一般函数一样,我们可以传递参数给它。可以将包含函数的任何属性(或全局的属性)作为参数传递回调函数。在上一个例子中,我们将包含函数的options作为参数传递给回调函数。下面的例子让我们传递一个全局变量或本地变量给回调函数:
//Global variable var generalLastName = "Clinton"; function getInput (options, callback) { allUserData.push (options); // Pass the global variable generalLastName to the callback function callback (generalLastName, options); }
3、在执行之前确保回调是一个函数
在调用之前,确保通过参数传递进来的回调是一个需要的函数通常是明智的。此外,让回调函数是可选的也是一个好的实践。
让我们重构一下上面例子中的getInput函数,确保回调函数做了适当的检查。
function getInput(options, callback) { allUserData.push(options); // Make sure the callback is a function if (typeof callback === "function") { // Call it, since we have confirmed it is callable callback(options); } }
如果getInput函数没有做适当的检查(检查callback是否是函数,或是否通过参数传递进来了),我们的代码将会导致运行时错误。
4、使用含有this对象的回调函数的问题
当回调函数是一个含有this对象的方法时,我们必须修改执行回调函数的方法以保护this对象的内容。否则this对象将会指向全局的window对象(如果回调函数传递给了全局函数),或指向包含函数。让我们看看下面的代码:
// Define an object with some properties and a method // We will later pass the method as a callback function to another function var clientData = { id: 094545, fullName: "Not Set", // setUserName is a method on the clientData object setUserName: function (firstName, lastName) { // this refers to the fullName property in this object this.fullName = firstName + " " + lastName; } } function getUserInput(firstName, lastName, callback) { // Do other stuff to validate firstName/lastName here // Now save the names callback (firstName, lastName); }
在下面的示例代码中,当clientData.setUserName被执行时,this.fullName不会设置clientData 对象中的属性fullName,而是设置window 对象中的fullName,因为getUserInput是一个全局函数。出现这种现象是因为在全局函数中this对象指向了window对象。
getUserInput ("Barack", "Obama", clientData.setUserName); console.log (clientData.fullName);// Not Set // The fullName property was initialized on the window object console.log (window.fullName); // Barack Obama
5、使用Call或Apply函数保护this对象
我们可以通过使用 Call 或 Apply函数来解决前面示例中的问题。到目前为止,我们知道JavaScript中的每一个函数都有两个方法:Call和Apply。这些方法可以被用来在函数内部设置this对象的内容,并内容传递给函数参数指向的对象。
Call takes the value to be used as the this object inside the function as the first parameter, and the remaining arguments to be passed to the function are passed individually (separated by commas of course). The Apply function's first parameter is also the value to be used as the thisobject inside the function, while the last parameter is an array of values (or the arguments object) to pass to the function. (该段翻译起来太拗口了,放原文自己体会)
这听起来很复杂,但让我们看看Apply和Call的使用是多么容易。为解决前面例子中出现的问题,我们使用Apply函数如下:
//Note that we have added an extra parameter for the callback object, called "callbackObj" function getUserInput(firstName, lastName, callback, callbackObj) { // Do other stuff to validate name here // The use of the Apply function below will set the this object to be callbackObj callback.apply (callbackObj, [firstName, lastName]); }
通过Apply函数正确地设置this对象,现在我们可以正确地执行回调函数并它正确地设置clientData对象中的fullName属性。
// We pass the clientData.setUserName method and the clientData object as parameters. The clientData object will be used by the Apply function to set the this object 
getUserInput ("Barack", "Obama", clientData.setUserName, clientData); // the fullName property on the clientData was correctly set console.log (clientData.fullName); // Barack Obama
我们也可以使用Call 函数,但在本例中我们使用的Apply 函数。
6、多重回调函数也是允许的
我们可以传递多个回调函数给另一个函数,就像传递多个变量一样。这是使用jQuery的AJAX函数的典型例子:
function successCallback() { // Do stuff before send } function successCallback() { // Do stuff if success message received } function completeCallback() { // Do stuff upon completion } function errorCallback() { // Do stuff if error received } $.ajax({ url:"http://fiddle.jshell.net/favicon.png", success:successCallback, complete:completeCallback, error:errorCallback });
四、“回调地狱”的问题和解决方案
异步代码执行是一种简单的以任意顺序执行的方式,有时是很常见的有很多层级的回调函数,你看起来像下面这样的代码。下面这种凌乱的代码称作“回调地狱”,因为它是一种包含非常多的回调的麻烦的代码。我是在node-mongodb-native里看到这个例子的,MongoDB驱动Node.js.示例代码就像这样:
var p_client = new Db('integration_tests_20', new Server("127.0.0.1", 27017, {}), {'pk':CustomPKFactory}); p_client.open(function(err, p_client) { p_client.dropDatabase(function(err, done) { p_client.createCollection('test_custom_key', function(err, collection) { collection.insert({'a':1}, function(err, docs) { collection.find({'_id':new ObjectID("aaaaaaaaaaaa")}, function(err, cursor) { cursor.toArray(function(err, items) { test.assertEquals(1, items.length); // Let's close the db p_client.close(); }); }); }); }); }); });
你不太可能在自己的代码里碰到这个的问题,但如果你碰到了(或以后偶然碰到了),那么有以下两种方式解决这个问题。
命名并定义你的函数,然后传递函数名作为回调,而不是在主函数的参数列表里定义一个匿名函数。
模块化:把你的代码划分成一个个模块,这样你可以空出一部分代码块做特殊的工作。然后你可以将这个模型引入到你的大型应用程序中。
五、实现自己的回调函数
现在你已经完全理解(我相信你已经理解了,如果没有请快速重新阅读一遍)了JavaScript关于回调的所用特性并且看到回调的使用是如此简单但功能却很强大。你应该看看自己的代码是否有机会使用回调函数,有以下需求时你可以考虑使用回调:
实现自己的回调函数很简单,在下面的例子中,我可以创建一个函数完成所用的工作:获取用户数据,使用用户数据生成一首通用的诗,使用用户数据来欢迎用户,但这个函数将会是一个凌乱的函数,到处是if/else的判断,甚至会有很多的限制并无法执行应用程序可能需要的处理用户数据的其它函数。
替而代之的是我让实现增加了回调函数,这样主函数获取用户数据后可以传递用户全名和性别给回调函数的参数并执行回调函数以完成任何任务。
简而言之,getUserInput函数是通用的,它可以执行多个拥有各种功能的回调函数。
// First, setup the generic poem creator function; it will be the callback function in the getUserInput function below. function genericPoemMaker(name, gender) { console.log(name + " is finer than fine wine."); console.log("Altruistic and noble for the modern time."); console.log("Always admirably adorned with the latest style."); console.log("A " + gender + " of unfortunate tragedies who still manages a perpetual smile"); } //The callback, which is the last item in the parameter, will be our genericPoemMaker function we defined above. function getUserInput(firstName, lastName, gender, callback) { var fullName = firstName + " " + lastName; // Make sure the callback is a function if (typeof callback === "function") { // Execute the callback function and pass the parameters to it callback(fullName, gender); } }
调用getUserInput函数并传递genericPoemMaker函数作为回调:
getUserInput("Michael", "Fassbender", "Man", genericPoemMaker); // Output /* Michael Fassbender is finer than fine wine. Altruistic and noble for the modern time. Always admirably adorned with the latest style. A Man of unfortunate tragedies who still manages a perpetual smile. */
因为getUserInput 函数只处理用户数据的输入,我们可以传递任何回调函数给它。例如我们可以像这样传递一个greetUser函数。
function greetUser(customerName, sex) { var salutation = sex && sex === "Man" ? "Mr." : "Ms."; console.log("Hello, " + salutation + " " + customerName); } // Pass the greetUser function as a callback to getUserInput getUserInput("Bill", "Gates", "Man", greetUser); // And this is the output Hello, Mr. Bill Gates
和上一个例子一样,我们调用了同一个getUserInput 函数,但这次却执行了完全不同的任务。
如你所见,回调函数提供了广泛的功能。尽管前面提到的例子非常简单,在你开始使用回调函数的时候思考一下你可以节省多少工作,如何更好地抽象你的代码。加油吧!在早上起来时想一想,在晚上睡觉前想一想,在你休息时想一想……
我们在JavaScript中经常使用回调函数时注意以下几点,尤其是现在的web应用开发,在第三方库和框架中
以上就是更加深入的学习了JavaScript的回调函数,希望对大家的学习有所帮助。