toString: Hebat dan Mengerikan

gambar


Fungsi toString dalam JavaScript mungkin adalah yang paling "implisit" yang dibahas baik di antara pengembang js sendiri maupun di antara pengamat eksternal. Dia adalah penyebab banyak lelucon dan meme tentang banyak operasi aritmatika yang mencurigakan, transformasi yang masuk ke dalam objek [objek Obyek] yang pingsan. Ini kebobolan, mungkin, hanya mengejutkan ketika bekerja dengan float64.


Kasus-kasus menarik yang harus saya amati, gunakan atau atasi, memotivasi saya untuk menulis tanya jawab. Kami akan berpacu dengan spesifikasi bahasa dan menggunakan contoh-contoh untuk menganalisis fitur toString yang tidak jelas.


Jika Anda mengharapkan panduan yang bermanfaat dan memadai, maka ini , ini, dan materi itu lebih cocok untuk Anda. Jika rasa ingin tahu Anda masih menang atas pragmatisme, maka silakan, di bawah kucing.


Yang perlu Anda ketahui


Fungsi toString adalah properti dari objek prototipe Object , dengan kata sederhana metodenya. Ini digunakan untuk konversi string objek dan harus mengembalikan nilai primitif dengan cara yang baik. Objek prototipe juga memiliki implementasinya: Fungsi, Array, String, Boolean, Number, Symbol, Date, RegExp, Error . Jika Anda mengimplementasikan objek prototipe (kelas), maka toString akan menjadi bentuk yang baik untuk itu.


JavaScript adalah bahasa dengan sistem tipe lemah: yang artinya memungkinkan kita untuk mencampur jenis yang berbeda, melakukan banyak operasi secara implisit. Dalam konversi, toString dipasangkan dengan valueOf untuk mengurangi objek ke primitif yang diperlukan untuk operasi. Misalnya, operator tambahan berubah menjadi penggabungan jika ada setidaknya satu baris di antara operator. Beberapa fungsi standar bahasa sebelum pekerjaan mereka menyebabkan argumen ke string: parseInt, decodeURI, JSON.parse, btoa, dan sebagainya.


Cukup banyak yang telah dikatakan dan diejek tentang casting implisit. Kami akan mempertimbangkan implementasi toString objek prototipe bahasa utama.


Object.prototype.toString


Jika kita beralih ke bagian yang sesuai dari spesifikasi, kita menemukan bahwa tugas utama dari default toString adalah untuk mendapatkan tag yang disebut untuk menyambung ke string yang dihasilkan:


"[object " + tag + "]" 

Untuk melakukan ini:


  1. Panggilan ke simbol toStringTag internal (atau properti pseudo [[Kelas]] dalam edisi lama) terjadi: ia memiliki banyak objek prototipe bawaan ( Peta, Matematika, JSON, dan lainnya).
  2. Jika ada yang hilang atau bukan string, maka sejumlah properti pseudo-internal lainnya dan metode yang memberi sinyal jenis objek disebutkan: [[Panggil]] untuk Fungsi , [[Nilai Tanggal ]] untuk Tanggal, dan sebagainya.
  3. Nah, jika tidak ada sama sekali, maka tag adalah "Objek" .

Mereka yang dipengaruhi oleh refleksi akan segera mencatat kemungkinan mendapatkan jenis objek dengan operasi sederhana (tetapi tidak direkomendasikan oleh spesifikasi, tetapi mungkin):


 const getObjT = obj => Object.prototype.toString.call(obj).match(/\[object\s(\w+)]/)[1]; 

Keunikan dari toString default adalah bahwa ia bekerja dengan nilai ini . Jika primitif, maka akan dilemparkan ke objek ( null dan undefined diperiksa secara terpisah). Tidak Ada TypeError :


 [Infinity, null, x => 1, new Date, function*(){}].map(getObjT); > ["Number", "Null", "Function", "Date", "GeneratorFunction"] 

Bagaimana ini bisa berguna? Misalnya, ketika mengembangkan alat untuk analisis kode dinamis. Memiliki kumpulan variabel dadakan yang digunakan selama kerja aplikasi, Anda dapat mengumpulkan statistik homogen yang berguna saat run-time.


Pendekatan ini memiliki satu kelemahan utama: tipe pengguna. Tidak sulit untuk menebak bahwa untuk contoh mereka, kita hanya mendapatkan "Objek" .


Kustom Symbol.toStringTag dan Function.name


OOP dalam JavaScript didasarkan pada prototipe, dan bukan pada kelas (seperti di Jawa), dan kami tidak memiliki metode getClass () siap pakai. Definisi eksplisit karakter toStringTag untuk tipe pengguna akan membantu menyelesaikan masalah:


 class Cat { get [Symbol.toStringTag]() { return 'Cat'; } } 

atau dalam gaya prototipe:


 function Dog(){} Dog.prototype[Symbol.toStringTag] = 'Dog'; 

Ada solusi alternatif melalui Function.name properti read-only, yang belum menjadi bagian dari spesifikasi, tetapi didukung oleh sebagian besar browser. Setiap instance dari objek / kelas prototipe memiliki tautan ke fungsi konstruktor yang dengannya ia dibuat. Jadi kita bisa mengetahui nama jenisnya:


 class Cat {} (new Cat).constructor.name < 'Cat' 

atau dalam gaya prototipe:


 function Dog() {} (new Dog).constructor.name < 'Dog' 

Tentu saja, solusi ini tidak berfungsi untuk objek yang dibuat menggunakan fungsi anonim ( "anonim" ) atau Object.create (null) , atau untuk primitif tanpa objek pembungkus ( null, undefined ).


Dengan demikian, untuk manipulasi jenis variabel yang andal, ada baiknya menggabungkan teknik-teknik terkenal, terutama berdasarkan pada tugas yang dihadapi. Dalam sebagian besar kasus, jenis dan contoh sudah cukup.


Function.prototype.toString


Kami sedikit terganggu, tetapi akibatnya kami sampai pada fungsi yang memiliki toString yang menarik. Pertama, lihat kode berikut:


 (function() { console.log('(' + arguments.callee.toString() + ')()'); })() 

Banyak yang mungkin menduga bahwa ini adalah contoh dari Quine . Jika Anda memuat skrip dengan konten seperti itu ke badan halaman, maka salinan kode sumber yang tepat akan ditampilkan di konsol. Ini karena panggilan toString dari fungsi arguments.callee .


Implementasi toString dari objek prototipe Function yang digunakan mengembalikan representasi string dari kode sumber fungsi, mempertahankan sintaks yang digunakan dalam definisi: FunctionDeclaration, FunctionExpression, ClassDeclaration, ArrowFunction , dll.


Misalnya, kami memiliki fungsi panah:


 const bind = (f, ctx) => function() { return f.apply(ctx, arguments); } 

Memanggil bind.toString () akan mengembalikan kepada kami representasi string dari ArrowFunction :


 "(f, ctx) => function() { return f.apply(ctx, arguments); }" 

Dan memanggil toString dari fungsi yang dibungkus sudah merupakan representasi string dari FunctionExpression :


 "function() { return f.apply(ctx, arguments); }" 

Contoh bind ini tidak disengaja, karena kami memiliki solusi siap pakai dengan Function.prototype.bind yang mengikat konteks, dan mengenai fungsi terikat asli , ada fitur Function.prototype.toString yang bekerja dengannya. Bergantung pada implementasinya, representasi dari fungsi yang dibungkus itu sendiri dan fungsi target dapat diperoleh. V8 dan SpiderMonkey versi terbaru dari chrome and ff:


 function getx() { return this.x; } getx.bind({ x: 1 }).toString() < "function () { [native code] }" 

Karena itu, kehati-hatian harus dilakukan dengan fitur yang didekorasi secara asli.


Berlatih menggunakan f.toString


Ada banyak opsi untuk menggunakan toString yang dimaksud, tetapi hanya mendesak sebagai alat pemrograman atau debug. Memiliki aplikasi tipikal yang serupa dalam logika bisnis cepat atau lambat akan menyebabkan palung yang tidak didukung.


Hal paling sederhana yang terlintas dalam pikiran adalah menentukan panjang fungsi :


 f.toString().replace(/\s+/g, ' ').length 

Lokasi dan jumlah karakter spasi putih dari hasil toString diberikan oleh spesifikasi untuk pembelian implementasi tertentu, oleh karena itu, untuk kebersihan, pertama-tama kami menghapus kelebihannya, yang mengarah ke tampilan umum. Omong-omong, di versi Gecko engine yang lebih lama, fungsinya memiliki parameter indentasi khusus yang membantu memformat indentasi.


Definisi nama parameter fungsi langsung terlintas dalam pikiran, yang dapat berguna untuk refleksi:


 f.toString().match(/^function(?:\s+\w+)?\s*\(([^\)]+)/m)[1].split(/\s*,\s*/) 

Solusi lutut ini cocok untuk sintaks FunctionDeclaration dan FunctionExpression . Jika Anda memerlukan yang lebih rinci dan akurat, saya sarankan Anda mencari contoh kode sumber kerangka kerja favorit Anda, yang mungkin memiliki beberapa jenis injeksi ketergantungan di bawah tenda, berdasarkan nama-nama parameter yang dinyatakan.


Opsi berbahaya dan menarik untuk mengganti fungsi melalui eval :


 const sum = (a, b) => a + b; const prod = eval(sum.toString().replace(/\+(?=\s*(?:a|b))/gm, '*')); sum(5, 10) < 15 prod(5, 10) < 50 

Mengetahui struktur fungsi asli, kami membuat yang baru dengan mengganti operator tambahan yang digunakan dalam tubuhnya dengan argumen dengan perkalian. Dalam hal kode yang dibuat oleh perangkat lunak atau tidak adanya antarmuka ekstensi fungsi, ini bisa berguna secara ajaib. Misalnya, jika Anda meneliti model matematika, memilih fungsi yang sesuai, bermain dengan operator dan koefisien.


Penggunaan yang lebih praktis adalah kompilasi dan distribusi template . Banyak implementasi mesin template mengkompilasi kode sumber template dan menyediakan fungsi data yang sudah membentuk HTML akhir (atau lainnya). Berikut ini adalah contoh dari fungsi _.template :


 const helloJst = "Hello, <%= user %>" _.template(helloJst)({ user: 'admin' }) < "Hello, admin" 

Tetapi bagaimana jika mengkompilasi template membutuhkan sumber daya perangkat keras atau klien sangat tipis? Dalam hal ini, kita bisa mengkompilasi template di sisi server dan memberikan klien bukan teks template, tetapi representasi string dari fungsi yang sudah selesai. Selain itu, Anda tidak perlu memuat pustaka template pada klien.


 const helloStr = _.template(helloJst).toString() helloStr < "function(obj) { obj || (obj = {}); var __t, __p = ''; with (obj) { __p += 'Hello, ' + ((__t = ( user )) == null ? '' : __t); } return __p }" 

Sekarang kita perlu menjalankan kode ini pada klien sebelum digunakan. Bahwa saat dikompilasi tidak ada Sintaksis karena sintaks FunctionExpression :


 const helloFn = eval(helloStr.replace(/^function\(obj\)/, 'obj=>')); 

atau lebih:


 const helloFn = eval(`const f = ${helloStr};f`); 

Atau sesuka Anda. Bagaimanapun:


 helloFn({ user: 'admin' }) < "Hello, admin" 

Ini mungkin bukan praktik terbaik untuk mengkompilasi template di sisi server dan mendistribusikannya ke klien lebih lanjut. Hanya sebuah contoh menggunakan sekelompok Function.prototype.toString dan eval .


Akhirnya, tugas lama untuk mendefinisikan nama fungsi (sebelum properti Function.name muncul) melalui toString :


 f.toString().match(/function\s+(\w+)(?=\s*\()/m)[1] 

Tentu saja, ini berfungsi baik dengan sintaks FunctionDeclaration . Solusi yang lebih cerdas akan membutuhkan kecocokan ekspresi reguler atau pencocokan pola.


Internet penuh dengan solusi menarik berdasarkan Function.prototype.toString , tanyakan saja. Bagikan pengalaman Anda dalam komentar: sangat menarik.


Array.prototype.toString


Implementasi toString dari objek prototipe Array bersifat generik dan dapat dipanggil untuk objek apa pun. Jika objek memiliki metode bergabung , maka hasil toString akan menjadi panggilannya, jika tidak, Object.prototype.toString .


Array , secara logis, memiliki metode gabungan yang menggabungkan representasi string dari semua elemennya melalui pemisah yang dilewatkan sebagai parameter (defaultnya adalah koma).


Misalkan kita perlu menulis fungsi yang membuat serial daftar argumennya. Jika semua parameter adalah primitif, maka dalam banyak kasus kita dapat melakukannya tanpa JSON.stringify :


 function seria() { return Array.from(arguments).toString(); } 

atau lebih:


 const seria = (...a) => a.toString(); 

Ingatlah bahwa string '10' dan nomor 10 akan diserialisasi dengan yang sama. Dalam masalah memoizer terpendek pada satu tahap, solusi ini digunakan.


Gabungan asli elemen array bekerja melalui siklus aritmatika dari 0 hingga panjang dan tidak memfilter untuk elemen yang hilang ( null dan tidak terdefinisi ). Sebaliknya, rangkaian terjadi dengan pemisah . Ini mengarah pada hal berikut:


 const ar = new Array(1000); ar.toString() < ",,,...,,," // 1000 times 

Oleh karena itu, jika karena satu dan lain alasan Anda menambahkan elemen dengan indeks besar ke array (misalnya, ini adalah id alami yang dihasilkan), dalam kasus apa pun tidak bergabung dan, karenanya, tidak mengarah ke string tanpa persiapan awal. Jika tidak, mungkin ada konsekuensi: Panjang string tidak valid, kehabisan memori atau hanya skrip yang menggantung. Gunakan fungsi-fungsi objek Nilai - nilai objek dan kunci untuk beralih di atas properti yang disebutkan sendiri dari objek saja:


 const k = []; k[2**10] = 1; k[2**20] = 2; k[2**30] = 3; Object.values(k).toString() < "1,2,3" Object.keys(k).toString() < "1024,1048576,1073741824" 

Tetapi jauh lebih baik untuk menghindari penanganan array seperti itu: kemungkinan besar objek bernilai kunci sederhana akan cocok untuk Anda sebagai penyimpanan.


Ngomong-ngomong, bahaya yang sama muncul saat membuat serial melalui JSON.stringify . Hanya lebih serius, karena elemen kosong dan tidak didukung sudah direpresentasikan sebagai "null" :


 const ar = new Array(1000); JSON.stringify(ar); < "[null,null,null,...,null,null,null]" // 1000 times 

Mengakhiri bagian ini, saya ingin mengingatkan Anda bahwa Anda dapat mendefinisikan metode bergabung Anda untuk tipe pengguna dan memanggil Array.prototype.toString.call sebagai alternatif untuk string, tetapi saya ragu bahwa ia memiliki penggunaan praktis.


Number.prototype.toString dan parseInt


Salah satu tugas favorit saya untuk kuis js adalah Apa yang akan mengembalikan panggilan parseInt berikutnya?


 parseInt(10**30, 2) 

Hal pertama yang dilakukan parseInt adalah secara implisit melemparkan argumen ke string dengan memanggil fungsi abstrak ToString , yang, tergantung pada jenis argumen, mengeksekusi cabang cast yang diinginkan. Untuk nomor jenis, berikut ini dilakukan:


  1. Jika nilainya NaN, 0, atau Infinity , maka kembalikan string yang sesuai.
  2. Jika tidak, algoritma mengembalikan catatan nomor yang paling nyaman untuk manusia: dalam bentuk desimal atau eksponensial.

Saya tidak akan menduplikasi algoritma untuk menentukan formulir yang disukai di sini, saya hanya akan mencatat yang berikut: jika jumlah digit dalam notasi desimal melebihi 21 , maka formulir eksponensial akan dipilih. Dan ini berarti bahwa dalam kasus kami parseInt tidak bekerja dengan "100 ... 000" tetapi dengan "1e30". Karena itu, jawabannya sama sekali tidak diharapkan 2 ^ 30. Siapa yang tahu sifat sihir nomor 21 ini - tulis!


Selanjutnya, parseInt melihat dasar sistem nomor radix yang digunakan (secara default 10, kita memiliki 2) dan memeriksa karakter string yang diterima untuk kompatibilitas dengan itu. Setelah bertemu 'e', ​​ia memotong seluruh ekor, hanya menyisakan "1". Hasilnya akan berupa bilangan bulat yang diperoleh dengan mengkonversi dari sistem dengan basis radix ke desimal - dalam kasus kami, ini adalah 1.


Prosedur Mundur:


 (2**30).toString(2) 

Di sinilah fungsi toString dipanggil dari objek prototipe Number , yang menggunakan algoritma yang sama untuk melemparkan nomor ke string. Ini juga memiliki parameter radix opsional. Hanya itu melempar RangeError untuk nilai yang tidak valid (harus bilangan bulat dari 2 hingga 36 inklusif), sementara parseInt mengembalikan NaN .


Perlu diingat batas atas sistem angka jika Anda berencana untuk mengimplementasikan fungsi hash yang eksotis: toString ini mungkin tidak bekerja untuk Anda.


Tugas untuk mengalihkan perhatian sejenak:


 '3113'.split('').map(parseInt) 

Apa yang akan kembali dan bagaimana cara memperbaikinya?


Kehilangan perhatian


Kami memeriksa toString tidak berarti bahkan semua objek prototipe asli. Sebagian, karena secara pribadi saya tidak perlu mendapat masalah dengan mereka, dan tidak ada banyak yang menarik di dalamnya. Selain itu, kami tidak menyentuh fungsi toLocaleString , karena akan menyenangkan untuk membicarakannya secara terpisah. Jika saya melakukan sesuatu dengan sia-sia kehilangan perhatian, kehilangan pandangan atau disalahpahami - pastikan untuk menulis!


Panggilan untuk tidak bertindak


Contoh-contoh yang saya kutip bukanlah resep siap pakai - hanya makanan yang dipikirkan. Selain itu, saya merasa tidak ada gunanya dan sedikit bodoh untuk membahas ini pada wawancara teknis: untuk ini ada topik abadi tentang penutupan, bergabung, loop acara, pola modul / fasad / mediator, dan pertanyaan “tentu saja” tentang [kerangka yang digunakan].


Artikel ini ternyata gado-gado, dan saya harap Anda menemukan sesuatu yang menarik untuk diri sendiri. PS Bahasa JavaScript - Luar Biasa!


Bonus


Dalam mempersiapkan materi ini untuk dipublikasikan, saya menggunakan Google Translate. Dan secara tidak sengaja saya menemukan efek yang menghibur. Jika Anda memilih terjemahan dari bahasa Rusia ke bahasa Inggris, masukkan "toString" dan mulai menghapusnya menggunakan tombol Backspace, maka kami akan mengamati:


bonus


Ironi sekali! Saya pikir saya jauh dari yang pertama, tetapi kalau-kalau saya mengirim tangkapan layar dengan skrip pemutaran. Sepertinya XSS diri yang tidak berbahaya, itu sebabnya saya membagikannya.

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


All Articles