Особливості роботи або «За що я люблю JavaScript» Замикання, Прототипування та Контекст
У даному топіці будуть розглядатися:
- Замикання
- Прототипування
- Контекст виконання
Передмова
Замикання, або «Ці незвичайні області видимості»
Суть замикань проста:всередині функції можна використовувати всі прем'яні, які доступні в тому місці, де функцію було оголошено.
При запуску коду виведе текст "Hello World", як і очікувалося. Суть того, що відбувається проста - створюється глобальна зміннаtitleзі значенням "Hello World", яке показується користувачеві за допомогоюalert-а. У цьому прикладі, навіть якщо ми опустимо ключове словоvar, код все одно спрацює правильно через глобальний контекст. Але про це згодом.
Тепер спробуємо оголосити ту саму змінну, але вже всередині функції:
В результаті запуску коду згенерується помилка "'title' is undefined" - "змінна 'title' не була оголошена". Це відбувається через механізм локальної області видимості змінних:всі змінні, оголошені всередині фукнції є локальними і видно лише всередині цієї функції. Або простіше: якщо ми оголосимо якусь змінну всередині функції, то поза цією функцією доступу до цієї змінної ми не матимемо.
Для того, щоб вивести напис "Hello World", необхідно викликатиalertвсередині функції, що викликається:
Або повернути значення з функції:
Оскільки функція — це об'єкт, це означає, що механізм областей видимості змінних поширюється і функції: функція, оголошена всередині інший функції, видно лише там, де було оголошено
У цьому прикладі зміннуtitleбуло оголошено двічі – вперше глобально, а вдруге – всередині функції. Завдяки тому, що всерединіфункціїexampleзміннаtitleбула оголошена за допомогою ключового словаvar, вона стає локальною і ніяк не пов'язана зі змінноюtitle, оголошеною до функції . В результаті виконання коду спочатку виведеться "internal" (внутрішня змінна), а потім "external" (глобальна змінна).
Якщо вилучити ключове словоvarз рядкаvar title = 'internal', то запустивши код, в результаті двічі отримаємо повідомлення "internal". Це відбувається через те, що при виклику функції ми оголошували не локальну зміннуtitle, а перезаписували значення глобальної змінної!
Таким чином можна побачити, що використання ключового слова var робить змінну локальною, гарантуючи відсутність конфліктів із зовнішніми змінними (наприклад, в PHP всі змінні всередині функції за умовчанням є локальними; і для того, щоб звернутися до глобальної змінної необхідно оголосити її глобальною за допомогою ключового слова global).
Отже, як визначити яка змінна використовується у функції?
- При спробі використовувати (набути значення) змінної згенерується помилка, що змінна не була оголошена
- При спробі привласнити змінну значення, змінна буде створена глобальної області видимості, і їй присвоиться значення.
Виконавши код, отримаємо обидва рази "internal". Привласнення значення змінної title всередині функціїAстворює глобальну змінну, яку можна використовувати поза функцією. Слід пам'ятати, що присвоєння значення змінної (отже й створення глобальної змінної) відбувається на етапі виклику функціїА, отже спроба викликатиalert(title)до виклику функціїAзгенеруєпомилку.
Як відомо, всі локальні змінні створюються заново під час кожного нового виклику функції. Наприклад, у нас є функціяA, всередині якої оголошується зміннаtitle:
Після того, як функціяAбуде виконана, зміннаtitleперестане існувати і до неї неможливо отримати доступ. Спроба якось звернутися до змінної викликає помилку, що змінну не було оголошено.
Перш ніж запустити цей приклад, спробуємо розглянути логічно поведінку змінноїtitle: при запуску функціїgetTitleзмінна створюється, а після закінчення виклику – знищується. Однак при виклику функціїgetTitleповертається об'єкт з двома динамічно-оголошеними функціямиshowTitleтаsetTitle, які використовують цю змінну. Що ж буде, якщо викликати ці функції?
Якщо запустити функціюgetTitleще раз, то можна побачити, що зміннаtitle, як і функціїshowTitle/setTitle, щоразу створюються заново, і ніяк не пов'язані з попередніми запусками:
Запустивши код (не забувши додати вище код функціїgetTitle), буде згенеровано два повідомлення: "Hello World 1" та "Hello World 2" (подібна поведінка використовується для емуляції приватних змінних (13)).
Залишивши теорію та найпростіші приклади, і спробуємо зрозуміти яку вигоду можна отримати із замикань на практиці:
Перше - це можливість не засмічувати глобальну область видимості.
Проблема конфліктів у світовій галузі видимості очевидна. Простий приклад: якщо на сторінці підключаються кілька JavaScript файлів, що оголошують функціюshowTitle, то при викликіshowTitleбуде виконуватися функція, оголошена останньою. Тосаме стосується і оголошених змінних.
У результаті всі оголошені змінні та функції не будуть доступні у глобальній галузі видимості, а отже, не буде жодних конфліктів.
В результаті виконання коду згенерується повідомлення "Hello World" - локальна функціяshowTitleстала доступна глобально під ім'ямshowSimpleTitle, при цьому використовує «замкнену» зміннуtitle, недоступну поза нашою анонімною функцією.
Т.к. ми все обернули на анонімну функцію, яка одразу ж виконується, цій функції можна передати параметри, які всередині цієї функції будуть доступні під локальними назвами. Приклад з jQuery:
Викликає помилку, якщо глобальна змінна $ не є jQuery. А таке трапляється, якщо крім jQuery використовується інша бібліотека, яка використовує функцію $, наприклад Prototype.JS. Рішення «в лоб»:
Працюватиме, і працюватиме правильно. Але не дуже гарно. Є більш красиве рішення - оголосити локальну змінну $ у вигляді аргументу функції, передавши туди об'єкт jQuery:
Якщо згадати, що всі аргументи функції за замовчуванням є локальними змінними, то стає зрозуміло, що тепер всередині нашої анонімної функції $ ніяк не пов'язаний з глобальним об'єктом $, є посиланням на об'єкт jQuery. Для того, щоб прийом з анонімної функції став зрозумілішим, можна анонімну функцію зробити неанонімною – оголосили функцію і відразу ж її запустили:
Ну, а якщо все ж таки знадобиться викликати глобальну функцію $, можна скористатисяwindow.$.
При використанні подієвої моделі часто виникають ситуації, коли потрібно повісити одну і ту ж подію, але на різні елементи. Наприклад, у нас є 10 div-елементів, на кліку на які потрібно викликатиalert(N), де N є унікальним номером елемента.
Найпростіше рішення з використанням замикання:
Проте виконання цього коду призводить до «несподіваного» результату — всі кліки виводять одне й те саме число — 11. Чи здогадуєтеся чому?
Відповідь проста: значення змінноїcounterбереться в момент кліка по елементу. Оскільки на той час значення змінної стало 11 (умова виходу з циклу), те й виводиться відповідно число 11 всім елементів.
Правильне рішення – динамічно генерувати функцію обробки кліка окремо для кожного елемента:
У цьому підході ми використовуємо анонімну функцію, яка набуває значенняcounterяк параметра і повертає динамічну функцію. Всередині анонімної функціїлокальназміннаiCounterмістить поточне значенняcounterна момент виклику функції. А оскільки при кожному виклик будь-якої функції всі локальні змінні оголошуються заново (створюється нове замикання), то при виклик нашої анонімної функції щоразу повертатиметься нова динамічна функція з вже «замкненим» номером.
Якщо говорити простіше, то запустивши функцію другий (третій, четвертий.) раз, всі локальні змінні будуть створені в пам'яті заново і не матимуть жодного відношення до змінних, створених під час попереднього виклику функції.
Важко? Думаю, що з першого разу так. Зате не потрібно мати купу глобальних змінних, і робити перевірки до функцій «звідки мене викликали. ». А з використанням jQuery.each, який за умовчанням викликає передану функцію, код стає ще простіше і читабельніше:
Завдяки замиканням можна писати красивий, короткий і зрозумілий код, називаючи змінні та функції так, як хочеться; і бути впевненим, що цей код небуде конфліктувати з іншими скриптами, що підключаються.
Прототипування або "Я хочу зробити об'єкт класу"
Якщо у нас багато об'єктів, то зручніше буде зробити окрему функцію, яка повертає об'єкт:
Те саме можна зробити з використанням прототипів:
І невеликий наочний приклад розширення можливостей існуючих об'єктів за допомогою прототипів
Необхідно одержати назву дня тижня від будь-якої дати, але вбудований об'єкт Date містить лише метод getDay, що повертає числове уявлення дня тижня від 0 до 6: від неділі до суботи.
Можна зробити так:
Або використовувати прототипування та розширити вбудований об'єкт дати:
Думаю, що другий спосіб більш елегантний і не засмічує область видимості «зайвою» функцією. З іншого боку, є думка, що розширення вбудованих об'єктів — поганий тон, якщо над кодом працює кілька людей. Тож із цим слід бути обережнішим.
Контекст виконання або "Цей загадковий this"
З прикладу видно, що за викликуexampleObject.showTitle()функція викликається як метод об'єкта, і всередині функціїthisпосилається на об'єктexampleObject, що викликав функцію. Самі собою функції ніяк не прив'язані до об'єкта і існують окремо. Прив'язка контексту відбувається безпосередньо під час виклику функції:
Якщо при виклику функції вона (функція) не посилається на жодний об'єкт, тоthisвсередині функції буде посилатися на глобальний об'єктwindow. Тобто. якщо просто викликатиshowTitle(), то буде згенерована помилка, що зміннаtitleне оголошена; проте якщо оголосити глобальну зміннуtitle, то функція виведе значення цієї змінної:
Щоб продемонструвати, щоконтекст функції визначається саме під час виклику, наведу приклад, де функція спочатку існує лише як метод об'єкта:
В результаті виконання виведеться повідомлення "Global Title", що означає, що під час виклику функціїthisвказує на глобальний об'єктwindow, а не на об'єктexampleObject. Це відбувається через те, що у рядкуvar showTitle = exampleObject.showTitleми отримуємо посилання тільки на функцію, і при викликіshowTitle()немає посилання на вихідний об'єктexampleObject, чомуthisпочинає посилатися на об'єктwindow.
Спрощуючи:якщо функція викликана як властивість об'єкта, тоthisбуде посилатися цей об'єкт. Якщо об'єкта, що викликає, немає,thisбуде посилатися на глобальний об'єктwindow.
Приклад частої помилки:
При натисканні на DIV з id "exampleDiv" замість очікуваного "Example Title", виведеться "Global Title". Це відбувається через те, що на подію натискання ми віддаємо функцію, але не віддаємо об'єкт; і, у результаті, функція запускається без прив'язки об'єкту. Щоб запустити функцію, прив'язану до об'єкта, потрібно викликати функцію з посиланням на об'єкт:
Однак усіма улюблений ІЕ до 9 версії не підтримує цю можливість. Тому більшість JS бібліотек самостійно реалізують цю можливість тим чи іншим способом. Наприклад, у jQuery це проксі:
Суть підходу досить проста - при виклику jQuery.proxy повертається анонімна функція, яка за допомогою замикань викликає вихідну функцію в контексті переданого об'єкта.
Без використання параметрів функції обидві працюють однаково - функціїapplyіcallвідрізняються лише способом передачі параметрів при викликі:
Більшерозгорнуту інформацію з порушених тем можна прочитати тут: