Затваряния в JavaScript, обяснени с примери

Какво представляват затварянията?

Затварянето е комбинацията от функция и лексикалната среда (обхват), в която е декларирана тази функция. Затварянията са основно и мощно свойство на Javascript. Тази статия обсъжда „как“ и „защо“ относно затварянията:

Пример

//we have an outer function named walk and an inner function named fly function walk (){ var dist = '1780 feet'; function fly(){ console.log('At '+dist); } return fly; } var flyFunc = walk(); //calling walk returns the fly function which is being assigned to flyFunc //you would expect that once the walk function above is run //you would think that JavaScript has gotten rid of the 'dist' var flyFunc(); //Logs out 'At 1780 feet' //but you still can use the function as above //this is the power of closures

Друг пример

function by(propName) { return function(a, b) { return a[propName] - b[propName]; } } const person1 = {name: 'joe', height: 72}; const person2 = {name: 'rob', height: 70}; const person3 = {name: 'nicholas', height: 66}; const arr_ = [person1, person2, person3]; const arr_sorted = arr_.sort(by('height')); // [ { name: 'nicholas', height: 66 }, { name: 'rob', height: 70 },{ name: 'joe', height: 72 } ]

Затварянето „помни“ средата, в която е създадено. Тази среда се състои от всички локални променливи, които са били в обхвата по време на създаването на затварянето.

function outside(num) { var rememberedVar = num; // In this example, rememberedVar is the lexical environment that the closure 'remembers' return function inside() { // This is the function which the closure 'remembers' console.log(rememberedVar) } } var remember1 = outside(7); // remember1 is now a closure which contains rememberedVar = 7 in its lexical environment, and //the function 'inside' var remember2 = outside(9); // remember2 is now a closure which contains rememberedVar = 9 in its lexical environment, and //the function 'inside' remember1(); // This now executes the function 'inside' which console.logs(rememberedVar) => 7 remember2(); // This now executes the function 'inside' which console.logs(rememberedVar) => 9 

Затварянията са полезни, защото ви позволяват да „запомните“ данни и след това да ви позволят да работите с тези данни чрез върнати функции. Това позволява на javascript да емулира частни методи, които се намират в други езици за програмиране. Частните методи са полезни за ограничаване на достъпа до код, както и за управление на вашето глобално пространство от имена.

Частни променливи и методи

Затварянията могат да се използват и за капсулиране на лични данни / методи. Разгледайте този пример:

const bankAccount = (initialBalance) => { const balance = initialBalance; return { getBalance: function() { return balance; }, deposit: function(amount) { balance += amount; return balance; }, }; }; const account = bankAccount(100); account.getBalance(); // 100 account.deposit(10); // 110

В този пример няма да имаме достъп balanceдо никъде извън bankAccountфункцията, което означава, че току-що сме създали частна променлива. Къде е затварянето? Е, помислете какво bankAccount()се връща. Той всъщност връща обект с куп функции вътре в него и въпреки това, когато се обадим account.getBalance(), функцията е в състояние да „запомни“ първоначалната си препратка към balance. Това е силата на затварянето, когато функция „запомня“ своя лексикален обхват (обхват на време на компилиране), дори когато функцията се изпълнява извън този лексикален обхват.

Емулиране на променливи с обхват на блок.

Javascript няма концепция за променливи с обхват на блок. Това означава, че когато дефинираме променлива вътре в forloop, например, тази променлива се вижда и извън forloop. И така, как затварянията могат да ни помогнат да разрешим този проблем? Нека да разгледаме.

 var funcs = []; for(var i = 0; i < 3; i++){ funcs[i] = function(){ console.log('My value is ' + i); //creating three different functions with different param values. } } for(var j = 0; j < 3; j++){ funcs[j](); // My value is 3 // My value is 3 // My value is 3 }

Тъй като променливата i няма обхват на блока, нейната стойност във всичките три функции беше актуализирана с брояча на цикли и създаде злонамерени стойности. Затварянето може да ни помогне да разрешим този проблем, като създадем моментна снимка на средата, в която е била функцията, когато е била създадена, запазвайки нейното състояние.

 var funcs = []; var createFunction = function(val){ return function() {console.log("My value: " + val);}; } for (var i = 0; i < 3; i++) { funcs[i] = createFunction(i); } for (var j = 0; j < 3; j++) { funcs[j](); // My value is 0 // My value is 1 // My value is 2 }

Късните версии на javascript es6 + имат нова ключова дума, наречена let, която може да се използва за придаване на променливата на blockcope. Има също така много функции (forEach) и цели библиотеки (lodash.js), които са предназначени за решаване на проблеми като тези, обяснени по-горе. Те със сигурност могат да повишат вашата производителност, но остава изключително важно да имате познания по всички тези проблеми, когато се опитвате да създадете нещо голямо.

Затварянията имат много специални приложения, които са полезни при създаването на големи javascript програми.

  1. Емулиране на частни променливи или капсулиране
  2. Извършване на асинхронни разговори от страна на сървъра
  3. Създаване на променлива с обхват на блок.

Емулиране на частни променливи.

За разлика от много други езици, Javascript няма механизъм, който ви позволява да създавате капсулирани променливи на екземпляр в обект. Наличието на променливи на публичен екземпляр може да причини много проблеми при изграждането на средни до големи програми. Въпреки това при затварянията този проблем може да бъде смекчен.

Подобно на предишния пример, можете да изграждате функции, които връщат обектни литерали с методи, които имат достъп до локалните променливи на обекта, без да ги излагате. По този начин, като ги направим ефективно частни.

Затварянията също могат да ви помогнат да управлявате глобалното си пространство от имена, за да избегнете сблъсъци с глобално споделени данни. Обикновено всички глобални променливи се споделят между всички скриптове във вашия проект, което определено ще ви създаде много проблеми при изграждането на средни до големи програми. Ето защо авторите на библиотеки и модули използват затваряния, за да скрият методите и данните на целия модул. Това се нарича модулен шаблон, той използва незабавно извикан функционален израз, който експортира само определена функционалност във външния свят, значително намалявайки количеството глобални препратки.

Ето кратка извадка от скелет на модул.

var myModule = (function() = { let privateVariable = 'I am a private variable'; let method1 = function(){ console.log('I am method 1'); }; let method2 = function(){ console.log('I am method 2, ', privateVariable); }; return { method1: method1, method2: method2 } }()); myModule.method1(); // I am method 1 myModule.method2(); // I am method 2, I am a private variable

Затварянията са полезни за улавяне на нови екземпляри на частни променливи, съдържащи се в „запомнената“ среда, и тези променливи могат да бъдат достъпни само чрез върнатата функция или методи.

Вектори

Векторът е може би най-простият тип колекция в Clojure. Можете да го мислите като масив в Javascript. Нека дефинираме прост вектор:

(def a-vector [1 2 3 4 5]) ;; Alternatively, use the vector function: (def another-vector (vector 1 2 3 4 5)) ;; You can use commas to separate items, since Clojure treats them as whitespace. (def comma-vector [1, 2, 3, 4, 5])

Ще видите, че той използва квадратни скоби, точно като масив в JS. Тъй като Clojure, подобно на JS, се набира динамично, векторите могат да съдържат елементи от всякакъв тип, включително други вектори.

(def mixed-type-vector [1 "foo" :bar ["spam" 22] #"^baz$"])

Добавяне на елементи към вектор

Можете да добавите елементи към вектор, като използвате conj. Можете също да се добавите към списък с помощта into, но имайте предвид, че intoе предназначен за обединяване на два вектора, така че и двата му аргумента трябва да са вектори, а използването intoе по-бавно от използването conj.

(time (conj [1 2] 3)) ; => "Elapsed time: 0.032206 msecs" ; [1 2 3] (time (into [1] [2 3])) ; => "Elapsed time: 0.078499 msecs" ; [1 2 3]
:rocket:

IDEEne!

Извличане на елементи от вектор

You can retrieve items from a vector using get. This is equivalent to using bracket notation to access items in an array in many imperative languages. Items in a vector are 0-indexed, counting from the left.

var arr = [1, 2, 3, 4, 5]; arr[0]; // => 1

In Clojure, this would be written like so:

(def a-vector [1 2 3 4 5]) (get a-vector 0) ; => 1

You can also give get a default value, if you give it an index that isn’t in the array.

;; the list doesn't have 2147483647 elements, so it'll return a string instead. (get a-vector 2147483646 "sorry, not found!") ; => "sorry, not found!"

Converting other collections into vectors

Non-vector data structures can be converted into vectors using the vec function. With hashmaps, this produces a 2D vector containing pairs of keys and values.

(vec '(1 2 3 4 5)) ; => [1 2 3 4 5] (vec {:jack "black" :barry "white"}) ; => [[:jack "black"] [:barry "white"]]

When to use a vector?

A vector should be used in almost all cases if you need a collection, because they have the shortest random-access times, which makes it easy to retrieve items from a vector. Note that vectors are ordered. If order doesn’t matter, it may be better to use a set. Also note that vectors are designed for appending items; if you need to prepend items, you might want to use a list.

More info on Closures:

  • Learn JavaScript closures in six minutes
  • A basic guide to closures in JavaScript
  • Discover the power of closures in VueJS
  • JavaScript closures explained by mailing a package