Next.jsでbind()
を使用する機会があったのですが、使い方がわかりにくいと感じましたのでまとめてみました。
一度使い方を押さえておけば、ドキュメントなどに出てきてもすんなり理解できるようになります。
bind()の使い方は2つある
bind()
の使い方は2つあり、別々に考えた方がわかりやすい です。
thisの参照先を変更した、新しい関数を作る
引数の予約
bind()
が使用されるとき1と2が同時に出てくるため混乱する原因になりますが、分けて順に説明していきます。
thisの参照先を変更した関数を作る
thisについて簡単に復習します。JavaScriptオブジェクト内部で定義された関数のthisの参照先は、そのオブジェクト自身になります。
オブジェクト内部の関数から、オブジェクト自身が持つプロパティを参照したいときなどにthisを使います。
{"code":"const car = {\n name: \"YARIS\",\n run: function(){\n console.log(`${this.name} is running`); \n }\n}\ncar.run();\n\/\/ YARIS is running.","language":"javascript","id":1,"filename":""}
bind()
で、thisの参照先を変更した関数を得ることができます。
以下の例では、もともとcatオブジェクトにはrun()
メソッドはありませんが、bind()
メソッドを使用して、carオブジェクトが持つrun()
メソッドのthisの参照先をcatに変更した関数catRun()
を得ることができます。
{"code":"const car = {\n name: \"yaris\",\n run: function(){\n console.log(`${this.name} is running.`);\n }\n}\nconst cat = {\n name: \"Charlie\"\n};\nconst catRun = car.run.bind(cat);\ncatRun();\n\/\/ Charlie is running.","language":"javascript","id":1,"filename":""}
catRun()
のthisはcatオブジェクトを指していますね。bind()
すると、新しい関数オブジェクトが作成されることにも注意しましょう。
.bind(object) thisの参照先をobjectに変更する
インスタンスのthisを参照先を変更した関数を得る
もう少し、実用に近い形で使い方を覚えましょう。
メソッドからオブジェクト自身を参照したい場合としては、クラス・インスタンスが最も一般的です。
インスタンスは、クラスから生成されるオブジェクトですので、インスタンスが持つメソッド内部のthisは、インスタンス自身を指すことになります。
以下では、Car
クラスを継承する2つのクラス、ToyotaCar
クラス・TeslaCar
クラスを実装してみました。
{"code":"\/\/ Car\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u306b\u5171\u901a\u3057\u3066\u6301\u305f\u305b\u305f\u3044\u30d7\u30ed\u30d1\u30c6\u30a3\u30fb\u30e1\u30bd\u30c3\u30c9\u3092\u5b9a\u7fa9\nclass Car {\n constructor(maker, model){\n this.maker = maker;\n this.model = model;\n }\n greeting(){\n console.log(`Hello, I'm ${this.model} made by ${this.maker}.`);\n }\n}\n\/\/ Car\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u7d99\u627f\u3059\u308bToyota Car\nclass ToyotaCar extends Car {\n constructor(model){\n super();\n this.maker = 'Toyota';\n this.model = model;\n }\n \/\/\u30cf\u30a4\u30d6\u30ea\u30c3\u30c9\u306ePR\u3092\u30b3\u30f3\u30bd\u30fc\u30eb\u8868\u793a\n PR(){\n console.log(`${this.model} has high fuel economy because of ${this.maker} Hybrid System.`);\n }\n}\n\/\/ Car\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u7d99\u627f\u3059\u308bTesla Car\nclass TeslaCar extends Car {\n constructor(model){\n super();\n this.maker = 'Tesla';\n this.model = model;\n }\n \/\/ \u96fb\u6c17\u99c6\u52d5\u306ePR\u3092\u30b3\u30f3\u30bd\u30fc\u30eb\u8868\u793a\n PR(){\n console.log(`${this.model} driven by electric motor with zero CO2 emission.`);\n }\n}","language":"javascript","id":1,"filename":""}
各々のクラスに定義されたメソッドに加えて、Car
クラスが持つメソッドも使用できます。
ToyotaCar
インスタンスのPR()
メソッドはハイブリッドシステムのPRをコンソール出力し、TeslaCar
インスタンスのPR()
メソッドは電気駆動のPRをコンソール出力します。
{"code":"\/\/ \u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u4f5c\u6210\nconst Prius = new ToyotaCar('Prius');\nconst ModelS = new TeslaCar('2023 TESLA Model S');\n\/\/ Car\u30af\u30e9\u30b9\u306e\u30e1\u30bd\u30c3\u30c9\u3082\u4f7f\u7528\u3067\u304d\u308b\nPrius.greeting();\nModelS.greeting();\n\/\/ \u5404\u30e1\u30fc\u30ab\u30fc\u306ePR\u3092\u51fa\u529b\nPrius.PR();\nModelS.PR();\n\/\/ \u30b3\u30f3\u30bd\u30fc\u30eb\u51fa\u529b\n\/\/ Hello, I'm Prius made by Toyota.\n\/\/ Hello, I'm 2023 TESLA Model S made by Tesla.\n\/\/ Prius has high fuel economy because of Toyota Hybrid System.\n\/\/ Tesla 2023 TESLA Model S driven by electric motor with zero CO2 emission.","language":"javascript","id":1,"filename":""}
ここで、ToyotaCar
クラスのインスタンスでも電気駆動のPRを出力するメソッドが欲しくなった場合 を考えます。
TeslaCar
インスタンスが持つPR()
メソッドのthisの参照先をbind()
で変更し、ToyotaCarインスタンスを指すようにした関数PR_Toyota_EV()
を作ります。
{"code":"const ModelS = new TeslaCar('2023 TESLA Model S');\n\/\/ bZ4X\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u4f5c\u6210\nconst bZ4X = new ToyotaCar('2022 bZ4X 4WD');\n\/\/ bind()\u3092\u4f7f\u3046\nconst PR_Toyota_EV = ModelS.PR.bind(bZ4X);\nPR_Toyota_EV();\n\/\/ Toyota 2022 bZ4X 4WD driven by electric motor with zero CO2 emission.","language":"javascript","id":1,"filename":""}
PR_Toyota_EV()
のthisはToyotaCar
のインスタンスbZ4X
を指すようになったので、bZ4Xの型番、メーカーがちゃんと出力されていることがわかります。
2022 bZ4X 4WDは、2023年11月にトヨタ自動車から発売された、トヨタ自動車初の電気自動車。
これが、bind()
でthisの参照先を変更する使い方です。
次に、bind()
のもう一つの使い方を説明していきます。
bind()は引数を”予約”する
1つ目の使い方ではbind()
の第一引数にthisの参照先を指定しました。bind()
は第二引数以降にも引数を取ることができ、bind()した関数の引数を予約します。
例を見た方がわかりやすいです。
以下のmySum()
は、2つの数を足した数を返すだけの単純な関数です。
{"code":"function mySum(a, b){\n return a + b;\n}\nconsole.log(mySum(3, 4));\n\/\/ 7","language":"javascript","id":1,"filename":""}
mySum()
をbind()
して引数に100を指定し、新しく関数bindSum()
を作ります。
bindSum()
は、必ず第一引数が100にセットされた状態で実行される ようになります。
{"code":"function mySum(a, b){\n return a + b;\n}\nconst bindSum = mySum.bind(null, 100);\nconsole.log(bindSum(5));\n\/\/ 105","language":"javascript","id":1,"filename":""}
引数1つだけでbindSum()
呼び出されている部分は、初見だと違和感のあるところですが、bind()
により第一引数が予め予約されているわけですね。
func.bind(object, arg1, arg2, …) 関数funcの第一引数、第二引数、…を、arg1, arg2, …で”予約”する
筆者個人としては、bind()
で引数を予約する使い方にはテクニカルなものが多く、自分で使用するというよりはドキュメントの中で見ることのほうが多い印象 です。
良い例を挙げるのが難しいですが、例えば汎用関数として作られた関数をbind()
して、使い方を一部限定するといった使い方はあるかもしれません(汎用関数の実装はそのまま残したい)。
{"code":"function linear_function(a, b, x) {\n return a*x + b;\n}\n\/\/ \u50be\u304d\u3092\u56fa\u5b9a\u3057\u305f\u95a2\u6570\nslopeBind_linearFunction = linear_function.bind(null, 1);\nconst y1 = slopeBind_linearFunction(-1, 3);\nconst y2 = slopeBind_linearFunction(-2, 3);\nconsole.log('y1', y1);\nconsole.log('y2', y2);\n\n\/\/ y = x + b, y1 = 3 + (-1)\n\/\/ y1 2\n\/\/ y = x + b, y2 = 3 + (-2)\n\/\/ y2 1","language":"javascript","id":1,"filename":""}
1次関数ax + bを計算するもともとの関数linear_function(a, b, x)
では傾きaと切片bを自由に指定できますが、bind()で傾きaを1で予約した関数 slopeBind_linearFunction()
は、傾きaが固定され、切片bのみを指定する関数になりました。
linear_functionが表すグラフは緑線のように自由に動くが、slopeBind_linearFunctionのグラフは紫線上のように傾きが固定された形
実用で筆者が経験した例としては、Next.jsにおいて、formのactionに指定したServer Actionに、追加の引数を渡したい 場合というものがあります(Next.js 14)。
bind()
でないといけない明確な理由があるのですが、本記事と異なり前提知識を必要とした話になってしまいますので、ここでは記載するに留めます。
以上でbind()
の使い方の説明を終わります。お疲れ様でした。
(補足)thisとグローバルオブジェクト
bind()
の使い方の1つ目として、thisの参照先の変更を挙げました。
thisを積極的に使用するとき、thisが特定のオブジェクトを指すことを意図して使用する場合がほとんど ですが、thisが使用されているスコープがグローバルスコープの場合にどうなるのか、最後に補足しておきます。
トップレベルで宣言された関数内部におけるthisは、環境のグローバルオブジェクトを指します。
グローバルオブジェクトはJSが実行される環境ごとに異なり、ブラウザー環境ではwindowです。
{"code":"const fn = function(){\n console.log(this);\n};\nfn();\n\/\/ this\u306e\u53c2\u7167\u5148\u306fwindow","language":"javascript","id":1,"filename":""}
ブラウザーで実行してみると、thisでwindowオブジェクトを参照できた
node環境においては、windowオブジェクトは存在せず、グローバルオブジェクトとしてglobalが存在します(strictモード ではない時のみ)。
{"code":"number = 777;\nconsole.log(global.number);\n\/\/ 777","language":"javascript","id":1,"filename":""}
strictモード のnode環境では、グローバルオブジェクトはundefinedであり存在しませんので、thisからグローバルオブジェクトを参照することはできません。
{"code":"\"use strict\"\nnumber = 777;\nfunction showFoo() {\n \/\/ \u30c8\u30c3\u30d7\u30ec\u30d9\u30eb\u3067\u5ba3\u8a00\u3055\u308c\u305f\u95a2\u6570\u306e\u4e2d\u3067this\u306f\u30b0\u30ed\u30fc\u30d0\u30eb\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\n console.log(this.number);\n}\n\/\/ strict\u30e2\u30fc\u30c9\u306b\u304a\u3044\u3066\u306f\u30b0\u30ed\u30fc\u30d0\u30eb\u30aa\u30d6\u30b8\u30a7\u30af\u30c8\u306fundefined\n\/\/ undefind","language":"javascript","id":1,"filename":""}
もっとも、グローバルオブジェクトの参照自体が意図しないバグの温床になるため、strictモードで禁止されているという経緯がありますので、thisから参照しようとする使い方自体避けるべきものと思われます。
ES6のモジュールは、自動的にstrictモード ES6のモジュールは、use strict
宣言がなくても自動的にstrictモードになりますので、モジュールのトップレベルで定義された関数内部のthisからグローバルオブジェクトを参照することもできません。 グローバルな名前空間を意図せず汚染しないために導入された機能がモジュールですので、この仕様も頷けます。