Странное поведение javascript toString()

Я пытаюсь использовать toString для временного вывода класса в DOM. У меня какое-то поведение, я не понимаю, где переопределитьtoString()всегда будет выводить начальное состояние. Однако, если используется внешняя функция (например, stateToString) или дажеJSON.stringify, обновленное состояние выводится, как я и ожидал.

Ниже моя попытка минимально воспроизвести это поведение. Повторюсь, я ожидаю, что все они будут изначально выводить:["initial"], что они и делают. Тем не менееtoString() вывод не обновляется при нажатии кнопки, но два других обновляются.

Это кажется особенно странным, поскольку stateToString а также State.toString кажутся по сути идентичными функциями, за исключением того, что один принимает состояние в качестве получателя, а другой - в качестве параметра.

Буду признателен, если кто-нибудь сможет объяснить, почему происходит такое поведение.

import React, { useReducer } from 'react';

class State { 
  constructor(xs) { this.xs = xs } 
  toString = () => `[${this.xs}]`
}

const stateToString = state => `[${state.xs}]`;

const reducer = (state, action) => ({
  ...state,
  xs: [...state.xs, action.x]
});

const App = () => {
  const [state, dispatch] = useReducer(reducer, new State(["initial"]));
  return (
    <div>
      <button onClick={() => dispatch({ x: Math.random() })}>click</button><br />
      toString:  {state.toString()}<br />
      print:     {stateToString(state)}<br />
      stringify: {JSON.stringify(state)}
    </div>
  );
};

export default App;

1 ответ

В toStringметод, который вы помещаете в State, привязан к исходному экземпляру состояния:

class State { 
  constructor(xs) { this.xs = xs } 
  toString = () => `[${this.xs}]` // Class field arrow function
}

Поле класса означает, что независимо от контекста вызова toString вызывается с помощью, он вернет this.xsисходного состояния. Даже если редуктор обновляет состояние, конструктор состояния больше не запускается.

На более поздних звонках App, создается начальное состояние, а затем выполняются некоторые действия по его обновлению, в результате чего stateпеременная является обновленным объектом, но у нее все еще естьtoStringметод привязан к начальному состоянию.

Вот пример поведения ванильного JS:

const obj = {
  val: 'val',
  toString: () => obj.val
};

const copiedObj = { ...obj, val: 'newVal' };
console.log(copiedObj.toString());

Если вы назначили function вместо стрелочной функции, тогда toStringбудет вызываться с вызывающим контекстом обновленного состояния, потому что он не привязан к начальному состоянию, поэтому он будет вызван с вызывающим контекстом обновленного состояния и правильно получитxs:

toString = function () {
    return `[${this.xs}]`;
}

Кстати, вы не можете использовать обычный метод, например

toString() {
    return `[${this.xs}]`;
}

потому что в вашем редукторе:

const reducer = (state, action) => ({
    ...state,
    xs: [...state.xs, action.x]
});

Синтаксис распространения принимает только перечислимые собственные свойства. С синтаксисом метода (например,toString() {) свойство помещается в прототип состояния, а не в фактический экземпляр, поэтому его не будет в конечномstate, а встроенный Object.prototype.toString вместо этого будет вызываться.

Другие вопросы по тегам