Jakiego rodzaju jest słowo kluczowe „zwrot”?

95

Używamy instrukcji return opcjonalnie w funkcjach JavaScript. To słowo kluczowe. Ale jaki jest rzeczywisty typ returnsiebie. Właściwie zdezorientowałem się, widząc przykład:

function add(a, b) {
  return (
    console.log(a + b),
    console.log(arguments)
  );
}

add(2, 2);

Wynik:

4
[2, 2]

Możemy więc przekazać do returninstrukcji wyrażenia oddzielone przecinkami . Czy to jest funkcja?

A zaczynając od tego, czy możemy zgadnąć, że każde słowo kluczowe w JavaScript jest ostatecznie funkcją?

Napisałem mały blog jako sedno tej dyskusji. Możesz to sprawdzić tutaj .

Arnab Das
źródło
106
Kiedy myślałeś, że rozumiesz JavaScript, pojawia się coś takiego ...
FP,
12
Wsporniki nie znaczy, że jesteś wywołanie funkcji, na przykład: var a = (2 + 2).
alexmac
12
Bardziej odpowiednim przykładem jest var a = (1, 2)- spowoduje to, że otrzymamy awartość 2ze względu na sposób działania operatora przecinka (tak, przecinek jest operatorem matematycznym, takim jak +lub |itp., A konkretnie w rachunku lambada operator przecinka implementuje kombinator K)
slebetman
6
Wtedy właściwym działaniem jest obalenie błędnego przekonania, a nie głosowanie przeciw.
el.pescado
5
@FlorianPeschka niezupełnie, jest to standardowe zachowanie w każdym języku o smaku C.
djechlin

Odpowiedzi:

129

Ale jaki jest rzeczywisty typ samego „zwrotu”.

Nie ma typu, nie jest wartością.

Próba typeof return;da ci Unexpected token return.

Więc możemy przekazać wyrażenia oddzielone przecinkami do instrukcji return. Czy to jest funkcja?

Nie, chociaż nawiasy mogą służyć do wywoływania funkcji, tutaj są one operatorem grupowania zawierającym kilka wyrażeń oddzielonych przecinkiem .

Bardziej użyteczną demonstracją byłoby:

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

console.log(add(2, 2));

Który wyprowadza, 0ponieważ wynik a + bjest ignorowany (znajduje się po lewej stronie operatora przecinka) i a - bjest zwracany.

Quentin
źródło
Ale w powyższym przykładzie każde z wyrażeń jest wykonywane po kolei. Myślę, że przypadek, który przedstawiłeś, różni się od tego, co podano w pierwotnym pytaniu. Chcę powiedzieć, gdzie jest stosowane pierwszeństwo operatora grupy.
Arnab Das
3
@ArnabDas - Tak, są wykonywane, ale returnnie widzą ich, o co prosiłeś .
Quentin
@ArnabDas - „Chcę powiedzieć, gdzie jest pierwszeństwo operatora grupy”. - Oczywiście w ramach operatora grupującego.
Quentin
11
@ArnabDas - Tak. a + bpo prostu nie ma widocznego efektu, ponieważ nie ma skutków ubocznych, a wartość jest odrzucana (cóż, biorąc pod uwagę, że nie ma praktycznego efektu, możliwe jest, że optymalizujący kompilator może ją usunąć, ale nie ma to praktycznej różnicy).
Quentin
1
@Roman, to prawda. Jedynym sposobem „pominięcia” wyrażenia, aby nie zostało wykonane (poza warunkami podobnymi do warunku if), jest ocena zwarcia. Operator przecinka nie powoduje zwarcia, więc obliczy obie strony. Wtedy zwróci ostatni. Jest to wyraźnie celem operatora przecinka, aby zgrupować wyrażenia w jednej instrukcji (zwykle wynikowa wartość całej instrukcji jest ignorowana), najczęściej używany do inicjalizacji zmiennej:var i = 2, j = 3, k = 4;
Jason
32

Jestem trochę zszokowany, że nikt tutaj nie odniósł się bezpośrednio do specyfikacji :

12.9 Składnia instrukcji return ReturnStatement: return; return [tu nie ma LineTerminator] Expression;

Semantyka

Program ECMAScript jest uważany za niepoprawny składniowo, jeśli zawiera instrukcję powrotu, której nie ma w obiekcie FunctionBody. Instrukcja return powoduje, że funkcja przerywa wykonywanie i zwraca wartość do obiektu wywołującego. Jeśli pominięto wyrażenie, wartość zwracana jest niezdefiniowana. W przeciwnym razie zwracana wartość jest wartością wyrażenia.

Wartość ReturnStatement jest szacowana w następujący sposób:

Jeśli wyrażenie nie jest obecne, zwróć (return, undefined, empty). Niech exprRefbędzie wynikiem oceny Wyrażenie. Wróć (return, GetValue(exprRef), empty).

Tak więc, ze względu na specyfikację, Twój przykład brzmi:

return ( GetValue(exprRef) )

gdzie exprRef = console.log(a + b), console.log(arguments)

Który zgodnie ze specyfikacją operatora przecinka ...

Semantyka

Wyrażenie produkcji: Expression, AssignmentExpression jest oceniane w następujący sposób:

Let lref be the result of evaluating Expression.
Call GetValue(lref).
Let rref be the result of evaluating AssignmentExpression.
Return GetValue(rref).

... oznacza, że ​​każde wyrażenie zostanie ocenione, aż do ostatniego elementu na liście przecinków, który stanie się wyrażeniem przypisania. Więc twój kod return (console.log(a + b) , console.log(arguments))będzie

1.) wydrukować wynik a + b

2.) Nic nie zostało do wykonania, więc wykonaj następne wyrażenie which

3.) drukuje arguments, a ponieważ console.log()nie określa instrukcji return

4.) Ocenia jako niezdefiniowany

5.) Który jest następnie zwracany dzwoniącemu.

Więc prawidłowa odpowiedź brzmi, returnnie ma typu, zwraca tylko wynik jakiegoś wyrażenia.

Na następne pytanie:

Więc możemy przekazać wyrażenia oddzielone przecinkami do instrukcji return. Czy to jest funkcja?

Nie. Przecinek w JavaScript jest operatorem zdefiniowanym w celu umożliwienia łączenia wielu wyrażeń w jeden wiersz i jest definiowany przez specyfikację w celu zwrócenia wartościowanego wyrażenia tego, co jest ostatnie na liście.

Nadal mi nie wierzysz?

<script>
alert(foo());
function foo(){
    var foo = undefined + undefined;
    console.log(foo);
    return undefined, console.log(1), 4;
}
</script>

Zagraj z tym kodem tutaj i pomieszaj z ostatnią wartością na liście. Zawsze zwróci ostatnią wartość na liście, w twoim przypadku tak się stanieundefined.

Na twoje ostatnie pytanie,

A zaczynając od tego, czy możemy zgadnąć, że każde słowo kluczowe w JavaScript jest ostatecznie funkcją?

Znowu nie. Funkcje mają bardzo specyficzną definicję w języku. Nie będę go tutaj przedrukowywać, ponieważ ta odpowiedź jest już bardzo długa.

avgvstvs
źródło
15

Testowanie, co się dzieje, gdy zwracasz wartości w nawiasach:

function foo() {
    return (1, 2);
}

console.log(foo());

Daje odpowiedź 2, więc wydaje się, że lista wartości oddzielonych przecinkami jest wynikiem ostatniego elementu na liście.

Naprawdę, nawiasy nie mają tutaj znaczenia, grupują operacje zamiast oznaczać wywołanie funkcji. Zaskakujące jest jednak to, że przecinek jest tutaj legalny. Znalazłem interesujący wpis na blogu o tym, jak radzimy sobie z przecinkiem:

https://javascriptweblog.wordpress.com/2011/04/04/the-javascript-comma-operator/

SpoonMeiser
źródło
Myślę więc, że coś takiego return ([1, 2])powinno wypisać obie wartości?
cst1992
1
To byłoby zwrócenie tablicy.
Testujemy pokrycie
console.log((1, 2))
thedayturns
9

returnnie jest funkcją. Jest to kontynuacja funkcji, w której występuje.

Pomyśl o instrukcji alert (2 * foo(bar));gdzie foojest nazwą funkcji. Kiedy ją oceniasz, widzisz, że musisz odłożyć na chwilę resztę zdania, aby skoncentrować się na ocenie foo(bar). Możesz wizualizować część, którą odłożyłeś na bok, jako coś w rodzaju alert (2 * _), z pustym miejscem do wypełnienia. Kiedy wiesz, jaka jest wartość foo(bar), podnosisz ją ponownie.

To, co odłożyłeś na bok, to kontynuacja rozmowy foo(bar).

Wywołanie returnprzekazuje wartość do tej kontynuacji.

Kiedy oceniasz funkcję wewnątrz foo, reszta fooczeka, aż ta funkcja zostanie zredukowana do wartości, a następnie fooponownie podnosi. Nadal masz cel do oceny foo(bar), po prostu został wstrzymany.

Kiedy oceniasz returnwewnątrz foo, żadna część nie fooczeka na wartość. returnnie zmniejsza się do wartości w miejscu, w fooktórym został użyty. Zamiast tego powoduje, że całe wywołanie zostaje foo(bar) zredukowane do wartości, a cel „oceny foo(bar)” jest uważany za zakończony i zdmuchnięty.

Ludzie zwykle nie mówią ci o kontynuacji, gdy dopiero zaczynasz programować. Myślą o tym jako o zaawansowanym temacie tylko dlatego, że jest kilka bardzo zaawansowanych rzeczy, które ludzie ostatecznie robią z kontynuacją. Ale prawda jest taka, że ​​używasz ich przez cały czas, za każdym razem, gdy wywołujesz funkcję.

Nathan Ellis Rasmussen
źródło
Ten rodzaj myślenia jest powszechny w językach funkcjonalnych, takich jak LISP i Haskell, ale nigdy nie widziałem go używanego w językach proceduralnych, takich jak javascript. Zazwyczaj nie myśli się o nich jako o kontynuacjach, ponieważ większość języków proceduralnych traktuje je jako przypadek szczególny. Na przykład nie można zapisać kontynuacji programu alert(2 * _)do późniejszego wykorzystania w Javascript (istnieje inna notacja do tego rodzaju czynności).
Cort Ammon
1
@CortAmmon Chociaż w pewnym sensie się z tobą zgadzam, zauważ, że Javascript ma dość mocne tło funkcjonalne (pierwotnie miał być implementacją podzbioru Scheme ze składnią podobną do C) i chociaż nie ma żadnych standardowych funkcji na kontynuacje manipulacji (np wezwanie / cc), to jest powszechną praktyką stosowania kontynuacji przechodzącą styl w wielu bibliotek javaScript, więc zrozumienia koncepcji we wczesnym stadium jest bardzo przydatne dla programistów JS.
Jules
Prawdą jest, że Javascript nie wykonuje pierwszorzędnych kontynuacji. returnOd wewnątrz foonie jest wartością; nie można było go spakować w strukturę danych, przypisać do zmiennej, poczekać chwilę, a następnie podać coś do niej później - a zwłaszcza nie można było podawać tego więcej niż raz. Ale, jak powiedziałem, zaawansowane tematy.
Nathan Ellis Rasmussen
Podobnie wdrażanie nowej struktury kontroli z wykorzystaniem kontynuacji: zaawansowane. Ale próba zaimplementowania my_iffunkcji, która zachowuje się tak samo jak wbudowana if()then{}else{}, i nie udaje się to zrobić, nawet jeśli pozwolisz sobie na użycie wbudowanej w niej funkcji - niezbyt zaawansowana i niezwykle pouczająca, zwłaszcza jeśli zamierzasz skończyć w kodzie, który przekazuje wywołania zwrotne.
Nathan Ellis Rasmussen
6

returntutaj jest czerwony śledź. Być może interesująca jest następująca odmiana:

function add(a, b) {
  return (
    console.log(a + b),
    console.log(arguments)
  );
}

console.log(add(2, 2));

który wyprowadza jako ostatnią linię

undefined

ponieważ funkcja w rzeczywistości nic nie zwraca. (Zwróciłby wartość zwracaną przez sekundę console.log, gdyby ją posiadał).

W obecnej sytuacji kod jest dokładnie taki sam jak

function add(a, b) {
    console.log(a + b);
    console.log(arguments);
}

console.log(add(2, 2));
tys
źródło
1

Ciekawym sposobem zrozumienia instrukcji return jest użycie operatora void Spójrz na ten kod

var console = {
    log: function(s) {
      document.getElementById("console").innerHTML += s + "<br/>"
    }
}

function funReturnUndefined(x,y) {
   return ( void(x+y) );
}

function funReturnResult(x,y) {
   return ( (x+y) );
}

console.log( funReturnUndefined(2,3) );
console.log( funReturnResult(2,3) );
<div id="console" />

Ponieważ returninstrukcja przyjmuje jeden argument, który jest [[expression]]i zwraca go wywołującemu na stosie, tj arguments.callee.caller. Następnie wykona, void(expression)a następnie zwróci undefined, to jest ocena operatora void.

loretoparisi
źródło