每天推薦一個 GitHub 優質開源項目和一篇精選英文科技或編程文章原文,歡迎關注開源日報。交流QQ群:202790710;微博:https://weibo.com/openingsource;電報群 https://t.me/OpeningSourceOrg


今日推薦開源項目:《暗黑破壞神 Devilution》傳送門:GitHub鏈接

推薦理由:GitHub 上面什麼都有的完美例證,這次居然可以看到暗黑破壞神……的源代碼,暗黑破壞神是暴雪公司很久以前的一款遊戲,現在已經停止維護了。對暗黑破壞神感興趣的朋友可能很願意看到這個,不過這個僅僅只是一代的暗黑破壞神,興許它以後可以獲得一些新的發展,比如更好的 UI 之類的,但是……它終究只是一代,偶爾回去看看,估計就差不多了。


今日推薦英文原文:《JavaScript Weekly: Making Sense of Closures》作者:Severin Perez

原文鏈接:https://medium.com/launch-school/javascript-weekly-making-sense-of-closures-daa2e0b56f88

推薦理由:介紹了在 JavaScript 中什麼是閉包和為什麼要使用閉包。

JavaScript Weekly: Making Sense of Closures

One of the most powerful features of JavaScript is the concept of closure. It is this feature that lets us do things like create private data and define application interfaces. However, for new JavaScript programmers, the idea of closures, and how to use them, can be a bit confusing. Closure, as a concept, is closely tied to the idea of scope. JavaScript is lexically-scoped (meaning the scope is defined lexically, in the source code), which might make you think that it should be easy to reason about which variables are accessible where. But in truth, doing so does take some practice. And when you throw closure into the mix, it only gets harder. So, today we』re going to talk a bit about closure, and hopefully by the time we』re done you』ll find it a bit easier to understand what closure is and why we use it.


A Brief Review of Scope

Before we get started with closure, we should do a brief review of scope — the two concepts are, after all, inextricably intertwined. For an in depth look at scope, you can check out this article. The short version is as follows. Scope is composed of a set of nested boundaries that define which variables are accessible at any given point in your code. The top layer of scope is global scope and is universally accessible. Nested within global scope are individual layers of local scope or function scope. When your program attempts to look up the value held in a given variable, it starts at the closest level of scope (often inside a function), and if no such variable exists there, it then moves up to higher levels of scope, checking each one in turn. Note that this is a unidirectional process — higher levels are scope are accessible to lower levels, but not vice versa. Per usual, this is probably easiest to see in code:

var event = "Coffee with Ada.";

function calendar() {
  var event = "Party at Charles' house.";
  
  console.log(event);
}

calendar();               // Logs: "Party at Charles' house."

console.log(event); // Logs: "Coffee with Ada."

Here we have a variable in global scope called event, which holds the value 「Coffee with Ada.」 We also have a function called calendar, which contains a variable that is also called event but has the value 「Party at Charles』 house.」 When we call the function calendar, and it goes looking for a variable called event to log to the console, it finds the variable of that name that is defined in local scope. However, when we attempt to log the value of event from outside of any functions, it finds the variable of that name defined in global scope instead.


Simple Closure

Now that we have refreshed our understanding of scope, let』s look at closure. We know that variables are accessible depending on where they are in scope, but what does that mean for our function calls? Well, when you define a function it closes over the currently accessible scope and retains access to the variables defined in that scope. So, if you define a function in global scope, it has access to all of the variables in global scope, but nothing else. On the other hand, if you define a function inside another function (that is, in function scope), then it will have access to the variables in function scope and in global scope. This is where things can get tricky though. Let』s look at an example.

var event = "Coffee with Ada.";

function describeEvent() {
  console.log(event);
}

function calendar() {
  var event = "Party at Charles' house.";
  
  describeEvent();
}

calendar(); // Logs: "Coffee with Ada."

In this snippet we have a global scope variable called event, a function defined in global scope called describeEvent, which simply logs the value of event, as it knows it. We also have another function called calendar that defines its own event variable in function scope and then calls describeEvent. So what then happens when we call the function calendar? You might expect it to log 「Party at Charles』 house.」, which is the value of the event variable inside calendar』s function scope. However, the actual output is 「Coffee with Ada.」 The reason is that our describeEvent function had already closed over the variable event in global scope. It doesn』t matter that we』re calling describeEvent from inside calendar. As far as describeEvent is concerned, the event variable inside calendar doesn』t exist. And that』s closure in action!


Closure in Higher-Order Functions

Knowing why a particular function accesses a particular variable is great, but knowing how to leverage that power is even better. The primary way that we can use closure is by using higher-order functions. Remember, functions in Javascript are first-class objects, meaning that they can either accept a function as a parameter or be returned by another function. A higher-order function is one that takes advantage of these attributes. For our purposes, we』re mostly concerned a function』s ability to return another function, because if our higher-order function returns a function, then the function that we』re returning will close over any variables defined in the higher-order function』s scope. Consider the following.

function makeEventDescriber(event, date) {
  return function() {
    console.log(date + ": " + event);
  };
}

var coffeeWithAda = makeEventDescriber("Coffee with Ada.", "8/1/2018");
var partyAtCharles = makeEventDescriber("Party at Charles' house.", "8/4/2018");

coffeeWithAda();          // Logs: "8/1/2018: Coffee with Ada."
partyAtCharles(); // Logs: "8/4/2018: Party at Charles' house."

This example defines a function called makeEventDescriber, which accepts an event and a date string as parameters. On being called, the function assigns the provided values to local scope variables named event and date. It then returns another function, which closes over those variables and uses them to log a value to the console. After defining this function, we then use it to make two new functions called coffeeWithAda and partyAtCharles respectively.

Now that we have two new functions, coffeeWithAda and partyAtCharles, what happens when we call them? Note that neither of them accepts any parameters, and yet, when we call them they output the strings that we expect. So how do they retain access to the date and event variables that they depend upon to function properly? And how is it that they have different values? All of this happens because of closure. The functions returned by makeEventDescriber have closed over the values provided to it when it was called, which is why coffeeWithAda and partyAtCharles have access to different values.


Fun with Partial Function Application

One of the neat things about closure is that it lets us create functions that have pre-applied parameters. This is called partial function application, and we can use it to make functions that make other functions. For example, what if we knew that we wanted to schedule lots of events with our friend Ada. Wouldn』t it be nice to not have to provide the name 「Ada」 to every event that we schedule with her? We can do exactly that with partial function application.

function schedulerMaker(name) {
  return function(event) {
    return function() {
      console.log(event + " with " + name + ".");
    };
  };
}

var adaScheduler = schedulerMaker("Ada");
var coffeeWithAda = adaScheduler("Coffee");

coffeeWithAda(); // Logs: "Coffee with Ada."

Here, we have a function called schedulerMaker, which has a single parameter that expects a name string. This function then returns another function, which accepts an event as its parameter. Finally, this function itself returns a final function, which when called, logs a string describing our event. That』s a lot of functions, so let』s try to follow what is happening.

First, we call schedulerMaker with the argument 「Ada」 and it gives us a new function, which we assign to the variable adaScheduler. For its part, adaScheduler closes over the variable name, which contains the value of 「Ada」, and therefore retains permanent access to it. We then call adaScheduler with the argument 「Coffee」, which it assigns to the variable event, and it in turn returns another function that is going to describe our event. Now, we have a function called coffeeWithAda that has, in effect, closed over two levels of scope, thus giving it access to both the event and name variables. And indeed, when we call coffeeWithAda, the message 「Coffee with Ada」 is logged to the console.

Keep in mind that the event and name variables we are using here are not universal. If we wanted, we could use schedulerMaker to create many more functions, each with a different name attached. We could then in turn use those functions to create many more event describers, and each one would have access to different values.


Private Data and Application Interfaces

Two of the biggest benefits of closure are the ability to create private data and to define application interfaces. Sometimes, you want to enforce the way in which a program interacts with data so that you can protect its integrity. By using closure, you can do exactly this. One common way of creating such an interface is by returning an object from a function. Methods defined on this object, just like any other functions, close over the current scope. This means that we can define private data inside the overall function scope, which will only be accessible to methods defined on our object. Here is an example:

function makeCalendar(name) {
  var calendar = {
    owner: name,
    events: [],
  };
  
  return {
    addEvent: function(event, dateString) {
      var eventInfo = {
        event: event,
        date: new Date(dateString),
      };
      calendar.events.push(eventInfo);
      calendar.events.sort(function(a, b) {
        return a.date - b.date;
      });
    },
    
    listEvents: function() {
      if (calendar.events.length > 0) {
        console.log(calendar.owner + "'s events are: ");
        
        calendar.events.forEach(function(eventInfo) {
          var dateStr = eventInfo.date.toLocaleDateString();
          var description = dateStr + ": " + eventInfo.event;
          
          console.log(description);
        });
      } else {
        console.log(calendar.owner + " has no events.");
      }
    },
  };
}

var babbageCalendar = makeCalendar("Charles Babbage");

babbageCalendar.addEvent("Coffee with Ada.", "8/7/2018");
babbageCalendar.addEvent("Difference Engine presentation.", "8/2/2018");

babbageCalendar.listEvents();
  /*
    Logs:
    Charles Babbage's events are: 
    8/2/2018: Difference Engine presentation.
    8/7/2018: Coffee with Ada.
  */

In this mini-program we have a function called makeCalendar which accepts a single parameter (a name of the calendar owner) and returns an object. This object has several methods defined on it to add and list calendar events. These methods, through closure, have access to a private calendar object that is defined in the overall function scope. This object is only accessible to those methods and not to the program as a whole. We see this in action at the bottom of the snippet where we create and interact with an object called babbageCalendar.

What is interesting about this pattern is that the calendar object that babbageCalendar is interacting with is completely private. There is no way to directly manipulate the calendar.events array or the calendar.owner string — we can only do so by using the explicitly defined interface that is returned by makeCalendar. This can be very powerful, but it has downsides as well. What if we wanted a way to change the date on a particular event? Well, we can only do that by going back to the original code and adding a changeDate method to the interface. As a result, this pattern is not particularly easy to extend, but for our purposes, it illustrates the power of closure.


TL;DR

In JavaScript, the concept of closure is closely tied to the concept of scope. Closure happens when you define a function, which then closes over all of the variables accessible to it in the current scope. Once a function is defined, it will retain access to those variables regardless of where it is called. This is useful behavior because it allows us to create functions that have access to private data. By doing this, we can leverage higher-order functions, partial function application, and private application interfaces.


And that』s all for today』s exploration of closure! Hopefully this has been a useful review of how closure works and how you can use it effectively. As always, happy coding!


每天推薦一個 GitHub 優質開源項目和一篇精選英文科技或編程文章原文,歡迎關注開源日報。交流QQ群:202790710;微博:https://weibo.com/openingsource;電報群 https://t.me/OpeningSourceOrg