Viime vuodet ovat osoittaneet, että web-kehitys on muuttumassa. Kun selaimiin lisättiin monia ominaisuuksia ja sovellusrajapintoja, niitä oli käytettävä oikein. Kieli, jolle olemme tämän kunnian velkaa, oli JavaScript.
Aluksi kehittäjät eivät olleet vakuuttuneita siitä, miten se oli suunniteltu, ja heillä oli enimmäkseen kielteisiä vaikutelmia tätä käsikirjoitusta käyttäessään. Ajan myötä kävi ilmi, että tässä kielessä on paljon potentiaalia, ja myöhemmät ECMAScript-standardit tekevät joistakin mekaniikoista inhimillisempiä ja yksinkertaisesti parempia. Tässä artikkelissa tarkastelemme joitakin niistä.
Arvojen tyypit JS:ssä
Tunnettu totuus JavaScript on se, että täällä kaikki on objektia. Oikeastaan kaikki: matriisit, funktiot, merkkijonot, numerot ja jopa booleanit. Kaikki arvotyypit esitetään objekteina, ja niillä on omat metodinsa ja kenttänsä. Voimme kuitenkin jakaa ne kahteen luokkaan: primitiiveihin ja rakenteisiin. Ensimmäisen luokan arvot ovat muuttumattomia, mikä tarkoittaa, että voimme määrittää jollekin muuttujalle uuden arvon, mutta emme voi muuttaa itse olemassa olevaa arvoa. Toinen luokka edustaa arvoja, joita voidaan muuttaa, joten ne on tulkittava ominaisuuksien kokoelmiksi, jotka voimme korvata tai vain kutsua metodeja, jotka on suunniteltu sitä varten.
Ilmoitettujen muuttujien laajuus
Ennen kuin menemme syvemmälle, selitetään, mitä laajuus tarkoittaa. Voimme sanoa, että scope on ainoa alue, jossa voimme käyttää ilmoitettuja muuttujia. Ennen ES6-standardia voisimme julistaa muuttujia var-lausekkeella ja antaa niille globaalin tai paikallisen laajuuden. Ensimmäinen on alue, jonka avulla voimme käyttää joitakin muuttujia missä tahansa sovelluksen osassa, ja toinen on tarkoitettu vain tietylle alueelle - lähinnä funktiolle.
Koska standardi ES2015, JavaScript on kolme tapaa ilmoittaa muuttujia, jotka eroavat avainsanalla. Ensimmäinen on kuvattu aiemmin: var-avainsanalla ilmoitetut muuttujat rajoittuvat nykyiseen funktiorunkoon. ES6-standardi sallii meidän ilmoittaa muuttujia inhimillisemmillä tavoilla - toisin kuin var-lausekkeilla, const- ja let-lausekkeilla ilmoitetut muuttujat on rajattu vain lohkoon. JS kuitenkin kohtelee const-lausetta melko epätavallisesti verrattuna muihin ohjelmointikielet - pysyvän arvon sijasta se säilyttää pysyvän viittauksen arvoon. Lyhyesti sanottuna voimme muuttaa const-lausekkeella ilmoitetun objektin ominaisuuksia, mutta emme voi korvata tämän muuttujan viittausta. Jotkut sanovat, että var-vaihtoehto ES6:ssa on itse asiassa let-lause. Ei, se ei ole, eikä var-lause ole eikä sitä luultavasti koskaan peruta. Hyvä käytäntö on välttää var-lausekkeiden käyttöä, koska ne aiheuttavat useimmiten enemmän ongelmia. Meidän on puolestaan käytettävä const-lauseita väärin, kunnes meidän on muutettava sen viittausta - silloin meidän on käytettävä let-lauseita.
Esimerkki odottamattomasta laajuuskäyttäytymisestä
Aloitetaan seuraavasta koodi:
(() => {
for (var i = 0; i {
console.log(`Value of "i": ${i}`);
}, 1000);
}
})();
Kun tarkastelemme sitä, näyttää siltä, että for-silmukka iteroi i-arvon ja kirjaa sekunnin kuluttua iteraattorin arvot: 1, 2, 3, 4, 5. No, näin ei kuitenkaan ole. Kuten edellä mainitsimme, var-lausekkeessa on kyse muuttujan arvon säilyttämisestä koko funktiorungon ajan; se tarkoittaa, että toisessa, kolmannessa ja niin edelleen iteraatiossa muuttujan i arvo korvataan seuraavalla arvolla. Lopulta silmukka päättyy, ja aikakatkaisun tikit näyttävät meille seuraavaa: 5, 5, 5, 5, 5, 5. Paras tapa säilyttää iteraattorin nykyinen arvo on käyttää sen sijaan let-lauseketta:
(() => {
for (let i = 0; i {
console.log(`Value of "i": ${i}`);
}, 1000);
}
})();
Yllä olevassa esimerkissä pidämme i-arvon vaikutusalueen nykyisessä iteraatiolohkossa, se on ainoa alue, jossa voimme käyttää tätä muuttujaa, eikä mikään voi ohittaa sitä tämän alueen ulkopuolelta. Tulos on tässä tapauksessa odotetunlainen: 1 2 3 4 5. Katsotaanpa, miten tätä tilannetta voidaan käsitellä var-lauseella:
(() => {
for (var i = 0; i {
setTimeout(() => {
console.log(`Value of "j": ${j}`);
}, 1000);
})(i);
}
})();
Koska var-lausekkeessa on kyse arvon säilyttämisestä funktiolohkon sisällä, meidän on kutsuttava määriteltyä funktiota, joka ottaa argumentin - iteraattorin nykyisen tilan arvon - ja sitten vain tehtävä jotain. Mikään julkilausutun funktion ulkopuolella ei ohita j-arvoa.
Esimerkkejä virheellisistä odotuksista kohteiden arvojen suhteen
Useimmin havaitsemani rikos liittyy rakenteiden voiman huomiotta jättämiseen ja niiden ominaisuuksien muuttamiseen, joita muutetaan myös muissa koodin osissa. Katsokaa nopeasti:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = DEFAULT_VALUE;
const bandInput = document.querySelector('#favorite-band');
const restoreDefaultButton = document.querySelector('#restore-painike');
bandInput.addEventListener('input', () => { {
currentValue.favoriteBand = bandInput.value;
}, false);
restoreDefaultButton.addEventListener('click', () => {
currentValue = DEFAULT_VALUE;
}, false);
Aloitetaan alusta: oletetaan, että meillä on malli, jolla on oletusominaisuuksia ja joka on tallennettu objektina. Haluamme painikkeen, joka palauttaa syöttöarvot oletusarvoihin. Kun olemme täyttäneet syötteen joillakin arvoilla, päivitämme mallin. Hetken kuluttua ajattelemme, että oletusvalinta oli yksinkertaisesti parempi, joten haluamme palauttaa sen. Napsautamme painiketta... eikä mitään tapahdu. Miksi? Koska emme ota huomioon viitattujen arvojen voimaa.
Tämä osa: const currentValue = DEFAULTVALUE kertoo JS:lle seuraavaa: ota viittaus DEFAULT-arvoon.VALUE-arvo ja määritä currentValue-muuttuja sen kanssa. Todellinen arvo tallennetaan muistiin vain kerran ja molemmat muuttujat osoittavat siihen. Joidenkin ominaisuuksien muuttaminen yhdessä paikassa tarkoittaa niiden muuttamista toisessa paikassa. Meillä on muutama tapa välttää tällaisia tilanteita. Yksi, joka täyttää tarpeemme, on spread-operaattori. Korjataanpa koodimme:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = { ...DEFAULT_VALUE };
const bandInput = document.querySelector('#favorite-band');
const restoreDefaultButton = document.querySelector('#restore-button');
bandInput.addEventListener('input', () => { {
currentValue.favoriteBand = bandInput.value;
}, false);
restoreDefaultButton.addEventListener('click', () => {
currentValue = { ...DEFAULT_VALUE };
}, false);
Tässä tapauksessa leviämisoperaattori toimii näin: se ottaa kaikki objektin ominaisuudet ja luo uuden objektin, joka on täynnä niitä. Tämän ansiosta currentValue- ja DEFAULT_VALUE-kohtien arvot eivät enää osoita samaan paikkaan muistissa, eivätkä kaikki muutokset, joita sovelletaan yhteen niistä, vaikuta muihin.
Okei, kysymys kuuluu: onko kyse vain maagisen leviämisoperaattorin käytöstä? Tässä tapauksessa - kyllä, mutta mallimme saattavat vaatia monimutkaisempaa kuin tämä esimerkki. Jos käytämme sisäkkäisiä objekteja, matriiseja tai muita rakenteellisia yksiköitä, ylimmän tason viitattavan arvon leviämisoperaattori vaikuttaa vain ylimpään tasoon, ja viitatut ominaisuudet jakavat edelleen saman muistipaikan. On olemassa monia ratkaisuja tämän ongelman käsittelyyn, kaikki riippuu tarpeistasi. Voimme kloonata objekteja jokaisella syvyystasolla tai monimutkaisemmissa operaatioissa käyttää immerin kaltaisia työkaluja, joiden avulla voimme kirjoittaa muuttumatonta koodia lähes kivuttomasti.
Sekoita kaikki yhteen
Onko soveltamisaloja ja arvotyyppejä koskevan tiedon yhdistelmä käyttökelpoinen? Tietenkin on! Rakennetaan jotain, joka käyttää molempia:
const useValue = (defaultValue) => {
const value = [...defaultValue];
const setValue = (newValue) => {
value.length = 0; // hankala tapa tyhjentää array.
newValue.forEach((item, index) => {
value[index] = item;
});
// tee muita juttuja
};
return [value, setValue];
};
const [animals, setAnimals] = useValue(['cat', 'dog']);
console.log(animals); // ['kissa', 'koira']
setAnimals(['hevonen', 'lehmä']);
console.log(animals); // ['hevonen', 'lehmä']);
Selitetään, miten tämä koodi toimii rivi riviltä. No, useValue-funktio luo matriisin defaultValue-argumentin perusteella; se luo muuttujan ja toisen funktion, sen muokkaajan. Tämä modificator ottaa uuden arvon, jota sovelletaan hankalalla tavalla olemassa olevaan arvoon. Funktion lopussa palautetaan arvo ja sen modifioija array-arvoina. Seuraavaksi käytämme luotua funktiota - ilmoitamme eläimet ja setAnimals palautettuina arvoina. Käytä niiden modificatoria tarkistaaksesi, vaikuttaako funktio animal-muuttujaan - kyllä, se toimii!
Mutta odota, mitä hienoa tässä koodissa oikein on? Viittaus säilyttää kaikki uudet arvot, ja voit lisätä tähän muokkaajaan oman logiikkasi, kuten esimerkiksi jotkin API:t tai osa ekosysteemiä joka antaa tietovirtasi tehon ilman vaivaa. Tätä hankalaa mallia käytetään usein nykyaikaisemmissa JS-kirjastoissa, joissa ohjelmoinnin funktionaalinen paradigma mahdollistaa sen, että koodi on vähemmän monimutkaista ja muiden ohjelmoijien on helpompi lukea sitä.
Yhteenveto
Kun ymmärrämme, miten kielen mekaniikka toimii konepellin alla, voimme kirjoittaa tietoisempaa ja kevyempää koodia. Vaikka JS ei olekaan matalan tason kieli ja pakottaa meidät tietämään jonkin verran siitä, miten muistia osoitetaan ja tallennetaan, meidän on silti pidettävä silmällä odottamatonta käyttäytymistä, kun muokkaamme objekteja. Toisaalta arvojen kloonien väärinkäyttö ei aina ole oikea tapa, ja virheellisellä käytöllä on enemmän haittoja kuin hyötyjä. Oikea tapa suunnitella tietovirta on miettiä, mitä tarvitset ja mitä mahdollisia esteitä voit kohdata sovelluksen logiikkaa toteutettaessa.
Lue lisää: