- Pilfunktioner ger en koncis syntax och inspelning
thislexiskt från deras omgivande omfattning, istället för att skapa sin egen bindning. - Värdet av
thisi vanliga funktioner beror på hur de anropas, vilket påverkar funktioner, metoder, konstruktörer, klasser och återanrop. - Pilfunktioner är idealiska för återanrop och arraymetoder, men är ett dåligt val för objektmetoder, DOM-händelsehanterare och konstruktörer.
- Att förstå när
thisär dynamisk kontra lexikal är avgörande för att undvika subtila buggar och för att välja mellan arrow och traditionella funktioner.
Om du någonsin har loggat this i olika JavaScript-funktioner och fått väldigt olika resultat, är du inte ensam. Många utvecklare stöter på fall där en metod skriver ut det förväntade objektet, en pilfunktion skriver ut window, och en kapslad pil pekar plötsligt "magiskt" tillbaka mot det omgivande objektet. Att förstå varför det händer är nyckeln till att skriva förutsägbar, felfri kod.
Pilfunktioner och this nyckelordsformen är en av de viktigaste (och mest missförstådda) kombinationerna i modern JavaScript. Pilfunktioner ser ut som bara en kortare syntax, men under huven ändrar de hur this hanteras, hur återanrop beter sig och även när du bör eller inte bör använda dem som metoder. Låt oss gå igenom allt steg för steg, från syntax till exekveringskontext, med hjälp av enkel engelska och massor av praktiska exempel.
Pilfunktionssyntax utan förvirring
Pilfunktioner är funktionsuttryck skrivna med => syntax istället för function nyckelord. Konceptuellt kan man tänka på dem som ett kompakt sätt att skriva: "ta dessa parametrar, utvärdera detta uttryck eller kodblock och returnera ett värde." Nedan är de fortfarande funktioner, men de beter sig olika på flera viktiga sätt.
Den mest grundläggande pilfunktionen mappas direkt till ett reguljärt funktionsuttryck. Till exempel detta klassiska funktionsuttryck:
const multiplyByTwo = function (value) { return value * 2; };
Kan skrivas om som en pilfunktion så här:
const multiplyByTwo = (value) => { return value * 2; };
Pilfunktioner lyser när kroppen är ett enda uttryck. Om brödtexten bara är en sats som returnerar något, kan du ta bort både klammerparenteserna och den explicita return, vilket möjliggör en implicit retur:
const multiplyByTwo = value => value * 2;
När det finns exakt en parameter kan du utelämna de omgivande parenteserna, men bara i det specifika fallet. So x => x * 2 är giltig, men om du har noll eller flera parametrar måste du behålla parenteserna:
- Nollparametrar:
() => 42 - En parameter:
x => x * 2or(x) => x * 2 - Två eller fler parametrar:
(x, y) => x + y
När du behöver mer än en sats i brödtexten måste du använda klammerparenteser och en explicit sats. return. I den situationen beter sig pilfunktioner som vanliga funktioner vad gäller returer: nej return, inget värde returnerat.
const feedCat = (status) => {
if (status === 'hungry') {
return 'Feed the cat';
} else {
return 'Do not feed the cat';
}
};
Var försiktig när du returnerar objektliteraler från pilfunktioner, eftersom objektets klammerparenteser kan förväxlas med en funktionskropp. För att undvika den tvetydigheten, radbryt objektliteralen inom parenteser så att JavaScript vet att det är ett uttryck som ska returneras:
const toObject = value => ({ result: value });
En sak till: pilfunktioner är alltid uttryck, aldrig deklarationer. Det betyder att de måste tilldelas en variabel, egenskap eller skickas som ett argument; de kan inte stå ensamma som function myFunc() {}, och de hämtas inte på samma sätt som funktionsdeklarationer, så du kan inte anropa dem innan de är definierade.
Vad exakt är this i JavaScript?
Nyckelordet this är en dynamisk bindning som JavaScript skapar åt dig när det kör en funktion eller en klassmetod. Du kan tänka på det som en osynlig parameter vars värde beror på hur och var funktionen anropas. Detta gör den kraftfull och flexibel, men också en stor källa till förvirring.
I en icke-strikt funktion, this löses alltid upp till något slags objekt; i strikt läge kan det bokstavligen vara vilket värde som helst, inklusive undefined. JavaScript bestämmer det värdet baserat på exekveringskontexten: vanlig funktion, metodanrop, konstruktoranrop, klass, globalt omfång eller pilfunktion.
På den översta nivån i ett klassiskt skript (inte en modul), this avser globalThis, vilket vanligtvis är webbläsarens window objekt. Så följande jämförelse i en webbläsare kommer att vara sann:
console.log(this === window); // true
I funktioner som inte är pilar, this bestäms helt av samtalsplatsen. Om du ringer obj.method(), sedan inuti method värdet av this is objOm du tar samma funktion och kallar den fristående som fn() i strikt läge, this blir undefined; i icke-strikt läge "ersätter" JavaScript this med globalThis.
Viktigt är att det som spelar roll inte är var funktionen definieras, utan hur den anropas. En metod kan finnas kvar i prototypkedjan eller omtilldelas till ett annat objekt och fortfarande se this som vilket objekt som faktiskt används vid anropstillfället. Att skicka runt en metod ändrar ofta dess this om du inte uttryckligen åtgärdar det.
Det finns också verktyg för att kontrollera this uttryckligen: call, apply, bindoch Reflect.apply. Dessa låter dig "injicera" önskat this värde: fn.call(obj, arg1, arg2) kommer att utföra fn med this satt till objSamma substitutionsregler gäller i icke-strikt läge: om du klarar null or undefined as this, de ersätts med globalThis; primitiver blir inramade i sina omslagsobjekt.
Återanrop lägger till ytterligare ett lager av indirektion, eftersom this styrs av den som ringer din återuppringning. Array-iterationsmetoder, den Promise konstruktorn och liknande API:er anropar vanligtvis återanrop med this satt till undefined (eller det globala objektet i slarvigt läge). Vissa API:er, som Array.prototype.forEach or Set.prototype.forEach, acceptera en separat thisArg parametern du kan använda för att ställa in återanropets this.
Andra API:er anropar avsiktligt återanrop med anpassade this värden. Till exempel, den reviver argument till JSON.parse och replacer för JSON.stringify motta this bundna till objektet som äger egenskapen som för närvarande bearbetas. Händelsehanterare i DOM:en är bundna till elementet de är kopplade till när de skrivs på det "klassiska" sättet.
Kärnidén: pilfunktioner skapar inte sina egna this
Det utmärkande draget för pilfunktioner är att de aldrig skapar en ny this bindande. Istället stänger de över (eller "fångar") this från den omgivande lexikala miljön i det ögonblick de skapas. När pilen körs senare återanvänder den helt enkelt det insamlade värdet, oavsett hur du anropar det.
I praktiken beter sig en pilfunktion som om den vore permanent autobunden till this av dess yttre omfattning. Det är därför metoder som call, applyoch bind kan inte ändra this för en pilfunktion: thisArg argumentet ignoreras helt enkelt. Du kan fortfarande skicka vanliga parametrar genom dem, men this värdet är låst.
Betrakta detta kodavsnitt i det globala omfånget av en skriptfil:
const arrow = () => console.log(this);
arrow();
Eftersom pilen är definierad i global kod, är dess this är den globala this (vanligtvis window i ett webbläsarskript), och det ändras aldrig. ringa arrow Som en vanlig funktion kommer att tilldela den till en egenskap eller skicka runt den alltid logga samma globala objekt när den anropas i detta sammanhang.
Det verkligt intressanta beteendet uppstår när du nästlar pilfunktioner inuti vanliga funktioner eller metoder. Eftersom pilen fångar den yttre funktionens this, blir det ett kraftfullt verktyg för återanrop som behöver referera tillbaka till sitt innehållande objekt utan den vanliga .bind(this) ceremoni.
const counter = {
id: 42,
start() {
setTimeout(() => {
console.log(this.id); // uses counter.id
}, 1000);
},
};
If start använde en traditionell anonym funktion inuti setTimeout, skulle du behöva binda manuellt this eller spara den till en variabel. Med pilar ärver återuppringningen naturligtvis this från start, vilket är counter, Så this.id utskrifter 42 som avsett.
Denna lexikala bindning förklarar också det klassiska "varför gör this "change"-frågan när man använder pilar i objektlitteraler. Titta på dessa två objekt:
const obj1 = {
speak() {
console.log(this);
}
};
const obj2 = {
speak: () => {
console.log(this);
}
};
ringa obj1.speak() utskrifter obj1, därför att speak är en vanlig metod och this ställs in baserat på samtalsplatsen. Däremot obj2.speak() loggar det yttre this (ofta window i webbläsare), eftersom pilen inte använder objektet som sin thisSjälva objektliteralen skapar inte en ny this omfattning; endast funktionskroppen gör det, och pilfunktioner hoppar över det steget.
Betrakta nu en objektmetod som skapar och omedelbart anropar en inre pil:
const obj3 = {
speak() {
(() => {
console.log(this);
})();
}
};
obj3.speak();
I den här situationen ärver den inre pilfunktionen this från speak, vilket är obj3 när den kallas som obj3.speak(). Även om pilen är en kapslad, omedelbart anropad funktion, pekar den fortfarande på obj3, inte det globala objektet. Det är essensen av lexikalisk this: den följer det omgivande siktet, inte själva pilens anropsplats.
this över funktioner, objekt och konstruktorer
Att verkligen behärska pilfunktioner och this, det hjälper att se hur this fungerar i alla större sammanhang: vanliga funktioner, metoder, konstruktörer, klasser och det globala omfånget. När dessa regler är tydliga är pilens beteende mycket lättare att resonera kring.
I en vanlig funktion (icke-pil), this beror 100% på hur funktionen anropas. Om du ringer fn() i strikt läge, this is undefined; i slarvigt läge gör substitution this blir globalThis. Om du ringer obj.fn()och sedan this is objFlytta fn till ett annat objekt eller till en variabel och värdet på this kommer att röra sig därefter.
I en metod definierad på en objektliteral, this är objektet som metoden används på, inte nödvändigtvis det där metoden ursprungligen definierades. If obj.__proto__ innehåller en metod och du anropar obj.method(), sedan inuti method, this is obj, inte prototypen.
Konstruktorer är ett annat specialfall: när du anropar en funktion med new, this är bunden till den nyligen skapade objektinstansen. Till exempel, i function User(name) { this.name = name; }, ringer new User('Alex') set this till det nya User objekt. Om konstruktorn explicit returnerar ett icke-primitivt objekt, ersätter det returnerade objektet this som det slutliga värdet av new uttryck.
Klassyntax bygger på dessa regler med två huvudkontexter: instans och statisk. Inuti en konstruktor eller en instansmetod, this pekar på den klassinstans du arbetar med. Inuti statiska metoder eller statiska initialiseringsblock, this refererar till själva klassen (eller den härledda klassen när den anropas genom arv). Instansfält utvärderas med this bunden till den nya instansen; statiska fält ser this som klasskonstruktorn.
Härledda klasskonstruktorer beter sig något annorlunda: tills du anropar super(), det finns ingen användbar this. åkalla super() initialiserar this genom att delegera till baskonstruktorn; att returnera innan det görs i en härledd konstruktor är endast tillåtet om du explicit returnerar ett annat objekt.
I det globala sammanhanget, this beror på hur JavaScript-miljön paketerar och exekverar din kod. I ett klassiskt webbläsarskript, toppnivå this är det globala objektet; i en ES-modul, toppnivå this är alltid undefinedNode.js CommonJS-moduler är internt inslagna och körs vanligtvis med this satt till module.exportsInline-händelsehanterarattribut i HTML körs med this inställda på det element de är kopplade till.
En subtil men viktig detalj: objektliteraler introducerar inte i sig något nytt this omfattning. Skriva const obj = { value: this }; inuti ett skript kommer att göra obj.value lika med det yttre this, inte objektet. Endast funktionskroppar (och klasskroppar) skapar en dedikerad this bindning; pilar hoppar avsiktligt över detta steg och ärver.
Varför pilfunktioner är bra för återanrop (och när de inte är det)
Eftersom pilfunktionerna stänger över this, de passar perfekt för många återanropsscenarier där du vill att återanropet ska fortsätta referera till det omgivande objektet eller kontexten. Detta är särskilt praktiskt med timers, promises och arraymetoder som map, filteroch reduce.
Tänk dig en metod som behöver uppdatera en egenskap upprepade gånger med hjälp av setInterval. Med hjälp av en traditionell funktion, this inuti återanropet skulle standardvärdet vara det globala objektet (eller vara undefined i strikt läge), så this.count skulle inte peka på din instans. Med en pilfunktion använder återanropet naturligtvis this av den yttre metoden.
function Counter() {
this.count = 0;
setInterval(() => {
this.count++;
}, 1000);
}
Tack vare pilen, this inuti intervallet refererar återuppringningen till Counter exempel, inte window. Om den återuppringningen vore en vanlig funktion skulle du antingen behöva .bind(this) eller en mellanliggande variabel som const self = this; att behålla referensen.
Pilfunktioner förenklar också kod med hjälp av arraymetoder, där man ofta inte bryr sig om this alls. När du skickar en traditionell funktion som en återanropsfunktion, är den implicita this är oftast undefined, och du kanske glömmer det. Pilar gör det visuellt uppenbart att funktionen bara är en ren mappning av indata till utgångar.
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2);
Det finns dock viktiga fall där pilfunktioner är fel val, särskilt när du behöver en dynamisk this. Två klassiska antimönster använder pilfunktioner som objektmetoder och som DOM-händelsehanterare som förlitar sig på this att vara elementet.
Tänk dig ett objekt som spårar en katts liv:
const cat = {
lives: 9,
jump: () => {
this.lives--; // bug: this is not cat
},
};
cat.jump();
Eftersom jump är en pil, this hänvisar inte till cat men till vad som helst this var där objektliteralen skapades (ofta det globala objektet). Den avsedda this.lives-- antingen kastar (i strikt läge) eller muterar tyst något orelaterat. Att använda en vanlig metodsyntax här är rätt drag.
DOM-händelselyssnare är liknande: standardmönstret this.classList.toggle('on') inuti ett händelseåteranrop är beroende av this att vara det element som utlöste händelsen. Med en pilfunktion, this pekar inte längre på elementet, så koden går sönder.
const button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on'); // this is not button
});
I den här situationen bör hanteraren vara en normal funktion så att this är bunden av webbläsaren till knappelementet. Pilfunktioner fungerar helt enkelt inte som drop-in-ersättningar om din logik förväntar sig this att vara det dynamiska händelsemålet.
En annan subtil nackdel är att pilfunktioner är syntaktiskt anonyma. De har vanligtvis inget eget namn (utöver eventuella variabler de är tilldelade till), vilket kan göra stackspårningar något mindre beskrivande och rekursion lite knepigare. I den mesta verkliga kodformen är det en hanterbar avvägning, men det är värt att komma ihåg.
Specialfall: getters, setters, bundna metoder och udda hörn
Getters och setters följer samma "call site"-regel: this är det objekt som egenskapen nås på, inte det där den ursprungligen definierades. Om en getter ärvs från en prototyp och du anropar den på ett härlett objekt, this inuti gettern refererar till det härledda objektet.
Bundna metoder skapade med Function.prototype.bind ge dig ett beteende som liknar pilfunktioner, men på nivån av vanliga funktioner. När du ringer f.bind(obj), skapar du en ny funktion vars this är permanent fixerad till obj, oavsett hur den anropas. Detta kan vara användbart i klasser när du behöver bevara this även om en metod är frikopplad.
class Example {
constructor() {
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log(this); // always the instance
}
}
Nackdelen med både bundna metoder och pilfunktioner som används som instansfält är att varje instans får sin egen kopia av funktionen, vilket kan öka minnesanvändningen. Denna avvägning är vanligtvis acceptabel när du bara binder ett litet antal metoder som ofta frikopplas, men det är något att vara medveten om i prestandakritisk kod.
Det finns också några äldre hörnfall där this beter sig annorlunda, till exempel inom en föråldrad with uttalande. Inuti en with (obj) { ... } block, anropar en funktion som är en egenskap hos obj fungerar effektivt som om du hade skrivit obj.method(), Så this är bunden till objModern kod bör undvika with, men förståelsen av detta undantag klargör att this beror fortfarande fundamentalt på hur ett funktionsanrop utformas.
Inline-händelsehanterare i HTML har också en speciell regel: den omgivande inline-hanterarkoden ser this som elementet, men inre funktioner definierade inuti den hanteraren återgår till det vanliga this regler. Så en inre traditionell funktion, inte bunden till någonting, kommer vanligtvis att se this as globalThis (eller undefined i strikt läge), inte elementet.
Slutligen, kom ihåg att pilfunktioner inte har en prototype egenskap och kan inte användas som konstruktorer med new. Försök new MyArrow() kommer att utlösa ett TypeError. Om du behöver en funktion som kan fungera som en konstruktor måste du använda en vanlig funktion eller en klass.
Att ha dessa detaljer i åtanke gör det mycket enklare att välja mellan pilfunktioner och traditionella funktioner. Använd pilar där du vill ha lexikalt this och koncis syntax, och återgå till vanliga funktioner när du behöver den dynamiska, anropsbaserade webbplatsdrivna this beteende eller konstruktorsemantik.
När du väl internaliserat hur this är bunden i varje situation, blir pilfunktioner en kraftfull allierad istället för en överraskande källa till buggar. De effektiviserar vanliga mönster som återanrop och enkla transformationer, medan vanliga funktioner fortsätter att hantera roller som är beroende av sina egna. this bindning, såsom metoder, konstruktörer och dynamiska händelsehanterare.