Kode JavaScript Buggy: 10 Kesalahan Paling Umum Dilakukan Pengembang JavaScript

Diterbitkan: 2022-03-11

Saat ini, JavaScript adalah inti dari hampir semua aplikasi web modern. Beberapa tahun terakhir khususnya telah menyaksikan proliferasi beragam pustaka dan kerangka kerja berbasis JavaScript yang kuat untuk pengembangan aplikasi halaman tunggal (SPA), grafik dan animasi, dan bahkan platform JavaScript sisi server. JavaScript telah benar-benar ada di mana-mana di dunia pengembangan aplikasi web dan oleh karena itu merupakan keterampilan yang semakin penting untuk dikuasai.

Pada tampilan pertama, JavaScript mungkin tampak cukup sederhana. Dan memang, untuk membangun fungsionalitas JavaScript dasar ke dalam halaman web adalah tugas yang cukup mudah bagi setiap pengembang perangkat lunak berpengalaman, bahkan jika mereka baru mengenal JavaScript. Namun bahasanya secara signifikan lebih bernuansa, kuat, dan kompleks daripada yang awalnya diyakini orang. Memang, banyak seluk-beluk JavaScript menyebabkan sejumlah masalah umum yang membuatnya tidak berfungsi – 10 di antaranya kita bahas di sini – yang penting untuk diperhatikan dan dihindari dalam upaya seseorang untuk menjadi pengembang JavaScript yang ahli.

Kesalahan Umum #1: Referensi yang salah untuk this

Saya pernah mendengar seorang komedian berkata:

Saya tidak benar-benar di sini, karena apa yang ada di sini, selain di sana, tanpa 't'?

Lelucon itu dalam banyak hal mencirikan jenis kebingungan yang sering muncul bagi pengembang terkait dengan kata kunci JavaScript this . Maksud saya, apakah this benar-benar ini, atau apakah itu sesuatu yang lain sama sekali? Atau tidak terdefinisi?

Karena teknik pengkodean JavaScript dan pola desain telah menjadi semakin canggih selama bertahun-tahun, ada peningkatan yang sesuai dalam proliferasi cakupan referensi diri dalam panggilan balik dan penutupan, yang merupakan sumber yang cukup umum dari "kebingungan ini/itu".

Pertimbangkan contoh cuplikan kode ini:

 Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(function() { this.clearBoard(); // what is "this"? }, 0); };

Menjalankan kode di atas menghasilkan kesalahan berikut:

 Uncaught TypeError: undefined is not a function

Mengapa?

Ini semua tentang konteks. Alasan Anda mendapatkan kesalahan di atas adalah karena, saat Anda memanggil setTimeout() , Anda sebenarnya sedang menjalankan window.setTimeout() . Akibatnya, fungsi anonim yang diteruskan ke setTimeout() sedang didefinisikan dalam konteks objek window , yang tidak memiliki metode clearBoard() .

Solusi tradisional yang sesuai dengan browser lama adalah dengan menyimpan referensi Anda ke this dalam variabel yang kemudian dapat diwarisi oleh penutupan; misalnya:

 Game.prototype.restart = function () { this.clearLocalStorage(); var self = this; // save reference to 'this', while it's still this! this.timer = setTimeout(function(){ self.clearBoard(); // oh OK, I do know who 'self' is! }, 0); };

Atau, di browser yang lebih baru, Anda dapat menggunakan metode bind() untuk meneruskan referensi yang tepat:

 Game.prototype.restart = function () { this.clearLocalStorage(); this.timer = setTimeout(this.reset.bind(this), 0); // bind to 'this' }; Game.prototype.reset = function(){ this.clearBoard(); // ahhh, back in the context of the right 'this'! };

Kesalahan Umum #2: Berpikir ada cakupan level blok

Seperti yang dibahas dalam Panduan Perekrutan JavaScript kami, sumber umum kebingungan di antara pengembang JavaScript (dan oleh karena itu sumber bug yang umum) mengasumsikan bahwa JavaScript membuat cakupan baru untuk setiap blok kode. Meskipun ini benar dalam banyak bahasa lain, itu tidak benar dalam JavaScript. Pertimbangkan, misalnya, kode berikut:

 for (var i = 0; i < 10; i++) { /* ... */ } console.log(i); // what will this output?

Jika Anda menebak bahwa panggilan console.log() akan menghasilkan undefined atau menimbulkan kesalahan, Anda salah menebak. Percaya atau tidak, itu akan menghasilkan 10 . Mengapa?

Dalam kebanyakan bahasa lain, kode di atas akan menyebabkan kesalahan karena "kehidupan" (yaitu, ruang lingkup) dari variabel i akan dibatasi untuk blok for . Namun, dalam JavaScript, ini tidak terjadi dan variabel i tetap berada dalam cakupan bahkan setelah for loop selesai, mempertahankan nilai terakhirnya setelah keluar dari loop. (Perilaku ini dikenal, kebetulan, sebagai pengangkat variabel).

Namun, perlu dicatat bahwa dukungan untuk cakupan level blok masuk ke JavaScript melalui kata kunci let yang baru. Kata kunci let sudah tersedia di JavaScript 1.7 dan dijadwalkan menjadi kata kunci JavaScript yang didukung secara resmi pada ECMAScript 6.

Baru mengenal JavaScript? Baca tentang cakupan, prototipe, dan banyak lagi.

Kesalahan Umum #3: Membuat kebocoran memori

Kebocoran memori hampir merupakan masalah JavaScript yang tak terhindarkan jika Anda tidak secara sadar mengkode untuk menghindarinya. Ada banyak cara bagi mereka untuk terjadi, jadi kami hanya akan menyoroti beberapa kejadian yang lebih umum.

Kebocoran Memori Contoh 1: Referensi yang menggantung ke objek yang tidak berfungsi

Perhatikan kode berikut:

 var theThing = null; var replaceThing = function () { var priorThing = theThing; // hold on to the prior thing var unused = function () { // 'unused' is the only place where 'priorThing' is referenced, // but 'unused' never gets invoked if (priorThing) { console.log("hi"); } }; theThing = { longStr: new Array(1000000).join('*'), // create a 1MB object someMethod: function () { console.log(someMessage); } }; }; setInterval(replaceThing, 1000); // invoke `replaceThing' once every second

Jika Anda menjalankan kode di atas dan memantau penggunaan memori, Anda akan menemukan bahwa Anda mengalami kebocoran memori besar, bocor satu megabyte per detik! Dan bahkan GC manual tidak membantu. Jadi sepertinya kita membocorkan longStr setiap kali replaceThing dipanggil. Tapi kenapa?

Mari kita periksa hal-hal secara lebih rinci:

Setiap objek theThing berisi objek longStr 1MB sendiri. Setiap detik, saat kita memanggil replaceThing , ia menyimpan referensi ke objek theThing di priorThing . Tapi kami masih tidak akan berpikir ini akan menjadi masalah, karena setiap kali melalui, priorThing yang direferensikan sebelumnya akan direferensikan (ketika priorThing direset melalui priorThing = theThing; ). Dan terlebih lagi, hanya dirujuk di bagian utama replaceThing dan dalam fungsi unused yang sebenarnya tidak pernah digunakan.

Jadi sekali lagi kita bertanya-tanya mengapa ada kebocoran memori di sini!?

Untuk memahami apa yang terjadi, kita perlu lebih memahami bagaimana segala sesuatunya bekerja di JavaScript di bawah tenda. Cara khas penutupan diimplementasikan adalah bahwa setiap objek fungsi memiliki tautan ke objek bergaya kamus yang mewakili ruang lingkup leksikalnya. Jika kedua fungsi yang didefinisikan di dalam replaceThing benar-benar menggunakan priorThing , penting bahwa keduanya mendapatkan objek yang sama, bahkan jika priorThing ditugaskan berulang kali, sehingga kedua fungsi berbagi lingkungan leksikal yang sama. Tetapi segera setelah variabel digunakan oleh penutupan apa pun, variabel itu berakhir di lingkungan leksikal yang dibagikan oleh semua penutupan dalam lingkup itu. Dan nuansa kecil itulah yang menyebabkan kebocoran memori yang mengerikan ini. (Lebih detail tentang ini tersedia di sini.)

Contoh Kebocoran Memori 2: Referensi melingkar

Pertimbangkan fragmen kode ini:

 function addClickHandler(element) { element.click = function onClick(e) { alert("Clicked the " + element.nodeName) } }

Di sini, onClick memiliki penutupan yang menyimpan referensi ke element (melalui element.nodeName ). Dengan juga menetapkan onClick ke element.click , referensi melingkar dibuat; yaitu: element -> onClick -> element -> onClick -> element

Menariknya, bahkan jika element dihapus dari DOM, referensi mandiri melingkar di atas akan mencegah element dan onClick dikumpulkan, dan karenanya, kebocoran memori.

Menghindari Kebocoran Memori: Apa yang perlu Anda ketahui

Manajemen memori JavaScript (dan, khususnya, pengumpulan sampah) sebagian besar didasarkan pada gagasan keterjangkauan objek.

Objek berikut diasumsikan dapat dijangkau dan dikenal sebagai "akar":

  • Objek yang direferensikan dari mana saja di tumpukan panggilan saat ini (yaitu, semua variabel dan parameter lokal dalam fungsi yang saat ini dipanggil, dan semua variabel dalam lingkup penutupan)
  • Semua variabel global

Objek disimpan dalam memori setidaknya selama mereka dapat diakses dari akar mana pun melalui referensi, atau rantai referensi.

Ada Pengumpul Sampah (GC) di browser yang membersihkan memori yang ditempati oleh objek yang tidak terjangkau; yaitu, objek akan dihapus dari memori jika dan hanya jika GC yakin bahwa objek tersebut tidak dapat dijangkau. Sayangnya, cukup mudah untuk berakhir dengan objek "zombie" mati yang sebenarnya tidak lagi digunakan tetapi GC masih menganggapnya "dapat dijangkau".

Terkait: Praktik Terbaik dan Kiat JavaScript oleh Pengembang Toptal

Kesalahan Umum #4: Kebingungan tentang kesetaraan

Salah satu kemudahan dalam JavaScript adalah ia akan secara otomatis memaksa nilai apa pun yang direferensikan dalam konteks boolean ke nilai boolean. Tetapi ada beberapa kasus di mana ini bisa membingungkan sekaligus nyaman. Beberapa hal berikut, misalnya, telah diketahui mengganggu banyak pengembang JavaScript:

 // All of these evaluate to 'true'! console.log(false == '0'); console.log(null == undefined); console.log(" \t\r\n" == 0); console.log('' == 0); // And these do too! if ({}) // ... if ([]) // ...

Berkenaan dengan dua yang terakhir, meskipun kosong (yang mungkin membuat orang percaya bahwa mereka akan dievaluasi menjadi false ), baik {} dan [] sebenarnya adalah objek dan objek apa pun akan dipaksa ke nilai boolean true dalam JavaScript, konsisten dengan spesifikasi ECMA-262.

Seperti yang ditunjukkan oleh contoh-contoh ini, aturan jenis paksaan terkadang bisa menjadi jelas seperti lumpur. Oleh karena itu, kecuali jenis paksaan diinginkan secara eksplisit, biasanya yang terbaik adalah menggunakan === dan !== (daripada == dan != ), untuk menghindari efek samping yang tidak diinginkan dari paksaan jenis. ( == dan != secara otomatis melakukan konversi jenis ketika membandingkan dua hal, sedangkan === dan !== melakukan perbandingan yang sama tanpa konversi jenis.)

Dan sepenuhnya sebagai sidepoint – tetapi karena kita berbicara tentang pemaksaan tipe dan perbandingan – perlu disebutkan bahwa membandingkan NaN dengan apa pun (bahkan NaN !) akan selalu menghasilkan false . Oleh karena itu Anda tidak dapat menggunakan operator kesetaraan ( == , === , != , !== ) untuk menentukan apakah suatu nilai adalah NaN atau tidak. Sebagai gantinya, gunakan fungsi isNaN() global bawaan:

 console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(isNaN(NaN)); // true

Kesalahan Umum #5: Manipulasi DOM yang tidak efisien

JavaScript membuatnya relatif mudah untuk memanipulasi DOM (yaitu, menambah, memodifikasi, dan menghapus elemen), tetapi tidak melakukan apa pun untuk mempromosikannya secara efisien.

Contoh umum adalah kode yang menambahkan serangkaian Elemen DOM satu per satu. Menambahkan elemen DOM adalah operasi yang mahal. Kode yang menambahkan beberapa elemen DOM secara berurutan tidak efisien dan kemungkinan tidak berfungsi dengan baik.

Salah satu alternatif yang efektif ketika beberapa elemen DOM perlu ditambahkan adalah dengan menggunakan fragmen dokumen, sehingga meningkatkan efisiensi dan kinerja.

Sebagai contoh:

 var div = document.getElementsByTagName("my_div"); var fragment = document.createDocumentFragment(); for (var e = 0; e < elems.length; e++) { // elems previously set to list of elements fragment.appendChild(elems[e]); } div.appendChild(fragment.cloneNode(true));

Selain peningkatan efisiensi yang melekat pada pendekatan ini, membuat elemen DOM yang dilampirkan itu mahal, sedangkan membuat dan memodifikasinya saat terlepas dan kemudian memasangnya menghasilkan kinerja yang jauh lebih baik.

Kesalahan Umum #6: Penggunaan definisi fungsi yang salah di dalam for loops

Pertimbangkan kode ini:

 var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example for (var i = 0; i < n; i++) { elements[i].onclick = function() { console.log("This is element #" + i); }; }

Berdasarkan kode di atas, jika ada 10 elemen input, mengklik salah satunya akan menampilkan "Ini adalah elemen #10"! Ini karena, pada saat onclick dipanggil untuk salah satu elemen, loop for di atas akan selesai dan nilai i sudah menjadi 10 (untuk semuanya ).

Namun, inilah cara kami memperbaiki masalah kode di atas, untuk mencapai perilaku yang diinginkan:

 var elements = document.getElementsByTagName('input'); var n = elements.length; // assume we have 10 elements for this example var makeHandler = function(num) { // outer function return function() { // inner function console.log("This is element #" + num); }; }; for (var i = 0; i < n; i++) { elements[i].onclick = makeHandler(i+1); }

Dalam versi kode yang direvisi ini, makeHandler segera dieksekusi setiap kali kita melewati loop, setiap kali menerima nilai i+1 saat itu dan mengikatnya ke variabel num yang dicakup. Fungsi luar mengembalikan fungsi dalam (yang juga menggunakan variabel num tercakup ini) dan klik elemen onclick ke fungsi dalam itu. Ini memastikan bahwa setiap onclick menerima dan menggunakan nilai i yang tepat (melalui variabel num yang dicakup).

Kesalahan Umum #7: Kegagalan untuk memanfaatkan warisan prototipe dengan benar

Persentase yang sangat tinggi dari pengembang JavaScript gagal untuk sepenuhnya memahami, dan oleh karena itu untuk sepenuhnya memanfaatkan, fitur pewarisan prototipe.

Berikut adalah contoh sederhana. Pertimbangkan kode ini:

 BaseObject = function(name) { if(typeof name !== "undefined") { this.name = name; } else { this.name = 'default' } };

Tampaknya cukup mudah. Jika Anda memberikan nama, gunakan itu, jika tidak atur nama ke 'default'; misalnya:

 var firstObj = new BaseObject(); var secondObj = new BaseObject('unique'); console.log(firstObj.name); // -> Results in 'default' console.log(secondObj.name); // -> Results in 'unique'

Tetapi bagaimana jika kita melakukan ini:

 delete secondObj.name;

Kami kemudian akan mendapatkan:

 console.log(secondObj.name); // -> Results in 'undefined'

Tetapi bukankah lebih baik jika ini kembali ke 'default'? Ini dapat dengan mudah dilakukan, jika kita memodifikasi kode asli untuk memanfaatkan pewarisan prototipe, sebagai berikut:

 BaseObject = function (name) { if(typeof name !== "undefined") { this.name = name; } }; BaseObject.prototype.name = 'default';

Dengan versi ini, BaseObject mewarisi properti name dari objek prototype , di mana ia disetel (secara default) ke 'default' . Jadi, jika konstruktor dipanggil tanpa nama, namanya akan default ke default . Demikian pula, jika properti name dihapus dari instance BaseObject , rantai prototipe kemudian akan dicari dan properti name akan diambil dari objek prototype di mana nilainya masih 'default' . Jadi sekarang kita mendapatkan:

 var thirdObj = new BaseObject('unique'); console.log(thirdObj.name); // -> Results in 'unique' delete thirdObj.name; console.log(thirdObj.name); // -> Results in 'default'

Kesalahan Umum #8: Membuat referensi yang salah ke metode instan

Mari kita definisikan objek sederhana, dan buat serta instance-nya, sebagai berikut:

 var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject();

Sekarang, untuk kenyamanan, mari buat referensi ke metode whoAmI , mungkin agar kita dapat mengaksesnya hanya dengan whoAmI() daripada obj.whoAmI() () yang lebih panjang :

 var whoAmI = obj.whoAmI;

Dan untuk memastikan semuanya terlihat copacetic, mari cetak nilai variabel whoAmI baru kita:

 console.log(whoAmI);

Keluaran:

 function () { console.log(this === window ? "window" : "MyObj"); }

OK keren. Terlihat baik-baik saja.

Tapi sekarang, lihat perbedaannya saat kita memanggil obj.whoAmI() vs. referensi kenyamanan kita whoAmI() :

 obj.whoAmI(); // outputs "MyObj" (as expected) whoAmI(); // outputs "window" (uh-oh!)

Apa yang salah?

Headfake di sini adalah, ketika kita melakukan tugas var whoAmI = obj.whoAmI; , variabel baru whoAmI didefinisikan di namespace global . Akibatnya, nilai this adalah window , bukan instance obj dari MyObject !

Jadi, jika kita benar-benar perlu membuat referensi ke metode objek yang ada, kita harus yakin melakukannya di dalam namespace objek itu, untuk mempertahankan nilai this . Salah satu cara untuk melakukan ini adalah, misalnya, sebagai berikut:

 var MyObject = function() {} MyObject.prototype.whoAmI = function() { console.log(this === window ? "window" : "MyObj"); }; var obj = new MyObject(); obj.w = obj.whoAmI; // still in the obj namespace obj.whoAmI(); // outputs "MyObj" (as expected) obj.w(); // outputs "MyObj" (as expected)

Kesalahan Umum #9: Memberikan string sebagai argumen pertama ke setTimeout atau setInterval

Sebagai permulaan, mari kita perjelas sesuatu di sini: Memberikan string sebagai argumen pertama untuk setTimeout atau setInterval itu sendiri bukanlah kesalahan. Ini adalah kode JavaScript yang sah. Masalahnya di sini lebih pada kinerja dan efisiensi. Apa yang jarang dijelaskan adalah bahwa, di balik tenda, jika Anda memasukkan string sebagai argumen pertama ke setTimeout atau setInterval , itu akan diteruskan ke konstruktor fungsi untuk diubah menjadi fungsi baru. Proses ini bisa lambat dan tidak efisien, dan jarang diperlukan.

Alternatif untuk melewatkan string sebagai argumen pertama untuk metode ini adalah dengan meneruskan sebuah fungsi . Mari kita lihat sebuah contoh.

Di sini, kemudian, akan menjadi penggunaan yang cukup umum dari setInterval dan setTimeout , meneruskan string sebagai parameter pertama:

 setInterval("logTime()", 1000); setTimeout("logMessage('" + msgValue + "')", 1000);

Pilihan yang lebih baik adalah meneruskan fungsi sebagai argumen awal; misalnya:

 setInterval(logTime, 1000); // passing the logTime function to setInterval setTimeout(function() { // passing an anonymous function to setTimeout logMessage(msgValue); // (msgValue is still accessible in this scope) }, 1000);

Kesalahan Umum #10: Gagal menggunakan "mode ketat"

Seperti yang dijelaskan dalam Panduan Perekrutan JavaScript kami, "mode ketat" (yaitu, termasuk 'use strict'; di awal file sumber JavaScript Anda) adalah cara untuk secara sukarela menerapkan penguraian yang lebih ketat dan penanganan kesalahan pada kode JavaScript Anda saat runtime, juga sebagai membuatnya lebih aman.

Meski harus diakui, kegagalan menggunakan mode ketat bukanlah sebuah “kesalahan” semata, penggunaannya semakin digalakkan dan kelalaiannya semakin dianggap sebagai bentuk yang buruk.

Berikut adalah beberapa manfaat utama dari mode ketat:

  • Membuat debugging lebih mudah. Kesalahan kode yang seharusnya diabaikan atau akan gagal secara diam-diam sekarang akan menghasilkan kesalahan atau melempar pengecualian, memperingatkan Anda lebih cepat tentang masalah dalam kode Anda dan mengarahkan Anda lebih cepat ke sumbernya.
  • Mencegah global yang tidak disengaja. Tanpa mode ketat, menetapkan nilai ke variabel yang tidak dideklarasikan secara otomatis membuat variabel global dengan nama itu. Ini adalah salah satu kesalahan paling umum dalam JavaScript. Dalam mode ketat, mencoba melakukannya akan menimbulkan kesalahan.
  • Menghilangkan paksaan this . Tanpa mode ketat, referensi ke nilai null atau undefined this secara otomatis dipaksa ke global. Ini dapat menyebabkan banyak kutu kepala dan jenis kutu rambut. Dalam mode ketat, referensi aa nilai null atau undefined this menimbulkan kesalahan.
  • Melarang duplikat nama properti atau nilai parameter. Mode ketat memunculkan kesalahan saat mendeteksi duplikat bernama properti di objek (misalnya, var object = {foo: "bar", foo: "baz"}; ) atau duplikat bernama argumen untuk fungsi (misalnya, function foo(val1, val2, val1){} ), sehingga menangkap apa yang hampir pasti merupakan bug dalam kode Anda yang mungkin telah membuang banyak waktu untuk melacaknya.
  • Membuat eval() lebih aman. Ada beberapa perbedaan dalam cara eval() berperilaku dalam mode ketat dan mode tidak ketat. Yang paling penting, dalam mode ketat, variabel dan fungsi yang dideklarasikan di dalam pernyataan eval() tidak dibuat dalam lingkup yang berisi ( mereka dibuat dalam lingkup yang berisi dalam mode non-ketat, yang juga dapat menjadi sumber masalah umum).
  • Melempar kesalahan pada penggunaan delete yang tidak valid. Operator delete (digunakan untuk menghapus properti dari objek) tidak dapat digunakan pada properti objek yang tidak dapat dikonfigurasi. Kode non-ketat akan gagal secara diam-diam ketika upaya dilakukan untuk menghapus properti yang tidak dapat dikonfigurasi, sedangkan mode ketat akan menimbulkan kesalahan dalam kasus seperti itu.

Bungkus

Seperti halnya dengan teknologi apa pun, semakin baik Anda memahami mengapa dan bagaimana JavaScript bekerja dan tidak, semakin solid kode Anda dan semakin Anda dapat secara efektif memanfaatkan kekuatan bahasa yang sebenarnya. Sebaliknya, kurangnya pemahaman yang tepat tentang paradigma dan konsep JavaScript memang menjadi penyebab banyak masalah JavaScript.

Membiasakan diri Anda dengan nuansa dan kehalusan bahasa secara menyeluruh adalah strategi paling efektif untuk meningkatkan kemahiran Anda dan meningkatkan produktivitas Anda. Menghindari banyak kesalahan umum JavaScript akan membantu ketika JavaScript Anda tidak berfungsi.

Terkait: Janji JavaScript: Tutorial dengan Contoh