Bagaimana cara kerja penutupan JavaScript?

7636

Bagaimana Anda menjelaskan penutupan JavaScript kepada seseorang dengan pengetahuan tentang konsep-konsep yang dikandungnya (misalnya fungsi, variabel, dan sejenisnya), tetapi tidak memahami penutupan sendiri?

Saya telah melihat contoh Skema yang diberikan di Wikipedia, tetapi sayangnya itu tidak membantu.

Zaheer Ahmed
sumber
391
Masalah saya dengan ini dan banyak jawaban adalah bahwa mereka mendekatinya dari perspektif abstrak, teoritis, daripada mulai dengan menjelaskan mengapa penutupan diperlukan dalam Javascript dan situasi praktis di mana Anda menggunakannya. Anda berakhir dengan artikel pertama yang harus Anda hancurkan, sepanjang waktu berpikir, "tapi, mengapa?". Saya hanya akan mulai dengan: penutupan adalah cara yang rapi untuk berurusan dengan dua realitas JavaScript berikut: a. ruang lingkup berada pada level fungsi, bukan level blok dan, b. sebagian besar dari apa yang Anda lakukan dalam JavaScript adalah asynchronous / event driven.
Jeremy Burton
53
@Redsandro Untuk satu, itu membuat kode event-driven jauh lebih mudah untuk ditulis. Saya mungkin menjalankan fungsi ketika halaman memuat untuk menentukan secara spesifik tentang HTML atau fitur yang tersedia. Saya dapat mendefinisikan dan mengatur handler dalam fungsi itu dan memiliki semua info konteks yang tersedia setiap kali handler dipanggil tanpa harus meminta lagi. Pecahkan masalah satu kali, gunakan kembali pada setiap halaman di mana handler itu diperlukan dengan pengurangan biaya overhead pada pemanggilan ulang handler. Anda pernah melihat data yang sama dipetakan ulang dua kali dalam bahasa yang tidak memilikinya? Penutupan membuatnya jauh lebih mudah untuk menghindari hal semacam itu.
Erik Reppen
1
@Erik Reppen terima kasih atas jawabannya. Sebenarnya, saya ingin tahu tentang manfaat dari closurekode yang sulit dibaca ini dibandingkan dengan Object Literalyang menggunakan kembali sendiri dan mengurangi biaya overhead yang sama, namun membutuhkan 100% lebih sedikit kode pembungkus.
Redsandro
6
Untuk programmer Java, jawaban singkatnya adalah bahwa fungsi itu setara dengan kelas dalam. Kelas dalam juga memegang pointer implisit ke instance dari kelas luar, dan digunakan untuk banyak tujuan yang sama (yaitu, membuat event handler).
Boris van Schooten
8
Saya menemukan contoh praktis ini sangat berguna: youtube.com/watch?v=w1s9PgtEoJs
Abhi

Jawaban:

7360

Penutupan adalah pasangan dari:

  1. Suatu fungsi, dan
  2. Referensi ke lingkup luar fungsi itu (lingkungan leksikal)

Lingkungan leksikal adalah bagian dari setiap konteks eksekusi (stack frame), dan merupakan peta antara pengidentifikasi (mis. Nama variabel lokal) dan nilai.

Setiap fungsi dalam JavaScript mempertahankan referensi ke lingkungan leksikal luarnya. Referensi ini digunakan untuk mengkonfigurasi konteks eksekusi yang dibuat ketika suatu fungsi dipanggil. Referensi ini memungkinkan kode di dalam fungsi untuk "melihat" variabel yang dideklarasikan di luar fungsi, terlepas dari kapan dan di mana fungsi dipanggil.

Jika suatu fungsi dipanggil oleh fungsi, yang pada gilirannya disebut oleh fungsi lain, maka rantai referensi ke lingkungan leksikal luar dibuat. Rantai ini disebut rantai lingkup.

Dalam kode berikut, innermembentuk penutupan dengan lingkungan leksikal dari konteks eksekusi yang dibuat saat foodipanggil, ditutup oleh variabel secret:

function foo() {
  const secret = Math.trunc(Math.random()*100)
  return function inner() {
    console.log(`The secret number is ${secret}.`)
  }
}
const f = foo() // `secret` is not directly accessible from outside `foo`
f() // The only way to retrieve `secret`, is to invoke `f`

Dengan kata lain: dalam JavaScript, fungsi membawa referensi ke "kotak negara" pribadi, yang hanya dapat diakses oleh mereka (dan fungsi lain yang dinyatakan dalam lingkungan leksikal yang sama). Kotak negara ini tidak terlihat oleh penelepon fungsi, memberikan mekanisme yang sangat baik untuk menyembunyikan data dan enkapsulasi.

Dan ingat: fungsi-fungsi dalam JavaScript dapat ditularkan seperti variabel (fungsi kelas satu), yang berarti pasangan fungsi dan keadaan ini dapat diteruskan ke program Anda: mirip dengan cara Anda memberikan instance kelas di C ++.

Jika JavaScript tidak memiliki penutupan, maka lebih banyak negara harus dilewati antara fungsi secara eksplisit , membuat daftar parameter lebih lama dan kode ribut.

Jadi, jika Anda ingin fungsi selalu memiliki akses ke bagian pribadi negara, Anda bisa menggunakan penutupan.

... dan sering kita lakukan ingin negara bergaul dengan fungsi. Misalnya, dalam Java atau C ++, ketika Anda menambahkan variabel instance pribadi dan metode ke kelas, Anda mengasosiasikan status dengan fungsionalitas.

Dalam C dan sebagian besar bahasa umum lainnya, setelah fungsi kembali, semua variabel lokal tidak lagi dapat diakses karena stack-frame dihancurkan. Dalam JavaScript, jika Anda mendeklarasikan suatu fungsi di dalam fungsi lain, maka variabel lokal dari fungsi luar tetap dapat diakses setelah kembali darinya. Dengan cara ini, dalam kode di atas, secrettetap tersedia untuk objek fungsi inner, setelah kembali dari foo.

Penggunaan Penutupan

Penutupan berguna setiap kali Anda membutuhkan keadaan pribadi yang terkait dengan suatu fungsi. Ini adalah skenario yang sangat umum - dan ingat: JavaScript tidak memiliki sintaksis kelas hingga 2015, dan itu masih tidak memiliki sintaksis bidang pribadi. Penutupan memenuhi kebutuhan ini.

Variabel Instance Pribadi

Dalam kode berikut, fungsi toStringmenutup rincian mobil.

function Car(manufacturer, model, year, color) {
  return {
    toString() {
      return `${manufacturer} ${model} (${year}, ${color})`
    }
  }
}
const car = new Car('Aston Martin','V8 Vantage','2012','Quantum Silver')
console.log(car.toString())

Pemrograman Fungsional

Dalam kode berikut, fungsi innerditutup atas keduanya fndan args.

function curry(fn) {
  const args = []
  return function inner(arg) {
    if(args.length === fn.length) return fn(...args)
    args.push(arg)
    return inner
  }
}

function add(a, b) {
  return a + b
}

const curriedAdd = curry(add)
console.log(curriedAdd(2)(3)()) // 5

Pemrograman Berorientasi Acara

Dalam kode berikut, fungsi onClickmenutup variabel BACKGROUND_COLOR.

const $ = document.querySelector.bind(document)
const BACKGROUND_COLOR = 'rgba(200,200,242,1)'

function onClick() {
  $('body').style.background = BACKGROUND_COLOR
}

$('button').addEventListener('click', onClick)
<button>Set background color</button>

Modularisasi

Dalam contoh berikut, semua detail implementasi disembunyikan di dalam ekspresi fungsi yang segera dieksekusi. Fungsi tickdan toStringmenutup negara swasta dan fungsi yang mereka butuhkan untuk menyelesaikan pekerjaan mereka. Penutupan memungkinkan kami melakukan modularisasi dan merangkum kode kami.

let namespace = {};

(function foo(n) {
  let numbers = []
  function format(n) {
    return Math.trunc(n)
  }
  function tick() {
    numbers.push(Math.random() * 100)
  }
  function toString() {
    return numbers.map(format)
  }
  n.counter = {
    tick,
    toString
  }
}(namespace))

const counter = namespace.counter
counter.tick()
counter.tick()
console.log(counter.toString())

Contohnya

Contoh 1

Contoh ini menunjukkan bahwa variabel lokal tidak disalin dalam penutupan: penutupan mempertahankan referensi ke variabel asli sendiri . Seolah-olah tumpukan-bingkai tetap hidup dalam memori bahkan setelah fungsi luar keluar.

function foo() {
  let x = 42
  let inner  = function() { console.log(x) }
  x = x+1
  return inner
}
var f = foo()
f() // logs 43

Contoh 2

Dalam kode berikut, tiga metode log, incrementdan updatesemuanya menutup lingkungan leksikal yang sama.

Dan setiap kali createObjectdipanggil, konteks eksekusi baru (stack frame) dibuat dan variabel yang sama sekali baru x, dan serangkaian fungsi baru ( logdll.) Dibuat, yang menutup variabel baru ini.

function createObject() {
  let x = 42;
  return {
    log() { console.log(x) },
    increment() { x++ },
    update(value) { x = value }
  }
}

const o = createObject()
o.increment()
o.log() // 43
o.update(5)
o.log() // 5
const p = createObject()
p.log() // 42

Contoh 3

Jika Anda menggunakan variabel yang dideklarasikan menggunakan var, berhati-hatilah Anda memahami variabel mana yang Anda tutupi. Variabel yang dideklarasikan menggunakan vardiangkat. Ini jauh dari masalah dalam JavaScript modern karena pengenalan letdan const.

Dalam kode berikut, setiap kali di sekitar loop, fungsi baru innerdibuat, yang menutup i. Tetapi karena var idiangkat di luar loop, semua fungsi dalam ini menutup variabel yang sama, yang berarti bahwa nilai akhir i(3) dicetak, tiga kali.

function foo() {
  var result = []
  for (var i = 0; i < 3; i++) {
    result.push(function inner() { console.log(i) } )
  }
  return result
}

const result = foo()
// The following will print `3`, three times...
for (var i = 0; i < 3; i++) {
  result[i]() 
}

Poin terakhir:

  • Setiap kali suatu fungsi dideklarasikan dalam JavaScript, penutupan dibuat.
  • Mengembalikan functiondari dalam fungsi lain adalah contoh klasik dari penutupan, karena keadaan di dalam fungsi luar secara implisit tersedia untuk fungsi dalam yang dikembalikan, bahkan setelah fungsi luar menyelesaikan eksekusi.
  • Setiap kali Anda menggunakan eval()di dalam suatu fungsi, penutupan digunakan. Teks yang Anda evaldapat referensi variabel lokal dari fungsi, dan dalam mode non-ketat Anda bahkan dapat membuat variabel lokal baru dengan menggunakan eval('var foo = …').
  • Ketika Anda menggunakan new Function(…)( Function constructor ) di dalam suatu fungsi, ia tidak menutup lingkungan leksikalnya: ia menutup konteks global sebagai gantinya. Fungsi baru tidak dapat merujuk variabel lokal dari fungsi luar.
  • Penutupan dalam JavaScript adalah seperti menjaga referensi ( BUKAN salinan) ke ruang lingkup pada titik deklarasi fungsi, yang pada gilirannya menyimpan referensi ke lingkup luarnya, dan seterusnya, semua jalan ke objek global di bagian atas rantai ruang lingkup.
  • Penutupan dibuat ketika suatu fungsi dideklarasikan; closure ini digunakan untuk mengkonfigurasi konteks eksekusi ketika fungsi dipanggil.
  • Satu set variabel lokal baru dibuat setiap kali suatu fungsi dipanggil.

Tautan

Ben Aston
sumber
74
Ini kedengarannya bagus: "Penutupan dalam JavaScript adalah seperti menyimpan salinan semua variabel lokal, sama seperti ketika fungsi keluar." Tapi itu menyesatkan karena beberapa alasan. (1) Panggilan fungsi tidak harus keluar untuk membuat penutupan. (2) Ini bukan salinan dari nilai - nilai variabel lokal tetapi variabel itu sendiri. (3) Tidak disebutkan siapa yang memiliki akses ke variabel-variabel ini.
dlaliberte
27
Contoh 5 menunjukkan "gotcha" di mana kode tidak berfungsi sebagaimana dimaksud. Tapi itu tidak menunjukkan cara memperbaikinya. Jawaban lain ini menunjukkan cara untuk melakukannya.
Matt
190
Saya suka bagaimana posting ini dimulai dengan huruf tebal besar yang bertuliskan "Closures Are Not Magic" dan mengakhiri contoh pertamanya dengan "Keajaibannya adalah bahwa dalam JavaScript referensi fungsi juga memiliki referensi rahasia untuk penutupan yang dibuatnya".
Andrew Macheret
6
Contoh # 3 adalah mencampur penutupan dengan javascripts mengangkat. Sekarang saya pikir menjelaskan hanya penutupan yang cukup sulit tanpa membawa perilaku mengangkat. Ini paling membantu saya: Closures are functions that refer to independent (free) variables. In other words, the function defined in the closure 'remembers' the environment in which it was created.dari developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
caramba
3
ECMAScript 6 dapat mengubah sesuatu dalam artikel hebat ini tentang penutupan. Misalnya, jika Anda menggunakan let i = 0bukan var i = 0dalam Contoh 5, maka testList()akan mencetak apa yang Anda inginkan pada awalnya.
Nier
3989

Setiap fungsi dalam JavaScript memelihara tautan ke lingkungan leksikal luarnya. Lingkungan leksikal adalah peta dari semua nama (mis. Variabel, parameter) dalam lingkup, dengan nilainya.

Jadi, setiap kali Anda melihat functionkata kunci, kode di dalam fungsi itu memiliki akses ke variabel yang dinyatakan di luar fungsi.

function foo(x) {
  var tmp = 3;

  function bar(y) {
    console.log(x + y + (++tmp)); // will log 16
  }

  bar(10);
}

foo(2);

Ini akan masuk 16karena fungsi barmenutup parameter xdan variabel tmp, yang keduanya ada di lingkungan leksikal dari fungsi luar foo.

Fungsi bar, bersama dengan hubungannya dengan lingkungan fungsi leksikal fooadalah penutupan.

Fungsi tidak harus kembali untuk membuat penutupan. Hanya berdasarkan deklarasi, setiap fungsi menutup lingkungan leksikal yang melingkupinya, membentuk penutupan.

function foo(x) {
  var tmp = 3;

  return function (y) {
    console.log(x + y + (++tmp)); // will also log 16
  }
}

var bar = foo(2);
bar(10); // 16
bar(10); // 17

Fungsi di atas juga akan mencatat 16, karena kode di dalamnya barmasih dapat merujuk ke argumen xdan variabel tmp, meskipun mereka tidak lagi secara langsung dalam cakupan.

Namun, karena tmpmasih berkeliaran di dalam barpenutupan, itu tersedia untuk ditambahkan. Itu akan bertambah setiap kali Anda menelepon bar.

Contoh paling sederhana dari penutupan adalah ini:

var a = 10;

function test() {
  console.log(a); // will output 10
  console.log(b); // will output 6
}
var b = 6;
test();

Ketika fungsi JavaScript dipanggil, konteks eksekusi baru ecdibuat. Bersama dengan argumen fungsi dan objek target, konteks eksekusi ini juga menerima tautan ke lingkungan leksikal dari konteks eksekusi pemanggilan, artinya variabel yang dideklarasikan di lingkungan leksikal luar (dalam contoh di atas, keduanya adan b) tersedia dari ec.

Setiap fungsi menciptakan penutupan karena setiap fungsi memiliki tautan ke lingkungan leksikal luarnya.

Perhatikan bahwa variabel itu sendiri terlihat dari dalam penutupan, bukan salinan.

Ali
sumber
24
@feeela: Ya, setiap fungsi JS membuat penutupan. Variabel yang tidak direferensikan kemungkinan akan memenuhi syarat untuk pengumpulan sampah di mesin JS modern, tetapi itu tidak mengubah fakta bahwa ketika Anda membuat konteks eksekusi, konteks itu memiliki referensi ke konteks eksekusi terlampir, dan variabel-variabelnya, dan fungsi itu adalah objek yang berpotensi dipindahkan ke ruang lingkup variabel yang berbeda, sambil mempertahankan referensi asli itu. Itu penutupnya.
@ Ali Saya baru saja menemukan bahwa jsFiddle yang saya berikan tidak benar-benar membuktikan apa pun, karena deletegagal. Namun demikian, lingkungan leksikal yang akan dibawa fungsi sebagai [[Cakupan]] (dan akhirnya digunakan sebagai basis untuk lingkungan leksikal itu sendiri ketika dipanggil) ditentukan ketika pernyataan yang mendefinisikan fungsi dieksekusi. Ini berarti bahwa fungsi yang menutup atas isi SELURUH lingkup mengeksekusi, terlepas dari nilai-nilai yang sebenarnya mengacu dan apakah itu lolos lingkup. Silakan lihat bagian 13.2 dan 10 di spec
Asad Saeeduddin
8
Ini adalah jawaban yang baik sampai mencoba menjelaskan jenis dan referensi primitif. Itu benar-benar salah dan berbicara tentang literal yang sedang disalin, yang benar-benar tidak ada hubungannya dengan apa pun.
Ry-
12
Penutupan adalah jawaban JavaScript untuk pemrograman berbasis kelas, berorientasi objek. JS bukan berbasis kelas, jadi kita harus menemukan cara lain untuk mengimplementasikan beberapa hal yang tidak dapat diimplementasikan sebaliknya.
Bartłomiej Zalewski
2
ini harus menjadi jawaban yang diterima. Keajaiban tidak pernah terjadi dalam fungsi batin. Ini terjadi ketika menetapkan fungsi luar ke variabel. Ini menciptakan konteks eksekusi baru untuk fungsi dalam, sehingga "variabel pribadi" dapat diakumulasikan. Tentu saja karena variabel fungsi luar yang ditugaskan untuk mempertahankan konteks. Jawaban pertama hanya membuat semuanya menjadi lebih kompleks tanpa menjelaskan apa yang sebenarnya terjadi di sana.
Albert Gao
2442

KATA PENGANTAR: jawaban ini ditulis ketika pertanyaannya adalah:

Seperti yang dikatakan Albert, "Jika Anda tidak dapat menjelaskannya kepada anak berusia enam tahun, Anda benar-benar tidak memahaminya sendiri." Saya mencoba menjelaskan penutupan JS kepada seorang teman berusia 27 tahun dan benar-benar gagal.

Adakah yang bisa menganggap bahwa saya berusia 6 tahun dan anehnya tertarik dengan subjek itu?

Saya cukup yakin saya adalah satu-satunya orang yang mencoba untuk mengambil pertanyaan awal secara harfiah. Sejak itu, pertanyaannya bermutasi beberapa kali, jadi jawaban saya sekarang mungkin tampak sangat konyol & tidak pada tempatnya. Semoga ide umum cerita tetap menyenangkan bagi sebagian orang.


Saya penggemar analogi dan metafora ketika menjelaskan konsep-konsep yang sulit, jadi izinkan saya mencoba cerita saya.

Pada suatu ketika:

Ada seorang putri ...

function princess() {

Dia hidup di dunia yang indah penuh petualangan. Dia bertemu Pangeran Tampan, berkuda mengelilingi dunianya dengan naga unicorn, bertempur, bertemu binatang yang bisa bicara, dan banyak hal fantastik lainnya.

    var adventures = [];

    function princeCharming() { /* ... */ }

    var unicorn = { /* ... */ },
        dragons = [ /* ... */ ],
        squirrel = "Hello!";

    /* ... */

Tetapi dia harus selalu kembali ke dunia tugasnya yang membosankan dan orang dewasa.

    return {

Dan dia sering memberi tahu mereka tentang petualangan terbarunya yang menakjubkan sebagai seorang putri.

        story: function() {
            return adventures[adventures.length - 1];
        }
    };
}

Tapi yang akan mereka lihat hanyalah seorang gadis kecil ...

var littleGirl = princess();

... bercerita tentang sihir dan fantasi.

littleGirl.story();

Dan meskipun orang dewasa tahu tentang putri asli, mereka tidak akan pernah percaya pada unicorn atau naga karena mereka tidak pernah bisa melihat mereka. Orang dewasa mengatakan bahwa mereka hanya ada di dalam imajinasi gadis kecil itu.

Tetapi kita tahu kebenaran yang sebenarnya; bahwa gadis kecil dengan putri di dalam ...

... benar-benar seorang putri dengan seorang gadis kecil di dalamnya.

Jacob Swartwood
sumber
340
Saya suka penjelasan ini, sungguh. Bagi mereka yang membacanya dan tidak mengikuti, analoginya adalah ini: fungsi princess () adalah ruang lingkup kompleks yang berisi data pribadi. Di luar fungsi, data pribadi tidak dapat dilihat atau diakses. Sang putri menyimpan unicorn, naga, petualangan dll dalam imajinasinya (data pribadi) dan orang dewasa tidak dapat melihatnya sendiri. TETAPI imajinasi sang putri terekam dalam penutupan story()fungsi tersebut, yang merupakan satu-satunya antarmuka yang littleGirlterpapar contohnya ke dunia sihir.
Patrick M
Jadi di sini storyadalah penutupan tetapi jika kode sudah var story = function() {}; return story;maka littleGirlakan menjadi penutupan. Setidaknya itulah kesan yang saya dapatkan dari penggunaan metode 'pribadi' MDN dengan penutupan : "Tiga fungsi publik itu adalah penutupan yang memiliki lingkungan yang sama."
icc97
16
@ icc97, ya, storyadalah penutup yang merujuk pada lingkungan yang disediakan dalam lingkup princess. princessjuga merupakan penutupan tersirat lainnya , yaitu princessdan littleGirlakan berbagi referensi ke parentsarray yang akan ada kembali di lingkungan / lingkup di mana littleGirlada dan princessdidefinisikan.
Jacob Swartwood
6
@BenjaminKrupp Saya telah menambahkan komentar kode eksplisit untuk menunjukkan / menyiratkan bahwa ada lebih banyak operasi di dalam tubuh princessdaripada apa yang tertulis. Sayangnya cerita ini sekarang agak tidak pada tempatnya di utas ini. Awalnya pertanyaannya adalah untuk "menjelaskan penutupan JavaScript ke yang berusia 5 tahun"; respons saya adalah satu-satunya yang bahkan berusaha melakukan itu. Saya tidak ragu bahwa itu akan gagal total, tetapi setidaknya respons ini mungkin memiliki kesempatan untuk mempertahankan minat anak berusia 5 tahun.
Jacob Swartwood
11
Sebenarnya, bagi saya ini masuk akal. Dan harus saya akui, akhirnya memahami penutupan JS dengan menggunakan dongeng putri dan petualangan membuat saya merasa agak aneh.
Crystallize
753

Mengambil pertanyaan dengan serius, kita harus mencari tahu apa yang khas 6 tahun mampu secara kognitif, meskipun harus diakui, orang yang tertarik pada JavaScript tidak begitu khas.

Tentang Perkembangan Anak: 5 hingga 7 Tahun dikatakan:

Anak Anda akan dapat mengikuti arahan dua langkah. Misalnya, jika Anda berkata kepada anak Anda, "Pergi ke dapur dan ambilkan aku kantong sampah" mereka akan dapat mengingat arah itu.

Kita dapat menggunakan contoh ini untuk menjelaskan penutupan, sebagai berikut:

Dapur adalah penutup yang memiliki variabel lokal, disebut trashBags. Ada fungsi di dalam dapur yang disebut getTrashBagyang mendapatkan satu kantong sampah dan mengembalikannya.

Kami dapat mengkodekan ini dalam JavaScript seperti ini:

function makeKitchen() {
  var trashBags = ['A', 'B', 'C']; // only 3 at first

  return {
    getTrashBag: function() {
      return trashBags.pop();
    }
  };
}

var kitchen = makeKitchen();

console.log(kitchen.getTrashBag()); // returns trash bag C
console.log(kitchen.getTrashBag()); // returns trash bag B
console.log(kitchen.getTrashBag()); // returns trash bag A

Poin lebih lanjut yang menjelaskan mengapa penutupan menarik:

  • Setiap kali makeKitchen()dipanggil, penutupan baru dibuat dengan tersendiri trashBags.
  • The trashBagsvariabel lokal ke dalam setiap dapur dan tidak dapat diakses di luar, tapi fungsi batin pada getTrashBagproperti tidak memiliki akses ke sana.
  • Setiap panggilan fungsi membuat penutupan, tetapi tidak akan perlu untuk menjaga penutupan di sekitar kecuali fungsi bagian dalam, yang memiliki akses ke bagian dalam penutupan, dapat dipanggil dari luar penutupan. Mengembalikan objek dengan getTrashBagfungsi melakukan itu di sini.
dlaliberte
sumber
6
Sebenarnya, membingungkan, panggilan fungsi makeKitchen adalah penutupan yang sebenarnya, bukan objek dapur yang dikembalikan.
dlaliberte
6
Setelah melalui yang lain, saya menemukan jawaban ini sebagai cara termudah untuk menjelaskan tentang apa dan mengapa penutupan.
Chetabahana
3
Terlalu banyak menu dan hidangan pembuka, tidak cukup daging dan kentang. Anda dapat meningkatkan jawaban itu hanya dengan satu kalimat pendek seperti: "Penutupan adalah konteks yang disegel dari suatu fungsi, karena kurangnya mekanisme pelingkupan yang disediakan oleh kelas."
Staplerfahrer
584

The Straw Man

Saya perlu tahu berapa kali sebuah tombol diklik dan melakukan sesuatu pada setiap klik ketiga ...

Solusi yang Cukup Jelas

// Declare counter outside event handler's scope
var counter = 0;
var element = document.getElementById('button');

element.addEventListener("click", function() {
  // Increment outside counter
  counter++;

  if (counter === 3) {
    // Do something every third time
    console.log("Third time's the charm!");

    // Reset counter
    counter = 0;
  }
});
<button id="button">Click Me!</button>

Sekarang ini akan berhasil, tetapi ia merambah ke lingkup luar dengan menambahkan variabel, yang tujuan utamanya adalah untuk melacak jumlah. Dalam beberapa situasi, ini lebih disukai karena aplikasi luar Anda mungkin memerlukan akses ke informasi ini. Namun dalam kasus ini, kami hanya mengubah perilaku klik setiap ketiga, jadi lebih baik menyertakan fungsi ini di dalam pengendali event .

Pertimbangkan opsi ini

var element = document.getElementById('button');

element.addEventListener("click", (function() {
  // init the count to 0
  var count = 0;

  return function(e) { // <- This function becomes the click handler
    count++; //    and will retain access to the above `count`

    if (count === 3) {
      // Do something every third time
      console.log("Third time's the charm!");

      //Reset counter
      count = 0;
    }
  };
})());
<button id="button">Click Me!</button>

Perhatikan beberapa hal di sini.

Dalam contoh di atas, saya menggunakan perilaku penutupan JavaScript. Perilaku ini memungkinkan setiap fungsi memiliki akses ke ruang lingkup di mana ia dibuat, tanpa batas. Untuk menerapkannya secara praktis, saya segera menjalankan fungsi yang mengembalikan fungsi lain, dan karena fungsi yang saya kembalikan memiliki akses ke variabel jumlah internal (karena perilaku penutupan yang dijelaskan di atas) ini menghasilkan ruang lingkup pribadi untuk penggunaan oleh hasil fungsi ... Tidak begitu sederhana? Mari kita encerkan ...

Penutupan satu garis sederhana

//          _______________________Immediately invoked______________________
//         |                                                                |
//         |        Scope retained for use      ___Returned as the____      |
//         |       only by returned function   |    value of func     |     |
//         |             |            |        |                      |     |
//         v             v            v        v                      v     v
var func = (function() { var a = 'val'; return function() { alert(a); }; })();

Semua variabel di luar fungsi yang dikembalikan tersedia untuk fungsi yang dikembalikan, tetapi mereka tidak secara langsung tersedia untuk objek fungsi yang dikembalikan ...

func();  // Alerts "val"
func.a;  // Undefined

Mendapatkan? Jadi, dalam contoh utama kami, variabel jumlah terdapat di dalam penutupan dan selalu tersedia untuk pengendali acara, sehingga mempertahankan statusnya dari klik ke klik.

Juga, keadaan variabel pribadi ini sepenuhnya dapat diakses, baik untuk pembacaan dan penugasan ke variabel cakupan pribadi.

Ini dia; Anda sekarang sepenuhnya merangkum perilaku ini.

Posting Blog Lengkap (termasuk pertimbangan jQuery)

jondavidjohn
sumber
11
Saya tidak setuju dengan definisi Anda tentang apa penutupan itu. Tidak ada alasan itu harus memohon sendiri. Ini juga agak sederhana (dan tidak akurat) untuk mengatakan itu harus "dikembalikan" (banyak diskusi tentang ini di komentar atas jawaban atas pertanyaan ini)
James Montagne
40
@ James bahkan jika Anda setuju, contohnya (dan seluruh posting) adalah salah satu yang terbaik yang pernah saya lihat. Meskipun pertanyaannya tidak lama dan belum terpecahkan bagi saya, pertanyaan itu layak mendapatkan +1.
e-satis
84
"Saya perlu tahu berapa kali sebuah tombol diklik, dan melakukan sesuatu pada setiap klik ketiga ..." INI menarik perhatian saya. Kasus penggunaan dan solusi yang menunjukkan bagaimana penutupan bukanlah hal yang misterius dan banyak dari kita telah menulisnya tetapi tidak tahu persis nama resminya.
Chris22
Contoh yang bagus karena menunjukkan bahwa "menghitung" dalam contoh ke-2 mempertahankan nilai "menghitung" dan tidak mengatur ulang ke 0 setiap kali "elemen" diklik. Sangat informatif!
Adam
+1 untuk perilaku penutupan . Bisakah kita membatasi perilaku penutupan ke fungsi dalam javascript atau konsep ini juga dapat diterapkan pada struktur bahasa lainnya?
Dziamid
493

Penutupan sulit dijelaskan karena mereka digunakan untuk membuat beberapa perilaku berfungsi yang diharapkan semua orang secara intuitif untuk bekerja. Saya menemukan cara terbaik untuk menjelaskannya (dan cara saya mempelajari apa yang mereka lakukan) adalah membayangkan situasi tanpa mereka:

    var bind = function(x) {
        return function(y) { return x + y; };
    }
    
    var plus5 = bind(5);
    console.log(plus5(3));

Apa yang akan terjadi di sini jika JavaScript tidak mengetahui penutupan? Cukup ganti panggilan di baris terakhir dengan badan metodenya (yang pada dasarnya adalah fungsi yang dipanggil) dan Anda dapat:

console.log(x + 3);

Sekarang, di mana definisi x? Kami tidak mendefinisikannya dalam ruang lingkup saat ini. Satu-satunya solusi adalah dengan membiarkan plus5 membawa ruang lingkup (atau lebih tepatnya, ruang lingkup induknya) sekitar. Dengan cara ini, xdidefinisikan dengan baik dan terikat dengan nilai 5.

Konrad Rudolph
sumber
11
Ini persis seperti contoh yang menyesatkan banyak orang untuk berpikir bahwa itu adalah nilai - nilai yang digunakan dalam fungsi yang dikembalikan, bukan variabel yang dapat diubah itu sendiri. Jika diubah menjadi "return x + = y", atau lebih baik dari itu dan fungsi lainnya "x * = y", maka akan menjadi jelas bahwa tidak ada yang sedang disalin. Bagi orang yang terbiasa menumpuk frame, bayangkan menggunakan heap frames, yang dapat terus ada setelah fungsi kembali.
Matt
14
@ Mat saya tidak setuju. Contoh tidak seharusnya mendokumentasikan semua properti secara lengkap. Ini dimaksudkan untuk menjadi reduktif dan menggambarkan fitur yang menonjol dari sebuah konsep. OP meminta penjelasan sederhana (“untuk anak berusia enam tahun”). Ambil jawaban yang diterima: Itu sama sekali gagal memberikan penjelasan yang ringkas, justru karena itu berusaha menjadi lengkap. (Saya setuju dengan Anda bahwa ini adalah properti penting JavaScript yang mengikat adalah dengan referensi alih-alih berdasarkan nilai ... tapi sekali lagi, penjelasan yang berhasil adalah yang mengurangi seminimal mungkin.)
Konrad Rudolph
@KonradRudolph Saya suka gaya dan singkatnya contoh Anda. Saya hanya menyarankan untuk mengubahnya sedikit sehingga bagian terakhir, "Satu-satunya solusi adalah ...", menjadi benar. Saat ini sebenarnya ada solusi lain yang lebih sederhana untuk skenario Anda, yang tidak sesuai dengan kelanjutan javascript, dan memang sesuai dengan kesalahpahaman umum tentang apa kelanjutan itu. Jadi contoh dalam bentuk saat ini berbahaya. Ini tidak ada hubungannya dengan properti daftar lengkap, itu harus dilakukan dengan memahami apa x dalam fungsi yang dikembalikan, yang setelah semua titik utama.
Matt
@ Mat Hmm, saya tidak yakin saya sepenuhnya mengerti Anda, tetapi saya mulai melihat bahwa Anda mungkin memiliki poin yang valid. Karena komentarnya terlalu pendek, dapatkah Anda menjelaskan apa yang Anda maksud di intisari / pastie atau di ruang obrolan? Terima kasih.
Konrad Rudolph
2
@KonradRudolph Saya pikir saya tidak jelas tentang tujuan dari x + = y. Tujuannya hanya untuk menunjukkan bahwa panggilan berulang ke fungsi yang dikembalikan tetap menggunakan variabel x yang sama (sebagai lawan dari nilai yang sama , yang orang bayangkan "dimasukkan" ketika fungsi tersebut dibuat). Ini seperti dua peringatan pertama di biola Anda. Tujuan dari fungsi tambahan x * = y adalah untuk menunjukkan bahwa beberapa fungsi yang dikembalikan semuanya memiliki x yang sama.
Matt
379

TLDR

Penutupan adalah hubungan antara fungsi dan lingkungan leksikal luarnya (mis. Seperti yang ditulis), sehingga pengidentifikasi (variabel, parameter, deklarasi fungsi dll.) Yang didefinisikan dalam lingkungan tersebut dapat dilihat dari dalam fungsi, terlepas dari kapan atau dari di mana fungsi dipanggil.

Detail

Dalam terminologi spesifikasi ECMAScript, penutupan dapat dikatakan diimplementasikan oleh [[Environment]]referensi dari setiap fungsi-objek, yang menunjuk ke lingkungan leksikal di mana fungsi didefinisikan.

Ketika suatu fungsi dipanggil melalui [[Call]]metode internal , [[Environment]]referensi pada objek-fungsi disalin ke referensi lingkungan luar dari catatan lingkungan dari konteks eksekusi yang baru dibuat (stack frame).

Dalam contoh berikut, fungsi fmenutup lingkungan leksikal dari konteks eksekusi global:

function f() {}

Dalam contoh berikut, fungsi hmenutup lingkungan leksikal fungsi g, yang, pada gilirannya, menutup lingkungan leksikal konteks eksekusi global.

function g() {
    function h() {}
}

Jika fungsi dalam dikembalikan oleh luar, maka lingkungan leksikal luar akan bertahan setelah fungsi luar kembali. Ini karena lingkungan leksikal luar perlu tersedia jika fungsi dalam pada akhirnya dipanggil.

Dalam contoh berikut, fungsi jmenutup lingkungan leksikal fungsi i, yang berarti bahwa variabel xterlihat dari fungsi dalam j, lama setelah fungsi imenyelesaikan eksekusi:

function i() {
    var x = 'mochacchino'
    return function j() {
        console.log('Printing the value of x, from within function j: ', x)
    }
} 

const k = i()
setTimeout(k, 500) // invoke k (which is j) after 500ms

Sebagai penutup, variabel-variabel di lingkungan leksikal luar itu sendiri tersedia, bukan salinan.

function l() {
  var y = 'vanilla';

  return {
    setY: function(value) {
      y = value;
    },
    logY: function(value) {
      console.log('The value of y is: ', y);
    }
  }
}

const o = l()
o.logY() // The value of y is: vanilla
o.setY('chocolate')
o.logY() // The value of y is: chocolate

Rantai lingkungan leksikal, dihubungkan antara konteks eksekusi melalui referensi lingkungan luar, membentuk rantai lingkup dan mendefinisikan pengidentifikasi yang terlihat dari fungsi yang diberikan.

Harap perhatikan bahwa dalam upaya meningkatkan kejelasan dan akurasi, jawaban ini telah banyak berubah dari aslinya.

Ben
sumber
56
Wow, tidak pernah tahu Anda bisa menggunakan pergantian string console.logseperti itu. Jika orang lain tertarik, ada lebih banyak: developer.mozilla.org/en-US/docs/DOM/…
Flash
7
Variabel yang ada dalam daftar parameter fungsi juga merupakan bagian dari penutupan (mis. Tidak hanya terbatas pada var).
Thomas Eding
Penutupan terdengar lebih seperti objek dan kelas dll. Tidak yakin mengapa tidak banyak orang membandingkan keduanya - akan lebih mudah bagi kita pemula untuk belajar!
almaruf
376

OK, kipas penutup berusia 6 tahun. Apakah Anda ingin mendengar contoh penutupan paling sederhana?

Mari kita bayangkan situasi selanjutnya: seorang pengemudi duduk di dalam mobil. Mobil itu ada di dalam pesawat. Pesawat ada di bandara. Kemampuan pengemudi untuk mengakses barang-barang di luar mobilnya, tetapi di dalam pesawat, bahkan jika pesawat itu meninggalkan bandara, adalah penutup. Itu dia. Saat Anda berusia 27 tahun, lihat penjelasan yang lebih terperinci atau pada contoh di bawah ini.

Inilah cara saya dapat mengubah cerita pesawat saya menjadi kode.

var plane = function(defaultAirport) {

  var lastAirportLeft = defaultAirport;

  var car = {
    driver: {
      startAccessPlaneInfo: function() {
        setInterval(function() {
          console.log("Last airport was " + lastAirportLeft);
        }, 2000);
      }
    }
  };
  car.driver.startAccessPlaneInfo();

  return {
    leaveTheAirport: function(airPortName) {
      lastAirportLeft = airPortName;
    }
  }
}("Boryspil International Airport");

plane.leaveTheAirport("John F. Kennedy");

Max Tkachenko
sumber
26
Dimainkan dengan baik dan menjawab poster asli. Saya pikir ini adalah jawaban terbaik. Saya akan menggunakan barang bawaan dengan cara yang sama: bayangkan Anda pergi ke rumah nenek dan Anda mengemas case nintendo DS Anda dengan kartu permainan di dalam kasing Anda, tetapi kemudian bungkus kasing di dalam ransel Anda dan juga meletakkan kartu permainan di kantong ransel Anda, dan KEMUDIAN Anda memasukkan semuanya ke dalam koper besar dengan lebih banyak kartu permainan di saku koper. Ketika Anda sampai di rumah Nenek, Anda dapat memainkan game apa pun di DS Anda selama semua case luar terbuka. atau sesuatu untuk efek itu.
slartibartfast
366

Ini adalah upaya untuk menghilangkan beberapa (mungkin) kesalahpahaman tentang penutupan yang muncul di beberapa jawaban lain.

  • Penutupan tidak hanya dibuat ketika Anda mengembalikan fungsi batin. Bahkan, fungsi melampirkan tidak perlu kembali sama sekali agar penutupannya dibuat. Sebagai gantinya, Anda dapat menetapkan fungsi bagian dalam Anda ke variabel di lingkup luar, atau meneruskannya sebagai argumen ke fungsi lain di mana ia bisa dipanggil segera atau kapan saja nanti. Oleh karena itu, penutupan fungsi penutup mungkin dibuat segera setelah fungsi penutup dipanggil karena setiap fungsi bagian dalam memiliki akses ke penutupan itu setiap kali fungsi bagian dalam dipanggil, sebelum atau setelah fungsi penutup kembali.
  • Penutupan tidak mereferensikan salinan nilai -nilai variabel lama dalam cakupannya. Variabel itu sendiri adalah bagian dari penutupan, dan nilai yang terlihat saat mengakses salah satu variabel tersebut adalah nilai terbaru pada saat diakses. Inilah mengapa fungsi dalam yang dibuat di dalam loop bisa rumit, karena masing-masing memiliki akses ke variabel luar yang sama daripada mengambil salinan variabel pada saat fungsi dibuat atau dipanggil.
  • "Variabel" dalam penutupan menyertakan fungsi apa pun yang dinyatakan dalam fungsi. Mereka juga menyertakan argumen fungsi. Penutupan juga memiliki akses ke variabel penutupannya yang mengandung, sampai ke lingkup global.
  • Penutupan menggunakan memori, tetapi tidak menyebabkan kebocoran memori karena JavaScript dengan sendirinya membersihkan struktur sirkulernya sendiri yang tidak dirujuk. Kebocoran memori Internet Explorer yang melibatkan penutupan dibuat ketika gagal untuk memutuskan nilai-nilai atribut DOM yang merujuk penutupan, dengan demikian mempertahankan referensi ke struktur yang mungkin melingkar.
dlaliberte
sumber
15
James, saya mengatakan penutupan "mungkin" dibuat pada saat panggilan fungsi melampirkan karena masuk akal bahwa suatu implementasi dapat menunda pembuatan penutupan sampai beberapa waktu kemudian, ketika memutuskan penutupan benar-benar diperlukan. Jika tidak ada fungsi bagian dalam yang ditentukan dalam fungsi penutup, maka tidak diperlukan penutupan. Jadi mungkin itu bisa menunggu sampai fungsi dalam pertama dibuat untuk kemudian membuat penutupan dari konteks panggilan fungsi melampirkan.
dlaliberte
9
@ Beetroot-Beetroot Misalkan kita memiliki fungsi dalam yang diteruskan ke fungsi lain di mana ia digunakan sebelum fungsi luar kembali, dan misalkan kita juga mengembalikan fungsi dalam yang sama dari fungsi luar. Secara identik fungsi yang sama dalam kedua kasus, tetapi Anda mengatakan bahwa sebelum fungsi luar kembali, fungsi bagian dalam "terikat" ke tumpukan panggilan, sedangkan setelah itu kembali, fungsi bagian dalam tiba-tiba terikat pada penutupan. Itu berperilaku identik dalam kedua kasus; semantiknya identik, jadi bukankah Anda hanya berbicara tentang detail implementasi?
dlaliberte
7
@ Beetroot-Beetroot, terima kasih atas tanggapan Anda, dan saya senang saya membuat Anda berpikir. Saya masih tidak melihat perbedaan semantik antara konteks langsung fungsi luar dan konteks yang sama ketika itu menjadi penutupan ketika fungsi kembali (jika saya mengerti definisi Anda). Fungsi batin tidak peduli. Pengumpulan sampah tidak peduli karena fungsi dalam mempertahankan referensi ke konteks / penutupan baik cara, dan penelepon fungsi luar hanya menjatuhkan referensi ke konteks panggilan. Tapi itu membingungkan orang, dan mungkin lebih baik menyebutnya konteks panggilan.
dlaliberte
9
Artikel itu sulit dibaca, tetapi saya pikir itu sebenarnya mendukung apa yang saya katakan. Dikatakan: "Penutupan dibentuk dengan mengembalikan objek fungsi [...] atau dengan secara langsung memberikan referensi ke objek fungsi tersebut untuk, misalnya, variabel global." Saya tidak bermaksud bahwa GC tidak relevan. Sebaliknya, karena GC, dan karena fungsi dalam melekat pada konteks panggilan fungsi luar (atau [[lingkup]] seperti artikel mengatakan), maka tidak masalah apakah panggilan fungsi luar kembali karena itu mengikat dengan bagian dalam fungsi adalah hal yang penting.
dlaliberte
3
Jawaban bagus! Satu hal yang harus Anda tambahkan adalah bahwa semua fungsi menutup seluruh konten dari ruang lingkup eksekusi di mana mereka didefinisikan. Tidak masalah apakah mereka merujuk ke beberapa atau tidak ada variabel dari lingkup induk: referensi ke lingkungan leksikal dari lingkup induk disimpan sebagai [[Cakupan]] tanpa syarat. Ini dapat dilihat dari bagian penciptaan fungsi dalam spesifikasi ECMA.
Asad Saeeduddin
236

Saya menulis posting blog sementara kembali menjelaskan penutupan. Inilah yang saya katakan tentang penutupan dalam hal mengapa Anda menginginkannya.

Penutupan adalah cara untuk membiarkan suatu fungsi memiliki variabel pribadi yang persisten - yaitu, variabel yang hanya diketahui oleh satu fungsi, di mana ia dapat melacak info dari waktu sebelumnya yang dijalankan.

Dalam pengertian itu, mereka membiarkan fungsi bertindak sedikit seperti objek dengan atribut pribadi.

Pos lengkap:

Jadi apa penutupan ini?

Nathan Long
sumber
Jadi bisakah manfaat utama penutupan dapat ditekankan dengan contoh ini? Katakanlah saya memiliki fungsi emailError (sendToAddress, errorString) kemudian saya dapat mengatakan devError = emailError("[email protected]", errorString)dan kemudian memiliki versi kustom saya sendiri dari fungsi emailError bersama?
Devin G Rhode
Penjelasan ini dan contoh sempurna terkait dalam tautan ke (closure thingys) adalah cara terbaik untuk memahami penutupan dan harus tepat di atas!
HopeKing
215

Penutupan sederhana:

Contoh sederhana berikut mencakup semua poin utama penutupan JavaScript. *  

Berikut adalah pabrik yang menghasilkan kalkulator yang dapat menambah dan mengalikan:

function make_calculator() {
  var n = 0; // this calculator stores a single number n
  return {
    add: function(a) {
      n += a;
      return n;
    },
    multiply: function(a) {
      n *= a;
      return n;
    }
  };
}

first_calculator = make_calculator();
second_calculator = make_calculator();

first_calculator.add(3); // returns 3
second_calculator.add(400); // returns 400

first_calculator.multiply(11); // returns 33
second_calculator.multiply(10); // returns 4000

Poin utama: Setiap panggilan untuk make_calculatormembuat variabel lokal baru n, yang terus dapat digunakan oleh kalkulator itu adddan multiplyfungsinya lama setelah make_calculatorkembali.

Jika Anda terbiasa dengan bingkai tumpukan, kalkulator ini tampak aneh: Bagaimana mereka dapat tetap mengakses nsetelah make_calculatorpengembalian? Jawabannya adalah membayangkan bahwa JavaScript tidak menggunakan "stack frames", tetapi sebaliknya menggunakan "heap frames", yang dapat bertahan setelah pemanggilan fungsi yang membuatnya kembali.

Fungsi dalam suka adddan multiply, yang mengakses variabel yang dinyatakan dalam fungsi luar ** , disebut closure .

Cukup banyak untuk penutupan.



* Sebagai contoh, ini mencakup semua poin dalam artikel "Penutup untuk Dummies" yang diberikan dalam jawaban lain , kecuali contoh 6, yang hanya menunjukkan bahwa variabel dapat digunakan sebelum dideklarasikan, fakta yang bagus untuk diketahui tetapi sama sekali tidak terkait dengan penutupan. Ini juga mencakup semua poin dalam jawaban yang diterima , kecuali untuk poin (1) yang berfungsi menyalin argumen mereka ke dalam variabel lokal (argumen fungsi yang disebutkan), dan (2) bahwa menyalin angka membuat angka baru, tetapi menyalin referensi objek memberi Anda referensi lain ke objek yang sama. Ini juga baik untuk diketahui tetapi sekali lagi sama sekali tidak terkait dengan penutupan. Ini juga sangat mirip dengan contoh dalam jawaban ini tetapi sedikit lebih pendek dan kurang abstrak. Itu tidak mencakup titikjawaban ini atau komentar ini , yaitu JavaScript yang membuatnya sulit untuk menyambungkan arusnilai variabel loop ke fungsi dalam Anda: Langkah "memasukkan" hanya dapat dilakukan dengan fungsi pembantu yang membungkus fungsi batin Anda dan dipanggil pada setiap iterasi loop. (Sebenarnya, fungsi bagian dalam mengakses salinan fungsi helper dari variabel, alih-alih menyambungkan sesuatu.) Sekali lagi, sangat berguna saat membuat penutupan, tetapi bukan bagian dari apa penutupan itu atau bagaimana kerjanya. Ada kebingungan tambahan karena penutupan bekerja secara berbeda dalam bahasa fungsional seperti ML, di mana variabel terikat dengan nilai daripada ruang penyimpanan, memberikan aliran konstan orang-orang yang memahami penutupan dengan cara (yaitu cara "menghubungkan") yaitu hanya salah untuk JavaScript, di mana variabel selalu terikat ke ruang penyimpanan, dan tidak pernah ke nilai.

** Setiap fungsi luar, jika beberapa bersarang, atau bahkan dalam konteks global, karena jawaban ini menunjukkan dengan jelas.

Matt
sumber
Apa yang akan terjadi jika Anda memanggil: second_calculator = first_calculator (); bukannya second_calculator = make_calculator (); ? Harus sama, kan?
Ronen Festinger
4
@Ronen: Karena first_calculatormerupakan objek (bukan fungsi) Anda tidak boleh menggunakan tanda kurung second_calculator = first_calculator;, karena ini adalah tugas, bukan panggilan fungsi. Untuk menjawab pertanyaan Anda, maka hanya akan ada satu panggilan ke make_calculator, jadi hanya satu kalkulator yang dibuat, dan variabel first_calculator dan second_calculator keduanya akan merujuk ke kalkulator yang sama, sehingga jawabannya adalah 3, 403, 4433, 44330.
Matt
204

Bagaimana saya menjelaskannya kepada anak berusia enam tahun:

Anda tahu bagaimana orang dewasa dapat memiliki rumah, dan mereka menyebutnya rumah? Ketika seorang ibu memiliki anak, anak itu tidak benar-benar memiliki apa pun, bukan? Tetapi orang tuanya memiliki rumah, jadi setiap kali seseorang bertanya kepada anak "Di mana rumahmu?", Dia dapat menjawab "rumah itu!", Dan menunjuk ke rumah orang tuanya. "Penutupan" adalah kemampuan anak untuk selalu (bahkan jika di luar negeri) dapat mengatakan bahwa ia memiliki rumah, meskipun sebenarnya orang tua yang memiliki rumah itu.

Magne
sumber
200

Bisakah Anda menjelaskan penutupan kepada anak berusia 5 tahun? *

Saya masih berpikir penjelasan Google bekerja dengan sangat baik dan singkat:

/*
*    When a function is defined in another function and it
*    has access to the outer function's context even after
*    the outer function returns.
*
* An important concept to learn in JavaScript.
*/

function outerFunction(someNum) {
    var someString = 'Hey!';
    var content = document.getElementById('content');
    function innerFunction() {
        content.innerHTML = someNum + ': ' + someString;
        content = null; // Internet Explorer memory leak for DOM reference
    }
    innerFunction();
}

outerFunction(1);​

Bukti bahwa contoh ini menciptakan penutupan bahkan jika fungsi dalam tidak kembali

* Pertanyaan # AC

Chris S
sumber
11
Kode ini "benar", sebagai contoh penutupan, meskipun tidak membahas bagian dari komentar tentang menggunakan penutupan setelah fungsi luar kembali. Jadi itu bukan contoh yang bagus. Ada banyak cara lain penutupan dapat digunakan yang tidak melibatkan mengembalikan fungsi batin. misalnya innerFunction dapat dialihkan ke fungsi lain di mana ia dipanggil segera atau disimpan dan dipanggil beberapa waktu kemudian, dan dalam semua kasus, ia memiliki akses ke konteks fungsi luar yang dibuat ketika dipanggil.
dlaliberte
6
@syockit Tidak, Moss salah. Penutupan dibuat terlepas dari apakah fungsi pernah keluar dari ruang lingkup di mana ia didefinisikan, dan referensi yang dibuat tanpa syarat untuk lingkungan leksikal induk membuat semua variabel dalam lingkup orangtua tersedia untuk semua fungsi, terlepas dari apakah mereka dipanggil di luar atau di dalam ruang lingkup di mana mereka diciptakan.
Asad Saeeduddin
176

Saya cenderung belajar lebih baik dengan perbandingan BAIK / BURUK. Saya suka melihat kode yang bekerja diikuti oleh kode yang tidak bekerja yang mungkin ditemui seseorang. Saya mengumpulkan jsFiddle yang melakukan perbandingan dan mencoba meredakan perbedaan menjadi penjelasan paling sederhana yang dapat saya berikan .

Penutupan dilakukan dengan benar:

console.log('CLOSURES DONE RIGHT');

var arr = [];

function createClosure(n) {
    return function () {
        return 'n = ' + n;
    }
}

for (var index = 0; index < 10; index++) {
    arr[index] = createClosure(index);
}

for (var index in arr) {
    console.log(arr[index]());
}
  • Dalam kode di atas createClosure(n)dipanggil dalam setiap iterasi dari loop. Perhatikan bahwa saya menamai variabel nuntuk menyorot bahwa itu adalah variabel baru yang dibuat dalam lingkup fungsi baru dan bukan variabel yang sama seperti indexyang terikat pada lingkup luar.

  • Ini menciptakan ruang lingkup baru dan nterikat pada ruang lingkup itu; ini berarti kami memiliki 10 cakupan terpisah, satu untuk setiap iterasi.

  • createClosure(n) mengembalikan fungsi yang mengembalikan n dalam lingkup itu.

  • Dalam setiap ruang lingkup nterikat dengan nilai apa pun yang dimilikinya saat createClosure(n)dipanggil sehingga fungsi bersarang yang dikembalikan akan selalu mengembalikan nilai nyang dimilikinya saat createClosure(n)dipanggil.

Penutupan yang dilakukan salah:

console.log('CLOSURES DONE WRONG');

function createClosureArray() {
    var badArr = [];

    for (var index = 0; index < 10; index++) {
        badArr[index] = function () {
            return 'n = ' + index;
        };
    }
    return badArr;
}

var badArr = createClosureArray();

for (var index in badArr) {
    console.log(badArr[index]());
}
  • Dalam kode di atas, loop telah dipindahkan dalam createClosureArray()fungsi dan fungsi sekarang hanya mengembalikan array yang sudah selesai, yang sekilas tampak lebih intuitif.

  • Apa yang mungkin tidak jelas adalah bahwa karena createClosureArray()hanya dipanggil sekali hanya satu ruang lingkup yang dibuat untuk fungsi ini, bukan satu untuk setiap iterasi dari loop.

  • Dalam fungsi ini variabel bernama indexdidefinisikan. Loop berjalan dan menambahkan fungsi ke array yang kembali index. Catatan yang indexdidefinisikan dalam createClosureArrayfungsi yang hanya dipanggil sekali saja.

  • Karena hanya ada satu ruang lingkup dalam createClosureArray()fungsi, indexhanya terikat pada nilai dalam ruang lingkup itu. Dengan kata lain, setiap kali loop mengubah nilai index, itu mengubahnya untuk semua yang mereferensikannya dalam lingkup itu.

  • Semua fungsi yang ditambahkan ke array mengembalikan indexvariabel SAMA dari lingkup induk di mana ia didefinisikan bukan 10 yang berbeda dari 10 cakupan yang berbeda seperti contoh pertama. Hasil akhirnya adalah bahwa semua 10 fungsi mengembalikan variabel yang sama dari cakupan yang sama.

  • Setelah loop selesai dan indexdilakukan modifikasi nilai akhir adalah 10, oleh karena itu setiap fungsi yang ditambahkan ke array mengembalikan nilai indexvariabel tunggal yang sekarang diatur ke 10.

Hasil

PENUTUPAN DILAKUKAN KANAN
n = 0
n = 1
n = 2
n = 3
n = 4
n = 5
n = 6
n = 7
n = 8
n = 9

PENUTUPAN SESUAI SALAH
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10 n = 10

Chev
sumber
1
Tambahan yang bagus, terima kasih. Hanya untuk membuatnya lebih jelas orang dapat membayangkan bagaimana array "buruk" dibuat dalam loop "buruk" dengan setiap iterasi: iterasi 1: [function () {return 'n =' + 0;}] iterasi ke-2: [( function () {return 'n =' + 1;}), (function () {return 'n =' + 1;})] iterasi ketiga: [(function () {return 'n =' + 2;}) , (function () {return 'n =' + 2;}), (function () {return 'n =' + 2;})] dll. Jadi, setiap kali ketika nilai indeks berubah, ia tercermin di semua fungsi sudah ditambahkan ke array.
Alex Alexeev
3
Menggunakan letuntuk varmemperbaiki perbedaan.
Rupam Datta
Bukankah di sini "Penutupan dilakukan dengan benar" adalah contoh dari "penutupan di dalam penutupan"?
TechnicalSmile
Maksud saya, setiap fungsi secara teknis merupakan penutup tetapi bagian yang penting adalah fungsi tersebut mendefinisikan variabel baru di dalamnya. Fungsi yang mendapatkan pengembalian hanya referensi yang ndibuat di penutupan baru. Kami hanya mengembalikan suatu fungsi sehingga kami dapat menyimpannya di dalam array dan menjalankannya nanti.
Chev
Jika Anda ingin hanya menyimpan hasilnya dalam array pada iterasi pertama maka Anda bisa inline seperti ini: arr[index] = (function (n) { return 'n = ' + n; })(index);. Tapi kemudian Anda menyimpan string yang dihasilkan dalam array daripada fungsi untuk memanggil yang mengalahkan titik contoh saya.
Chev
164

Wikipedia pada penutupan :

Dalam ilmu komputer, penutupan adalah fungsi bersama-sama dengan lingkungan referensi untuk nama-nama nonlokal (variabel bebas) dari fungsi itu.

Secara teknis, dalam JavaScript , setiap fungsi adalah penutupan . Itu selalu memiliki akses ke variabel yang didefinisikan dalam lingkup sekitarnya.

Karena konstruksi yang mendefinisikan ruang lingkup dalam JavaScript adalah fungsi , bukan blok kode seperti dalam banyak bahasa lain, apa yang biasanya kita maksud dengan penutupan dalam JavaScript adalah fungsi yang bekerja dengan variabel non-lokal yang didefinisikan dalam fungsi sekitarnya yang sudah dijalankan .

Penutupan sering digunakan untuk membuat fungsi dengan beberapa data pribadi yang tersembunyi (tetapi tidak selalu demikian).

var db = (function() {
    // Create a hidden object, which will hold the data
    // it's inaccessible from the outside.
    var data = {};

    // Make a function, which will provide some access to the data.
    return function(key, val) {
        if (val === undefined) { return data[key] } // Get
        else { return data[key] = val } // Set
    }
    // We are calling the anonymous surrounding function,
    // returning the above inner function, which is a closure.
})();

db('x')    // -> undefined
db('x', 1) // Set x to 1
db('x')    // -> 1
// It's impossible to access the data object itself.
// We are able to get or set individual it.

ems

Contoh di atas menggunakan fungsi anonim, yang dieksekusi sekali. Tetapi tidak harus demikian. Itu bisa dinamai (misalnya mkdb) dan dieksekusi nanti, menghasilkan fungsi basis data setiap kali dipanggil. Setiap fungsi yang dihasilkan akan memiliki objek basis data tersembunyi sendiri. Contoh penggunaan lain dari penutupan adalah ketika kita tidak mengembalikan fungsi, tetapi objek yang berisi beberapa fungsi untuk tujuan yang berbeda, masing-masing fungsi tersebut memiliki akses ke data yang sama.

mykhal
sumber
2
Ini adalah penjelasan terbaik untuk penutupan JavaScript. Seharusnya jawaban yang dipilih. Sisanya cukup menghibur tetapi yang ini sebenarnya berguna secara praktis untuk pembuat kode JavaScript dunia nyata.
geoidesic
136

Saya menyusun tutorial JavaScript interaktif untuk menjelaskan cara kerja penutupan. Apa itu Penutupan?

Inilah salah satu contohnya:

var create = function (x) {
    var f = function () {
        return x; // We can refer to x here!
    };
    return f;
};
// 'create' takes one argument, creates a function

var g = create(42);
// g is a function that takes no arguments now

var y = g();
// y is 42 here
Nathan Whitehead
sumber
128

Anak-anak akan selalu mengingat rahasia yang telah mereka bagi dengan orang tua mereka, bahkan setelah orang tua mereka pergi. Inilah fungsi penutupan untuk fungsi.

Rahasia untuk fungsi JavaScript adalah variabel pribadi

var parent = function() {
 var name = "Mary"; // secret
}

Setiap kali Anda menyebutnya, "nama" variabel lokal dibuat dan diberi nama "Mary". Dan setiap kali fungsi keluar, variabelnya hilang dan namanya dilupakan.

Seperti yang Anda tebak, karena variabel dibuat kembali setiap kali fungsi dipanggil, dan tidak ada orang lain yang akan mengetahuinya, pasti ada tempat rahasia tempat mereka disimpan. Itu bisa disebut Kamar Rahasia atau tumpukan atau ruang lingkup lokal tetapi tidak terlalu penting. Kita tahu mereka ada di sana, di suatu tempat, tersembunyi di dalam ingatan.

Tetapi, dalam JavaScript ada hal yang sangat istimewa ini, fungsi-fungsi yang dibuat di dalam fungsi-fungsi lain, juga dapat mengetahui variabel lokal orang tua mereka dan menjaganya selama mereka masih hidup.

var parent = function() {
  var name = "Mary";
  var child = function(childName) {
    // I can also see that "name" is "Mary"
  }
}

Jadi, selama kita berada di fungsi orangtua, ia dapat membuat satu atau lebih fungsi anak yang berbagi variabel rahasia dari tempat rahasia.

Tetapi yang menyedihkan adalah, jika anak itu juga merupakan variabel pribadi dari fungsi orang tuanya, ia juga akan mati ketika orang tua berakhir, dan rahasia akan mati bersama mereka.

Jadi untuk hidup, anak harus pergi sebelum terlambat

var parent = function() {
  var name = "Mary";
  var child = function(childName) {
    return "My name is " + childName  +", child of " + name; 
  }
  return child; // child leaves the parent ->
}
var child = parent(); // < - and here it is outside 

Dan sekarang, meskipun Mary "tidak lagi berlari", ingatannya tidak hilang dan anaknya akan selalu mengingat namanya dan rahasia lain yang mereka bagi selama mereka bersama.

Jadi, jika Anda memanggil anak "Alice", dia akan merespons

child("Alice") => "My name is Alice, child of Mary"

Hanya itu yang bisa diceritakan.

Tero Tolonen
sumber
15
Ini adalah penjelasan yang paling masuk akal bagi saya karena tidak mengasumsikan pengetahuan sebelumnya yang signifikan tentang istilah teknis. Penjelasan terpilih di sini mengasumsikan orang yang tidak memahami penutupan memiliki pemahaman penuh dan lengkap tentang istilah-istilah seperti 'ruang lingkup leksikal' dan 'konteks pelaksanaan' - sementara saya dapat memahami ini secara konseptual, saya tidak berpikir saya sebagai nyaman dengan rincian mereka sebagaimana mestinya, dan penjelasan tanpa jargon di dalamnya sama sekali adalah apa yang membuat penutupan akhirnya klik untuk saya, terima kasih. Sebagai bonus, saya pikir itu juga menjelaskan ruang lingkup apa yang sangat ringkas.
Emma W
103

Saya tidak mengerti mengapa jawabannya sangat rumit di sini.

Berikut ini adalah penutupan:

var a = 42;

function b() { return a; }

Iya. Anda mungkin menggunakannya beberapa kali sehari.


Tidak ada alasan untuk percaya bahwa penutupan adalah peretasan desain yang rumit untuk mengatasi masalah tertentu. Tidak, penutupan hanya tentang menggunakan variabel yang berasal dari cakupan yang lebih tinggi dari perspektif di mana fungsi itu dinyatakan (tidak dijalankan) .

Sekarang apa yang memungkinkan Anda lakukan bisa menjadi lebih spektakuler, lihat jawaban lain.

floribon
sumber
5
Jawaban ini sepertinya tidak membantu orang yang bingung. Setara kasar dalam bahasa pemrograman tradisional mungkin untuk membuat b () sebagai metode pada objek yang juga memiliki konstanta pribadi atau properti a. Menurut saya, yang mengejutkan adalah bahwa objek lingkup JS secara efektif menyediakan aproperti daripada konstanta. Dan Anda hanya akan melihat perilaku penting itu jika Anda memodifikasinya, seperti dalamreturn a++;
Jon Coombs
1
Persis seperti yang dikatakan Jon. Sebelum saya akhirnya menutup rapat, saya kesulitan menemukan contoh-contoh praktis. Ya, floribon membuat penutupan, tetapi bagi saya yang tidak berpendidikan, ini sama sekali tidak mengajarkan saya apa-apa.
Chev
3
Ini tidak mendefinisikan apa penutupan itu - itu hanya contoh yang menggunakan penutupan. Dan itu tidak membahas nuansa apa yang terjadi ketika ruang lingkup berakhir; Saya tidak berpikir ada yang punya pertanyaan tentang pelingkupan leksikal ketika semua lingkup masih ada, dan terutama dalam kasus variabel global.
Gerard ONeill
92

Contoh untuk titik pertama oleh dlaliberte:

Penutupan tidak hanya dibuat ketika Anda mengembalikan fungsi batin. Bahkan, fungsi melampirkan tidak perlu kembali sama sekali. Sebagai gantinya, Anda dapat menetapkan fungsi bagian dalam ke variabel di lingkup luar, atau meneruskannya sebagai argumen ke fungsi lain tempat variabel itu dapat segera digunakan. Oleh karena itu, penutupan fungsi penutup mungkin sudah ada pada saat fungsi penutup dipanggil karena fungsi dalam apa pun memiliki akses ke sana segera setelah dipanggil.

var i;
function foo(x) {
    var tmp = 3;
    i = function (y) {
        console.log(x + y + (++tmp));
    }
}
foo(2);
i(3);
someisaac
sumber
Klarifikasi kecil tentang kemungkinan ambiguitas. Ketika saya berkata, "Sebenarnya, fungsi penutup tidak perlu kembali sama sekali." Saya tidak bermaksud "tidak mengembalikan nilai" tetapi "masih aktif". Jadi contoh tidak menunjukkan aspek itu, meskipun itu menunjukkan cara lain fungsi bagian dalam dapat diteruskan ke lingkup luar. Poin utama yang saya coba buat adalah tentang waktu pembuatan penutupan (untuk fungsi penutup), karena beberapa orang tampaknya berpikir itu terjadi ketika fungsi penutup kembali. Diperlukan contoh berbeda untuk menunjukkan bahwa penutupan dibuat ketika suatu fungsi dipanggil .
dlaliberte
89

Penutupan adalah tempat fungsi dalam memiliki akses ke variabel di fungsi luarnya. Itu mungkin penjelasan satu baris paling sederhana yang bisa Anda dapatkan untuk penutupan.

Rakesh Pai
sumber
35
Itu hanya setengah penjelasan. Yang penting untuk dicatat tentang penutupan adalah bahwa jika fungsi dalam masih dirujuk setelah fungsi luar telah keluar, nilai-nilai lama dari fungsi luar masih tersedia untuk fungsi dalam.
pcorcoran
22
Sebenarnya, tidak lama nilai dari fungsi luar yang tersedia untuk fungsi batin, tapi lama variabel , yang mungkin memiliki nilai-nilai baru jika beberapa fungsi mampu mengubahnya.
dlaliberte
86

Saya tahu sudah ada banyak solusi, tetapi saya kira skrip kecil dan sederhana ini dapat berguna untuk menunjukkan konsep:

// makeSequencer will return a "sequencer" function
var makeSequencer = function() {
    var _count = 0; // not accessible outside this function
    var sequencer = function () {
        return _count++;
    }
    return sequencer;
}

var fnext = makeSequencer();
var v0 = fnext();     // v0 = 0;
var v1 = fnext();     // v1 = 1;
var vz = fnext._count // vz = undefined
Gerardo Lima
sumber
82

Anda sedang tidur dan Anda mengundang Dan. Anda memberi tahu Dan untuk membawa satu XBox controller.

Dan mengundang Paul. Dan meminta Paul untuk membawa satu controller. Berapa banyak pengendali yang dibawa ke pesta?

function sleepOver(howManyControllersToBring) {

    var numberOfDansControllers = howManyControllersToBring;

    return function danInvitedPaul(numberOfPaulsControllers) {
        var totalControllers = numberOfDansControllers + numberOfPaulsControllers;
        return totalControllers;
    }
}

var howManyControllersToBring = 1;

var inviteDan = sleepOver(howManyControllersToBring);

// The only reason Paul was invited is because Dan was invited. 
// So we set Paul's invitation = Dan's invitation.

var danInvitedPaul = inviteDan(howManyControllersToBring);

alert("There were " + danInvitedPaul + " controllers brought to the party.");
StewShack
sumber
80

Penulis Closures telah menjelaskan penutupan dengan cukup baik, menjelaskan alasan mengapa kami membutuhkannya dan juga menjelaskan LexicalEnvironment yang diperlukan untuk memahami penutupan.
Berikut ringkasannya:

Bagaimana jika suatu variabel diakses, tetapi itu bukan lokal? Seperti di sini:

Masukkan deskripsi gambar di sini

Dalam hal ini, interpreter menemukan variabel di LexicalEnvironmentobjek luar .

Proses ini terdiri dari dua langkah:

  1. Pertama, ketika fungsi f dibuat, itu tidak dibuat di ruang kosong. Ada objek LexicalEnvironment saat ini. Dalam kasus di atas, itu jendela (a tidak ditentukan pada saat pembuatan fungsi).

Masukkan deskripsi gambar di sini

Ketika suatu fungsi dibuat, ia akan mendapatkan properti tersembunyi, bernama [[Cakupan]], yang mereferensikan LexicalEnvironment saat ini.

Masukkan deskripsi gambar di sini

Jika variabel dibaca, tetapi tidak dapat ditemukan di mana pun, kesalahan dihasilkan.

Fungsi bersarang

Fungsi dapat bersarang di dalam satu sama lain, membentuk rantai Lingkungan Lexical yang juga dapat disebut rantai lingkup.

Masukkan deskripsi gambar di sini

Jadi, fungsi g memiliki akses ke g, a dan f.

Penutupan

Fungsi bersarang dapat terus hidup setelah fungsi luar selesai:

Masukkan deskripsi gambar di sini

Menandai Lingkungan Lexical:

Masukkan deskripsi gambar di sini

Seperti yang kita lihat, this.sayadalah properti di objek pengguna, jadi itu terus hidup setelah Pengguna selesai.

Dan jika Anda ingat, ketika this.saydibuat, itu (karena setiap fungsi) mendapatkan referensi internal this.say.[[Scope]]ke LexicalEnvironment saat ini. Jadi, Lingkungan Lexical dari eksekusi Pengguna saat ini tetap dalam memori. Semua variabel Pengguna juga adalah propertinya, sehingga mereka juga dijaga dengan hati-hati, tidak dibuang seperti biasanya.

Intinya adalah untuk memastikan bahwa jika fungsi dalam ingin mengakses variabel luar di masa depan, ia dapat melakukannya.

Untuk meringkas:

  1. Fungsi bagian dalam menyimpan referensi ke Lingkungan Luar Lexical.
  2. Fungsi dalam dapat mengakses variabel dari itu kapan saja bahkan jika fungsi luar selesai.
  3. Peramban menyimpan LexicalEnvironment dan semua propertinya (variabel) dalam memori sampai ada fungsi internal yang merujuknya.

Ini disebut penutupan.

Arvand
sumber
78

Fungsi JavaScript dapat mengakses:

  1. Argumen
  2. Lokal (yaitu, variabel lokal dan fungsi lokal mereka)
  3. Lingkungan, yang meliputi:
    • global, termasuk DOM
    • apa pun di fungsi luar

Jika suatu fungsi mengakses lingkungannya, maka fungsinya adalah penutupan.

Perhatikan bahwa fungsi luar tidak diperlukan, meskipun mereka menawarkan manfaat yang tidak saya bahas di sini. Dengan mengakses data di lingkungannya, penutupan membuat data tetap hidup. Dalam subkap fungsi luar / dalam, fungsi luar dapat membuat data lokal dan akhirnya keluar, namun, jika ada fungsi dalam yang bertahan setelah fungsi luar keluar, maka fungsi dalam menyimpan data lokal fungsi luar hidup.

Contoh penutupan yang menggunakan lingkungan global:

Bayangkan bahwa peristiwa tombol Stack Overflow Vote-Up dan Vote-Down diimplementasikan sebagai penutupan, voteUp_click dan voteDown_click, yang memiliki akses ke variabel eksternal adalah VotedUp dan isVotedDown, yang didefinisikan secara global. (Demi kesederhanaan, saya mengacu pada tombol Vote Pertanyaan StackOverflow, bukan array tombol Jawab Vote.)

Ketika pengguna mengklik tombol VoteUp, fungsi voteUp_click memeriksa apakah isVotedDown == benar untuk menentukan apakah akan memilih atau hanya membatalkan suara turun. Function voteUp_click adalah penutup karena mengakses lingkungannya.

var isVotedUp = false;
var isVotedDown = false;

function voteUp_click() {
  if (isVotedUp)
    return;
  else if (isVotedDown)
    SetDownVote(false);
  else
    SetUpVote(true);
}

function voteDown_click() {
  if (isVotedDown)
    return;
  else if (isVotedUp)
    SetUpVote(false);
  else
    SetDownVote(true);
}

function SetUpVote(status) {
  isVotedUp = status;
  // Do some CSS stuff to Vote-Up button
}

function SetDownVote(status) {
  isVotedDown = status;
  // Do some CSS stuff to Vote-Down button
}

Keempat fungsi ini adalah penutupan karena mereka semua mengakses lingkungan mereka.

John Pick
sumber
59

Sebagai ayah dari seorang anak berusia 6 tahun, yang saat ini mengajar anak-anak kecil (dan seorang pemula yang relatif berkode tanpa pendidikan formal sehingga koreksi akan diperlukan), saya pikir pelajaran akan tetap terbaik melalui permainan langsung. Jika anak 6 tahun siap untuk memahami apa penutupan itu, maka mereka cukup tua untuk pergi sendiri. Saya sarankan menempelkan kode ke jsfiddle.net, menjelaskan sedikit, dan meninggalkan mereka sendiri untuk membuat lagu yang unik. Teks penjelasan di bawah ini mungkin lebih cocok untuk anak berusia 10 tahun.

function sing(person) {

    var firstPart = "There was " + person + " who swallowed ";

    var fly = function() {
        var creature = "a fly";
        var result = "Perhaps she'll die";
        alert(firstPart + creature + "\n" + result);
    };

    var spider = function() {
        var creature = "a spider";
        var result = "that wiggled and jiggled and tickled inside her";
        alert(firstPart + creature + "\n" + result);
    };

    var bird = function() {
        var creature = "a bird";
        var result = "How absurd!";
        alert(firstPart + creature + "\n" + result);
    };

    var cat = function() {
        var creature = "a cat";
        var result = "Imagine That!";
        alert(firstPart + creature + "\n" + result);
    };

    fly();
    spider();
    bird();
    cat();
}

var person="an old lady";

sing(person);

INSTRUKSI

DATA: Data adalah kumpulan fakta. Ini bisa berupa angka, kata, ukuran, pengamatan atau bahkan hanya deskripsi benda. Anda tidak bisa menyentuhnya, menciumnya, atau merasakannya. Anda dapat menuliskannya, berbicara dan mendengarnya. Anda bisa menggunakannya untuk membuat aroma dan rasa sentuh menggunakan komputer. Ini dapat dimanfaatkan oleh komputer menggunakan kode.

KODE: Semua tulisan di atas disebut kode . Itu ditulis dalam JavaScript.

JAVASCRIPT: JavaScript adalah bahasa. Seperti Bahasa Inggris atau Bahasa Prancis atau Bahasa Mandarin adalah bahasa. Ada banyak bahasa yang dimengerti oleh komputer dan prosesor elektronik lainnya. Agar JavaScript dipahami oleh komputer, ia membutuhkan juru bahasa. Bayangkan jika seorang guru yang hanya berbicara bahasa Rusia datang untuk mengajar kelas Anda di sekolah. Ketika guru mengatakan "все садятся", kelas tidak akan mengerti. Tapi untungnya Anda memiliki murid Rusia di kelas Anda yang memberi tahu semua orang bahwa ini berarti "semua orang duduk" - jadi Anda semua melakukannya. Kelasnya seperti komputer dan murid Rusia adalah penerjemahnya. Untuk JavaScript, juru bahasa yang paling umum disebut browser.

BROWSER: Ketika Anda terhubung ke Internet di komputer, tablet atau telepon untuk mengunjungi situs web, Anda menggunakan browser. Contoh yang mungkin Anda ketahui adalah Internet Explorer, Chrome, Firefox, dan Safari. Browser dapat memahami JavaScript dan memberi tahu komputer apa yang perlu dilakukan. Instruksi JavaScript disebut fungsi.

FUNGSI: Fungsi dalam JavaScript seperti pabrik. Mungkin pabrik kecil dengan hanya satu mesin di dalamnya. Atau mungkin mengandung banyak pabrik kecil lainnya, masing-masing dengan banyak mesin melakukan pekerjaan yang berbeda. Di pabrik pakaian kehidupan nyata Anda mungkin memiliki rim kain dan kumparan benang masuk dan T-shirt dan celana jeans keluar. Pabrik JavaScript kami hanya memproses data, tidak dapat menjahit, mengebor lubang, atau melelehkan logam. Di pabrik JavaScript kami data masuk dan data keluar.

Semua data ini kedengarannya agak membosankan, tetapi sangat keren; kita mungkin memiliki fungsi yang memberi tahu robot apa yang harus dibuat untuk makan malam. Katakanlah saya mengundang Anda dan teman Anda ke rumah saya. Anda suka kaki ayam terbaik, saya suka sosis, teman Anda selalu menginginkan apa yang Anda inginkan dan teman saya tidak makan daging.

Saya tidak punya waktu untuk berbelanja, jadi fungsi perlu tahu apa yang kita miliki di lemari es untuk membuat keputusan. Setiap bahan memiliki waktu memasak yang berbeda dan kami ingin semuanya disajikan panas oleh robot secara bersamaan. Kita perlu menyediakan fungsi dengan data tentang apa yang kita sukai, fungsinya bisa 'berbicara' ke lemari es, dan fungsi itu bisa mengendalikan robot.

Suatu fungsi biasanya memiliki nama, tanda kurung dan kurung kurawal. Seperti ini:

function cookMeal() {  /*  STUFF INSIDE THE FUNCTION  */  }

Perhatikan itu /*...*/dan //hentikan kode yang sedang dibaca oleh browser.

NAME: Anda dapat memanggil fungsi apa saja kata yang Anda inginkan. Contoh "cookMeal" adalah tipikal dalam menggabungkan dua kata bersama dan memberikan yang kedua huruf kapital di awal - tetapi ini tidak perlu. Itu tidak dapat memiliki ruang di dalamnya, dan itu tidak bisa menjadi nomor sendiri.

PARENTES: "Kurung" atau ()kotak surat di pintu fungsi pabrik JavaScript atau kotak pos di jalan untuk mengirim paket informasi ke pabrik. Terkadang kotak pos mungkin ditandai misalnya cookMeal(you, me, yourFriend, myFriend, fridge, dinnerTime) , dalam hal ini Anda tahu data apa yang harus Anda berikan.

BRACES: "Kawat gigi" yang terlihat seperti ini {}adalah jendela berwarna dari pabrik kami. Dari dalam pabrik Anda bisa melihat keluar, tetapi dari luar Anda tidak bisa melihatnya.

CONTOH KODE PANJANG DI ATAS

Kode kita dimulai dengan fungsi kata , jadi kita tahu itu salah! Kemudian nama fungsi tersebut dinyanyikan - itulah deskripsi saya sendiri tentang fungsi tersebut. Kemudian tanda kurung () . Tanda kurung selalu ada untuk suatu fungsi. Kadang-kadang mereka kosong, dan kadang-kadang mereka memiliki sesuatu dalam satu ini memiliki sebuah kata dalam.: (person). Setelah ini ada penjepit seperti ini {. Ini menandai awal dari fungsi sing () . Ini memiliki pasangan yang menandai akhir dari bernyanyi () seperti ini}

function sing(person) {  /* STUFF INSIDE THE FUNCTION */  }

Jadi fungsi ini mungkin ada hubungannya dengan bernyanyi, dan mungkin perlu beberapa data tentang seseorang. Di dalamnya ada instruksi untuk melakukan sesuatu dengan data itu.

Sekarang, setelah fungsi bernyanyi () , di dekat akhir kode adalah garis

var person="an old lady";

VARIABEL: Huruf var berarti "variabel". Variabel seperti amplop. Di luar amplop ini ditandai "orang". Di bagian dalamnya berisi selembar kertas dengan informasi yang dibutuhkan fungsi kita, beberapa huruf dan spasi bergabung bersama seperti seutas tali (disebut string) yang membuat frasa membaca "seorang wanita tua". Amplop kami dapat berisi hal-hal lain seperti angka (disebut bilangan bulat), instruksi (disebut fungsi), daftar (disebut array ). Karena variabel ini ditulis di luar semua kurung kurawal {}, dan karena Anda dapat melihat melalui jendela yang berwarna saat Anda berada di dalam kurung kurawal, variabel ini dapat dilihat dari mana saja dalam kode. Kami menyebutnya 'variabel global'.

VARIABEL GLOBAL: seseorang adalah variabel global, yang berarti bahwa jika Anda mengubah nilainya dari "seorang wanita tua" menjadi "seorang pria muda", orang itu akan tetap menjadi seorang pria muda sampai Anda memutuskan untuk mengubahnya lagi dan bahwa fungsi lain di kode dapat melihat bahwa itu adalah seorang pria muda. Tekan F12tombol atau lihat pengaturan Opsi untuk membuka konsol pengembang browser dan ketik "orang" untuk melihat apa nilai ini. Ketik person="a young man"untuk mengubahnya dan kemudian ketik "orang" lagi untuk melihat bahwa itu telah berubah.

Setelah ini, kita memiliki garis

sing(person);

Baris ini memanggil fungsi, seolah-olah memanggil anjing

"Ayo bernyanyi , Ayo, cari orang !"

Ketika browser telah memuat kode JavaScript dan mencapai garis ini, itu akan memulai fungsinya. Saya meletakkan baris di bagian akhir untuk memastikan bahwa browser memiliki semua informasi yang diperlukan untuk menjalankannya.

Fungsi mendefinisikan tindakan - fungsi utama adalah tentang bernyanyi. Ini berisi variabel yang disebut firstPart yang berlaku untuk bernyanyi tentang orang yang berlaku untuk masing-masing ayat dari lagu: "Ada" + orang + "yang menelan". Jika Anda mengetik firstPart ke konsol, Anda tidak akan mendapatkan jawaban karena variabel dikunci dalam suatu fungsi - browser tidak dapat melihat di dalam jendela berwarna kawat gigi.

PENUTUP: Penutupan adalah fungsi yang lebih kecil yang ada di dalam fungsi sing besar () . Pabrik kecil di dalam pabrik besar. Mereka masing-masing memiliki kawat gigi sendiri yang berarti bahwa variabel di dalamnya tidak dapat dilihat dari luar. Itu sebabnya nama variabel ( makhluk dan hasil ) dapat diulang dalam penutupan tetapi dengan nilai yang berbeda. Jika Anda mengetik nama-nama variabel ini di jendela konsol, Anda tidak akan mendapatkan nilainya karena disembunyikan oleh dua lapisan jendela berwarna.

Penutupan semua tahu apa variabel fungsi sing () dipanggil firstPart , karena mereka bisa melihat keluar dari jendela berwarna mereka.

Setelah penutupan datang garis

fly();
spider();
bird();
cat();

Fungsi sing () akan memanggil masing-masing fungsi ini sesuai urutan yang diberikan. Maka pekerjaan fungsi sing () akan dilakukan.

bersyukur
sumber
56

Oke, berbicara dengan anak berusia 6 tahun, saya mungkin akan menggunakan asosiasi berikut.

Bayangkan - Anda bermain dengan saudara-saudari kecil Anda di seluruh rumah, dan Anda berkeliling dengan mainan Anda dan membawa beberapa dari mereka ke kamar kakak Anda. Setelah beberapa saat, kakakmu kembali dari sekolah dan pergi ke kamarnya, dan dia mengunci di dalamnya, jadi sekarang kamu tidak bisa mengakses mainan yang tertinggal di sana lagi secara langsung. Tapi Anda bisa mengetuk pintu dan meminta mainan itu kepada kakak Anda. Ini disebut penutupan mainan ; saudaramu yang menebusnya untukmu, dan dia sekarang berada di luar jangkauan .

Bandingkan dengan situasi ketika pintu dikunci oleh angin dan tidak ada orang di dalam (pelaksanaan fungsi umum), dan kemudian beberapa kebakaran lokal terjadi dan membakar ruangan (pengumpul sampah: D), dan kemudian sebuah ruangan baru dibangun dan sekarang Anda dapat meninggalkan mainan lain di sana (contoh fungsi baru), tetapi tidak pernah mendapatkan mainan yang sama yang tersisa di contoh ruang pertama.

Untuk anak yang sudah lanjut, saya akan menempatkan sesuatu seperti yang berikut ini. Ini tidak sempurna, tetapi membuat Anda merasa tentang apa itu:

function playingInBrothersRoom (withToys) {
  // We closure toys which we played in the brother's room. When he come back and lock the door
  // your brother is supposed to be into the outer [[scope]] object now. Thanks god you could communicate with him.
  var closureToys = withToys || [],
      returnToy, countIt, toy; // Just another closure helpers, for brother's inner use.

  var brotherGivesToyBack = function (toy) {
    // New request. There is not yet closureToys on brother's hand yet. Give him a time.
    returnToy = null;
    if (toy && closureToys.length > 0) { // If we ask for a specific toy, the brother is going to search for it.

      for ( countIt = closureToys.length; countIt; countIt--) {
        if (closureToys[countIt - 1] == toy) {
          returnToy = 'Take your ' + closureToys.splice(countIt - 1, 1) + ', little boy!';
          break;
        }
      }
      returnToy = returnToy || 'Hey, I could not find any ' + toy + ' here. Look for it in another room.';
    }
    else if (closureToys.length > 0) { // Otherwise, just give back everything he has in the room.
      returnToy = 'Behold! ' + closureToys.join(', ') + '.';
      closureToys = [];
    }
    else {
      returnToy = 'Hey, lil shrimp, I gave you everything!';
    }
    console.log(returnToy);
  }
  return brotherGivesToyBack;
}
// You are playing in the house, including the brother's room.
var toys = ['teddybear', 'car', 'jumpingrope'],
    askBrotherForClosuredToy = playingInBrothersRoom(toys);

// The door is locked, and the brother came from the school. You could not cheat and take it out directly.
console.log(askBrotherForClosuredToy.closureToys); // Undefined

// But you could ask your brother politely, to give it back.
askBrotherForClosuredToy('teddybear'); // Hooray, here it is, teddybear
askBrotherForClosuredToy('ball'); // The brother would not be able to find it.
askBrotherForClosuredToy(); // The brother gives you all the rest
askBrotherForClosuredToy(); // Nothing left in there

Seperti yang Anda lihat, mainan yang tersisa di kamar masih dapat diakses melalui saudara dan tidak masalah jika ruangan itu terkunci. Ini adalah jsbin untuk bermain-main dengannya.

dmi3y
sumber
49

Jawaban untuk anak berusia enam tahun (dengan asumsi dia tahu apa fungsi itu dan apa variabelnya, dan apa datanya):

Fungsi dapat mengembalikan data. Satu jenis data yang dapat Anda kembalikan dari suatu fungsi adalah fungsi lainnya. Ketika fungsi baru itu dikembalikan, semua variabel dan argumen yang digunakan dalam fungsi yang membuatnya tidak hilang. Sebaliknya, fungsi induk itu "ditutup." Dengan kata lain, tidak ada yang bisa melihat ke dalamnya dan melihat variabel yang digunakan kecuali untuk fungsi yang dikembalikan. Fungsi baru itu memiliki kemampuan khusus untuk melihat kembali ke dalam fungsi yang membuatnya dan melihat data di dalamnya.

function the_closure() {
  var x = 4;
  return function () {
    return x; // Here, we look back inside the_closure for the value of x
  }
}

var myFn = the_closure();
myFn(); //=> 4

Cara lain yang sangat sederhana untuk menjelaskannya adalah dalam hal ruang lingkup:

Setiap kali Anda membuat ruang lingkup yang lebih kecil di dalam ruang lingkup yang lebih besar, ruang lingkup yang lebih kecil akan selalu dapat melihat apa yang ada di ruang lingkup yang lebih besar.

Stupid Stupid
sumber
49

Fungsi dalam JavaScript bukan hanya referensi ke sekumpulan instruksi (seperti dalam bahasa C), tetapi juga mencakup struktur data tersembunyi yang terdiri dari referensi untuk semua variabel nonlokal yang digunakannya (variabel yang ditangkap). Fungsi dua bagian seperti itu disebut penutupan. Setiap fungsi dalam JavaScript dapat dianggap sebagai penutupan.

Penutupan adalah fungsi dengan status. Ini agak mirip dengan "ini" dalam arti bahwa "ini" juga menyediakan status untuk fungsi tetapi fungsi dan "ini" adalah objek yang terpisah ("ini" hanyalah parameter mewah, dan satu-satunya cara untuk mengikatnya secara permanen ke fungsi adalah untuk membuat penutupan). Sementara "ini" dan fungsi selalu hidup secara terpisah, suatu fungsi tidak dapat dipisahkan dari penutupannya dan bahasa tidak menyediakan sarana untuk mengakses variabel yang ditangkap.

Karena semua variabel eksternal yang dirujuk oleh fungsi bersarang leksikal sebenarnya adalah variabel lokal dalam rantai fungsi leksikal yang melingkupinya (variabel global dapat diasumsikan sebagai variabel lokal dari beberapa fungsi root), dan setiap eksekusi tunggal dari suatu fungsi menciptakan instance baru dari variabel lokalnya, ini mengikuti bahwa setiap eksekusi fungsi yang mengembalikan (atau mentransfernya, seperti mendaftarkannya sebagai panggilan balik) fungsi bersarang menciptakan penutupan baru (dengan serangkaian variabel nonlokal yang direferensikan dan berpotensi unik yang mewakili pelaksanaannya konteks).

Juga, harus dipahami bahwa variabel lokal dalam JavaScript dibuat bukan pada frame stack, tetapi pada heap dan dihancurkan hanya ketika tidak ada yang mereferensikannya. Ketika suatu fungsi kembali, referensi ke variabel lokalnya dikurangi, tetapi mereka bisa tetap non-nol jika selama eksekusi saat ini mereka menjadi bagian dari penutupan dan masih direferensikan oleh fungsi bersarang secara leksikal (yang dapat terjadi hanya jika referensi ke fungsi bersarang ini dikembalikan atau sebaliknya ditransfer ke beberapa kode eksternal).

Sebuah contoh:

function foo (initValue) {
   //This variable is not destroyed when the foo function exits.
   //It is 'captured' by the two nested functions returned below.
   var value = initValue;

   //Note that the two returned functions are created right now.
   //If the foo function is called again, it will return
   //new functions referencing a different 'value' variable.
   return {
       getValue: function () { return value; },
       setValue: function (newValue) { value = newValue; }
   }
}

function bar () {
    //foo sets its local variable 'value' to 5 and returns an object with
    //two functions still referencing that local variable
    var obj = foo(5);

    //Extracting functions just to show that no 'this' is involved here
    var getValue = obj.getValue;
    var setValue = obj.setValue;

    alert(getValue()); //Displays 5
    setValue(10);
    alert(getValue()); //Displays 10

    //At this point getValue and setValue functions are destroyed
    //(in reality they are destroyed at the next iteration of the garbage collector).
    //The local variable 'value' in the foo is no longer referenced by
    //anything and is destroyed too.
}

bar();
srgstm
sumber
47

Mungkin sedikit melebihi segalanya kecuali yang paling dewasa sebelum berumur enam tahun, tetapi beberapa contoh yang membantu membuat konsep penutupan dalam JavaScript klik untuk saya.

Penutupan adalah fungsi yang memiliki akses ke ruang lingkup fungsi lain (variabel dan fungsinya). Cara termudah untuk membuat penutupan adalah dengan fungsi dalam suatu fungsi; alasannya adalah bahwa dalam JavaScript suatu fungsi selalu memiliki akses ke ruang lingkup fungsi yang mengandungnya.

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        alert(outerVar);
    }
    
    innerFunction();
}

outerFunction();

PERINGATAN: monyet

Dalam contoh di atas, fungsi luar disebut yang pada gilirannya memanggil fungsi internal. Perhatikan bagaimana outerVar tersedia untuk fungsi dalam, dibuktikan dengan itu dengan benar memperingatkan nilai outerVar.

Sekarang pertimbangkan hal berikut:

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        return outerVar;
    }
    
    return innerFunction;
}

var referenceToInnerFunction = outerFunction();
alert(referenceToInnerFunction());

PERINGATAN: monyet

referenceToInnerFunction diatur ke outerFunction (), yang hanya mengembalikan referensi ke innerFunction. Ketika referenceToInnerFunction dipanggil, ia mengembalikan outerVar. Sekali lagi, seperti di atas, ini menunjukkan bahwa fungsi dalam memiliki akses ke outerVar, variabel fungsi luar. Selain itu, menarik untuk dicatat bahwa ia mempertahankan akses ini bahkan setelah fungsi luar selesai dijalankan.

Dan di sinilah segalanya menjadi sangat menarik. Jika kami menyingkirkan outerFunction, katakan setel ke null, Anda mungkin berpikir bahwa referenceToInnerFunction akan kehilangan aksesnya ke nilai outerVar. Tapi ini bukan masalahnya.

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        return outerVar;
    }
    
    return innerFunction;
}

var referenceToInnerFunction = outerFunction();
alert(referenceToInnerFunction());

outerFunction = null;
alert(referenceToInnerFunction());

ALERT: monyet ALERT: monyet

Tapi bagaimana bisa begitu? Bagaimana referenceToInnerFunction masih dapat mengetahui nilai outerVar sekarang bahwa outerFunction telah disetel ke nol?

Alasan bahwa referenceToInnerFunction masih dapat mengakses nilai outerVar adalah karena ketika penutupan pertama kali dibuat dengan menempatkan innerFunction di dalam outerFunction, innerFunction menambahkan referensi ke ruang lingkup outerFunction (variabel dan fungsinya) ke rantai cakupannya. Apa artinya ini adalah innerFunction memiliki pointer atau referensi ke semua variabel outerFunction, termasuk outerVar. Jadi, bahkan ketika outerFunction telah selesai dieksekusi, atau bahkan jika itu dihapus atau diatur ke nol, variabel-variabel dalam ruang lingkupnya, seperti outerVar, tetap ada di memori karena referensi yang luar biasa kepada mereka di bagian fungsi dalam yang telah dikembalikan ke referenceToInnerFunction. Untuk benar-benar melepaskan outerVar dan variabel luar OuterFunction dari memori Anda harus menyingkirkan referensi luar biasa ini kepada mereka,

//////////

Dua hal lain tentang penutupan harus diperhatikan. Pertama, penutupan akan selalu memiliki akses ke nilai terakhir dari fungsi yang mengandungnya.

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        alert(outerVar);
    }
    
    outerVar = "gorilla";

    innerFunction();
}

outerFunction();

PERINGATAN: gorila

Kedua, ketika sebuah penutupan dibuat, ia mempertahankan referensi ke semua variabel dan fungsi fungsi yang melampirkannya; tidak bisa memilih. Dan dengan demikian, penutupan harus digunakan dengan hemat, atau setidaknya dengan hati-hati, karena mereka bisa intensif memori; banyak variabel dapat disimpan dalam memori lama setelah fungsi yang mengandung selesai dieksekusi.

Michael Dziedzic
sumber
45

Saya cukup mengarahkan mereka ke halaman Mozilla Closures . Ini adalah penjelasan terbaik, paling ringkas dan sederhana tentang dasar-dasar penutupan dan penggunaan praktis yang saya temukan. Sangat disarankan bagi siapa pun yang belajar JavaScript.

Dan ya, saya bahkan merekomendasikannya kepada anak berusia 6 tahun - jika anak berusia 6 tahun itu belajar tentang penutupan, maka logis mereka siap untuk memahami penjelasan singkat dan sederhana yang disediakan dalam artikel.

mjmoody383
sumber
Saya setuju: halaman Mozilla tersebut sangat sederhana dan ringkas. Hebatnya posting Anda belum begitu dihargai seperti yang lain.
Brice Coustillas