valueOf () vs. 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
Вони дають відповідно "Об'єкт" і "Фу". Веселі речі.
brainjam

Власне, настороженість (х); дає "foo" і window.console.log (x); дає "foo {}" у Firebug та весь об'єкт у консолі Chrome.
brainjam

У Firefox 33.0.2 alert(x)відображається fooта window.console.log(x)відображається Object { toString: x.toString(), valueOf: x.valueOf() }.
Джон Сондерсон

Відповіді:


107

Причина, чому ("x =" + x) дає "x = значення", а не "x = натягування", полягає в наступному. Оцінюючи "+", javascript спочатку збирає примітивні значення операндів, а потім вирішує, чи слід застосовувати додавання чи конкатенацію, виходячи з типу кожного примітиву.

Отже, ось, як ви думаєте, це працює

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 робить Invoke ToString, але це не той випадок, тому що «+» не нав'язує який - або контекст типу.


3
Чи не повинно бути умовне в "фактично" блоці читання ", якщо (pa є рядок & & pb рядок)"? Тобто "&&" замість "||" ?
brainjam

3
Стандарт напевно говорить "чи" (див. Посилання).
користувач187291

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повернути непримітивні елементи, вони ігноруються. Якщо жоден з них не існує, або жоден з них не повертає примітиву, тоді a TypeErrorкидається.
бчеррі

1
Спасибі Бхеррі, це той калібр відповідей, на який я сподівався. Але не повинно x + "x ="; вихід "42x ="? І х + "1"; вихід 421? Також у вас є URL-адреса відповідної частини стандарту ECMAScript?
brainjam

2
Насправді "+" не використовує підказки (див. $ 11.6.1), тому ToPrimitive викликає [[DefaultValue]](no-hint), що еквівалентно [[DefaultValue]](number).
користувач187291

9
Схоже, це не так для вбудованого класу Date. ("" + new Date(0)) === new Date(0).toString(). Об'єкт Date, здається, завжди повертає своє toString()значення, коли його щось додає.
kpozin

7
+1 і Thx! Я знайшов ваш блог-пост, в якому ви докладно розгорнули цю відповідь і хотів посилання / поділитися нею тут Це було справді корисним доповненням до цієї відповіді (включаючи коментар Дмитра А. Сошнікова).
GitaarLAB

1

TLDR

Примус типу або неявна конверсія типів дозволяє слабко вводити текст і використовується в усьому JavaScript. Більшість операторів (за винятком винятків операторів суворої рівності ===та !==) та операцій з перевірки значення (наприклад, if(value)...) будуть примушувати надані їм значення, якщо типи цих значень не одразу сумісні з операцією.

Точний механізм, який використовується для примусового значення, залежить від вираження, що оцінюється. У питанні використовується оператор додавання .

Оператор додавання спочатку переконається, що обидва операнди є примітивними, що в цьому випадку передбачає виклик valueOfметоду. toStringМетод не викликається в даному випадку , оскільки переопределяется valueOfметод на об'єкті xповертає елементарне значення.

Потім, оскільки один із операндів у питанні є рядком, обидва операнди перетворюються на рядки. Цей процес використовує абстрактні, внутрішні операції ToString(примітка: з великої літери) і відрізняється від toStringметоду на об'єкті (або його прототипі ланцюга).

Нарешті, отримані рядки з'єднуються.

Деталі

На прототипі кожного об'єкта функції конструктора, відповідного кожному мовному типу в JavaScript (тобто номер, BigInt, String, Boolean, Symbol та Object), є два способи: valueOfі toString.

Мета valueOf- отримати первісне значення, пов'язане з об'єктом (якщо він має його). Якщо об’єкт не має основного примітивного значення, то об'єкт просто повертається.

Якщо valueOfвиклик проти примітиву, то примітив автоматично заповнюється в звичайний спосіб, а базове примітивне значення повертається. Зауважте, що для рядків основне примітивне значення (тобто значення, яке повертається valueOf) є самим представленням рядків.

Наступний код показує, що valueOfметод повертає основне примітивне значення з об’єкта обгортки, і він показує, як немодифіковані екземпляри об'єкта, які не відповідають примітивам, не мають примітивного значення для повернення, тому вони просто повертаються самі.

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викликається необов'язковим "натяком" на "число" або "рядок". Якщо підказку пропущено, підказкою за замовчуванням є "число" (якщо @@toPrimitiveметод не був переопрацьований). Якщо натяк 'string', то toStringспробується перший, а valueOfдругий, якщо toStringне повернув примітив. Інше, навпаки. Підказка залежить від операції, яка вимагає перетворення.

Оператор додавання не надає підказки, тому valueOfспробується спочатку. Оператор віднімання надає підказку «число», тому valueOfспробується спочатку. Єдині ситуації, які я можу знайти в специфікації, в якій натяк "строка", є:

  1. Object#toString
  2. Абстрактна операція ToPropertyKey, яка перетворює аргумент у значення, яке може використовуватися як ключ властивості

Пряме перетворення типу

У кожного оператора є свої правила завершення своєї роботи. Оператор додавання спочатку використає ToPrimitiveдля забезпечення кожного операнда примітивом; тоді, якщо будь-який операнд є рядком, то він навмисно буде викликати абстрактну операцію ToStringна кожному операнді, щоб надати порядок конкатенації рядків, який ми очікуємо за допомогою рядків. Якщо після ToPrimitiveкроку обидва операнди не є рядками, то виконується арифметичне додавання.

На відміну від додавання, оператор віднімання не має перевантаженої поведінки, і тому він буде викликати toNumericкожен операнд, попередньо перетворивши їх у примітиви за допомогою ToPrimitive.

Так:

 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.

Наступна сітка показує результати примусу для оператора абстрактної рівності ( ==) ( джерело ):

введіть тут опис зображення

Дивіться також .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.