neljapäev, 29. aprill 2010

JS: Kuidas hoida "this" sulundites alles

Üheks tuntud probleemiks JavaScriptis on muutuja this sidumine funktsioonis a) aktiivse objekti meetodi juurde või b) kõigil teistel juhtudel globaalse objekti window juurde. Sulundite korral kui funktsiooni deklaratsioon asub objekti meetodi sees ei liigu this sulundfunktsiooni edasi ning see tekitab tihti segadust.

var omadus = 321;
var objekt = {
    omadus: 123,
    meetod: function(){
        alert(this.omadus); // 123
        window.setTimeout(function(){
            alert(this.omadus); // 321
        },0);
    }
}
objekt.meetod();

Nagu näites näha asub objekti meetodi meetod sees kaks alert käsklust, mis mõlemad väljastavad this.omadus sisu. Esimene alert käsklus asub otse objekti meetodis, teine aga selle meetodi skoopi jäävas sulundfunktsioonis. Mõlemad kasutavad küll sama nimetust muutuja esitamiseks (this.omadus), kuid ometigi on tulemus täiesti erinev - esimesel juhul korrektselt 123, teisel juhul aga hoopis 321.

Siit selgubki, et this on seatud korrektselt ainult vahetult meetodi skoobis, kuid mitte sulundina selle sees. Viimasel juhul on ei viita this enam mitte aktiivsele objektile, milleks peaks olema näite juures objekt, vaid hoopis globaalsele objektile window. Kuna aga kõik "globaalsed muutujad," (st. ei kuulu kindlasse skoopi) sh. omadus on tegelikult window objekti omadusteks, siis this.omadus anonüümses sulundfunktsioonis teisendub window.omadus ja sellest juba lihtsalt omadus muutujaks, mille väärtuseks ongi näites 321.

Juhul kui kasutusel on Prototype raamistik saab olukorra mugavaks lahendamiseks kasutada raamistiku poolt pakutavat vahekihti loovat meetodit bind, mis seob this väärtuse funktsioonis vajaliku objektiga.

var objekt = {
    omadus: 123,
    meetod: function(){
        alert(this.omadus); // 123
        window.setTimeout((function(){
            alert(this.omadus); // 123
        }).bind(this),0);
    }
}
objekt.meetod();

See meetod võtab funktsiooniobjekti kujul (function(){}) ning rakendab sellel Prototype lisatud funktsiooni meetodit bind mis omakorda tagastab uue funktsiooniobjekti, mis antakse setTimeout parameetriks.

Kuidas bind töötab?

Juhul kui Prototype puudub võib sellise funktsiooni meetodi lihtsalt ise teha.

Function.prototype.bind = function(){
    var that = this,
        params = Array.prototype.slice.call(arguments),
        obj = params.shift();
    return function(){
        that.apply(obj, params);
    }
}

Näites on defineeritud kolm muutujat:
that on viide funktsiooniobjekti juurde, params on funktsiooni kõiki parameetreid sisaldav arguments objekt teisendatuna massiiviks (arguments on massiivilaadne objekt, aga mitte massiiv) ning obj, milleks saab params massiivi esimene objekt.

Funktsioon tagastab uue funktsiooniobjekti, mille käivitamisel käivitatakse esialgne funktsioon, aga juba korrektselt seatud this väärtusega.

Arvestada tuleb ainult et native objektide prototype muutmine pole reeglina kunagi hea mõte ning võibolla tasuks funktsiooni meetod defineerida hoopis eraldisesiva funktsioonina.

4 kommentaari:

Rene Saarsoo ütles ...

Ma arvan, et Function'i prototüübile meetodite lisamine endas küll erilist ohtu ei kätke. Igatahes mitte suuremat ohtu kui lihtsalt globaalsete funktsioonide tekitamine.

Lisaks pean mainima, et mina eelistaks sellist bind() implementatsiooni, mis mitte lihtsalt ei asendaks argumendid vaid curry'ks nad. Nõnda, et saaks kirjutada nii:

function liida(a, b) {
this.msg(a + b);
}
var obj = {msg: alert};

liida.bind(obj)(1, 2); --> 3
liida.bind(obj, 1)(2); --> 3
liida.bind(obj, 1, 2)(); --> 3

Ka Prototype raamistikus on bind() nõndaviisi implementeeritud.

Andris ütles ...

Function prototype kallal toimetamine on tõesti suhteliselt ohutu, kuid ma pigem jään native elementide prototüüpide toimetamise suhtes konservatiivsele seisukohale.

jQuery tegijad leiavad et igasugune native elementide muutmine on täielikult välistatud, samas kui Prototype tegijad laiendavad neid elemente üsna vabalt. Minu seisukoht on seal kuskil vahepeal - kasutatav põhiteek võib muuta prototüüpe palju heaks arvab, kuid ma ise ning soovitavalt ka täiendavad abiteegid seda kindlasti teha ei tohiks. Seega jQuery seisukohaga, et prototüüpe kohe üldse mitte puutuda ei tohi, ei ole ma küll nõus, kuid ise seda ka tegema ei kipu.

Aga see on muidugi ainult maitse küsimus.

Rene Saarsoo ütles ...

Siin on vast oluline vahe, et kas sa arendad libraryt või lihtsalt omaenda veebilehte.

Library puhul on mu meelest parem väga täpselt piiritleda, millega tegeletakse ja mitte torkida asjasse mittepuutuvaid asju. Näiteks jQuery on peaasjalikult DOM-i manipuleerimise library ning ega ta mujale suurt ei trügi.

Samas Prototype ja Mootools on rohkem sellised kõik-ühes libraryd. Õigupoolest, eks nad nimetavadki ennast rohkem frameworkideks. Ja frameworkidega on see värk, et sa saad reeglina korraga vaid ühte kasutada.

Andris ütles ...

100% nõus (Y)