valueOf () vs. toString () di Javascript

115

Dalam Javascript setiap objek memiliki metode valueOf () dan toString (). Saya akan berpikir bahwa metode toString () dipanggil setiap kali konversi string dipanggil, tetapi tampaknya itu dikalahkan oleh valueOf ().

Misalnya kode

var x = {toString: function() {return "foo"; },
         valueOf: function() {return 42; }};
window.console.log ("x="+x);
window.console.log ("x="+x.toString());

akan mencetak

x=42
x=foo

Ini menurut saya terbalik .. jika x adalah bilangan kompleks, misalnya, saya ingin valueOf () memberi saya besarnya, tetapi setiap kali saya ingin mengonversi ke string, saya menginginkan sesuatu seperti "a + bi". Dan saya tidak ingin memanggil toString () secara eksplisit dalam konteks yang menyiratkan string.

Apakah begini adanya?

otak macet
sumber
6
Sudahkah Anda mencoba window.console.log (x);atau alert (x);?
Li0liQ
5
Mereka masing-masing memberikan "Object" dan "foo". Hal menyenangkan.
brainjam
Sebenarnya, waspada (x); memberikan "foo", dan window.console.log (x); memberikan "foo {}" di Firebug dan seluruh Object di konsol Chrome.
brainjam
Di Firefox 33.0.2 alert(x)ditampilkan foodan window.console.log(x)ditampilkan Object { toString: x.toString(), valueOf: x.valueOf() }.
John Sonderson

Jawaban:

107

Alasan mengapa ("x =" + x) memberikan "x = nilai" dan bukan "x = tostring" adalah sebagai berikut. Saat mengevaluasi "+", pertama-tama javascript mengumpulkan nilai primitif dari operan, lalu memutuskan apakah penambahan atau penggabungan harus diterapkan, berdasarkan jenis setiap primitif.

Jadi, menurut Anda inilah cara kerjanya

a + b:
    pa = ToPrimitive(a)
    if(pa is string)
       return concat(pa, ToString(b))
    else
       return add(pa, ToNumber(b))

dan inilah yang sebenarnya terjadi

a + b:
    pa = ToPrimitive(a)
    pb = ToPrimitive(b)*
    if(pa is string || pb is string)
       return concat(ToString(pa), ToString(pb))
    else
       return add(ToNumber(pa), ToNumber(pb))

Artinya, toString diterapkan ke hasil valueOf, bukan ke objek asli Anda.

Untuk referensi lebih lanjut, lihat bagian 11.6.1 Operator Penambahan (+) di Spesifikasi Bahasa ECMAScript.


* Ketika dipanggil dalam konteks string, ToPrimitive memang memanggil toString, tetapi ini tidak terjadi di sini, karena '+' tidak memberlakukan konteks tipe apa pun.

pengguna187291
sumber
3
Bukankah kondisional dalam blok "sebenarnya" dibaca "jika (pa adalah string && pb adalah string)"? Yaitu "&&" bukan "||" ?
brainjam
3
Standar pasti mengatakan "atau" (lihat tautan).
pengguna187291
2
Ya, itu benar - prioritas diberikan ke string di atas jenis lain dalam penggabungan. Jika salah satu operan adalah string, semuanya akan digabungkan sebagai string. Jawaban yang bagus.
devios1
76

Ini sedikit lebih detail, sebelum saya mendapatkan jawabannya:

var x = {
    toString: function () { return "foo"; },
    valueOf: function () { return 42; }
};

alert(x); // foo
"x=" + x; // "x=42"
x + "=x"; // "42=x"
x + "1"; // 421
x + 1; // 43
["x=", x].join(""); // "x=foo"

The toStringfungsi tidak "palsu" oleh valueOfpada umumnya. Standar ECMAScript sebenarnya menjawab pertanyaan ini dengan cukup baik. Setiap objek memiliki [[DefaultValue]]properti, yang dihitung sesuai permintaan. Saat meminta properti ini, penerjemah juga memberikan "petunjuk" untuk nilai seperti apa yang diharapkannya. Jika petunjuknya String, maka toStringdigunakan sebelumnya valueOf. Tapi, jika petunjuknya Number, maka valueOfakan digunakan dulu. Perhatikan bahwa jika hanya ada satu, atau mengembalikan non-primitif, biasanya akan memanggil yang lain sebagai pilihan kedua.

The +Operator selalu menyediakan petunjuk yang Number, bahkan jika operan pertama adalah nilai string. Meskipun meminta xuntuk perusahaan Numberrepresentasi, karena operan pertama mengembalikan sebuah string dari [[DefaultValue]], itu tidak penggabungan string.

Jika Anda ingin menjamin yang toStringdipanggil untuk penggabungan string, gunakan array dan .join("")metode.

(ActionScript 3.0 sedikit mengubah perilaku +, namun. Jika salah satu operand adalah a String, itu akan memperlakukannya sebagai operator penggabungan string dan menggunakan petunjuk Stringketika dipanggil [[DefaultValue]]. Jadi, di AS3, contoh ini menghasilkan "foo, x = foo, foo = x, foo1, 43, x = foo ".)

bcherry.dll
sumber
1
Perhatikan juga bahwa jika valueOfatau toStringmengembalikan non-primitif, mereka diabaikan. Jika tidak ada, atau tidak ada yang mengembalikan primitif, maka a TypeErrordilemparkan.
bcherry
1
Terima kasih bcherry, ini adalah jawaban yang saya harapkan. Tapi seharusnya x + "x ="; menghasilkan "42x ="? Dan x + "1"; menghasilkan 421? Juga, apakah Anda memiliki URL untuk bagian yang relevan dari standar ECMAScript?
brainjam
2
Sebenarnya, '+' tidak menggunakan petunjuk (lihat $ 11.6.1) oleh karena itu ToPrimitive memanggil [[DefaultValue]](no-hint), yang setara dengan [[DefaultValue]](number).
pengguna187291
9
Ini tampaknya tidak menjadi kasus untuk kelas Tanggal bawaan. ("" + new Date(0)) === new Date(0).toString(). Objek Tanggal sepertinya selalu mengembalikan toString()nilainya saat ditambahkan ke sesuatu.
kpozin
7
+1 & Terima Kasih! Saya menemukan entri blog Anda di mana Anda menjelaskan jawaban ini dan ingin menautkan / membagikannya di sini. Itu adalah tambahan yang sangat membantu untuk jawaban ini (termasuk komentar Dmitry A. Soshnikov).
GitaarLAB
1

TLDR

Paksaan tipe, atau konversi tipe implisit, memungkinkan pengetikan lemah dan digunakan di seluruh JavaScript. Kebanyakan operator (dengan pengecualian penting dari operator kesetaraan yang ketat ===dan !==), dan operasi pemeriksaan nilai (misalnya if(value)...), akan memaksa nilai yang diberikan kepada mereka, jika jenis nilai tersebut tidak segera kompatibel dengan operasi.

Mekanisme tepat yang digunakan untuk memaksa nilai bergantung pada ekspresi yang dievaluasi. Dalam pertanyaan, operator penjumlahan sedang digunakan.

Operator penjumlahan pertama-tama akan memastikan kedua operan adalah primitif, yang, dalam hal ini, melibatkan pemanggilan valueOfmetode. The toStringMetode ini tidak disebut dalam hal ini karena ditimpa valueOfmetode pada objek xmengembalikan nilai primitif.

Kemudian, karena salah satu operan yang dimaksud adalah string, maka kedua operan tersebut akan diubah menjadi string. Proses ini menggunakan abstrak, operasi internal ToString(catatan: huruf besar), dan berbeda dari toStringmetode pada objek (atau rantai prototipe).

Akhirnya, string yang dihasilkan digabungkan.

Detail

Pada prototipe setiap objek fungsi konstruktor yang sesuai dengan setiap jenis bahasa dalam JavaScript (mis. Number, BigInt, String, Boolean, Symbol, dan Object), ada dua metode: valueOfdan toString.

Tujuannya valueOfadalah untuk mengambil nilai primitif yang terkait dengan suatu objek (jika ada). Jika sebuah objek tidak memiliki nilai primitif yang mendasari, maka objek tersebut dikembalikan begitu saja.

Jika valueOfdipanggil terhadap primitif, maka primitif tersebut otomatis dimasukkan ke dalam kotak dengan cara normal, dan nilai primitif yang mendasarinya dikembalikan. Perhatikan bahwa untuk string, nilai primitif yang mendasari (yaitu nilai yang dikembalikan oleh valueOf) adalah representasi string itu sendiri.

Kode berikut menunjukkan bahwa valueOfmetode mengembalikan nilai primitif yang mendasari dari objek pembungkus, dan ini menunjukkan bagaimana contoh objek yang tidak dimodifikasi yang tidak sesuai dengan primitif, tidak memiliki nilai primitif untuk dikembalikan, jadi mereka hanya mengembalikan dirinya sendiri.

console.log(typeof new Boolean(true)) // 'object'
console.log(typeof new Boolean(true).valueOf()) // 'boolean'
console.log(({}).valueOf()) // {} (no primitive value to return)

Tujuan dari toString, di sisi lain, adalah mengembalikan representasi string dari suatu objek.

Sebagai contoh:

console.log({}.toString()) // '[object Object]'
console.log(new Number(1).toString()) // '1'

Untuk sebagian besar operasi, JavaScript akan secara diam-diam mencoba mengonversi satu atau lebih operan ke jenis yang diperlukan. Perilaku ini dipilih untuk membuat JavaScript lebih mudah digunakan. JavaScript awalnya tidak memiliki pengecualian , dan ini mungkin juga berperan dalam keputusan desain ini. Jenis konversi jenis implisit ini disebut pemaksaan jenis, dan ini adalah dasar dari sistem jenis JavaScript yang longgar (lemah). Aturan rumit di balik perilaku ini dimaksudkan untuk memindahkan kerumitan typecasting ke dalam bahasa itu sendiri, dan keluar dari kode Anda.

Selama proses koersif, ada dua mode konversi yang dapat terjadi:

  1. Konversi objek menjadi primitif (yang mungkin melibatkan konversi tipe itu sendiri), dan
  2. Konversi langsung ke jenis tertentu misalnya, menggunakan fungsi objek konstruktor dari salah satu tipe primitif (yaitu. Number(), Boolean(), String()Dll)

Konversi Menjadi Primitif

Ketika mencoba untuk mengubah tipe non-primitif menjadi primitif untuk dioperasikan, operasi abstrak ToPrimitivedipanggil dengan "petunjuk" opsional dari 'nomor', atau 'string'. Jika petunjuk dihilangkan, petunjuk default adalah 'nomor' (kecuali jika @@toPrimitivemetode telah diganti). Jika petunjuknya adalah 'string', maka toStringdicoba dulu, dan valueOfkedua jika toStringtidak mengembalikan primitif. Lain, sebaliknya. Petunjuknya tergantung pada operasi yang meminta konversi.

Operator tambahan tidak memberikan petunjuk, jadi valueOfdicoba dulu. Operator pengurangan memberikan petunjuk 'angka', jadi valueOfcoba dulu. Satu-satunya situasi yang dapat saya temukan dalam spesifikasi di mana petunjuknya adalah 'string' adalah:

  1. Object#toString
  2. Operasi abstrak ToPropertyKey, yang mengubah argumen menjadi nilai yang dapat digunakan sebagai kunci properti

Konversi Jenis Langsung

Setiap operator memiliki aturannya sendiri untuk menyelesaikan operasinya. Operator tambahan pertama-tama akan digunakan ToPrimitiveuntuk memastikan setiap operand adalah primitif; kemudian, jika salah satu operand adalah string, maka dengan sengaja akan memanggil operasi abstrak ToStringpada setiap operan, untuk menyampaikan perilaku penggabungan string yang kita harapkan dengan string. Jika, setelah ToPrimitivelangkah, kedua operan bukan string, maka dilakukan penjumlahan aritmatika.

Tidak seperti penjumlahan, operator pengurangan tidak memiliki perilaku kelebihan beban, sehingga akan memanggil toNumericsetiap operan yang telah mengonversinya terlebih dahulu menjadi penggunaan primitif ToPrimitive.

Begitu:

 1  +  1   //  2                 
'1' +  1   // '11'   Both already primitives, RHS converted to string, '1' + '1',   '11'
 1  + [2]  // '12'   [2].valueOf() returns an object, so `toString` fallback is used, 1 + String([2]), '1' + '2', 12
 1  + {}   // '1[object Object]'    {}.valueOf() is not a primitive, so toString fallback used, String(1) + String({}), '1' + '[object Object]', '1[object Object]'
 2  - {}   // NaN    {}.valueOf() is not a primitive, so toString fallback used => 2 - Number('[object Object]'), NaN
+'a'       // NaN    `ToPrimitive` passed 'number' hint), Number('a'), NaN
+''        // 0      `ToPrimitive` passed 'number' hint), Number(''), 0
+'-1'      // -1     `ToPrimitive` passed 'number' hint), Number('-1'), -1
+{}        // NaN    `ToPrimitive` passed 'number' hint', `valueOf` returns an object, so falls back to `toString`, Number('[Object object]'), NaN
 1 + 'a'   // '1a'    Both are primitives, one is a string, String(1) + 'a'
 1 + {}    // '1[object Object]'    One primitive, one object, `ToPrimitive` passed no hint, meaning conversion to string will occur, one of the operands is now a string, String(1) + String({}), `1[object Object]`
[] + []    // ''     Two objects, `ToPrimitive` passed no hint, String([]) + String([]), '' (empty string)
 1 - 'a'   // NaN    Both are primitives, one is a string, `ToPrimitive` passed 'number' hint, 1-Number('a'), 1-NaN, NaN
 1 - {}    // NaN    One primitive, one is an object, `ToPrimitive` passed 'number' hint, `valueOf` returns object, so falls back to `toString`, 1-Number([object Object]), 1-NaN, NaN
[] - []    // 0      Two objects, `ToPrimitive` passed 'number' hint => `valueOf` returns array instance, so falls back to `toString`, Number('')-Number(''), 0-0, 0

Perhatikan bahwa Dateobjek intrinsik itu unik, karena itu adalah satu-satunya intrinsik yang mengganti @@toPrimitivemetode default , di mana petunjuk default dianggap sebagai 'string' (bukan 'number'). Alasan untuk memiliki ini, adalah agar Dateinstance diterjemahkan ke string yang dapat dibaca secara default, bukan nilai numeriknya, demi kenyamanan programmer. Anda dapat menimpa objek @@toPrimitiveAnda sendiri menggunakan Symbol.toPrimitive.

Grid berikut menunjukkan hasil pemaksaan untuk operator persamaan abstrak ( ==) ( sumber ):

masukkan deskripsi gambar di sini

Lihat juga .

Ben Aston
sumber