Lista zabawnych i podchwytliwych przykładów JavaScript
JavaScript to świetny język. Ma prostą składnię, duży ekosystem i, co najważniejsze, wspaniałą społeczność.
Jednocześnie wszyscy wiemy, że JavaScript jest dość zabawnym językiem z podchwytliwymi częściami. Niektóre z nich mogą szybko zamienić naszą codzienną pracę w piekło, a niektóre mogą rozśmieszyć nas na głos.
Oryginalny pomysł na WTFJS należy do Brian Leroux. Ta lista jest bardzo zainspirowana jego przemową “WTFJS” na dotJS 2012:
Możesz zainstalować ten podręcznik za pomocą npm
. Po prostu uruchom:
$ npm install -g wtfjs
Powinieneś być teraz w stanie uruchomić wtfjs
w linii poleceń. Spowoduje to otwarcie instrukcji w wybranym $PAGER
. W przeciwnym razie możesz kontynuować czytanie tutaj.
Źródło jest dostępne tutaj: https://github.com/denysdovhan/wtfjs
Obecnie są następujące tłumaczenia wtfjs:
- 💪🏻 Motywacja
- ✍🏻 Notacja
- 👀 Przykłady
[]
jest równe![]
true
nie jest równe![]
, ale też nie równe[]
- prawda to fałsz
- baNaNa
NaN
nie jestNaN
- To jest fail
[]
jest prawdziwe, ale nietrue
null
jest fałszywe, ale niefalse
document.all
jest obiektem, ale jest undefined- Minimalna wartość jest większa od zera
- funkcja nie jest funkcją
- Dodawanie tablic
- Trailing commas in array
- Równość tablic to potwór
undefined
orazNumber
parseInt
jest złym gościem- Matematyka z
true
ifalse
- Komentarze HTML są obowiązujące w JavaScript
NaN
isnota number[]
inull
są obiektami- Magicznie rosnące liczby
- Precyzja
0.1 + 0.2
- Patching numbers
- Porównanie trzech liczb
- Zabawna matematyka
- Dodanie RegExps
- Stringi nie są instancjami
String
- Wywoływanie funkcji za pomocą backticksa
- Call call call
- Właściwość
constructor
- Obiekt jako klucz właściwości obiektu
- Dostęp do prototypów za pomocą
__proto__
`${{Object}}`
- Destrukturyzacja z wartościami domyślnymi
- Dots and spreading
- Etykiety
- Zagnieżdżone etykiety
- Podstępny
try..catch
- Czy to wielokrotne dziedziczenie?
- A generator which yields itself
- Klasa klasy
- Non-coercible objects
- Podstępne funkcje strzałkowe
- Funkcje strzałkowe nie mogą być konstruktorami
arguments
i funkcje strzałkowe- Podstępny return
- Chaining assignments on object
- Dostęp do właściwości obiektu za pomocą tablic
- Null and Relational Operators
Number.toFixed()
display different numbersMath.max()
mniej niżMath.min()
- Comparing
null
to0
- Redeklaracja tej samej zmiennej
- Domyślne zachowanie Array.prototype.sort()
- 📚 Inne materiały
- 🎓 Licencja
Dla zabawy
— “Just for Fun: The Story of an Accidental Revolutionary”, Linus Torvalds
Głównym celem tej listy jest zebranie szalonych przykładów i wyjaśnienie, w jaki sposób działają, jeśli to możliwe. Tylko dlatego, że fajnie jest nauczyć się czegoś, czego wcześniej nie znaliśmy.
Jeśli jesteś początkujący, możesz skorzystać z tych notatek, aby głębiej zagłębić się w JavaScript. Mam nadzieję, że te notatki zmotywują cię do spędzenia więcej czasu na czytaniu specyfikacji.
Jeśli jesteś profesjonalnym programistą, możesz rozważyć te przykłady, jako świetne źródło informacji o wszystkich dziwactwach i nieoczekiwanych krawędziach naszego ukochanego JavaScript.
W każdym razie po prostu przeczytaj to. Prawdopodobnie znajdziesz coś nowego.
// ->
służy do wyświetlenia wyniku wyrażenia. Na przykład:
1 + 1; // -> 2
// >
oznacza wynik console.log
lub wyświetlenie innego wyniku. Na przykład:
console.log("hello, world!"); // > hello, world!
//
jest tylko komentarzem używanym w celu wyjaśnienia. Przykład:
// Assigning a function to foo constant
const foo = function() {};
Tablica jest równa zanegowanej tablicy:
[] == ![]; // -> true
Abstrakcyjny operator równości przekształca obie strony na liczby, aby je porównać, a obie strony stają się liczbą 0
z różnych powodów. Tablice są prawdziwe, więc po prawej stronie przeciwieństwem prawdziwej wartości jest false
, który jest następnie wymuszany na 0
. Po lewej jednak pusta tablica jest wymuszana na liczbę, nie będąc najpierw wartością logiczną, a puste tablice są wymuszane na 0
, mimo że są prawdziwe.
Oto jak to wyrażenie upraszcza:
+[] == +![];
0 == +false;
0 == 0;
true;
Zobacz też []
jest prawdziwe, ale nie true
.
Tablica nie jest równa true
, ale zanegowana tablica też nie jest równa true
;
Tablica jest równa false
, zanegowana tablica również jest równa false
:
true == []; // -> false
true == ![]; // -> false
false == []; // -> true
false == ![]; // -> true
true == []; // -> false
true == ![]; // -> false
// According to the specification
true == []; // -> false
toNumber(true); // -> 1
toNumber([]); // -> 0
1 == 0; // -> false
true == ![]; // -> false
![]; // -> false
true == false; // -> false
false == []; // -> true
false == ![]; // -> true
// According to the specification
false == []; // -> true
toNumber(false); // -> 0
toNumber([]); // -> 0
0 == 0; // -> true
false == ![]; // -> true
![]; // -> false
false == false; // -> true
!!"false" == !!"true"; // -> true
!!"false" === !!"true"; // -> true
Rozważ to krok po kroku:
// true is 'truthy' and represented by value 1 (number), 'true' in string form is NaN.
true == "true"; // -> false
false == "false"; // -> false
// 'false' is not the empty string, so it's a truthy value
!!"false"; // -> true
!!"true"; // -> true
"b" + "a" + +"a" + "a"; // -> 'baNaNa'
To stary żart w JavaScript, ale odnowiony. Oto oryginał:
"foo" + +"bar"; // -> 'fooNaN'
Wyrażenie jest oceniane jako 'foo' + (+'bar')
, które konwertuje 'bar'
nie na liczbę.
NaN === NaN; // -> false
Specyfikacja ściśle określa logikę tego zachowania:
- Jeśli
Type(x)
jest różny odType(y)
, zwraca false.- Jeśli
Type(x)
jest Number, wtedy
- Jeśli
x
jest NaN, zwraca false.- Jeśli
y
jest NaN, zwraca false.- … … …
Zgodnie z definicją NaN
z IEEE:
Możliwe są cztery wzajemnie wykluczające się relacje: mniejszy, równy, większy niż i nieuporządkowany. Ostatni przypadek powstaje, gdy co najmniej jednym operandem jest NaN. Każdy NaN porównuje się nieuporządkowany ze wszystkim, w tym samym sobą.
— “What is the rationale for all comparisons returning false for IEEE754 NaN values?” at StackOverflow
Nie uwierzyłbyś, ale …
(![] + [])[+[]] +
(![] + [])[+!+[]] +
([![]] + [][[]])[+!+[] + [+[]]] +
(![] + [])[!+[] + !+[]];
// -> 'fail'
Po rozbiciu masy symboli na części zauważamy, że często występuje następujący wzór:
![] + []; // -> 'false'
![]; // -> false
Więc próbujemy dodać []
do false
. Ale z powodu wielu wywołań funkcji wewnętrznych (binary + Operator
-> ToPrimitive
-> [[DefaultValue]]
) w końcu konwertujemy odpowiedni operand na ciąg:
![] + [].toString(); // 'false'
Myśląc o łańcuchu jako tablicy, możemy uzyskać dostęp do jego pierwszego znaku za pośrednictwem [0]
:
"false"[0]; // -> 'f'
Reszta jest oczywista, ale i
jest podchwytliwe. i
w fail
jest pobierany przez generowanie ciągu 'falseundefined'
i łapanie elementu na indeks ['10']
Tablica jest prawdziwą wartością, jednak nie jest równa true
.
!![] // -> true
[] == true // -> false
Oto linki do odpowiednich sekcji specyfikacji ECMA-262:
Pomimo faktu, żenull
jest wartością fałszywą, nie jest równa false
.
!!null; // -> false
null == false; // -> false
W tym samym czasie inne wartości fałszywe, takie jak 0
lub ''
są równe do false
.
0 == false; // -> true
"" == false; // -> true
Wytłumaczenie jest takie samo jak w poprzednim przykładzie. Oto odpowiedni link:
⚠️ Jest to część interfejsu API przeglądarki i nie będzie działać w środowisku Node.js⚠️
Pomimo faktu, że document.all
jest obiektem tablicowym i daje dostęp do węzłów DOM na stronie, odpowiada na funkcję typeof
jako undefined
.
document.all instanceof Object; // -> true
typeof document.all; // -> 'undefined'
W tym samym czasie, document.all
nie jest równe undefined
.
document.all === undefined; // -> false
document.all === null; // -> false
Ale w tym samym czasie:
document.all == null; // -> true
document.all
kiedyś był sposobem na dostęp do elementów DOM, w szczególności w starszych wersjach IE. Chociaż nigdy nie był standardem, był szeroko stosowany w starszym kodzie JS. Kiedy standard rozwijał się z nowymi interfejsami API (takimi jakdocument.getElementById
), to wywołanie interfejsu API stało się przestarzałe i komitet standardowy musiał zdecydować, co z nim zrobić. Ze względu na szerokie zastosowanie postanowili zachować interfejs API, ale wprowadzili umyślne naruszenie specyfikacji JavaScript. Powód, dla którego reaguje nafalse
podczas korzystania ze Strict Equality Comparison zundefined
gdytrue
podczas korzystania z Abstract Equality Comparison wynika z umyślnego naruszenia specyfikacji, która wyraźnie na to pozwala.— “Obsolete features - document.all” na WhatWG - HTML spec — “Chapter 4 - ToBoolean - Falsy values” na YDKJS - Types & Grammar
Number.MIN_VALUE
jest najmniejszą liczbą, która jest większa od zera:
Number.MIN_VALUE > 0; // -> true
Number.MIN_VALUE
jest5e-324
, np. najmniejsza liczba dodatnia, która może być reprezentowana z precyzją zmiennoprzecinkową, tj. jest tak blisko, jak można dojść do zera. Określa najlepszą rozdzielczość, jaką mogą zaoferować floaty.Teraz ogólna najmniejsza wartość to
Number.NEGATIVE_INFINITY
chociaż nie jest to tak naprawdę liczbowe w ścisłym tego słowa znaczeniu.— “Why is
0
less thanNumber.MIN_VALUE
in JavaScript?” na StackOverflow
⚠️ Bug obecny w wersji V8 5.5 lub nowszej (Node.js <=7)⚠️
Wszyscy wiecie o irytującym niezdefiniowany nie jest funkcją, ale co z tym?
// Declare a class which extends null
class Foo extends null {}
// -> [Function: Foo]
new Foo() instanceof null;
// > TypeError: function is not a function
// > at … … …
To nie jest część specyfikacji. To tylko błąd, który został już naprawiony, więc nie powinno być z tym problemu w przyszłości.
Co jeśli spróbujesz dodać dwie tablice?
[1, 2, 3] + [4, 5, 6]; // -> '1,2,34,5,6'
Zachodzi konkatenacja. Krok po kroku wygląda to tak:
[1, 2, 3] +
[4, 5, 6][
// call toString()
(1, 2, 3)
].toString() +
[4, 5, 6].toString();
// concatenation
"1,2,3" + "4,5,6";
// ->
("1,2,34,5,6");
Utworzyłeś tablicę z 4 pustymi elementami. Mimo wszystko otrzymasz tablicę z trzema elementami ze względu na końcowe przecinki:
let a = [, , ,];
a.length; // -> 3
a.toString(); // -> ',,'
Trailing commas (czasami nazywane "final commas") może być przydatne podczas dodawania nowych elementów, parametrów lub właściwości do kodu JavaScript. Jeśli chcesz dodać nową właściwość, możesz po prostu dodać nową linię bez modyfikowania poprzedniej poprzedniej linii, jeśli linia ta już używa przecinka końcowego. To sprawia, że różnice w kontroli wersji są czystsze, a edycja kodu może być mniej kłopotliwa.
— Trailing commas na MDN
Równość tablic jest potworem w JS, jak widać poniżej:
[] == '' // -> true
[] == 0 // -> true
[''] == '' // -> true
[0] == 0 // -> true
[0] == '' // -> false
[''] == 0 // -> true
[null] == '' // true
[null] == 0 // true
[undefined] == '' // true
[undefined] == 0 // true
[[]] == 0 // true
[[]] == '' // true
[[[[[[]]]]]] == '' // true
[[[[[[]]]]]] == 0 // true
[[[[[[ null ]]]]]] == 0 // true
[[[[[[ null ]]]]]] == '' // true
[[[[[[ undefined ]]]]]] == 0 // true
[[[[[[ undefined ]]]]]] == '' // true
Powinieneś uważnie obserwować powyższe przykłady! Zachowanie opisano w rozdziale 7.2.13 Abstract Equality Comparison specyfikacji.
Jeśli nie przekażemy żadnych argumentów do konstruktura Number
, otrzymamy 0
. Wartość undefined
jest przypisana do formalnych argumentów, gdy nie ma rzeczywistych argumentów, więc możesz się spodziewać, że Number
bez argumentów dostanie undefined
jako wartość jego parametru. Jednak kiedy przekażemy undefined
, dostaniemy NaN
.
Number(); // -> 0
Number(undefined); // -> NaN
Zgodnie ze specyfikacją:
- Jeśli do wywołania tej funkcji nie zostaną przekazane żadne argumenty, pozwól
n
być+0
. - Inaczej, pozwól
n
być ?ToNumber(value)
. - W przypadku
undefined
,ToNumber(undefined)
powinno zwrócićNaN
.
Oto odpowiednia sekcja:
parseInt
słynie ze swoich dziwactw:
parseInt("f*ck"); // -> NaN
parseInt("f*ck", 16); // -> 15
💡 Wytłumaczenie: Dzieje się tak, ponieważ parseInt
będzie kontynuować analizowanie znak po znaku, dopóki nie trafi na postać, której nie zna. f
w 'f*ck'
jest cyfrą szesnastkową 15
.
Parsowanie Infinity
do integer jest czymś…
//
parseInt("Infinity", 10); // -> NaN
// ...
parseInt("Infinity", 18); // -> NaN...
parseInt("Infinity", 19); // -> 18
// ...
parseInt("Infinity", 23); // -> 18...
parseInt("Infinity", 24); // -> 151176378
// ...
parseInt("Infinity", 29); // -> 385849803
parseInt("Infinity", 30); // -> 13693557269
// ...
parseInt("Infinity", 34); // -> 28872273981
parseInt("Infinity", 35); // -> 1201203301724
parseInt("Infinity", 36); // -> 1461559270678...
parseInt("Infinity", 37); // -> NaN
Uważaj na parsowanie null
także:
parseInt(null, 24); // -> 23
💡 Wytłumaczenie:
Konwertuje
null
na string"null"
i próbuje to przekonwertować. W przypadku podstaw od 0 do 23 nie ma cyfr, które mógłby przekonwertować, więc zwraca NaN. Na 24,"n"
, 14ta litera, jest dodawana do systemu liczbowego. Na 31,"u"
, 21sza litera, jest dodawana, a cały ciąg można dekodować. Na 37 nie ma już żadnego poprawnego zestawu liczb, który można by wygenerować iNaN
jest zwrócony.— “parseInt(null, 24) === 23… wait, what?” na StackOverflow
Nie zapomnij o ósemkach:
parseInt("06"); // 6
parseInt("08"); // 8 if support ECMAScript 5
parseInt("08"); // 0 if not support ECMAScript 5
💡 Wytłumaczenie: Jeśli ciąg wejściowy zaczyna się od "0", podstawa to osiem (ósemka) lub 10 (dziesiętnie). To, która podstawa jest wybrana, zależy od implementacji. ECMAScript 5 określa, że używana jest liczba 10 (dziesiętna), ale nie wszystkie przeglądarki obsługują to jeszcze. Z tego powodu zawsze określaj podstawę podczas używania parseInt
.
parseInt
zawsze konwertuj dane wejściowe na ciąg:
parseInt({ toString: () => 2, valueOf: () => 1 }); // -> 2
Number({ toString: () => 2, valueOf: () => 1 }); // -> 1
Zachowaj ostrożność podczas analizowania wartości zmiennoprzecinkowych
parseInt(0.000001); // -> 0
parseInt(0.0000001); // -> 1
parseInt(1 / 1999999); // -> 5
💡 Wytłumaczenie: ParseInt
pobiera argument ciągu i zwraca liczbę całkowitą określonej podstawy. ParseInt
usuwa również wszystko po pierwszej wartości cyfrowej i włącznie z nią w parametrze ciągu. 0.000001
jest konwertowany na ciąg znaków "0.000001"
i parseInt
zwraca 0
. Gdy 0.0000001
jest konwertowany na ciąg, który jest traktowany jako "1e-7"
i stąd parseInt
zwraca 1
. 1/1999999
jest interpretowane jako 5.00000250000125e-7
i parseInt
zwraca 5
.
Zróbmy trochę matematyki:
true +
true(
// -> 2
true + true
) *
(true + true) -
true; // -> 3
Hmmm… 🤔
Możemy narzucić wartości do liczb za pomocą konstruktora Number
. To całkiem oczywiste że true
będzie zmienione na 1
:
Number(true); // -> 1
Jednoargumentowy operator plus próbuje przeliczyć swoją wartość na liczbę. Może konwertować reprezentacje ciągu liczb całkowitych i liczb zmiennoprzecinkowych, a także wartości nie łańcuchowe true
, false
, i null
. Jeśli nie może przeanalizować określonej wartości, oceni to jako NaN
. To oznacza, że możemy narzucić true
na 1
łatwiej:
+true; // -> 1
Podczas dodawania lub mnożenia, metoda ToNumber
jest przywoływana. Zgodnie ze specyfikacją ta metoda zwraca:
Jeśli
argument
jest true, zwraca 1. Jeśliargument
jest false, zwraca +0.
Dlatego możemy dodawać wartości logiczne jako liczby regularne i uzyskiwać prawidłowe wyniki.
Odpowiednie sekcje:
Będziesz pod wrażeniem, ale <!--
(który jest znany jako komentarz HTML) jest poprawnym komentarzem w JavaScript.
// valid comment
<!-- valid comment too
Pod wrażeniem? Komentarze w formacie HTML miały umożliwić przeglądarkom, które nie rozumieją tagu <script>
degradować z wdziękiem. Te przeglądarki, np. Netscape 1.x nie są już popularne. Dlatego naprawdę nie ma sensu umieszczać komentarzy HTML w tagach skryptu.
Ponieważ Node.js jest oparty na silniku V8, komentarze podobne do HTML są obsługiwane również przez środowisko uruchomieniowe Node.js. Ponadto są częścią specyfikacji:
Typ NaN
jest 'number'
:
typeof NaN; // -> 'number'
Wytłumaczenia jak operatory typeof
i instanceof
działają:
typeof []; // -> 'object'
typeof null; // -> 'object'
// however
null instanceof Object; // false
Zachowanie operatora typeof
jest zdefiniowane w tej sekcji specyfikacji:
Zgodnie ze specyfikacją, operator typeof
zwraca ciąg zgodnie z Table 35: typeof
Operator Results. For null
, ordinary, standard exotic and non-standard exotic objects, which do not implement [[Call]]
, it returns the string "object"
.
Możesz jednak sprawdzić typ obiektu, używając metody toString
.
Object.prototype.toString.call([]);
// -> '[object Array]'
Object.prototype.toString.call(new Date());
// -> '[object Date]'
Object.prototype.toString.call(null);
// -> '[object Null]'
999999999999999; // -> 999999999999999
9999999999999999; // -> 10000000000000000
10000000000000000; // -> 10000000000000000
10000000000000000 + 1; // -> 10000000000000000
10000000000000000 + 1.1; // -> 10000000000000002
Jest to spowodowane standardem IEEE 754-2008 dla binarnej arytmetyki zmiennoprzecinkowej. W tej skali zaokrągla się do najbliższej liczby parzystej. Czytaj więcej:
- 6.1.6 The Number Type
- IEEE 754 on Wikipedia
Dobrze znany żart. An addition of 0.1
and 0.2
is deadly precise:
0.1 +
0.2(
// -> 0.30000000000000004
0.1 + 0.2
) ===
0.3; // -> false
Odpowiedź na pytanie ”Is floating point math broken?” ze StackOverflow:
Stałe
0.2
i0.3
w twoim programie będzie również przybliżenie ich prawdziwych wartości. Zdarza się, że najbliższadouble
do0.2
jest większa niż liczba wymierna0.2
, ale najbliższadouble
do0.3
jest mniejsza niż liczba wymierna0.3
. Suma0.1
i0.2
kończy się na wartości większej od liczby wymiernej0.3
, a zatem nie zgadza się ze stałą w kodzie.
Ten problem jest tak znany, że istnieje nawet strona internetowa o nazwie 0.30000000000000004.com. Występuje w każdym języku wykorzystującym matematykę zmiennoprzecinkową, nie tylko JavaScript.
Możesz dodać własne metody do wrapowania obiektów takich jak Number
lub String
.
Number.prototype.isOne = function() {
return Number(this) === 1;
};
(1.0).isOne(); // -> true
(1).isOne(); // -> true
(2.0)
.isOne()(
// -> false
7
)
.isOne(); // -> false
Oczywiście możesz rozszerzyć obiekt Number
jak każdy inny obiekt w JavaScript. Jednak nie jest zalecane, jeśli zachowanie zdefiniowanej metody nie jest częścią specyfikacji. Oto lista właściwości Number
:
1 < 2 < 3; // -> true
3 > 2 > 1; // -> false
Dlaczego to działa w ten sposób? Problem tkwi w pierwszej części wyrażenia. Oto jak to działa:
1 < 2 < 3; // 1 < 2 -> true
true < 3; // true -> 1
1 < 3; // -> true
3 > 2 > 1; // 3 > 2 -> true
true > 1; // true -> 1
1 > 1; // -> false
Możemy to naprawić za pomocą Operatora większy lub równy (>=
):
3 > 2 >= 1; // true
Przeczytaj więcej o operatorach relacyjnych w specyfikacji:
Często wyniki operacji arytmetycznych w JavaScript mogą być dość nieoczekiwane. Rozważ te przykłady:
3 - 1 // -> 2
3 + 1 // -> 4
'3' - 1 // -> 2
'3' + 1 // -> '31'
'' + '' // -> ''
[] + [] // -> ''
{} + [] // -> 0
[] + {} // -> '[object Object]'
{} + {} // -> '[object Object][object Object]'
'222' - -'111' // -> 333
[4] * [4] // -> 16
[] * [] // -> 0
[4, 4] * [4, 4] // NaN
Co dzieje się w pierwszych czterech przykładach? Oto mała tabelka, aby zrozumieć dodawanie w JavaScript:
Number + Number -> addition
Boolean + Number -> addition
Boolean + Boolean -> addition
Number + String -> concatenation
String + Boolean -> concatenation
String + String -> concatenation
Co z innymi przykładami? Metody ToPrimitive
i ToString
są domyślnie wywoływane dla []
i {}
przed dodaniem. Przeczytaj więcej o procesie oceny w specyfikacji:
- 12.8.3 The Addition Operator (
+
) - 7.1.1 ToPrimitive(
input
[,PreferredType
]) - 7.1.12 ToString(
argument
)
Szczególnie, {} + []
tutaj jest wyjątek. Powód, dla którego się różni z [] + {}
polega na tym, że bez nawiasów interpretuje się go jako blok kodu, a następnie jako jedność +, konwertując []
na liczbę. Wygląda następująco:
{
// a code block here
}
+[]; // -> 0
Aby uzyskać ten sam wynik jak [] + {}
możemy owrapować to w nawias.
({} + []); // -> [object Object]
Czy wiesz, że możesz dodawać takie liczby?
// Patch a toString method
RegExp.prototype.toString =
function() {
return this.source;
} /
7 /
-/5/; // -> 2
"str"; // -> 'str'
typeof "str"; // -> 'string'
"str" instanceof String; // -> false
Konstruktor String
zwraca string:
typeof String("str"); // -> 'string'
String("str"); // -> 'str'
String("str") == "str"; // -> true
Spróbujmy z new
:
new String("str") == "str"; // -> true
typeof new String("str"); // -> 'object'
Obiekt? Co to jest?
new String("str"); // -> [String: 'str']
Więcej informacji o konstruktorze String w specyfikacji:
Zadeklarujmy funkcję, która rejestruje wszystkie parametry w konsoli:
function f(...args) {
return args;
}
Bez wątpienia wiesz, że możesz wywołać tę funkcję w następujący sposób:
f(1, 2, 3); // -> [ 1, 2, 3 ]
Ale czy wiesz, że możesz wywołać dowolną funkcję za pomocą backticksa?
f`true is ${true}, false is ${false}, array is ${[1, 2, 3]}`;
// -> [ [ 'true is ', ', false is ', ', array is ', '' ],
// -> true,
// -> false,
// -> [ 1, 2, 3 ] ]
Cóż, to wcale nie jest magia, jeśli jesteś obeznany z Tagged template literals. W powyższym przykładzie, funkcja f
jest znacznikiem literału szablonu. Tagi przed literałem szablonu umożliwiają analizowanie literałów szablonu za pomocą funkcji. Pierwszy argument funkcji znacznika zawiera tablicę wartości ciągów. Pozostałe argumenty są powiązane z wyrażeniami. Przykład:
function template(strings, ...keys) {
// do something with strings and keys…
}
To jest magia z tyłu słynnej biblioteki o nazwie 💅 styled-components, która jest popularna w społeczności React.
Link do specyfikacji:
Znalezione przez @cramforce
console.log.call.call.call.call.call.apply(a => a, [1, 2]);
Uwaga, może to popsuć ci umysł! Spróbuj odtworzyć ten kod w swojej głowie: stosujemy metodę call
za pomocą metodyapply
. Czytaj więcej:
- 19.2.3.3 Function.prototype.call(
thisArg
, ...args
) - **19.2.3.1 ** Function.prototype.apply(
thisArg
,argArray
)
const c = "constructor";
c[c][c]('console.log("WTF?")')(); // > WTF?
Rozważmy ten przykład krok po kroku:
// Declare a new constant which is a string 'constructor'
const c = "constructor";
// c is a string
c; // -> 'constructor'
// Getting a constructor of string
c[c]; // -> [Function: String]
// Getting a constructor of constructor
c[c][c]; // -> [Function: Function]
// Call the Function constructor and pass
// the body of new function as an argument
c[c][c]('console.log("WTF?")'); // -> [Function: anonymous]
// And then call this anonymous function
// The result is console-logging a string 'WTF?'
c[c][c]('console.log("WTF?")')(); // > WTF?
Object.prototype.constructor
zwraca referencję do funkcji konstruktora Object
który utworzył obiekt instancji. W przypadku łańcuchów jest to String
, w przypadku liczb jest to Number
i tak dalej.
{ [{}]: {} } // -> { '[object Object]': {} }
Dlaczego to działa? Tutaj używamy Computed property name. Gdy przekazujesz obiekt między tymi nawiasami, wymusza on obiekt na ciąg, więc otrzymujemy klucz właściwości '[object Object]'
i wartość {}
.
Możemy zrobić "brackets hell" jak tutaj:
({ [{}]: { [{}]: {} } }[{}][{}]); // -> {}
// structure:
// {
// '[object Object]': {
// '[object Object]': {}
// }
// }
Przeczytaj więcej na temat literałów obiektowych tutaj:
Jak wiemy, prymitywy nie mają prototypów. Jeśli jednak spróbujemy uzyskać wartość __proto__
dla prymitywów otrzymalibyśmy to:
(1).__proto__.__proto__.__proto__; // -> null
Dzieje się tak, ponieważ gdy coś nie ma prototypu, zostanie ono zawinięte w obiekt wrappera za pomocą metody ToObject
. Więc krok po kroku:
(1)
.__proto__(
// -> [Number: 0]
1
)
.__proto__.__proto__(
// -> {}
1
).__proto__.__proto__.__proto__; // -> null
Oto więcej informacji na temat __proto__
:
Jaki jest wynik poniższego wyrażenia?
`${{ Object }}`;
Odpowiedź to:
// -> '[object Object]'
Zdefiniowaliśmy obiekt z właściwością Object
używając Shorthand property notation:
{
Object: Object;
}
Następnie przekazaliśmy ten obiekt do literału szablonu, więc metoda toString
wywołuje ten obiekt. Właśnie dlatego otrzymujemy string '[object Object]'
.
Rozważ ten przykład:
let x,
{ x: y = 1 } = { x };
y;
Powyższy przykład to świetne zadanie na rozmowę kwalifikacyjną. Jaka jest wartość y
? Odpowiedź to:
// -> 1
let x,
{ x: y = 1 } = { x };
y;
// ↑ ↑ ↑ ↑
// 1 3 2 4
W powyższym przykładzie:
- Deklarujemy
x
z brakiem wartości, więc jestundefined
. - Wtedy pakujemy wartość
x
we własność obiektux
. - Następnie wyodrębniamy wartość
x
używając destrukturyzacji i chcemy to przypisać doy
. Jeśli wartość nie zostanie zdefiniowana, wówczas użyjemy „1
jako wartości domyślnej. - Zwróć wartość
y
.
- Object initializer at MDN
Ciekawe przykłady można skomponować z rozmieszczaniem tablic. Rozważ to:
[...[..."..."]].length; // -> 3
Czemu 3
? Kiedy korzystamy ze spread operatora, metoda @@iterator
jest wywołana, a zwrócony iterator służy do uzyskania wartości do iteracji. Domyślny iterator łańcucha rozdziela łańcuch na znaki. Po rozłożeniu pakujemy te znaki do tablicy. Następnie rozkładamy tę tablicę ponownie i pakujemy z powrotem do tablicy.
String '...'
składa się z trzech znaków .
, więc długość wynikowej tablicy wynosi 3
.
Teraz krok po kroku:
[...'...'] // -> [ '.', '.', '.' ]
[...[...'...']] // -> [ '.', '.', '.' ]
[...[...'...']].length // -> 3
Oczywiście możemy rozkładać i wrapować elementy tablicy tyle razy, ile chcemy:
[...'...'] // -> [ '.', '.', '.' ]
[...[...'...']] // -> [ '.', '.', '.' ]
[...[...[...'...']]] // -> [ '.', '.', '.' ]
[...[...[...[...'...']]]] // -> [ '.', '.', '.' ]
// and so on …
Niewielu programistów wie o etykietach w JavaScript. Są dość interesujące:
foo: {
console.log("first");
break foo;
console.log("second");
}
// > first
// -> undefined
Instrukcja z etykietą jest używana z instrukcją break
lub continue
. Możesz użyć etykiety do zidentyfikowania pętli, a następnie użyć instrukcji break
lub continue
, aby wskazać, czy program powinien przerwać pętlę, czy kontynuować jej wykonywanie.
W powyższym przykładzie identyfikujemy etykietę foo
. Po tym console.log ('first');
wykonuje, a następnie przerywamy wykonywanie.
Przeczytaj więcej o etykietach w JavaScript:
a: b: c: d: e: f: g: 1, 2, 3, 4, 5; // -> 5
Podobnie jak w poprzednich przykładach, skorzystaj z poniższych linków:
Co zwróci to wyrażenie?? 2
czy 3
?
(() => {
try {
return 2;
} finally {
return 3;
}
})();
Odpowiedź to 3
. Zaskoczony?
Spójrz na poniższy przykład:
new class F extends (String, Array) {}(); // -> F []
Czy to wielokrotne dziedziczenie? Nie.
Interesującą częścią jest wartość klauzuli extends
((String, Array)
). Operator grupowania zawsze zwraca ostatni argument, więc (String, Array)
jest właściwie po prostu Array
. Oznacza to, że właśnie stworzyliśmy klasę, która rozszerza Array
.
Consider this example of a generator which yields itself:
(function* f() {
yield f;
})().next();
// -> { value: [GeneratorFunction: f], done: false }
Jak widać, zwrócona wartość jest obiektem wraz z nią value
równa do f
. W takim przypadku możemy zrobić coś takiego:
(function* f() {
yield f;
})()
.next()
.value()
.next()(
// -> { value: [GeneratorFunction: f], done: false }
// and again
function* f() {
yield f;
}
)()
.next()
.value()
.next()
.value()
.next()(
// -> { value: [GeneratorFunction: f], done: false }
// and again
function* f() {
yield f;
}
)()
.next()
.value()
.next()
.value()
.next()
.value()
.next();
// -> { value: [GeneratorFunction: f], done: false }
// and so on
// …
Aby zrozumieć, dlaczego to działa w ten sposób, przeczytaj następujące sekcje specyfikacji:
Rozważ tę zaciemnioną składnię:
typeof new class {
class() {}
}(); // -> 'object'
Wygląda na to, że deklarujemy klasę wewnątrz klasy. Powinien być jednak błąd, ale otrzymujemy ciąg 'object'
.
Od ery ECMAScript 5 słowa kluczowe są dozwolone jako nazwy własności. Pomyśl o tym jako o tym prostym przykładzie obiektu:
const foo = {
class: function() {}
};
I znormalizowane skróty definicji metod ES6. Ponadto klasy mogą być anonimowe. Więc jeśli opuścimy część :function
, otrzymamy:
class {
class() {}
}
Wynik domyślnej klasy jest zawsze prostym obiektem. I jego typ powinien zwrócić 'object'
.
Przeczytaj więcej tutaj:
Dzięki dobrze znanym symbolom można pozbyć się typu coercion. Spójrz:
function nonCoercible(val) {
if (val == null) {
throw TypeError("nonCoercible should not be called with null or undefined");
}
const res = Object(val);
res[Symbol.toPrimitive] = () => {
throw TypeError("Trying to coerce non-coercible object");
};
return res;
}
Teraz możemy użyć tego w następujący sposób:
// objects
const foo = nonCoercible({ foo: "foo" });
foo * 10; // -> TypeError: Trying to coerce non-coercible object
foo + "evil"; // -> TypeError: Trying to coerce non-coercible object
// strings
const bar = nonCoercible("bar");
bar + "1"; // -> TypeError: Trying to coerce non-coercible object
bar.toString() + 1; // -> bar1
bar === "bar"; // -> false
bar.toString() === "bar"; // -> true
bar == "bar"; // -> TypeError: Trying to coerce non-coercible object
// numbers
const baz = nonCoercible(1);
baz == 1; // -> TypeError: Trying to coerce non-coercible object
baz === 1; // -> false
baz.valueOf() === 1; // -> true
Rozważ poniższy przykład:
let f = () => 10;
f(); // -> 10
Okej, w porządku, ale co z tym:
let f = () => {};
f(); // -> undefined
Możesz oczekiwać {}
zamiast undefined
. Wynika to z faktu, że nawiasy klamrowe są częścią składni funkcji strzałkowych, więc f
zwróci niezdefiniowane. Możliwe jest jednak zwrócenie obiektu {}
bezpośrednio z funkcji strzałkowej, poprzez umieszczenie wartości zwracanej w nawiasach.
let f = () => ({});
f(); // -> {}
Rozważ poniższy przykład:
let f = function() {
this.a = 1;
};
new f(); // -> { 'a': 1 }
Teraz spróbuj zrobić to samo z funkcją strzałkową:
let f = () => {
this.a = 1;
};
new f(); // -> TypeError: f is not a constructor
Funkcje strzałkowe nie mogą być używane jako konstruktory i będą zgłaszać błąd, gdy będą używane z nowym. Ponieważ ma leksykalne this
i nie ma właściwości prototype
, więc nie miałoby to większego sensu.
Rozważ poniższy przykład:
let f = function() {
return arguments;
};
f("a"); // -> { '0': 'a' }
Teraz spróbuj zrobić to samo z funkcją strzałkową:
let f = () => arguments;
f("a"); // -> Uncaught ReferenceError: arguments is not defined
Funkcje strzałkowe to lekka wersja zwykłych funkcji z naciskiem na bycie krótkim i leksykalnym this
. Jednocześnie funkcje strzałkowe nie zapewniają wiązania dla obiektu arguments
. Jako prawidłową alternatywę użyj rest parameters
, aby osiągnąć ten sam wynik:
let f = (...args) => args;
f("a");
- Arrow functions na MDN.
Wyrażenie return
jest również podstępne. Rozważ to:
(function() {
return
{
b: 10;
}
})(); // -> undefined
return
i zwrócone wyrażenie musi znajdować się w tym samym wierszu:
(function() {
return {
b: 10
};
})(); // -> { b: 10 }
Wynika to z koncepcji o nazwie Automatyczne wstawianie średników, która automatycznie wstawia średniki po większości nowych linii. W pierwszym przykładzie między wyrażeniem return
a literałem obiektu wstawiono średnik, więc funkcja zwraca undefined
, a literał obiektu nigdy nie jest oceniany.
var foo = { n: 1 };
var bar = foo;
foo.x = foo = { n: 2 };
foo.x; // -> undefined
foo; // -> {n: 2}
bar; // -> {n: 1, x: {n: 2}}
Z prawej do lewej, {n: 2}
jest przypisany do foo, a wynik tego przypisania {n: 2}
jest do foo.x, i dlatego bar jest {n: 1, x: {n: 2}}
jako bar jest referencją do foo. Ale czemu foo.x jest undefined podczas gdy bar.x nie jest ?
Foo and bar references the same object {n: 1}
, and lvalues are resolved before assignations. foo = {n: 2}
is creating a new object, and so foo is updated to reference that new object. The trick here is foo in foo.x = ...
as a lvalue was resolved beforehand and still reference the old foo = {n: 1}
object and update it by adding the x value. After that chain assignments, bar still reference the old foo object, but foo reference the new {n: 2}
object, where x is not existing.
Jest to równoważne z:
var foo = { n: 1 };
var bar = foo;
foo = { n: 2 }; // -> {n: 2}
bar.x = foo; // -> {n: 1, x: {n: 2}}
// bar.x point to the address of the new foo object
// it's not equivalent to: bar.x = {n: 2}
var obj = { property: 1 };
var array = ["property"];
obj[array]; // -> 1
Co z tablicami pseudo-wielowymiarowymi?
var map = {};
var x = 1;
var y = 2;
var z = 3;
map[[x, y, z]] = true;
map[[x + 10, y, z]] = true;
map["1,2,3"]; // -> true
map["11,2,3"]; // -> true
Operator nawiasów klamrowych []
konwertuje przekazane wyrażenie za pomocą toString
. Konwersja tablicy jednoelementowej na ciąg znaków jest zbliżona do konwersji zawartego elementu na ciąg znaków:
["property"].toString(); // -> 'property'
null > 0; // false
null == 0; // false
null >= 0; // true
Long story short, if null
is less than 0
is false
, then null >= 0
is true
. Read in-depth Wytłumaczenie for this here.
Number.toFixed()
może zachowywać się trochę dziwnie w różnych przeglądarkach. Sprawdź ten przykład:
(0.7875).toFixed(3);
// Firefox: -> 0.787
// Chrome: -> 0.787
// IE11: -> 0.788
(0.7876).toFixed(3);
// Firefox: -> 0.788
// Chrome: -> 0.788
// IE11: -> 0.788
Podczas gdy twoim pierwszym instynktem może być to, że IE11 jest poprawny, a Firefox / Chrome są w błędzie, w rzeczywistości Firefox / Chrome bardziej bezpośrednio przestrzegają standardów liczbowych (zmiennoprzecinkowy IEEE-754), podczas gdy IE11 nieznacznie ich nie przestrzega (prawdopodobnie), aby dać wyraźniejsze wyniki.
Możesz zobaczyć, dlaczego tak się dzieje po kilku szybkich testach:
// Confirm the odd result of rounding a 5 down
(0.7875).toFixed(3); // -> 0.787
// It looks like it's just a 5 when you expand to the
// limits of 64-bit (double-precision) float accuracy
(0.7875).toFixed(14); // -> 0.78750000000000
// But what if you go beyond the limit?
(0.7875).toFixed(20); // -> 0.78749999999999997780
Floating point numbers are not stored as a list of decimal digits internally, but through a more complicated methodology that produces tiny inaccuracies that are usually rounded away by toString and similar calls, but are actually present internally.
In this case, that "5" on the end was actually an extremely tiny fraction below a true 5. Rounding it at any reasonable length will render it as a 5... but it is actually not a 5 internally.
IE11, however, will report the value input with only zeros appended to the end even in the toFixed(20) case, as it seems to be forcibly rounding the value to reduce the troubles from hardware limits.
See for reference NOTE 2
on the ECMA-262 definition for toFixed
.
Math.min(1, 4, 7, 2); // -> 1
Math.max(1, 4, 7, 2); // -> 7
Math.min(); // -> Infinity
Math.max(); // -> -Infinity
Math.min() > Math.max(); // -> true
- Why is Math.max() less than Math.min()? od Charlie Harvey
Następujące wyrażenia wydają się wprowadzać w sprzeczność:
null == 0; // -> false
null > 0; // -> false
null >= 0; // -> true
Jak null
nie może być ani równy ani większy od 0
, jeśli null>=0' jest w rzeczywistości
true`? (Działa to również z mniej niż w ten sam sposób.)
Sposób oceny tych trzech wyrażeń jest różny i jest odpowiedzialny za wywołanie tego nieoczekiwanego zachowania.
Po pierwsze, abstrakcyjne porównanie równości null == 0
. Zwykle, jeśli ten operator nie może poprawnie porównać wartości po obu stronach, konwertuje obie liczby na liczby i porównuje liczby. Następnie możesz spodziewać się następującego zachowania:
// This is not what happens
(null == 0 + null) == +0;
0 == 0;
true;
Jednak, zgodnie z dokładnym odczytaniem specyfikacji, konwersja liczb tak naprawdę nie zachodzi po stronie, która jest null
lub undefined
. Dlatego jeśli po jednej stronie znaku równości występuje null
, druga strona musi być null
lub undefined
, aby wyrażenie mogło zwrócić true
. Ponieważ tak nie jest, zwracane jest false
.
Następnie relacyjne porównanie null> 0
. Algorytm tutaj, w przeciwieństwie do abstrakcyjnego operatora równości, przekonwertuje null
na liczbę. Dlatego otrzymujemy takie zachowanie:
null > 0
+null = +0
0 > 0
false
Wreszcie relacyjne porównanie null >= 0
. Można argumentować, że to wyrażenie powinno być wynikiem null> 0 || null == 0
; gdyby tak było, powyższe wyniki oznaczałyby, że byłoby to również false
. Jednak operator > =
w rzeczywistości działa w zupełnie inny sposób, co w zasadzie ma przeciwne działanie niż operator <
. Ponieważ nasz przykład z operatorem większym niż powyżej odnosi się również do operatora mniejszego niż, oznacza to, że to wyrażenie jest w rzeczywistości oceniane tak:
null >= 0;
!(null < 0);
!(+null < +0);
!(0 < 0);
!false;
true;
JS pozwala na ponowne zdefiniowanie zmiennych:
a;
a;
// This is also valid
a, a;
Działa również w trybie ścisłym:
var a, a, a;
var a;
var a;
Wszystkie definicje są scalone w jedną definicję.
Wyobraź sobie, że musisz posortować tablicę liczb.
[ 10, 1, 3 ].sort() // -> [ 1, 10, 3 ]
Domyślna kolejność sortowania opiera się na konwersji elementów na ciągi, a następnie porównaniu ich sekwencji wartości jednostek kodu UTF-16.
Przekaż comparefn
jeśli spróbujesz posortować cokolwiek poza ciągiem znaków.
[ 10, 1, 3 ].sort((a, b) => a - b) // -> [ 1, 3, 10 ]
- wtfjs.com — zbiór tych bardzo wyjątkowych nieprawidłowości, niespójności i po prostu bolesnie nieintuicyjnych momentów dla języka webowego.
- Wat — Lightning talk od Gary Bernhardt z CodeMash 2012
- What the... JavaScript? — Kyle Simpsons mówi dla Forward 2 o próbach "wyciągnięcia szaleństwa" z JavaScript. Chce pomóc ci w tworzeniu czystszego, bardziej eleganckiego, bardziej czytelnego kodu, a następnie zainspirować ludzi do współpracy w społeczności open source.
Wersja polska od @mbiesiad