fujjima’s blog

主に備忘録

JSのbind、及び`this`について理解を少し深める

bindそのものについて

JSのbindの用法を知り、「どういう時に使われるのか」が漠然とイメージできるようになりたかったため、概要と使われ方を調べた。

とりあえず以下のサイトのデモコードを自分なりにいじってどういう挙動なのかは調べた。

Function.prototype.bind() - JavaScript | MDN

const module = {
  a: 50,
  b: 30,
  text: 'this is test',
  showText: function(){
    return this.text;
  },
};

const moduleShow  = module.showText;
console.log(moduleShow);

// moduleから抜き出した関数にmodule自体をbindする
// moduleShow内のthis = bindされたmoduleとなる
// そのため、this.text = module.text、になる
const boundGetX = moduleShow.bind(module);
console.log(boundGetX());


その他、bindの概要、callとの違いについては下記の記事を見てイメージは出来た。 JavaScript thisの内容を指定する(bindメソッド) | ITSakura

ただ、こいつがどういう目的で使われるかは分からなかった。*1

単純に、関数内のthis(thisがない場合はbind内の第一引数にnullを指定する)を特定のものに設定する(=undefinedにしない)場合にbindを使う、という使い方でいいのだろうか。

アロー関数内でのthisの扱われ方とbindの比較

ここまで調べて、そういえばJSの通常関数とアロー関数ではthisの指すものが異なる、という話しを思い出した。

【JavaScript】アロー関数式を学ぶついでにthisも復習する話 - Qiita

JavaScript: 通常の関数とアロー関数の違いは「書き方だけ」ではない。異なる性質が10個ほどある。 - Qiita


両者のthisに関する違いとしては、アロー関数はthisを束縛し、通常関数はthisを束縛しない(=関数呼び出し元(レシーバ)をthisとする)という点である。

下記のコードを例に比較する。

  • 通常関数
this.name = "globalName";

// 通常関数
function showName() {
  console.log(this.name);
}

let arrowFunc = {
  name: "john",
  func: showName,
};

arrowFunc.func();
=> john

// bindを使った場合
showName.bind(arrowFunc)();
=> john

arrowFunc.func()の返り値がjohnとなっており、これはarrowFunc.nameの値であることが分かる。 このことから、arrowFunc.func()、つまりはarrowFunc.showName()のshowName内部のthisはarrowFunc(レシーバ)を指していたことが分かる。

  • アロー関数
this.name = "globalName";

// アロー関数
const showName = () => {
  console.log(this.name);
};

let arrowFunc = {
  name: "john",
  func: showName,
};

arrowFunc.func();
=> globalName

arrowFunc.func()の返り値が「globalName」となっている。これは最初に定義したthis.name = "globalName"のnameの値が表示されていることが分かる。 ここで、アロー関数内のthisについてのリファレンスを見てみる。

アロー関数自身は this を持ちません。レキシカルスコープの this 値を使います。つまり、アロー関数内の this 値は通常の変数検索ルールに従います。このためスコープに this 値がない場合、その一つ外側のスコープで this 値を探します。

アロー関数 - JavaScript | MDN


アロー関数であるshowNameが定義された際、関数内のthisは更に外側のthisを見ていたことになる。 今回Node上で上記コードを実行したため、thisとして定義されていたのは{name: "globalName"}だったため、showName関数内のconsole.logで表示されたthis.nameは、コードの最初に定義したthis.nameだった、ということになる。


ちなみにトップレベル、グローバルスコープあたりはここを参考にした。

JavaScriptのトップレベルスコープは常にグローバルスコープではなかった - Qiita

まとめ

改めてbind、通常関数とアロー関数それぞれでのthisの使い方を確認した。 通常関数とアロー関数の違いについては他にも色々あるが、一旦知りたいと思っていたことを確認することができた。

*1:こういったサンプルを見て、「じゃあこういうものにも応用できるな」と考えつく能力もプログラマに必要不可欠な能力だよな、と最近感じている

需要が薄いMaterial-UIのvalidationの一例

空文字を許容しない、というバリデーションを一部分だけ書きたい、しかしそのために本格的なバリデーションの機構を組むのはめんどくさくて死にそう、という時にその場しのぎで書いたもの。

<TextField
  label="名前"
  variant="outlined"
  margin="normal"
  required

  // input.valueに入力された文字が入っているものとして、空文字の場合は input.value はfalseを返す
  // 上記の場合に{ error: true }を返すことで <TextField error /> と書いた時と同じようになる
  // { error: false } の場合はerrorは指定されていない状態になる
  {...(!input.value ? { error: true } : { error: false })}
/>

Material-UIのTextFieldをtype="date"にした際のdefaultValueに指定する日付のフォーマットについて備忘録

material-uiのTextFiledというAPIの中で、type="date"のようにタイプを指定することでDate pickerのように使用することができる。

Date picker, Time picker React components - Material-UI

その際、デフォルトの日付をdefaultValueというオプションで設定できるが、この部分で少し詰まったので備忘録として残す。

ダメだった例

// 年、月、日の区切りがスラッシュになっているとdefaultValueとして認識されない
const today = dayjs().format('YYYY/MM/DD')

<TextField
  label="testDay"
  type="date"
  defaultValue={today}
  margin="normal"
/>

f:id:fujjima:20210101144435p:plain

良かった例

// 年、月、日の区切りがハイフンだときちんとdefaultValueとして指定できる
const today = dayjs().format('YYYY-MM-DD')

<TextField
  label="testDay"
  type="date"
  defaultValue={today}
  margin="normal"
/>

f:id:fujjima:20210101144333p:plain

詳細は調査中だが、同じようにdatapickerとして使用できるKeyboardDatePickerではformatにformat="MM/dd/yyyy"のように指定してもいける感じなので、どうしてこのような仕様にしているかはよく分からない。 しかも表示される時にはYYYY/MM/DD形式だし。スラッシュどこに行った。

https://material-ui.com/components/pickers/#material-ui-pickers

rbenvによるバージョン切り替えの際の備忘録

githubからforkしてきたプロジェクトのrubyのバージョン関連で問題が生じたので、経過とともにメモする。

経過&メモ

  • forkしてきたプロジェクトの.ruby-versionに書かれているrubyのバージョンを確認
  • rbenv install x.x.xで該当のrubyのバージョンを指定してインストール
  • 念の為rbenv rehash実行

ここで、rbenv rehashをする意味を再度確認した

「~/.rbenv/versions/2.x.y/bin/ 以下に置いてあるコマンド群を ~/.rbenv/shims/以下に置いて使えるようにする」

rbenv rehashは何をやっているのか? · DQNEO日記

  • bundle install実行しようとしたら下記のエラーが表示されて出来ない
Warning: the running version of Bundler (1.17.2) is older than the version that created the lockfile (1.17.3). We suggest you upgrade to the latest version of Bundler by running `gem install bundler`.
Your Ruby version is 2.6.3, but your Gemfile specified 2.6.5

bundlerのバージョンが古いと言う警告と、rubyのバージョンが.ruby-versionに書かれているものよりも古いというエラー。

  • とりあえず、どこのrubyが呼ばれているかwhich rubyで確認
    • /usr/bin/rubyの方が呼ばれており、rbenvでインストールしたrubyが呼ばれていないことを確認した
      • rbenvの方のrubyを呼び出したいので、下記の流れでpathを参照できるようにする
~$ vim ~/.bash_profile

// .bash_profileが開かれるので、下記一行を追加して保存する
export PATH="$HOME/.rbenv/shims:$PATH"

// 変更を反映させる
~$ source ~/.bash_profile
  • これでbundle installしたところ正常にインストールできた

振り返り

前職に勤めていた時には、会社用のレポジトリは基本的に会社のPC上で触っていて、個人で何かする時には個人のPCでやっていたためrubyのバージョン切り替えをあまりやっていなかった。 今回、久方ぶりにforkしてきたリポジトリを動かす、ということをしたのでrubyのバージョン切り替え周りで少し手間取ってしまった。 rbenv、bundlerあたりはこういったケースをきちんとメモしておいてその都度理解を深めるのがいいかなと感じる。 実装周りが落ち着けば一度きちんと見たい。

setStateで古いstateを見てしまった

基本的なところだけど、少し悩んでしまったのでメモ。

アプリのタイマー機能を実装しようとした際、setIntervalを使用しても時間が加算されない問題が発生した。 原因としては、setStateにオブジェクトを渡した場合、stateを即座にアップデートすることを保証しないというもの。

コンポーネントの state – React

今回のような、setIntervalでsetStateが呼ばれるなど参照したいstateが絶えず更新されているものである場合、「setStateに更新用の関数を渡し、その関数内の引数で最新のstateにアクセスして操作する」のが正しい実装内容となる。

駄目な例

下記の例だと、setStateにtimeに1を足した値を引数として渡しているが、この時にtimeが参照しているのは更新前のtimeである。 つまり、0秒から時間の加算が始まったとして、

// timeは親のコンポーネントからもらってくる数値
const [time, setTime] = useState(props.time || null);

useEffect(() => {
    const { taskId, recordingTaskId } = props;
    if (taskId && taskId === recordingTaskId) {
      // setIntervalで時間加算の関数(addSecond)を1秒毎に実行させる
      setTimerId(setInterval(addSecond, 1000));
    } else return;
  }, [props.recordingTaskId]);

  const addSecond = () => {
    // ここでsetStateに「time + 1」という値を渡してしまっているため、常に古いstateを見てしまっている
    setTime(time + 1);
  };

良い例

// timeは親のコンポーネントからもらってくる数値
const [time, setTime] = useState(props.time || null);

useEffect(() => {
    const { taskId, recordingTaskId } = props;
    if (taskId && taskId === recordingTaskId) {
      // setIntervalで時間加算の関数(addSecond)を1秒毎に実行させる
      setTimerId(setInterval(addSecond, 1000));
    } else return;
  }, [props.recordingTaskId]);

  const addSecond = () => {
    // 更新関数を渡しているため、この中のtは最新のstateが入っていることが保証されている
    setTime(t => t + 1);
  };

JSで引数にundefinedを渡すと、引数なしとして扱われる

今更ながら表題のことを確認した。

備忘録として下記のコードともに残した。

const test = [1,2,3]

const reducer = (state = test, num) => {
  if(state === null) return console.log('this is null')
  console.log(state[0], num)
}

reducer(null, 5)
reducer(undefined, 5)
reducer()


> "this is null"
> 1 5
> 1 undefined

親から子コンポーネントのstateにcreateRefを使ってアクセスする

表題の通りだけど、個人的におすすめしない。

refでDOM要素にもコンポーネントにも自由にアクセスできるようにはなるが、せっかくpropsでコンポーネント間のやり取りができるようになっているので、あまりそれを崩したくない。 ちなみに、reactのリファレンスにはrefを使うのに適したケースとして以下が挙げられている。

  • フォーカス、テキストの選択およびメディアの再生の管理
  • アニメーションの発火
  • サードパーティの DOM ライブラリとの統合

Ref と DOM – React

が、仕方ない場面はあるのでこういった方法を採用することもある。

下記のようにして、子コンポーネントのstateにアクセスできる。

// クラスA内にBという子コンポーネントがあるとする

class A extends React.Component {
  constructor(props) {
    super(props);

    // stateを取得したい子コンポーネントにtestRefを渡すために、ここで作成
    this.testRef = React.createRef();
  }

  // クラスB内に作成したtestRefを渡す
  <B
    ref={this.testRef}
  />

  // Bコンポーネント内の value というstateにアクセスできる
  handleBstate = () => {
    const test = this.testRef.current.state.value
  }

}

繰り返すが、あまりおすすめはしない。 瞬間的に子コンポーネントの値を取得したい、関数を利用したいというケースでもなければあまり使わない方が無難だと思う。