Memecahkan teka-teki menyenangkan di JavaScript

Kisah kami dimulai dengan tweet dari Tomas Lakoma, di mana dia mengundang Anda untuk membayangkan bahwa pertanyaan seperti itu menemui Anda dalam sebuah wawancara.







Bagi saya, reaksi terhadap pertanyaan semacam itu dalam sebuah wawancara bergantung pada apa sebenarnya itu. Jika pertanyaannya benar-benar adalah apa nilainya tree



, maka kode tersebut dapat dengan mudah dimasukkan ke dalam konsol dan mendapatkan hasilnya.



Namun, jika pertanyaannya adalah bagaimana Anda akan menyelesaikan masalah ini, maka semuanya menjadi sangat aneh dan mengarah ke pengujian pengetahuan tentang seluk-beluk JavaScript dan kompiler. Pada artikel ini saya akan mencoba memilah semua kebingungan ini dan mendapatkan kesimpulan yang menarik.



Saya sedang streaming proses untuk memecahkan masalah ini di Twitch . Siarannya panjang, tetapi Anda dapat melihat lagi proses langkah demi langkah untuk memecahkan masalah tersebut.



Penalaran umum



Pertama, mari ubah kode ke format yang dapat disalin:



let b = 3, d = b, u = b;
const tree = ++d * d*b * b++ +
 + --d+ + +b-- +
 + +d*b+ +
 u
      
      





Saya segera melihat beberapa keanehan dan memutuskan bahwa beberapa trik kompilator dapat digunakan di sini. Soalnya, JavaScript biasanya menambahkan titik koma di akhir setiap baris, kecuali ada ekspresi yang tidak dapat diganggu . Dalam kasus ini, +



di akhir setiap baris ia memberi tahu kompiler bahwa tidak perlu menghentikan konstruksi ini.



Baris pertama hanya membuat tiga variabel dan memberinya nilai 3



. 3



Adalah nilai primitif, jadi setiap kali salinan dibuat, itu dibuat oleh nilai , jadi semua variabel baru dibuat dengan nilai 3



... Jika JavaScript akan menetapkan nilai ke variabel ini dengan referensi , maka setiap variabel baru akan mengarah ke variabel yang digunakan sebelumnya, tetapi tidak membuat nilai untuk dirinya sendiri.



informasi tambahan



Prioritas dan Asosiatif Operator



Ini adalah konsep kunci untuk menyelesaikan tugas yang menakutkan ini. Singkatnya, mereka menentukan urutan di mana kombinasi ekspresi JavaScript dievaluasi.



Prioritas operator



T: Apa perbedaan antara kedua ekspresi ini?



3 + 5 * 5
      
      





5 * 5 + 3
      
      





Dari segi hasil, tidak ada perbedaan. Siapapun yang ingat pelajaran matematika sekolah tahu bahwa perkalian dilakukan sebelum penjumlahan. Dalam bahasa Inggris, kita mengingat urutannya sebagai BODMAS (Brackets Off Divide Multiply Add Subtract - tanda kurung, derajat, pembagian, perkalian, penjumlahan, pengurangan). JavaScript memiliki konsep serupa yang disebut Operator Precedence: itu berarti urutan di mana kita mengevaluasi ekspresi. Jika kami ingin memaksa komputasi terlebih dahulu 3 + 5



, kami akan melakukan hal berikut:



(3+5) * 5
      
      





Tanda kurung memaksa bagian ekspresi ini dievaluasi terlebih dahulu, karena operator memiliki prioritas yang ()



lebih tinggi daripada operator *



.



Setiap operator JavaScript diutamakan, jadi dengan begitu banyak operator di tree



dalamnya, kita perlu mencari tahu dalam urutan apa mereka akan dievaluasi. Hal ini sangat penting terutama apa yang --



akan mengubah nilai b



dan d



, jadi kita perlu tahu kapan ekspresi ini dievaluasi relatif terhadap yang lainnya tree



.



Penting: Tabel Prioritas Operator dan Informasi Tambahan



Asosiatif



Asosiatif digunakan untuk menentukan ekspresi urutan mana yang dievaluasi dalam operator dengan prioritas yang sama. Sebagai contoh:



a + b + c
      
      





Tidak ada operator yang diutamakan dalam ekspresi ini karena hanya ada satu operator. Jadi, bagaimana cara menghitungnya - bagaimana (a + b) + c



atau bagaimana a + (b + c)



?



Saya tahu hasilnya akan sama, tetapi kompiler perlu mengetahui hal ini agar dapat memilih satu operasi terlebih dahulu dan kemudian melanjutkan komputasi. Dalam hal ini, jawaban yang benar adalah (a + b) + c



karena operatornya adalah +



asosiasi kiri, yaitu mengevaluasi ekspresi di sebelah kiri terlebih dahulu.



“Mengapa tidak membuat semua operator tidak berasosiasi?” Anda mungkin bertanya.



Nah, mari kita ambil contoh seperti ini:



a = b + c
      
      





Jika kita menggunakan rumus asosiatif kiri, kita dapatkan



(a = b) + c
      
      





Tapi tunggu, ini terlihat aneh, dan bukan itu yang saya maksud. Jika kita ingin ungkapan ini bekerja hanya dengan menggunakan asosiasi kiri, maka kita harus melakukan sesuatu seperti ini:



a + b = c
      
      





Ini diubah menjadi (a + b) = c



, yaitu, pertama a + b



, dan kemudian nilai hasil ini ditetapkan ke variabel c



.



Jika kami harus berpikir seperti ini, JavaScript akan jauh lebih membingungkan, itulah sebabnya kami menggunakan asosiasi yang berbeda untuk operator yang berbeda - ini membuat kode lebih mudah dibaca. Ketika kita membaca a = b + c



, urutan kalkulasi tampak alami bagi kita, terlepas dari kenyataan bahwa segala sesuatu diatur dengan lebih cerdik di dalam dan menggunakan operan asosiatif kanan dan kiri.



Anda mungkin memperhatikan masalah asosiatif dalam a = b + c



... Jika kedua operator memiliki asosiasi yang berbeda, bagaimana Anda mengetahui ekspresi mana yang harus dievaluasi terlebih dahulu? Jawaban: operator dengan prioritas operator yang lebih tinggi , seperti pada bagian sebelumnya! Dalam hal ini +



memiliki prioritas yang lebih tinggi, oleh karena itu dihitung terlebih dahulu.



Saya telah menambahkan penjelasan yang lebih detail di akhir artikel, atau Anda dapat membaca informasi lebih lanjut .



Memahami bagaimana ekspresi pohon kita dievaluasi



Setelah memahami prinsip-prinsip ini, kita dapat mulai menganalisis masalah kita. Ini menggunakan banyak operator, dan tidak adanya tanda kurung membuatnya sulit untuk dipahami. Jadi, tambahkan saja tanda kurung, daftar semua operator yang digunakan beserta prioritas dan asosiatifnya.



(operator dengan variabel x): Sebuah prioritas asosiatif
x ++: 18 tidak
x--: 18 tidak
++ x: 17 Baik
--x: 17 Baik
+ x: 17 Baik
*: limabelas kiri
x + y: 14 kiri
=: 3 Baik


Tanda kurung



Perlu disebutkan di sini bahwa menambahkan tanda kurung dengan benar adalah tugas yang sulit. Saya memeriksa bahwa jawabannya dihitung dengan benar di setiap tahap, tetapi ini tidak menjamin bahwa tanda kurung saya selalu ditempatkan dengan benar! Jika Anda mengetahui alat untuk penempatan brace otomatis, silakan email saya.



Mari kita cari tahu urutan ekspresi dievaluasi dan tambahkan tanda kurung untuk menunjukkannya. Saya akan menunjukkan kepada Anda langkah demi langkah bagaimana saya sampai pada hasil akhir, hanya berpindah dari operator dengan prioritas tertinggi ke bawah.



Postfix ++ dan postfix -



const tree = ++d * d*b * (b++) +
 + --d+ + +(b--) +
 + +d*b+ +
 u
      
      





Unary +, prefix ++ dan prefix -



Kami memiliki masalah kecil di sini, tetapi saya akan mulai dengan mengevaluasi operator unary +



, dan kemudian kita akan sampai ke titik masalah.



const tree = ++d * d*b * (b++) +
 + --d+ (+(+(b--))) +
 (+(+(d*b+ (+
 u))))
      
      





Dan di sinilah kesulitan muncul.



+ --d+
      
      





--



dan +()



memiliki prioritas yang sama. Bagaimana kita tahu bagaimana cara menghitungnya? Mari kita rumuskan masalah dengan cara yang lebih sederhana:



let d = 10
const answer = + --d
      
      





Ingat, +



ini bukan penjumlahan, tapi plus unary, atau positivity. Anda dapat melihatnya sebagai -1



, hanya ini dia +1



.



Solusinya kita evaluasi dari kanan ke kiri, karena operator diutamakan ini asosiatif kanan .



Jadi ekspresi kita diubah menjadi + (--d)



.



Untuk memahami hal ini, coba bayangkan bahwa semua operator adalah sama. Dalam hal ini, itu + +1



akan menjadi ekuivalen (+ (+1))



menurut logika, yang mana 1 — 1 — 1



ekuivalen dengan ((1 — 1) — 1)



... Perhatikan bahwa hasil operator asosiatif kanan dalam notasi dengan tanda kurung adalah kebalikan dari kasus dengan operator kiri?



Jika kita menerapkan logika yang sama ke titik masalah, maka kita mendapatkan yang berikut:



const tree = ++d * d*b * (b++) +
 (+ (--d)) + (+(+(b--))) +
 (+(+(d*b+ (+
 u))))
      
      





Dan terakhir, dengan memasukkan tanda kurung untuk yang terakhir ++



, kita mendapatkan:



const tree = (++d) * d*b * (b++) +
 (+ (--d)) + (+(+(b--))) +
 (+(+(d*b+ (+
 u))))
      
      





Perkalian (*)



Sekali lagi kita harus berurusan dengan asosiatif, tetapi kali ini dengan operator yang sama, yang dibiarkan asosiatif. Dibandingkan dengan langkah sebelumnya, ini seharusnya mudah!



const tree = ((((++d) * d) * b) * (b++)) +
 (+ (--d)) + (+(+(b--))) +
 (+(+((d*b) + (+u))))
      
      





Kami telah mencapai tahap yang memungkinkan untuk memulai penghitungan. Dimungkinkan untuk menambahkan tanda kurung untuk operator penugasan, tetapi saya pikir ini akan lebih membingungkan daripada lebih mudah dibaca, jadi kami tidak akan melakukannya. Perhatikan bahwa ekspresi di atas hanya sedikit lebih rumit x = a + b + c



.



Kami dapat mempersingkat beberapa operator unary, tetapi saya akan menyimpannya jika mereka penting.



Dengan membagi ekspresi menjadi beberapa bagian, kita dapat memahami tahapan perhitungan individual dan mengembangkannya.



let b = 3, d = b, u = b;
 
const treeA = ((((++d) * d) * b) * (b++))
const treeB = (+ (--d)) + (+(+(b--)))
const treeC = (+(+((d*b) + (+u))))
const tree = treeA + treeB + treeC
      
      





Setelah itu selesai, kita dapat mulai mengeksplorasi penghitungan nilai yang berbeda. Mari kita mulai dengan treeA.



PohonA



let b = 3, d = b, u = b;
const treeA = (((++d) * d) * b) * (b++)
      
      





Hal pertama yang akan dievaluasi di sini adalah ekspresi ++d



yang akan kembali 4



dan bertambah d



.



// b = 3
// d = 4
((4 * d) * b) * (b++)
      
      





Kemudian dieksekusi 4*d



: kita tahu bahwa pada tahap ini d adalah 4, jadi 4*4



16.



// b = 3
// d = 4
(16 * b) * (b++)
      
      





Hal yang menarik dari langkah ini adalah kita akan mengalikan dengan b sebelum menaikkan b, sehingga perhitungan dilakukan dari kiri ke kanan. 16 * 3 = 48



...



// b = 3
// d = 4
48 * (b++)
      
      





Di atas, kita berbicara tentang apa yang ++



memiliki prioritas lebih tinggi daripada *



, jadi ini dapat ditulis sebagai 48 * b++



, tetapi ada trik lain di sini - nilai yang dikembalikan b++



adalah nilai sebelum kenaikan, bukan setelah kenaikan. Jadi meskipun b akhirnya menjadi 4, nilai perkaliannya akan menjadi 3.



// b = 3
// d = 4
48 * 3
// b = 4
// d = 4
      
      





48 * 3



adalah sama 144



, jadi setelah menghitung bagian pertama b dan d sama dengan 4, dan hasil pernyataannya adalah 144







let b = 4, d = 4, u = 3;
 
const treeA = 144
const treeB = (+ (--d)) + (+(+(b--)))
const treeC = (+(+((d*b) + (+u))))
const tree = treeA + treeB + treeC
      
      





PohonB



const treeB = (+ (--d)) + (+(+(b--)))
      
      





Pada titik ini, kita dapat melihat bahwa operator unary sebenarnya tidak melakukan apapun. Jika kita mempersingkatnya, kita akan sangat menyederhanakan ekspresi.



// b = 4
// d = 4
const treeB = (--d) + (b--)
      
      





Kami telah melihat trik ini di atas. --d



kembali 3



, tetapi b--



kembali 4



, tetapi pada saat ekspresi dievaluasi, keduanya akan diberi nilai 3.



const treeB = 3 + 4
// b = 3
// d = 3
      
      





Jadi sekarang tugas kita terlihat seperti ini:



let b = 3, d= 3, u = 3;
 
const treeA = 144
const treeB = 7
const treeC = (+(+((d*b) + (+u))))
const tree = treeA + treeB + treeC
      
      





TreeC



Dan kita hampir selesai!



// b = 3
// d = 3
// u = 3
const treeC = (+(+((d*b) + (+u))))
      
      





Mari kita singkirkan operator unary yang mengganggu itu terlebih dahulu.



// b = 3
// d = 3
// u = 3
const treeC = (+(+((d*b) + u)))
      
      





Kami menyingkirkannya, tetapi di sini Anda harus berhati-hati dengan tanda kurung, dll.



// b = 3
// d = 3
// u = 3
const treeC = (d*b) + u
      
      





Ini sangat sederhana sekarang. 3 * 3



sederajat 9



, 9 + 3



sederajat 12



, dan akhirnya, kami memiliki ...



Menjawab!



let b = 3, d= 3, u = 3;
 
const treeA = 144
const treeB = 7
const treeC = 12
const tree = treeA + treeB + treeC
      
      





144 + 7 + 12



setara 163



. Jawaban untuk masalah: 163



.



Kesimpulan



JavaScript dapat membuat Anda bingung dengan berbagai cara yang aneh dan menyenangkan. Tetapi dengan memahami cara kerja bahasa, Anda bisa mendapatkan alasan paling mendasar untuk ini.



Secara umum, jalan menuju solusi bisa lebih informatif daripada jawabannya, dan solusi mini yang ditemukan di sepanjang jalan bisa mengajari kita sesuatu sendiri.



Patut dikatakan bahwa saya memeriksa pekerjaan saya menggunakan konsol browser dan lebih menarik bagi saya untuk merekayasa balik solusinya daripada menyelesaikan masalah berdasarkan prinsip-prinsip dasar.



Bahkan jika Anda tahu cara memecahkan masalah, ada banyak ambiguitas sintaksis yang perlu ditangani selama proses tersebut. Dan saya yakin banyak dari Anda memperhatikan ketika Anda melihat ekspresi pohon kami. Saya telah mencantumkan beberapa di antaranya di bawah ini, tetapi masing-masing bernilai artikel terpisah!



Saya juga ingin mengucapkan terima kasih kepada https://twitter.com/AnthonyPAlicea, yang tanpanya saya tidak akan pernah bisa memahami semuanya, dan https://twitter.com/tlakomy untuk pertanyaan ini.



Catatan dan keanehan



Saya telah menyoroti teka-teki mini yang saya temui di sepanjang jalan di bagian terpisah sehingga proses mencari solusi tetap transparan.



Bagaimana pengaruhnya terhadap perubahan urutan variabel



Tonton video ini



let x = 10
console.log(x++ + x)
      
      





Beberapa pertanyaan bisa ditanyakan di sini. Apa yang akan dicetak ke konsol dan berapa nilainya x



pada baris kedua?



Jika Anda pikir ini adalah nomor yang sama, maka permisi, saya memperdaya Anda. Triknya adalah apa yang x++ + x



dihitung sebagai (x++) + x



, dan ketika mesin JavaScript menghitung sisi kiri (x++)



, itu melakukan kenaikan x



, jadi ketika menyangkut + x



, nilai x sama 11



, bukan 10



.



Pertanyaan rumit lainnya - nilai apa yang dikembalikannya x++



?



Saya telah memberikan petunjuk yang cukup jelas tentang apa sebenarnya jawabannya 10



.



Inilah perbedaan antara x++



dan ++x



. Jika kita melihat pada fungsi yang mendasari operator, mereka terlihat seperti ini:



function ++x(x) {
  const oldValue = x;
  x = x + 1;
  return oldValue;
}
function x++(x) {
  x = x + 1;
  return x
}
      
      





Melihat mereka dengan cara ini, kita bisa mengerti itu



let x = 10
console.log(x++ + x)
      
      





akan berarti apa yang x++



dikembalikannya 10



, dan pada saat evaluasi, + x



nilainya adalah 11



. Oleh karena itu, ini akan dicetak ke konsol 21



, dan nilai x akan sama dengan 11



.



Tugas yang relatif sederhana ini menunjuk ke anti-pola umum yang digunakan di seluruh kode - ekspresi campur aduk dan efek samping . Keterangan lebih lanjut.



Mungkinkah ada dua operator dengan prioritas yang sama tetapi asosiasi berbeda?



Mari bergerak dalam urutan dan lupakan kata "asosiatif" untuk saat ini.



Mari kita ambil operator +



dan =



, dan rangkum situasinya.



Itu ditunjukkan di atas apa yang a + b + c



dihitung sebagai (a + b) + c



, karena +



dibiarkan asosiatif.



a = b = c



dihitung a = (b = c)



karena =



asosiatif yang tepat. Perhatikan bahwa ini =



mengembalikan nilai yang ditetapkan ke variabel, sehingga a



akan sama dengan nilai b



setelah mengevaluasi ekspresi.



Mari ganti operan dengan prioritasnya:



a left b left c = (a left b) left c
a right b right c = a right (b right c)

  

a left b right c = ?
a right b left c = ?
      
      





Lihat bahwa contoh kedua secara logis tidak mungkin? a + b = c



hanya mungkin karena +



lebih diutamakan =



, sehingga parser tahu apa yang harus dilakukan. Jika dua operator memiliki prioritas yang sama, tetapi asosiativitas berbeda, maka pengurai sintaks tidak akan dapat menentukan dalam urutan apa untuk melakukan tindakan!



Jadi, untuk meringkas: tidak, operator dengan prioritas yang sama tidak dapat memiliki asosiasi yang berbeda!



Sangat mengherankan bahwa di F # Anda dapat mengubah asosiasi fungsi dengan cepat, itulah mengapa saya dapat berbicara tentang asosiasi tanpa menjadi gila! Keterangan lebih lanjut.



Operator Unary



Hal yang menarik ditemukan saat mengurai urutan kalkulasi +n



dan ++n



.



Tidak dapat dijalankan -- -i



karena -



mengembalikan angka, dan angka tidak dapat ditambah atau dikurangi, dan tidak dapat dilakukan ---i



karena artinya ---



ambigu (ini -- -



atau - --



? Lihat komentar di bawah.), Tetapi Anda dapat melakukan ini:



let i = 10
console.log(-+-+-+-+-+--i)
      
      





Kepositifan bingung



Salah satu masalah paling bermasalah adalah ambiguitas +



dalam JavaScript. Simbol yang sama, seperti yang terlihat di bawah, digunakan dalam empat fungsi berbeda:



let i = 10
console.log(i++ + + ++i)
      
      





Setiap operan memiliki arti, prioritas, dan asosiatifnya sendiri. Ini mengingatkan saya pada teka-teki kata terkenal:



Buffalo buffalo Buffalo buffalo buffalo Buffalo buffalo .



Operator atau penugasan unary?



+



bisa berarti operator unary atau penugasan. Apa dalam kasus u



masalah dari awal artikel?



... +
u
      
      





Pada akhirnya jawabannya tergantung pada ... apa itu. Jika kita menulis semuanya dalam satu baris



... + u
      
      





maka jawabannya akan berbeda untuk x + u



dan x - + u



. Dalam kasus pertama, simbol itu berarti penjumlahan, dan dalam kasus kedua - unary +



. Satu-satunya cara untuk mengetahui artinya adalah dengan mengurai ekspresi lainnya sampai hanya ada satu operator tersisa untuk mewakili!






Periklanan



VDS untuk pemrogram dengan perangkat keras terbaru, perlindungan serangan, dan banyak pilihan sistem operasi. Konfigurasi maksimum adalah 128 inti CPU, RAM 512 GB, NVMe 4000 GB.






All Articles