Откройте для себя силу замыканий в JavaScript

внешний интерфейс

Оригинальный адрес: Discover the power of closures in JavaScript
Оригинальный автор: Cristi Salcescu
Переводчик: wcflmy

Замыкание — это внутренняя функция, которая имеет доступ к внешней области видимости, даже если внешняя область действия завершила выполнение.

сфера

Область действия определяет время жизни этой переменной и ее видимость. Когда мы создаем функцию или{}блок, создается новая область. Следует отметить, что поvarСоздаваемые переменные имеют только область действия, в то время какletиconstПеременные создаются как с функцией, так и с областью действия блока.

вложенная область

существуетJavascriptФункции могут быть вложены внутрь функции следующим образом:

(function autorun(){
    let x = 1;
    function log(){ 
       console.log(x); 
    }
    log();
})();

log()является вложеннымautorun()функции внутри функций. существуетlog()Функции могут обращаться к переменным через внешние функцииx. В настоящее время,log()Функция — это замыкание.

Замыкания — это внутренние функции, мы можем сделать это, вызвав их внутри функции или{}Внутри блока определена функция для создания замыкания.

область действия внешней функции

Внутренняя функция может обращаться к переменным, определенным во внешней функции, даже после завершения выполнения внешней функции. следующее:

(function autorun(){
    let x = 1;
    setTimeout(function log(){
      console.log(x);
    }, 10000);
})();

Более того, внутренняя функция также может получить доступ к формальным параметрам, определенным во внешней функции, следующим образом:

(function autorun(p){
    let x = 1;
    setTimeout(function log(){
      console.log(x);//1
      console.log(p);//10
    }, 10000);
})(10);

область действия внешнего блока

Внутренняя функция может обращаться к переменным, определенным во внешнем блоке, даже после завершения выполнения внешнего блока следующим образом:

{
    let x = 1;
    setTimeout(function log(){
      console.log(x);
    }, 10000);
}

лексический охват

Лексическая область видимости означает, что внутренняя функция определяет свою внешнюю область видимости при ее определении.

СмотретьследующееКод:

(function autorun(){
    let x = 1;
    function log(){
      console.log(x);
    };
    
    function run(fn){
      let x = 100;
      fn();
    }
    
    run(log);//1
})();

log()Функция — это замыкание, и здесь она обращается кautorun()в функцииxпеременная вместоrunпеременные в функции.

Внешняя область замыкания определяется при его определении, а не при его выполнении.

autorun()Объем функцийlog()Лексический объем функции.

цепочка прицелов

Каждая область имеет ссылку на свою родительскую область. Когда мы используем переменную,Javascript引擎Он будет искаться в текущей области видимости по имени переменной. Если она не будет найдена, поиск продолжится вверх по цепочке областей видимости, покаglobalглобальный масштаб.

Примерследующее:

let x0 = 0;
(function autorun1(){
 let x1 = 1;
  
 (function autorun2(){
   let x2 = 2;
  
   (function autorun3(){
     let x3 = 3;
      
     console.log(x0 + " " + x1 + " " + x2 + " " + x3);//0 1 2 3
    })();
  })();
})();

Мы видим, что,autorun3()Эта внутренняя функция может обращаться к своим локальным переменным.x3, вы также можете получить доступ к внешней областиx1иx2переменные, а в глобальной области видимостиx0Переменная. То есть: замыкание имеет доступ ко всем переменным, определенным в его внешней (родительской) области видимости.

После выполнения внешней области

Замыкания действительно работают только тогда, когда внутренняя функция жива (все еще ссылается в другом месте) после завершения выполнения внешней области. Например, следующие ситуации:

  • В асинхронных задачах, например.timerтаймеры, обработка событий,Ajaxиспользуется как обратный вызов в запросе
  • Возвращается внешней функцией в качестве возвращаемого результата, или внутренняя функция упоминается в возвращаемом объекте результата.

Рассмотрим следующееПример:

Timer

(function autorun(){
    let x = 1;
    setTimeout(function log(){
      console.log(x);
    }, 10000);
})();

Переменнаяxбудет жить до тех пор, пока не выполнится обратный вызов таймера илиclearTimeout()называется. Если используется здесьsetInterval(), то переменнаяxвыживет, покаclearInterval()называется.

Примечание переводчика: переменная в исходном текстеxвыжил, покаsetTimeout()илиsetInterval()называется неправильным.

Event

(function autorun(){
    let x = 1;
    $("#btn").on("click", function log(){
      console.log(x);
    });
})();

когда переменнаяxПри использовании в обработчике событий он будет жить до тех пор, пока обработчик событий не будет удален.

Ajax

(function autorun(){
    let x = 1;
    fetch("http://").then(function log(){
      console.log(x);
    });
})();

ПеременнаяxБудет жить до тех пор, пока бэкэнд не вернет результат и не будет выполнена функция обратного вызова.

В предыдущих примерах мы видим, чтоlog()Функция сохраняется после выполнения родительской функции.log()Функция — это замыкание.

Кромеtimerтаймеры, обработка событий,AjaxОбщие асинхронные задачи, такие как запросы и некоторые другие асинхронные задачиAPIНапримерHTML5 Geolocation,WebSockets , requestAnimationFrame()Эта особенность замыканий также будет использоваться.

Время жизни переменной зависит от времени жизни замыкания. Переменные во внешней области видимости, на которые ссылается замыкание, будут жить до тех пор, пока функция замыкания не будет уничтожена. Если на переменную ссылаются несколько замыканий, переменная не будет уничтожена до тех пор, пока все замыкания не будут удалены сборщиком мусора.

Замки и петли

Замыкания только хранят ссылки на внешние переменные, но не копируют значения этих внешних переменных. Посмотреть нижеПример:

function initEvents(){
  for(var i=1; i<=3; i++){
    $("#btn" + i).click(function showNumber(){
      alert(i);//4
    });
  }
}
initEvents();

В этом примере мы создаем 3 замыкания, все ссылающиеся на одну и ту же переменную.i, и эти три замыкания являются обработчиками событий. из-за переменнойiПо мере увеличения цикла окончательный результат будет тем же самым значением.

Самый простой способ исправить это -forиспользуется в блоках операторовletобъявление переменной, которая будет использоваться в каждом цикле какforБлок операторов создает новую локальную переменную. следующее:

function initEvents(){
  for(let i=1; i<=3; i++){
    $("#btn" + i).click(function showNumber(){
      alert(i);//1 2 3
    });
  }
}
initEvents();

Однако, если переменная объявлена ​​вforЕсли он находится вне блока операторов, он используетсяletОбъявление переменной, все замыкания по-прежнему будут ссылаться на одну и ту же переменную, и окончательный вывод будет иметь одно и то же значение.

Закрытие и инкапсуляция

Инкапсуляция означает сокрытие информации.

Функции и частное состояние

С замыканиями мы можем создавать функции с приватным состоянием, а замыкания инкапсулируют состояние.

Заводской шаблон и частные объекты-прототипы

Давайте сначала рассмотрим общий способ создания объектов с помощью прототипов, а именно:

let todoPrototype = {
  toString : function() {
    return this.id + " " + this.userName + ": " + this.title;
  }
}
function Todo(todo){
  let newTodo = Object.create(todoPrototype);
  Object.assign(newTodo, todo);
  return newTodo;
}

В этом примереtodoPrototypeОбъект-прототип является глобальным объектом.

Мы можем использовать замыкания для создания объектов-прототипов только один раз, и они могут использоваться всеми.TodoВызовы функций являются общедоступными и гарантированно конфиденциальными.Примерследующее:

let Todo = (function createTodoFactory(){
  let todoPrototype = {
    toString : function() {
      return this.id + " " + this.userName + ": " + this.title;
    }
  }
  return function(todo){
    let newTodo = Object.create(todoPrototype);
    Object.assign(newTodo, todo);
    return newTodo;
  }
})();
let todo = Todo({id : 1, title: "This is a title", userName: "Cristi", completed: false });

здесь,Todo()это функция с частным состоянием.

Заводской шаблон и частный конструктор

ПроверятьследующееКод:

let Todo = (function createTodoFactory(){
 function Todo(spec){
   Object.assign(this, spec);
 }
 
 return function(spec){
   let todo = new Todo(spec);
   return Object.freeze(todo);
 }
})();

здесь,Todo()Фабричная функция — это замыкание. передать его, использовать или не использоватьnew, мы все можем создавать неизменяемые объекты, объект-прототип создается только один раз и является приватным.

let todo = Todo({title : "A description"});
todo.title = "Another description"; 
// Cannot assign to read only property 'title' of object
todo.toString = function() {};
//Cannot assign to read only property 'toString' of object

Кроме того, в моментальном снимке памяти мы можем идентифицировать эти примеры объектов по именам их конструкторов.

Memory snapshot in Chrome DevTools

Функция перевода и приватная карта

С замыканиями мы можем создатьmap, который используется во всех вызовах перевода и является закрытым.

Примерследующее:

let translate = (function(){
  let translations = {};
  translations["yes"] = "oui";
  translations["no"]  = "non";
  
  return function(key){
    return translations[key];
  }
})();
translate("yes"); //oui

функция автоинкрементного генератора

С замыканиями мы можем создавать автоинкрементные генераторные функции. Кроме того, внутреннее состояние является частным.Примерследующее:

function createAGenerate(count, increment) {
  return function(){
    count += increment;
    return count;
  }
}
let generateNextNumber = createAGenerate(0, 1);
console.log(generateNextNumber()); //1
console.log(generateNextNumber()); //2
console.log(generateNextNumber()); //3
let generateMultipleOfTen = createAGenerate(0, 10);
console.log(generateMultipleOfTen()); //10
console.log(generateMultipleOfTen()); //20
console.log(generateMultipleOfTen()); //30

Примечание переводчика: неправильно выводить 0, 1, 2, 0, 10, 20 последовательно в исходном тексте, спасибо @Round за исправление

Объекты и частное состояние

В приведенном выше примере мы можем создать функцию с закрытым состоянием. В то же время мы также можем создать несколько функций с одним и тем же приватным состоянием. Исходя из этого, мы также можем создать объект с приватным состоянием.

Примерследующее:

function TodoStore(){
  let todos = [];
  
  function add(todo){
    todos.push(todo);
  }
  function get(){
    return todos.filter(isPriorityTodo).map(toTodoViewModel);
  }
  
  function isPriorityTodo(todo){
     return task.type === "RE" && !task.completed;
  }
  
  function toTodoViewModel(todo) {
     return { id : todo.id, title : todo.title };
  }
  
  return Object.freeze({
    add,
    get
  });
}

TodoStore()Функция возвращает объект с приватным состоянием. Извне мы не можем получить доступ к приватномуtodosпеременная иaddиgetОба замыкания имеют одинаковое частное состояние. это здесь,TodoStore()является заводской функцией.

Замыкания против чистых функций

Замыкания — это функции, которые ссылаются на переменные во внешней области видимости.

Для лучшего понимания мы разделили внутреннюю функцию на два аспекта: замыкание и чистую функцию:

  • Замыкания — это функции, которые ссылаются на переменные во внешней области видимости.
  • Чистые функции — это те, которые не ссылаются на переменные во внешней области видимости, они обычно возвращают значение и не имеют побочных эффектов.

В приведенном выше примереadd()иget()функции являются замыканиями, аisPriorityTodo()иtoTodoViewModel()является чистой функцией.

Применение замыканий в функциональном программировании

Замыкания также широко используются в функциональном программировании. Например,underscoreв исходном кодеРаздел, связанный с функциямиВсе функции используют замыкания.

Декоратор функции — это функция более высокого порядка, которая принимает одну функцию в качестве аргумента и возвращает другую функцию, а возвращаемая функция — это разновидность функции-аргумента — Аллонже Javascript

Функции декоратора также используют замыкания.

Давайте посмотрим на следующееnotЭта простая функция декоратора:

function not(fn){
  return function decorator(...args){
    return !fn.apply(this, args);
  }
}

decorator()Функция ссылается на переменную fn внешней области видимости, поэтому это замыкание.

Если вы хотите узнать больше о декораторах, вы можете проверить этостатья.

вывоз мусора

существуетJavascript, локальные переменные уничтожаются, когда функция завершает выполнение, если на них еще нет ссылок. Когда сами замыкания также подвергаются сборке мусора, приватное состояние в этих замыканиях также подвергается сборке мусора. Обычно мы можем добиться этого, разорвав ссылку на замыкание.

на этопример, мы сначала создаемadd()Закрытие.

let add = (function createAddClosure(){
    let arr = [];
    return function(obj){
       arr.push(obj);
    }
})();

Затем мы определяем еще две функции:

  • addALotOfObjects()к переменной закрытияarrдобавляется объект.
  • clearAllObjects()Установите функцию закрытия наnull.

И обе функции используются как обработчики событий:

function addALotOfObjects(){
    for(let i=1; i<=10000;i++) {
       add(new Todo(i));
    }
}
function clearAllObjects(){
    if(add){
       add = null;
    }
}
$("#add").click(addALotOfObjects);
$("#clear").click(clearAllObjects);

когда я нажимаюAddКогда кнопка нажата, она перейдет к переменной закрытияarrДобавить 10 000todoобъект, снимок памяти выглядит следующим образом:

Memory snapshot after adding 10000 to-dos

когда я нажимаюClearкнопку, мы устанавливаем ссылку закрытия наnull. Затем переменная закрытияarrБудет собран мусор, снимок памяти выглядит следующим образом:

Memory snapshot after setting the closure reference to null

Избегайте глобальных переменных

существуетJavascript, мы можем легко создавать глобальные переменные. Все, что определено в функции и{}Переменные вне блока являются глобальными, как и функции, определенные в глобальной области видимости.

Вот пример определения фабричных функций, которые создают разные объекты. Чтобы не помещать все фабричные функции в глобальную область видимости, проще всего повесить их вappпод глобальными переменными.

Примерследующее:

let app = Loader();
app.factory(function DataService(args){ return {}});
app.factory(function Helper(args){ return {}});
app.factory(function Mapper(args){ return {}});
app.factory(function Model(args){});

app.factory()Методы также могут группировать различные фабричные функции в разные модули. Следующий пример —TimerЗаводские функции подразделяются наtoolsпод модуль.

app.factory("tools")(function Timer(args){ return {}});

мы можемappобъект выставленstartметод в качестве точки входа приложения, через функцию обратного вызоваfactoriesпараметры для доступа к этим заводским функциям. здесьstart()Функцию можно вызвать только один раз, как показано ниже:

app.start(function startApplication(factories){
  let helper = factories.Helper();
  
  let dataService = factories.DataService();
  let model = factories.Model({
      dataService : dataService,
      helper : helper,
      timer : factories.tools.Timer()
  });
});

A Composition Root is a (preferably) unique location in an application where modules are composed together.

Mark Seemann

loaderобъект

разрешите намappидеально подходит какloaderобъект,Примерследующее:

function Loader(){
  let modules = Object.create(null);
  let started = false;
  
  function getNamespaceModule(modulesText){
    let parent = modules;
    if(modulesText){
      let parts = modulesText.split('.');
      for(let i=0; i<parts.length; i++){
        let part = parts[i];
        if (typeof parent[part] === "undefined") {
          parent[part] = Object.create(null);
        }
        
        parent = parent[part];
      }
    }
    
    return parent;
  }
  
  function addFunction(namespace, fn){
    if(typeof(fn) !== "function") {
      throw "Only functions can be added";
    }
       
    let module = getNamespaceModule(namespace);
    let fnName = fn.name;    
    module[fnName] = fn;
  }
  
  function addNamespace(namespace){
    return function(fn){
      addFunction(namespace, fn)
    }
  }
  
  function factory(){
    if(typeof(arguments[0]) === "string"){
      return addNamespace(arguments[0]);
    } else {
      return addFunction(null, arguments[0]);
    }
  }
  
  function start(startApplication){
    if(started){
      throw "App can be started only once";
    }
     
    startApplication(Object.freeze(modules));
    started = true;
  }
  
  return Object.freeze({
    factory,
    start
  });
};
let app = Loader();

factory()метод добавления новых фабричных функций во внутренние переменныеmodulesсередина.

start()Метод вызовет функцию обратного вызова и получит доступ к внутренним переменным в функции обратного вызова.

пройти черезfactory()Определите фабричную функцию, которая будетstart()Это такой аккуратный и элегантный способ быть единственной точкой входа для вызова различных фабричных функций в приложении для создания различных объектов.

это здесь,factoryиstartВсе замыкания.

Суммировать

Замыкание — это внутренняя функция, которая может обращаться к переменным во внешней области видимости.

Эти ссылочные переменные не уничтожаются до тех пор, пока не будет уничтожено замыкание.

закрытие делаетtimerтаймеры, обработка событий,AJAXАсинхронные задачи, такие как запросы, проще.

Инкапсуляция может быть достигнута с помощью замыканий.

Наконец, хотите получить больше информации оJavascriptДля получения знаний, связанных с функциями, вы можете проверить следующие статьи:

Discover Functional Programming in JavaScript with this thorough introduction

Discover the power of first class functions

How point-free composition will make you a better functional programmer

Here are a few function decorators you can write from scratch

Make your code easier to read with Functional Programming