Menghibur JavaScript: Tanpa kurung kurawal

gambar


JavaScript selalu mengejutkan saya, pertama-tama, karena mungkin tidak seperti bahasa luas lainnya yang mendukung kedua paradigma pada saat yang sama: pemrograman normal dan abnormal. Dan jika hampir semuanya telah dibaca tentang praktik dan templat terbaik yang memadai, maka dunia yang indah tentang bagaimana tidak menulis kode tetapi Anda bisa, tetap hanya sedikit terbuka.


Dalam artikel ini, kami akan menganalisis tugas lain yang dibuat yang membutuhkan penyalahgunaan solusi normal yang tidak dapat dimaafkan.


Tugas sebelumnya:



Kata-kata


Terapkan fungsi dekorator yang menghitung jumlah panggilan ke fungsi yang diteruskan dan menyediakan kemampuan untuk mendapatkan nomor ini sesuai permintaan. Solusinya tidak menggunakan kurung keriting dan variabel global.

Penghitung panggilan hanyalah alasan, karena ada console.count () . Intinya adalah bahwa fungsi kami mengakumulasi beberapa data saat memanggil fungsi yang dibungkus dan menyediakan antarmuka tertentu untuk mengaksesnya. Itu bisa menyimpan semua hasil panggilan, dan mengumpulkan log, dan semacam memoisasi. Hanya kontra - primitif dan dapat dimengerti oleh semua orang.


Semua kompleksitas berada dalam batasan abnormal. Anda tidak dapat menggunakan kurung kurawal, yang berarti Anda harus mempertimbangkan kembali praktik sehari-hari dan sintaksis biasa.


Solusi kebiasaan


Pertama, Anda harus memilih titik awal. Biasanya, jika bahasa atau ekstensi tidak menyediakan fungsi dekorasi yang diperlukan, kami akan mengimplementasikan beberapa wadah sendiri: fungsi yang dibungkus, akumulasi data, dan antarmuka untuk mengaksesnya. Ini sering merupakan kelas:


class CountFunction { constructor(f) { this.calls = 0; this.f = f; } invoke() { this.calls += 1; return this.f(...arguments); } } const csum = new CountFunction((x, y) => x + y); csum.invoke(3, 7); // 10 csum.invoke(9, 6); // 15 csum.calls; // 2 

Ini tidak cocok untuk kita segera, karena:


  1. Dalam JavaScript, Anda tidak dapat menerapkan properti pribadi dengan cara ini: kami berdua dapat membaca panggilan instance (yang kami butuhkan) dan menulis nilai dari luar (yang kami TIDAK perlu). Tentu saja, kita bisa menggunakan closure di konstruktor , tapi lalu apa artinya kelas? Dan saya masih takut menggunakan bidang pribadi yang segar tanpa babel 7 .
  2. Bahasa ini mendukung paradigma fungsional, dan menciptakan contoh melalui yang baru tampaknya bukan solusi terbaik di sini. Lebih baik menulis fungsi yang mengembalikan fungsi lain. Ya!
  3. Akhirnya, sintaks ClassDeclaration dan MethodDefinition tidak akan memungkinkan kita untuk menyingkirkan semua kurung kurawal.

Tetapi kami memiliki pola modul yang luar biasa yang mengimplementasikan privasi menggunakan penutupan:


 function count(f) { let calls = 0; return { invoke: function() { calls += 1; return f(...arguments); }, getCalls: function() { return calls; } }; } const csum = count((x, y) => x + y); csum.invoke(3, 7); // 10 csum.invoke(9, 6); // 15 csum.getCalls(); // 2 

Anda sudah bisa bekerja dengan ini.


Keputusan yang menghibur


Mengapa kawat gigi digunakan di sini? Ini adalah 4 kasus berbeda:


  1. Menentukan isi fungsi hitungan ( FunctionDeclaration )
  2. Inisialisasi objek yang dikembalikan
  3. Definisi tubuh fungsi panggil ( FunctionExpression ) dengan dua ekspresi
  4. Mendefinisikan tubuh fungsi getCalls ( FunctionExpression ) dengan satu ekspresi

Mari kita mulai dengan paragraf kedua . Faktanya, kita tidak perlu mengembalikan objek baru, sementara mempersulit pemanggilan fungsi final melalui pemanggilan. Kita dapat memanfaatkan fakta bahwa fungsi dalam JavaScript adalah objek, yang artinya dapat berisi bidang dan metode sendiri. Mari kita buat fungsi return df kita dan menambahkan metode getCalls ke dalamnya , yang melalui penutupan akan memiliki akses ke panggilan seperti sebelumnya:


 function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = function() { return calls; } return df; } 

Lebih menyenangkan bekerja dengan ini:


 const csum = count((x, y) => x + y); csum(3, 7); // 10 csum(9, 6); // 15 csum.getCalls(); // 2 

Poin keempat jelas: kita hanya mengganti FunctionExpression dengan ArrowFunction . Tidak adanya kurung kurawal akan memberi kita catatan singkat tentang fungsi panah jika ada ekspresi tunggal di tubuhnya:


 function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = () => calls; return df; } 

Dengan yang ketiga - semuanya lebih rumit. Ingat bahwa hal pertama yang kami lakukan adalah mengganti FunctionExpression dengan memanggil fungsi dengan FunctionDeclaration df . Untuk menulis ulang ini ke ArrowFunction, dua masalah harus dipecahkan: untuk tidak kehilangan akses ke argumen (sekarang ini adalah pseudo-array argumen ) dan untuk menghindari badan fungsi dari dua ekspresi.


Masalah pertama akan membantu kita mengatasi argumen parameter fungsi yang ditentukan secara eksplisit dengan operator spread . Dan untuk menggabungkan dua ekspresi menjadi satu, Anda bisa menggunakan logika AND . Tidak seperti operator penghubung logis klasik yang mengembalikan Boolean, operator ini menghitung operan dari kiri ke kanan ke "false" pertama dan mengembalikannya, dan jika semua "benar" - maka nilai terakhir. Peningkatan pertama dari penghitung akan memberi kita 1, yang berarti bahwa sub-ekspresi ini akan selalu dilemparkan ke true. Pengurangan ke "kebenaran" dari hasil pemanggilan fungsi pada sub-ekspresi kedua tidak menarik bagi kami: dalam hal apa pun, kalkulator berhenti melakukannya. Sekarang kita bisa menggunakan ArrowFunction :


 function count(f) { let calls = 0; let df = (...args) => (calls += 1) && f(...args); df.getCalls = () => calls; return df; } 

Anda dapat menghias catatan sedikit menggunakan kenaikan awalan:


 function count(f) { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; } 

Solusi ke titik pertama dan paling sulit akan dimulai dengan mengganti FunctionDeclaration dengan ArrowFunction . Tapi kita masih memiliki tubuh dalam kurung kurawal:


 const count = f => { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; }; 

Jika kita ingin menghilangkan kurung kurawal membingkai tubuh fungsi, kita harus menghindari mendeklarasikan dan menginisialisasi variabel melalui let . Dan kami memiliki dua variabel keseluruhan: panggilan dan df .


Pertama, mari kita berurusan dengan konter. Kita dapat membuat variabel lokal dengan mendefinisikannya dalam daftar parameter fungsi, dan mentransfer nilai awal dengan memanggilnya menggunakan IIFE (Ekspresi Fungsi yang Segera Diminta):


 const count = f => (calls => { let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; })(0); 

Masih untuk menggabungkan tiga ekspresi menjadi satu. Karena kami memiliki ketiga ekspresi yang mewakili fungsi yang selalu dapat direduksi menjadi true, kami juga dapat menggunakan logika DAN :


 const count = f => (calls => (df = (...args) => ++calls && f(...args)) && (df.getCalls = () => calls) && df)(0); 

Tetapi ada opsi lain untuk ekspresi gabungan: menggunakan operator koma . Itu lebih disukai, karena tidak berurusan dengan transformasi logis yang tidak perlu dan memerlukan kurung lebih sedikit. Operan dievaluasi dari kiri ke kanan, dan hasilnya adalah nilai yang terakhir:


 const count = f => (calls => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0); 

Kurasa aku berhasil membodohimu? Kami dengan berani menyingkirkan deklarasi variabel df dan hanya menyisakan penugasan fungsi panah kami. Dalam hal ini, variabel ini akan dideklarasikan secara global, yang tidak dapat diterima! Untuk df , kami mengulang inisialisasi variabel lokal dalam parameter fungsi IIFE kami, tetapi kami tidak akan melewatkan nilai awal apa pun:


 const count = f => ((calls, df) => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0); 

Dengan demikian tujuan tercapai.


Variasi pada Tema


Menariknya, kami dapat menghindari membuat dan menginisialisasi variabel lokal, beberapa ekspresi dalam blok fungsi, dan membuat objek literal. Pada saat yang sama, solusi asli tetap bersih: tidak adanya variabel global, privasi counter, akses ke argumen fungsi yang dibungkus.


Secara umum, Anda dapat mengambil implementasi apa pun dan mencoba melakukan hal serupa. Misalnya, polyfill untuk fungsi bind dalam hal ini cukup sederhana:


 const bind = (f, ctx, ...a) => (...args) => f.apply(ctx, a.concat(args)); 

Namun, jika argumen f bukan fungsi, kita harus melemparkan pengecualian dengan cara yang baik. Dan pengecualian lemparan tidak dapat dilemparkan dalam konteks ekspresi. Anda dapat menunggu ekspresi lemparan (tahap 2) dan coba lagi. Atau apakah seseorang sudah memiliki pemikiran sekarang?


Atau pertimbangkan kelas yang menggambarkan koordinat suatu titik:


 class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `(${this.x}, ${this.y})`; } } 

Yang dapat diwakili oleh suatu fungsi:


 const point = (x, y) => (p => (px = x, py = y, p.toString = () => ['(', x, ', ', y, ')'].join(''), p))(new Object); 

Hanya di sini kita telah kehilangan warisan prototipe: toString adalah properti dari objek prototipe Point , bukan objek yang dibuat secara terpisah. Bisakah ini dihindari jika Anda berusaha keras?


Dalam hasil transformasi, kami mendapatkan campuran pemrograman fungsional yang tidak sehat dengan peretasan imperatif dan beberapa fitur bahasa itu sendiri. Jika Anda memikirkannya, ini bisa menjadi obfuscator kode sumber yang menarik (tetapi tidak praktis). Anda dapat membuat versi sendiri dari tugas "bracketing obfuscator" dan menghibur kolega dan teman JavaScripter di waktu luang mereka dari pekerjaan yang bermanfaat.


Kesimpulan


Pertanyaannya adalah, kepada siapa ini berguna dan mengapa itu dibutuhkan? Ini benar-benar berbahaya bagi pemula, karena membentuk gagasan keliru tentang kerumitan dan penyimpangan bahasa yang berlebihan. Tetapi ini bisa bermanfaat bagi para praktisi, karena memungkinkan Anda untuk melihat fitur-fitur bahasa dari sisi lain: panggilan itu bukan untuk menghindari, dan panggilan itu untuk mencoba untuk menghindari di masa depan.

Source: https://habr.com/ru/post/id428337/


All Articles