Javascript è asincrono per sua natura. Il tuo codice non può aspettare. Invece, tu dici "quando succede, chiama questa funzione". Ciò significa che il codice Javascript è pieno di funzioni. Funzioni che vengono passate ad altre funzioni, chiamate da loro e talvolta anche restituite da esse per essere chiamate in seguito. Queste funzioni spesso vogliono utilizzare alcuni dati che il loro ambito di definizione è disponibile, ma il loro chiamante (il browser) non lo fa.
function getSize(src, callback){
var img = new Image();
img.onload = function(event){
console.log(event);
callback(img.width, img.height);
}
img.src = src
}
Ora, sembra un codice normale. Tranne che utilizza l'ambito di chiusura! img
e callback
sono entrambi chiusi da img.onload
. Ora, immagina di non avere chiusure. Quali sono le altre opzioni? Potresti trovare img
nell'oggetto evento, ma sicuramente non callback
. È necessaria una funzione che accetta due tipi di argomenti: quelli impostati quando si definisce una funzione e quelli impostati quando lo si chiama. Oppure, puoi dire al sistema tutto ciò di cui hai bisogno. Oppure, è necessario un meccanismo di curriculum nativo *. Non vuoi dire al codice straniero tutto ciò che sai solo perché devi reagire ai suoi eventi, quindi supponiamo che questo meccanismo esista. ES5 introduce bind
:
* currying è una tecnica in cui una nuova funzione viene creata in anticipo vincolando gli argomenti a una funzione esistente e inviando gli argomenti come gli argomenti rimanenti alla funzione originale. Il valore di ritorno della funzione originale viene quindi restituito
function getSize(src, callback){
var img = new Image();
img.onload = function(callback, img, event){
callback(img.width, img.height);
}.bind(null, callback, img); //not needed if you have closures
img.src = src
}
Quali sono gli argomenti su bind
? null
imposta il contesto this
. Il nostro ipotetico metodo curry
non lo farebbe. Quindi, callback
e img
. Questi devono essere nello stesso ordine degli argomenti a img.onload
. Questo è accettabile ogni tanto, ma non se devi curry in salita in entrambe le direzioni. Ancora più importante, è accettabile se hai tre argomenti, ma non se ne hai trenta.
Ora, per la parte divertente: l'ambito globale è tecnicamente anche un ambito di chiusura. Ma, anche se non lo fosse, non si desidera ancora definire le funzioni di utilità del modulo nell'ambito globale (problemi di namespacing). Le funzioni sono memorizzate in variabili!
getSize = function(Image, src, callback){
var img = new Image();
img.onload = function(callback, img, event){
callback(img.width, img.height);
}.bind(null, callback, img); //not needed if you have closures
img.src = src
}.bind(null, Image) //not needed if you have closures
Ora, supponi di voler trasformare le coordinate dell'immagine per qualche motivo, e che la trasformazione dipende dalla fonte. Dove dobbiamo aggiungere transform
e src
?
getSize = function(Image, transform, src, callback){ //here
var img = new Image();
img.onload = function(transform, src, callback, img, event){ //here
var coord = transform(img.width, img.height, src); //used here
callback(coord.x, coord.y);
}.bind(null, transform, src, callback, img); //here
img.src = src;
}.bind(null, transform, Image); //here
Riesci a individuare l'errore? Sì, gli argomenti di collegamento esterni sono nell'ordine sbagliato.
È possibile aggirare questo tenendo tutti i parametri in un oggetto globale. Naturalmente, non si desidera modificare l'ambito del chiamante, in modo da definire il proprio ambito ogni volta che si desidera passare alcuni argomenti aggiuntivi. Con alcune buone convenzioni, questo potrebbe essere ragionevolmente privo di errori:
var scope = {Image: Image, ...}
getSize = function(scope, src, callback){
var img = new scope.Image();
var scope = {scope: scope, src: src, callback: callback, img:img};
img.onload = function(scope, event){
var coord = scope.transform(scope.img.width, scope.img.height, scope.src);
callback(coord.x, coord.y);
}.bind(null, scope);
img.src = src;
}.bind(null, scope)
Puoi individuare il bug ora? scope.transform
è undefined
. Abbiamo bisogno di scope.scope.transform
Ne esiste un altro. Puoi individuarlo anche tu? Ora questo sta diventando ridicolo, anche se qui abbiamo usato una variabile a una lettera per scope
. Forse potremmo utilizzare la catena del prototipo:
var scope = {Image: Image, ...}
getSize = function(scope, src, callback){
var img = new scope.Image();
var scope = scope.Object.create(scope)
scope.src = src;
scope.callback = callback;
scope.img = img;
img.onload = function(scope, event){
var coord = scope.transform(scope.img.width, scope.img.height, scope.src);
callback(coord.x, coord.y);
}.bind(null, scope);
img.src = src;
}.bind(null, scope)
naturalmente, potremmo definire le nostre variabili locali direttamente nello scope in modo da non doverle dichiarare due volte se alcune funzioni interne potrebbero usarlo. Sfortunatamente, non c'è un tale trucco per gli argomenti. C'è la variabile arguments
, ma usarla per il nostro scopo è un po 'caotica ( scope.getSizeArgs[0]
; quick - quale è?). Tuttavia, finora, per aggirare la mancanza di chiusure abbiamo:
- per ogni ambito, ha creato una variabile speciale
scope
, con l'ambito del genitore come prototipo.
- ha aggiunto tutti gli argomenti ad esso
- lo ha usato per tutte le variabili locali che potrebbero essere necessarie in una funzione interna.
Certo, sta diventando ridicolo. Forse se la lingua in qualche modo sapeva che se usassimo una variabile non dichiarata, dovrebbe cercarla nello scope di definizione, e persino creare tale oggetto scope
per noi quando necessario, in modo che le variabili possano sopravvivere alle loro funzioni di definizione? Bene, lo fa.
function getSize(src, callback){
var img = new Image();
img.onload = function(event);
console.log(event);
var coords = transform(img.width, img.height);
callback(coords.x, coords.y);
}
img.src = src;
}
Inoltre, questo passaggio esplicito ha un altro svantaggio: ogni volta che si scrive sull'ambito, si scrive sempre su una variabile locale. Non è possibile modificare una variabile da una funzione interna. Certo, potresti utilizzare matrici a un elemento o un tipo speciale Reference<T>
, ma questo ha un odore di un linguaggio non eccezionale. Con chiusure, puoi farlo direttamente. Ne hai davvero bisogno?
var width = window.clientWidth;
window.addEventListener('resize', function(){width = window.clientWidth});
setInterval(function(){
...
Si potrebbe dire che a volte si desidera l'ambito del chiamante in quanto l'alternativa sarebbe avere 30 argomenti. Ma il chiamante non vuole tu per vedere il loro ambito locale, e non vogliono dover evitare nomi di variabili che un giorno potresti considerare significativi. Puoi ancora passare oggetti, ma non li chiami scope. Alcuni potrebbero dire "oggetto di trasferimento dati"; Preferisco il termine "named arguments"
$.ajax({
url: "http://www.example.com/cors-api/echo.json",
dataType: "json",
success: function(response){
..
Ora, per il classico esempio di chiusura scope, completa con un'espressione di funzione immediatamente invocata per limitare l'ambito della variabile statica nextId
:
var getId = (function(){
var nextId = 0;
return function getId(){ //named for clarity
return nextId++;
}
})()
getId() //0
getId() //1
getId() //2