valueOf () so với toString () trong Javascript


115

Trong Javascript mọi đối tượng đều có phương thức valueOf () và toString (). Tôi đã nghĩ rằng phương thức toString () được gọi bất cứ khi nào một chuyển đổi chuỗi được gọi, nhưng rõ ràng nó bị đánh bại bởi valueOf ().

Ví dụ, mã

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

sẽ in

x=42
x=foo

Điều này khiến tôi giật mình .. ví dụ: nếu x là một số phức, tôi sẽ muốn valueOf () cung cấp cho tôi độ lớn của nó, nhưng bất cứ khi nào tôi muốn chuyển đổi thành một chuỗi, tôi sẽ muốn một cái gì đó giống như "a + bi". Và tôi không muốn phải gọi toString () một cách rõ ràng trong các ngữ cảnh ngụ ý một chuỗi.

Đây có phải là cách nó được?


6
Bạn đã thử window.console.log (x);hoặc alert (x);?
Li0liQ

5
Họ cho "Đối tượng" và "foo" tương ứng. Công cụ thú vị.
brainjam

Trên thực tế, alert (x); cho "foo", và window.console.log (x); cung cấp "foo {}" trong Firebug và toàn bộ Đối tượng trong bảng điều khiển Chrome.
brainjam

Trong Firefox 33.0.2 alert(x)hiển thị foowindow.console.log(x)hiển thị Object { toString: x.toString(), valueOf: x.valueOf() }.
John Sonderson

Câu trả lời:


107

Lý do tại sao ("x =" + x) cho "x = value" chứ không phải "x = tostring" là như sau. Khi đánh giá "+", javascript đầu tiên thu thập các giá trị nguyên thủy của các toán hạng, sau đó quyết định xem có nên áp dụng phép cộng hay phép nối hay không, dựa trên kiểu của mỗi toán hạng.

Vì vậy, đây là cách bạn nghĩ rằng nó hoạt động

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

và đây là những gì thực sự xảy ra

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

Nghĩa là, toString được áp dụng cho kết quả của valueOf, không phải cho đối tượng ban đầu của bạn.

Để tham khảo thêm, hãy xem phần 11.6.1 Toán tử Phép cộng (+) trong Đặc tả ngôn ngữ ECMAScript.


* Khi được gọi trong ngữ cảnh chuỗi, ToPrimitive sẽ gọi toString, nhưng đây không phải là trường hợp ở đây, vì '+' không thực thi bất kỳ ngữ cảnh kiểu nào.


3
Không nên điều kiện trong khối "thực sự" đọc "if (pa là chuỗi && pb là chuỗi)"? Tức là "&&" thay vì "||" ?
brainjam

3
Tiêu chuẩn chắc chắn nói "hoặc" (xem liên kết).
user187291

2
Vâng, chính xác là đúng - được ưu tiên cho các chuỗi hơn các loại khác trong phép nối. Nếu một trong hai toán hạng là một chuỗi, toàn bộ sẽ được nối thành một chuỗi. Câu trả lời tốt.
defos1

76

Đây là một chút chi tiết trước khi tôi đi đến câu trả lời:

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"

Các toStringhàm được không "vu cáo" bởi valueOfnói chung. Tiêu chuẩn ECMAScript thực sự trả lời câu hỏi này khá tốt. Mọi đối tượng đều có một thuộc [[DefaultValue]]tính, được tính toán theo yêu cầu. Khi yêu cầu thuộc tính này, trình thông dịch cũng cung cấp một "gợi ý" về loại giá trị mà nó mong đợi. Nếu gợi ý là String, thì toStringđược sử dụng trước đó valueOf. Nhưng, nếu gợi ý là Number, sau đó valueOfsẽ được sử dụng đầu tiên. Lưu ý rằng nếu chỉ có một hoặc nó trả về một giá trị không phải nguyên thủy, thì nó thường sẽ gọi cái kia là lựa chọn thứ hai.

Các +nhà điều hành luôn luôn cung cấp các gợi ý Number, ngay cả khi các toán hạng đầu tiên là một giá trị chuỗi. Ngay cả khi nó yêu cầu biểu diễn xcủa nó Number, vì toán hạng đầu tiên trả về một chuỗi từ [[DefaultValue]], nó thực hiện nối chuỗi.

Nếu bạn muốn đảm bảo rằng toStringđược gọi để nối chuỗi, hãy sử dụng một mảng và .join("")phương thức.

( +Tuy nhiên, ActionScript 3.0 sửa đổi một chút hành vi của . Nếu một trong hai toán hạng là a String, nó sẽ coi nó như một toán tử nối chuỗi và sử dụng gợi ý Stringkhi nó gọi [[DefaultValue]]. Vì vậy, trong AS3, ví dụ này cho kết quả "foo, x = foo, foo = x, foo1, 43, x = foo ".)


1
Cũng lưu ý rằng nếu valueOfhoặc toStringtrả về không phải nguyên thủy, chúng sẽ bị bỏ qua. Nếu không tồn tại hoặc không trả về giá trị nguyên thủy, thì a TypeErrorsẽ được ném.
bcherry

1
Cảm ơn bcherry, đây là tầm cỡ của câu trả lời mà tôi đã hy vọng. Nhưng không nên x + "x ="; sản lượng "42x ="? Và x + "1"; sản lượng 421? Ngoài ra, bạn có URL cho phần liên quan của tiêu chuẩn ECMAScript không?
brainjam

2
Trên thực tế, '+' không sử dụng gợi ý (xem $ 11.6.1) do đó ToPrimitive gọi [[DefaultValue]](no-hint), tương đương với [[DefaultValue]](number).
user187291

9
Điều này dường như không đúng đối với lớp Ngày tích hợp. ("" + new Date(0)) === new Date(0).toString(). Đối tượng Date dường như luôn trả về toString()giá trị của nó khi nó được thêm vào một thứ gì đó.
kpozin

7
+1 & Thx! Tôi tìm thấy bài đăng trên blog của bạn, trong đó bạn giải thích về câu trả lời này và muốn liên kết / chia sẻ nó ở đây. Nó thực sự hữu ích bổ sung cho câu trả lời này (bao gồm cả nhận xét của Dmitry A. Soshnikov).
GitaarLAB

1

TLDR

Kiểu ép buộc hoặc chuyển đổi kiểu ngầm, cho phép nhập yếu và được sử dụng trong JavaScript. Hầu hết các toán tử (với ngoại lệ đáng chú ý là các toán tử bình đẳng nghiêm ngặt ===!==), và các hoạt động kiểm tra giá trị (ví dụ. if(value)...), Sẽ ép buộc các giá trị được cung cấp cho chúng, nếu các loại giá trị đó không tương thích ngay với phép toán.

Cơ chế chính xác được sử dụng để ép buộc một giá trị phụ thuộc vào biểu thức được đánh giá. Trong câu hỏi, toán tử bổ sung đang được sử dụng.

Toán tử bổ sung trước tiên sẽ đảm bảo cả hai toán hạng là nguyên thủy, trong trường hợp này, liên quan đến việc gọi valueOfphương thức. Các toStringphương pháp không được gọi trong trường hợp này vì ghi đè valueOfphương pháp trên đối tượng xtrả về một giá trị nguyên thủy.

Sau đó, vì một trong các toán hạng trong câu hỏi là một chuỗi, nên cả hai toán hạng đều được chuyển đổi thành chuỗi. Quá trình này sử dụng thao tác trừu tượng, hoạt động nội bộ ToString(lưu ý: viết hoa) và khác với toStringphương thức trên đối tượng (hoặc chuỗi nguyên mẫu của nó).

Cuối cùng, các chuỗi kết quả được nối với nhau.

Chi tiết

Trên nguyên mẫu của mọi đối tượng hàm tạo tương ứng với mọi loại ngôn ngữ trong JavaScript (ví dụ: Number, BigInt, String, Boolean, Symbol và Object), có hai phương thức: valueOftoString.

Mục đích của valueOflà lấy giá trị nguyên thủy được liên kết với một đối tượng (nếu nó có). Nếu một đối tượng không có giá trị nguyên thủy cơ bản, thì đối tượng đó chỉ được trả về.

Nếu valueOfđược gọi chống lại một nguyên thủy, thì nguyên thủy sẽ được tự động đóng hộp theo cách thông thường và giá trị nguyên thủy cơ bản được trả về. Lưu ý rằng đối với các chuỗi, giá trị nguyên thủy cơ bản (tức là giá trị được trả về bởi valueOf) chính là biểu diễn chuỗi.

Đoạn mã sau đây cho thấy rằng valueOfphương thức trả về giá trị nguyên thủy cơ bản từ một đối tượng trình bao bọc và nó cho thấy cách các cá thể đối tượng chưa được sửa đổi không tương ứng với nguyên thủy, không có giá trị nguyên thủy để trả về, vì vậy chúng chỉ tự trả về.

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

Mặt khác, mục đích của toStringnó là trả về một biểu diễn chuỗi của một đối tượng.

Ví dụ:

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

Đối với hầu hết các thao tác, JavaScript sẽ cố gắng chuyển đổi một cách âm thầm một hoặc nhiều toán hạng thành kiểu bắt buộc. Hành vi này được chọn để làm cho JavaScript dễ sử dụng hơn. JavaScript ban đầu không có ngoại lệ và điều này cũng có thể đóng một vai trò trong quyết định thiết kế này. Kiểu chuyển đổi kiểu ngầm này được gọi là kiểu ép buộc và nó là cơ sở của hệ thống kiểu lỏng lẻo (yếu) của JavaScript. Các quy tắc phức tạp đằng sau hành vi này nhằm chuyển sự phức tạp của việc đánh máy vào chính ngôn ngữ và ra khỏi mã của bạn.

Trong quá trình cưỡng chế, có hai phương thức chuyển đổi có thể xảy ra:

  1. Chuyển đổi một đối tượng thành nguyên thủy (có thể liên quan đến chính chuyển đổi kiểu) và
  2. Chuyển đổi trực tiếp với một trường hợp loại hình cụ thể, sử dụng một đối tượng hàm constructor của một trong những loại nguyên thủy (tức là. Number(), Boolean(), String()Vv)

Chuyển đổi sang nguyên thủy

Khi cố gắng chuyển đổi các kiểu không nguyên thủy thành các kiểu nguyên thủy sẽ được vận hành, thao tác trừu tượng ToPrimitiveđược gọi với một "gợi ý" tùy chọn là 'số' hoặc 'chuỗi'. Nếu gợi ý bị bỏ qua, gợi ý mặc định là 'số' (trừ khi @@toPrimitivephương thức đã được ghi đè). Nếu gợi ý là 'chuỗi', thì toStringlần đầu tiên được thử và valueOflần thứ hai nếu toStringkhông trả về giá trị nguyên thủy. Khác, ngược lại. Gợi ý phụ thuộc vào thao tác yêu cầu chuyển đổi.

Toán tử bổ sung cung cấp không có gợi ý, vì vậy hãy valueOfthử trước. Toán tử phép trừ cung cấp một gợi ý về 'số', vì vậy valueOfsẽ được thử trước. Các tình huống duy nhất tôi có thể tìm thấy trong thông số kỹ thuật mà gợi ý là 'chuỗi' là:

  1. Object#toString
  2. Hoạt động trừu tượng ToPropertyKey, chuyển đổi một đối số thành một giá trị có thể được sử dụng làm khóa thuộc tính

Chuyển đổi loại trực tiếp

Mỗi nhà điều hành có các quy tắc riêng để hoàn thành hoạt động của họ. Toán tử cộng đầu tiên sẽ sử dụng ToPrimitiveđể đảm bảo mỗi toán hạng là một nguyên thủy; sau đó, nếu một trong hai toán hạng là một chuỗi, thì nó sẽ cố ý gọi phép toán trừu tượng ToStringtrên mỗi toán hạng, để thực hiện hành vi nối chuỗi mà chúng ta mong đợi với các chuỗi. Nếu sau ToPrimitivebước, cả hai toán hạng không phải là chuỗi, thì phép cộng số học được thực hiện.

Không giống như phép cộng, toán tử phép trừ không có hành vi quá tải và do đó sẽ gọi toNumerictrên mỗi toán hạng đã chuyển đổi chúng đầu tiên thành nguyên thủy bằng cách sử dụng ToPrimitive.

Vì thế:

 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

Lưu ý rằng Dateđối tượng nội tại là duy nhất, trong đó nó là đối tượng nội tại duy nhất ghi đè @@toPrimitivephương thức mặc định , trong đó gợi ý mặc định được cho là 'chuỗi' (thay vì 'số'). Lý do của việc này là Datetheo mặc định, các phiên bản dịch sang chuỗi có thể đọc được, thay vì giá trị số của chúng, để thuận tiện cho người lập trình. Bạn có thể ghi đè @@toPrimitivecác đối tượng của riêng mình bằng cách sử dụng Symbol.toPrimitive.

Lưới sau đây hiển thị kết quả cưỡng chế cho toán tử bình đẳng trừu tượng ( ==) ( nguồn ):

nhập mô tả hình ảnh ở đây

Xem cũng .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.