valueOf () เทียบกับ toString () ใน Javascript


115

ใน Javascript ทุกอ็อบเจ็กต์มีเมธอด valueOf () และ toString () ฉันคิดว่าเมธอด toString () ถูกเรียกใช้เมื่อใดก็ตามที่มีการเรียกใช้การแปลงสตริง แต่เห็นได้ชัดว่ามันถูกทรัมป์โดย valueOf ()

ตัวอย่างเช่นรหัส

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

จะพิมพ์

x=42
x=foo

สิ่งนี้ทำให้ฉันถอยหลัง .. ถ้า x เป็นจำนวนเชิงซ้อนตัวอย่างเช่นฉันต้องการให้ valueOf () ให้ขนาดของมันแก่ฉัน แต่เมื่อใดก็ตามที่ฉันต้องการแปลงเป็นสตริงฉันต้องการอะไรเช่น "a + bi" และฉันไม่ต้องการเรียก toString () อย่างชัดเจนในบริบทที่ส่อสตริง

เป็นแบบนี้หรือเปล่า


6
คุณเคยลองwindow.console.log (x);หรือยังalert (x);?
Li0liQ

5
พวกเขาให้ "Object" และ "foo" ตามลำดับ สิ่งที่สนุก.
brainjam

จริงๆแล้ว alert (x); ให้ "foo" และ window.console.log (x); ให้ "foo {}" ใน Firebug และ Object ทั้งหมดในคอนโซล Chrome
brainjam

ใน Firefox 33.0.2 alert(x)การแสดงfooและการแสดงwindow.console.log(x) Object { toString: x.toString(), valueOf: x.valueOf() }
John Sonderson

คำตอบ:


107

เหตุผลที่ ("x =" + x) ให้ "x = value" และไม่ใช่ "x = tostring" มีดังต่อไปนี้ เมื่อประเมิน "+" จาวาสคริปต์จะรวบรวมค่าดั้งเดิมของตัวถูกดำเนินการก่อนจากนั้นจึงตัดสินใจว่าควรใช้การเพิ่มหรือการเรียงต่อกันหรือไม่โดยพิจารณาจากประเภทของตัวดำเนินการแต่ละตัว

นี่คือวิธีที่คุณคิดว่ามันได้ผล

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

และนี่คือสิ่งที่เกิดขึ้นจริง

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))

นั่นคือ toString ถูกนำไปใช้กับผลลัพธ์ของ valueOf ไม่ใช่กับวัตถุดั้งเดิมของคุณ

สำหรับการอ้างอิงเพิ่มเติมโปรดดูส่วน 11.6.1 ตัวดำเนินการเพิ่มเติม (+)ในข้อกำหนดภาษา ECMAScript


* เมื่อเรียกในบริบทสตริง ToPrimitive จะเรียกใช้ toString แต่นี่ไม่ใช่กรณีนี้เนื่องจาก '+' ไม่บังคับใช้บริบทประเภทใด ๆ


3
เงื่อนไขในบล็อก "จริง" ไม่ควรอ่าน "ถ้า (pa คือสตริง && pb เป็นสตริง)" ได้แก่ "&&" แทน "||" ?
brainjam

3
มาตรฐานระบุว่า "หรือ" (ดูลิงก์)
user187291

2
ใช่ถูกต้องแล้ว - มีการกำหนดลำดับความสำคัญให้กับสตริงมากกว่าประเภทอื่น ๆ ในการเรียงต่อกัน หากทั้งสองตัวถูกดำเนินการคือสตริงเป็นสิ่งทั้งหมดจะถูกตัดแบ่งเป็นสตริง คำตอบที่ดี.
devios1

76

นี่คือรายละเอียดเล็กน้อยก่อนที่ฉันจะได้รับคำตอบ:

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"

toStringฟังก์ชั่นไม่ได้ "กุ" โดยvalueOfทั่วไป มาตรฐาน ECMAScript ตอบคำถามนี้ได้ค่อนข้างดี ทุกออบเจ็กต์มี[[DefaultValue]]คุณสมบัติซึ่งคำนวณตามความต้องการ เมื่อขอคุณสมบัตินี้ล่ามยังให้ "คำใบ้" สำหรับมูลค่าที่คาดว่าจะได้รับ หากคำใบ้เป็นStringแล้วถูกนำมาใช้ก่อนtoString valueOfแต่ถ้าเป็นคำใบ้Numberก็valueOfจะใช้ก่อน โปรดทราบว่าหากมีเพียงตัวเดียวหรือส่งกลับค่าที่ไม่ใช่แบบดั้งเดิมก็มักจะเรียกอีกตัวหนึ่งว่าเป็นตัวเลือกที่สอง

ตัว+ดำเนินการจะให้คำใบ้เสมอNumberแม้ว่าตัวถูกดำเนินการตัวแรกจะเป็นค่าสตริงก็ตาม แม้ว่าจะขอxการNumberเป็นตัวแทนเนื่องจากตัวถูกดำเนินการแรกส่งคืนสตริงจาก[[DefaultValue]]มันจะทำการต่อสตริง

หากคุณต้องการรับประกันว่าtoStringถูกเรียกสำหรับการต่อสายอักขระให้ใช้อาร์เรย์และ.join("")วิธีการ

(ActionScript 3.0 เล็กน้อยปรับเปลี่ยนลักษณะการทำงานของ+อย่างไรก็ตามหากตัวถูกดำเนินการเป็น a ตัวดำเนินการStringจะถือว่าเป็นตัวดำเนินการต่อสายอักขระและใช้คำใบ้Stringเมื่อมีการเรียก[[DefaultValue]]ดังนั้นใน AS3 ตัวอย่างนี้จะให้ "foo, x = foo, foo = x, foo1, 43, x = foo ")


1
โปรดทราบว่าหากvalueOfหรือtoStringส่งคืน non-primitives จะถูกละเว้น หากไม่มีอยู่หรือไม่คืนค่าดั้งเดิมก็TypeErrorจะถูกโยนทิ้ง
bcherry

1
ขอบคุณ bcherry นี่คือความสามารถของคำตอบที่ฉันหวังไว้ แต่ไม่ควร x + "x ="; ผลตอบแทน "42x ="? และ x + "1"; ผลตอบแทน 421? นอกจากนี้คุณมี URL สำหรับส่วนที่เกี่ยวข้องของมาตรฐาน ECMAScript หรือไม่
brainjam

2
จริงๆแล้ว '+' ไม่ได้ใช้คำใบ้ (ดู $ 11.6.1) ดังนั้น ToPrimitive จึงเรียกใช้[[DefaultValue]](no-hint)ซึ่งเทียบเท่ากับ[[DefaultValue]](number).
user187291

9
ดูเหมือนจะไม่เป็นเช่นนั้นสำหรับคลาส Date ที่มีอยู่แล้วภายใน ("" + new Date(0)) === new Date(0).toString(). วัตถุวันที่ดูเหมือนจะคืนtoString()ค่าเสมอเมื่อมันถูกเพิ่มเข้าไปในบางสิ่ง
kpozin

7
+1 & ขอบคุณ! ฉันพบบล็อกโพสต์ของคุณซึ่งคุณได้อธิบายรายละเอียดเกี่ยวกับคำตอบนี้และต้องการเชื่อมโยง / แบ่งปันที่นี่ คำตอบนี้มีประโยชน์มาก (รวมถึงความคิดเห็นของ Dmitry A.Soshnikov)
GitaarLAB

1

TLDR

การบังคับประเภทหรือการแปลงประเภทโดยปริยายช่วยให้การพิมพ์อ่อนแอและใช้ตลอดจาวาสคริปต์ ตัวดำเนินการส่วนใหญ่ (ยกเว้นที่น่าสังเกตของตัวดำเนินการความเสมอภาคที่เข้มงวด===และ!==) และการดำเนินการตรวจสอบค่า (เช่นif(value)...) จะบังคับใช้ค่าที่ให้มาหากประเภทของค่าเหล่านั้นไม่สามารถเข้ากันได้กับการดำเนินการในทันที

กลไกที่แม่นยำที่ใช้ในการบังคับค่าขึ้นอยู่กับนิพจน์ที่กำลังประเมิน ในคำถามนี้กำลังใช้ตัวดำเนินการเพิ่มเติม

ขั้นแรกตัวดำเนินการเพิ่มเติมจะตรวจสอบให้แน่ใจว่าตัวถูกดำเนินการทั้งสองเป็นแบบดั้งเดิมซึ่งในกรณีนี้เกี่ยวข้องกับการเรียกใช้valueOfเมธอด toStringวิธีการไม่ได้เรียกว่าในกรณีนี้เพราะแทนที่valueOfวิธีการบนวัตถุที่xส่งกลับค่าดั้งเดิม

จากนั้นเนื่องจากหนึ่งในตัวถูกดำเนินการในคำถามเป็นสตริงตัวถูกดำเนินการทั้งสองจะถูกแปลงเป็นสตริง กระบวนการนี้ใช้นามธรรมการดำเนินการภายในToString(หมายเหตุ: ตัวพิมพ์ใหญ่) และแตกต่างจากtoStringวิธีการบนวัตถุ (หรือห่วงโซ่ต้นแบบ)

สุดท้ายสตริงผลลัพธ์จะถูกเชื่อมต่อกัน

รายละเอียด

บนต้นแบบของอ็อบเจ็กต์ฟังก์ชันคอนสตรัคเตอร์ทุกตัวที่สอดคล้องกับทุกประเภทภาษาใน JavaScript (เช่น Number, BigInt, String, Boolean, Symbol และ Object) มีสองวิธี: valueOfและtoString.

จุดประสงค์valueOfคือเพื่อดึงค่าดั้งเดิมที่เกี่ยวข้องกับอ็อบเจ็กต์ (ถ้ามี) หากออบเจ็กต์ไม่มีค่าดั้งเดิมพื้นฐานก็จะส่งคืนวัตถุ

หากvalueOfมีการเรียกใช้กับไพรเมทีฟดั้งเดิมจะถูกใส่กล่องอัตโนมัติตามวิธีปกติและค่าดั้งเดิมที่อยู่เบื้องหลังจะส่งกลับ โปรดสังเกตว่าสำหรับสตริงค่าดั้งเดิมที่อยู่เบื้องหลัง (เช่นค่าที่ส่งคืนโดยvalueOf) คือการแทนค่าสตริงเอง

โค้ดต่อไปนี้แสดงให้เห็นว่าvalueOfเมธอดส่งคืนค่าพื้นฐานจากอ็อบเจ็กต์ wrapper และแสดงให้เห็นว่าอินสแตนซ์อ็อบเจ็กต์ที่ไม่ได้ปรับเปลี่ยนที่ไม่สอดคล้องกับไพรเมทีฟไม่มีค่าดั้งเดิมที่จะส่งคืนอย่างไรดังนั้นพวกมันจึงส่งคืนตัวเอง

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

ในทางกลับกันจุดประสงค์toStringคือส่งคืนการแสดงสตริงของวัตถุ

ตัวอย่างเช่น:

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

สำหรับการดำเนินการส่วนใหญ่ JavaScript จะพยายามแปลงตัวถูกดำเนินการอย่างน้อยหนึ่งรายการเป็นประเภทที่ต้องการ พฤติกรรมนี้ถูกเลือกเพื่อทำให้ JavaScript ใช้งานได้ง่ายขึ้น JavaScript ในตอนแรกไม่มีข้อยกเว้นและสิ่งนี้อาจมีบทบาทในการตัดสินใจออกแบบนี้ด้วย การแปลงประเภทโดยนัยประเภทนี้เรียกว่าการบังคับประเภทและเป็นพื้นฐานของระบบประเภทหลวม (อ่อนแอ) ของ JavaScript กฎที่ซับซ้อนเบื้องหลังพฤติกรรมนี้มีจุดมุ่งหมายเพื่อย้ายความซับซ้อนของการพิมพ์เป็นภาษาตัวเองและออกจากโค้ดของคุณ

ในระหว่างกระบวนการบีบบังคับมีสองโหมดของการแปลงที่สามารถเกิดขึ้นได้:

  1. การแปลงออบเจ็กต์เป็นแบบดั้งเดิม (ซึ่งอาจเกี่ยวข้องกับการแปลงประเภทเอง) และ
  2. แปลงโดยตรงตัวอย่างประเภทที่เฉพาะเจาะจงโดยใช้วัตถุที่ฟังก์ชั่นคอนสตรัคของหนึ่งในรูปแบบดั้งเดิม (เช่น. Number(), Boolean(), String()ฯลฯ )

การแปลงเป็นดั้งเดิม

เมื่อพยายามแปลงชนิดที่ไม่ใช่แบบดั้งเดิมไปเป็นแบบดั้งเดิมที่จะดำเนินการการดำเนินการนามธรรมToPrimitiveจะถูกเรียกโดยใช้ "คำใบ้" ของ "จำนวน" หรือ "สตริง" ที่เป็นทางเลือก หากคำใบ้ถูกละไว้คำใบ้เริ่มต้นคือ 'number' (เว้นแต่จะ@@toPrimitiveมีการแทนที่เมธอดนั้น) หากคำใบ้เป็น 'สตริง' toStringจะถูกลองก่อนและvalueOfครั้งที่สองถ้าtoStringไม่ส่งคืนค่าดั้งเดิม อื่น ๆ ในทางกลับกัน คำใบ้ขึ้นอยู่กับการดำเนินการขอการแปลง

ตัวดำเนินการเพิ่มเติมไม่มีคำใบ้จึงvalueOfพยายามก่อน ตัวดำเนินการลบให้คำใบ้ของ 'จำนวน' ดังนั้นจึงvalueOfต้องพยายามก่อน สถานการณ์เดียวที่ฉันพบในข้อมูลจำเพาะซึ่งคำใบ้คือ 'สตริง' คือ:

  1. Object#toString
  2. การดำเนินการนามธรรมToPropertyKeyซึ่งแปลงอาร์กิวเมนต์เป็นค่าที่อาจใช้เป็นคีย์คุณสมบัติ

การแปลงประเภทโดยตรง

แต่ละตัวดำเนินการมีกฎของตัวเองในการดำเนินการให้เสร็จสิ้น ตัวดำเนินการเพิ่มเติมจะใช้ToPrimitiveเพื่อให้แน่ใจว่าแต่ละตัวถูกดำเนินการเป็นแบบดั้งเดิม จากนั้นถ้าตัวถูกดำเนินการใด ๆ เป็นสตริงจากนั้นก็จะเรียกใช้การดำเนินการนามธรรมToStringกับตัวถูกดำเนินการแต่ละตัวโดยเจตนาเพื่อส่งมอบพฤติกรรมการต่อสายอักขระที่เราคาดหวังด้วยสตริง ถ้าหลังจากToPrimitiveขั้นตอนตัวถูกดำเนินการทั้งสองไม่ใช่สตริงการบวกเลขคณิตจะดำเนินการ

ซึ่งแตกต่างนอกจากนี้ผู้ประกอบการลบไม่ได้มากเกินไปพฤติกรรมและอื่น ๆ จะเรียกในแต่ละตัวถูกดำเนินการมีแปลงแรกพวกเขาที่จะใช้วิทยาการtoNumericToPrimitive

ดังนั้น:

 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

โปรดสังเกตว่าDateวัตถุที่อยู่ภายในนั้นไม่ซ้ำกันเนื่องจากเป็นวัตถุภายในเท่านั้นที่จะแทนที่@@toPrimitiveเมธอดเริ่มต้นซึ่งคำใบ้เริ่มต้นสันนิษฐานว่าเป็น 'สตริง' (แทนที่จะเป็น 'ตัวเลข') เหตุผลในการมีสิ่งนี้คือการให้Dateอินสแตนซ์แปลเป็นสตริงที่อ่านได้ตามค่าเริ่มต้นแทนที่จะเป็นค่าตัวเลขเพื่อความสะดวกของโปรแกรมเมอร์ คุณสามารถแทนที่@@toPrimitiveในวัตถุของคุณเองโดยใช้Symbol.toPrimitive.

ตารางต่อไปนี้แสดงผลการบีบบังคับสำหรับตัวดำเนินการความเท่าเทียมเชิงนามธรรม ( ==) (ที่มา ):

ใส่คำอธิบายภาพที่นี่

ดูยัง

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.