最初に結論(tl;dr)
DAZ Scriptはthis
の挙動もjavaScriptと同じですが、以下の場合においてのみ挙動が異なります。
コールバック関数内でthis
を使うとグローバル変数を参照するが、このときDAZ Scriptではグローバル変数には誰もいないことにされる。
以上です。
グローバル変数上にvar x = 0;
などを宣言していても、コールバック関数からはこれを参照することが出来ません。
詳しい説明は「this
で指定される領域を保持する」の項を参照してください。
概要
DAZ Scriptはthis
の挙動もjavaScriptと同じです。
JavaScriptのthis
の挙動は以下の記事が参考になります。
JavaScriptにおける関数の呼び出し方のパターンとそれに伴う関数内のthisの変化
大規模なプログラムが作られることが無いDAZ Scriptでは、this
の出番は少ないかもしれませんが、イベント時に呼び出すコールバック関数では用いる機会もでてくると思います。
通常の呼び出しではJavaScriptと違いはありませんが、「最初に結論(tl;dr)」で述べたように、クリック処理などコールバック関数に指定した時、若干挙動が異なります。
参考に挙げた記事に書かれている4パターンをDAZ Scriptで検証してみます。
- パターン1.関数として呼び出す
- パターン2.オブジェクトのメソッドとして呼び出す
- パターン3.コンストラクタとして呼び出す
- パターン4.apply()、もしくはcall()で呼び出す
続いて、コールバック関数に指定した時の挙動も検証してみたいと思います。
- パターン5.コールバック関数として呼び出す(「1.関数として呼び出す」と同じ)
- パターン6.コールバック関数として呼び出す(「2.オブジェクトとして呼び出す」と同じ)
また、TypeScriptを用いての解決方法も解説したいと思います。
通常の関数呼び出しのthis
の挙動を検証する
次の項より、JavaScriptとDAZ Scriptでthis
の挙動を検証していきます。
左がJavaScriptをChromeで実行した結果で、右がDAZ StudioのScript IDEペインで実行した結果です。
パターン1.関数として呼び出す
このパターンでは、this
を指定したときはグローバル変数を取得しています。正確に言うと、JavaScriptではWindow
オブジェクトを参照します。これがJavaScript環境においてグローバル変数に相当するものです。
DAZ Script側には、Window
オブジェクトに相当するものがないため、エラーとなってしまいます。
パターン2.オブジェクトのメソッドとして呼び出す
パターン1と異なり、this
を指定したときは、example2
オブジェクトを取得していることに注目してください。このため、this.x
とすると、example2オブジェクト内のプロパティにアクセスすることが出来ます。
逆にthis
を指定しなかったときは、グローバル変数領域のx
を参照しようとしますが、グローバル変数にはx
がないためエラーとなっています。
これはJavaScript、DAZ Scriptともに共通の動作です。
パターン3.コンストラクタとして関数を呼び出す
DAZ Scriptでは使う機会は無いかもしれませんが、コンストラクタなどもJavaScriptと同様に使うことが出来ます。
パターン1のときとコードはほぼ同じなのですが、JavaScriptではthis
を指定したときにグローバル変数ではなくExample3オブジェクトを指し示します。
これはコンストラクタが特殊な呼ばれ方をするためです。関数をコンストラクタとして使用するには、newキーワードで関数を呼び出します。
DAZ Scriptも同様にthis
を指定したときの挙動が異なりますが、内部の処理がわからないため、これ以上の解説はできません。すみません…。
ただ参考記事の方で、
コンストラクタ呼び出しでは新しいオブジェクトが生成されます。
なので、上記のコードを実行させるとコンソールには
example {}
と新しく生成された空のオブジェクトが入ります。ちなみに本題には関係ないのですが、JavaScriptにおけるコンストラクタには
・呼び出されると新しい空のオブジェクトを生成する
・関数に明示的な返り値がなければ、その新しいオブジェクトがコンストラクタの値として返される
という特徴があり、普通の関数とは明確に違う使われ方をします。
とありますので、print(this);
の実行結果の[Objects Object]は、空のExample3オブジェクトを指し示していると思われます。
その後の処理のprints(this.x);
がundefinedとなっているのも、まだこの生成されたばかりの空のExample3オブジェクトにxプロパティが無いためと思われます。
パターン4.apply()、もしくはcall()で呼び出す
こちらもDAZ Scriptでは使う機会が無いと思いますが、紹介しておきます。
パターン1と同じですね。
コールバック呼び出しでのthis
の挙動を検証する
パターン5.コールバック関数として呼び出す(「パターン1.関数呼び出し」と同じ)
今回解説したかったのは、このコールバック関数のパターンです。 QtScriptに由来するためか、DAZ Scriptはコールバック関数での挙動がJavaScriptと若干異なります。
この差異が際立つのは、DAZ Scriptのコールバック関数がグローバル変数にアクセスしようとしたときです。
例えば、グローバル変数を用意していない下記の例では特に違いがありません。
しかし、グローバル変数を追加して、実行してみると、JavaScriptとDAZ Scriptで実行結果が異なっているのがわかります。
JavaScriptではthis
を指定するとグローバル変数に定義したxとyを呼び出します。(これが直感的に意図した動作かどうかは置いといて…)
DAZ Scriptでは依然NaNと表示され、グローバル変数のxとyは存在しないかのような扱いになっています。
パターン6.コールバック関数として呼び出す(「パターン2.オブジェクトのメソッドとして呼び出す」と同じ)
続いて、さらに別の方法でコールバック関数を指定してみます。
このパターン6では、パターン2と同様にオブジェクトのメソッドを用意し、それをコールバック関数として指定しています。
まず、グローバル変数無し版です。
続いて、グローバル変数を追加して実行してみます。
パターン5と同じ様に、DAZ Scriptの方はグローバル変数のxとyを探し出せず、NaNとなっています。
このように、DAZ ScriptとJavaScriptでは、コールバック関数にthis
を使ったときの挙動が若干異なります。
しかしながら、通常this
を使うシーンではこのような使い方をすることはないと思います。
JavaScriptでもそうですが、this
を用いるときはインスタンスを模した挙動を実現したいときです。そのため、後述するように_this
変数を用いて、変数を束縛してコールバックするテクニックが活用されることが多いと思います。
コールバック時にもthis
を扱えるようにしたい(パターン6改定)
this
は何が嬉しいのか?
JAVAなどのオブジェクト指向言語のthis
と同じ様な使い方もできるのが、JavaScriptのthis
の使い所です。
JAVAなどのオブジェクト指向の言語では、this
は自身のインスタンスのプロパティへアクセスするために用いられます。
JavaScriptにもthis
キーワードはありましたが、本来の目的は異なっていました。[1]オブジェクト参照のためのthisの使用しかし、使い方によってはオブジェクト指向のものと同じような使い方ができることがわかり、今日ではそちらの使われ方のほうが主流となっています。
ところが、そもそもクラスがないJavaScriptでは、this
は使用する場所や使い方によって、指定されるオブジェクトが大きく異なるという問題が発生してしまいました。これはバグの温床となり、JavaScript開発者の頭を悩ませました。
ES2015以降はかなり改善され、JavaScript界隈では問題視されることも少なくなりましたが、DAZ Scriptでは依然古いECMA Script(gen3)に準拠した言語仕様となっているため、この問題が避けて通れないものとなっています。
this
で指定される領域を保持する
パターン6の例では、「オブジェクトのメソッド」に処理を格納しています。
その後、その「オブジェクトのメソッド」をコールバック関数にしています。
パターン2で示したように、本来this.x
はSwitchTimer2.x
を示し、x
はグローバル変数のx
を示します。 インスタンスを模すことができ、ここがまさにthis
の本来の使い所なのですが、コールバック関数にすると、同じオブジェクト内のx
やy
には、もはやアクセスすることができなくなってしまいました。
これは困りました。わざわざオブジェクトを用意した意味がありません。
そこで、コールバック関数を指定するときに同じオブジェクト内のプロパティをいったん_this
のような変数に格納しておいて、コールバック時にもその変数経由でプロパティにアクセスするテクニックがJavaScriptではよく用いられます。
具体的には下記のようになります。(ソース名:「パターン6改定」)
このテクニックを使うと、そもそものコールバック関数の指定方法も変わり、「関数を生成する関数を実行し、生成された関数をコールバック時に呼び出されるように指定する」というものになります。
早口言葉のようでややこしいのですが、「パターン6改定」のDAZ Script側で35行目connect( oTimer2, "timeout()", SwitchTimer2.getInner() );
としている部分がそうです。関数名を指定をしていたパターン6から、関数の実行に変わっています。また、関数の名称もgetプレフィックスが追加され、パターン6で言うところのinner関数を取得する趣旨の名称になっています。
this.x
でそのままSwitchTimer2.x
にアクセスしようとしたパターン6と、_this
変数を用いてthisの内容をその変数経由でアクセスするようにした「パターン6改定」の概念を図にして比べてみます。
【パターン6で起こっていること】
【「パターン6改定」で起こっていること】
そもそも、コールバック時にはそれまでとは別の世界のinner関数が実行されていると思ってください。
その世界にはSwitchTimer2オブジェクトはもはや居ません。これがコールバック関数でSwitchTimer2.x
にアクセスすることができない理由です。 もはやこれは、this
が云々とかは関係のない話です。
_this
変数を用いることで、SwitchiTimer2にあったxとyを「コールバック時に参照される世界」にまで持ってくることができます。これによってコールバック関数でxとyにアクセスすることが出来るようになります。 (SwitchTimer2そのものは「コールバック時に参照される世界」には持ってこれないので、残念ながらSwitchTimer2.x
といった形式ではアクセスできません。代わりに_this.x
と言った形式でアクセスすることになります。)
ちなみに黄色の吹き出しは、DAZ Script独特の現象を説明しています。
JavaScriptではコールバック関数でthisを用いたとき、「通常のスクリプト実行時に参照される世界」と同じグローバル変数を参照しますが、DAZ Scriptではそれとは異なる「コールバック時に参照される世界」のグローバル変数を参照しようとするようです。
このため、DAZ Scritptではコールバック関数でグローバル変数を参照できません。
これがパターン5とパターン6で発生していた、
グローバル変数のxとyを探し出せず、NaNとなっています。
の原因です。
TypeScriptを活用してみる
以前の記事「TypeScriptのアロー関数を用いて、DAZ Scriptのクリック処理を実装する」でも示したとおり、このややこしいthis
の挙動は、TypeScriptのアロー関数を活用することでだいぶ緩和できます。
ちょうど上で紹介した_this = this;
のように、変数を用いてコールバック関数にオブジェクト内のプロパティに渡すテクニックを、自動で実装してくれるためです。
TypeScriptでは、classとアロー関数を活用することで、パターン6のような構成の関数を作ることが出来ます。
なお、アロー関数で示すthis
とJavaScriptのthis
は、見た目は同じですが厳密にはその機能は異なります。 あくまでクラスを使った時に、インスタンスとしてアクセスしたい時に使うのがTypeScriptのthis
です。オブジェクト指向言語のthis
そのものです。
JavaScriptのthis
は、自身のオブジェクトを参照するときに使います。これはJavaScript独特の設計です。[2]脚注1にも書いてあるとおり、JavaScriptのthis
は、元々JavaScriptがHTML内に埋め込まれて使われる事を念頭に置いて用意されています。
それでは、アロー関数を用いたTypeScriptの例を以下に示します。
//////////////////////////////////////////////////////
///TypesCriptのアロー関数を使って異なるインスタンスのコールバック関数を実装した例
let x = 1;
let y = 2;
class SwitchTimer {
x: number;
y: number;
constructor(in_x: number, in_y: number) {
this.x = in_x;
this.y = in_y;
}
innerThis = (): void => {
console.log(this);
}
inner = (): void => {
console.log(this.x + this.y);
}
innerGlobal = (): void => {
console.log(x + y);
}
}
(function () {
let timer1 = new SwitchTimer(3, 4);
let timer2 = new SwitchTimer(5, 6);
let timer3 = new SwitchTimer(7, 8);
setTimeout(timer1.innerThis, 1000); //1秒後にthisの内容を表示
setTimeout(timer2.inner, 3000); //3秒後に11を表示
setTimeout(timer3.innerGlobal, 5000); //5秒後に3を表示(グローバル変数の計算結果)
})();
=>
がアロー関数です。
15行目のinner = (): void =>{
は、オブジェクトのメソッドinnerに「引数なし、戻り値void」の無名関数を格納するを意味します。
この中で実行されるthis
はSwitchTimerクラスのインスタンスを参照してくれるようになります。
今回、3行目と4行目にグローバル変数のxとyを用意していますが、これがないと19行目のthis
を指定していないconsole.log(x + y);
はTypeScriptではコンパイルエラーになります。
TypeScriptは通常のオブジェクト指向言語と同様にthis
を扱うため、このように曖昧な指定は許しません。this
がついているものはインスタンスのプロパティ、ついていないものはグローバル変数として区別します。オブジェクト指向言語では当たり前といえば当たり前なんですが、JavaScriptはそうではないので…。
さて、これをJavaScriptに変換すると次のようになります。
//////////////////////////////////////////////////////
///TypesCriptのアロー関数を使って異なるインスタンスのコールバック関数を実装した例
var x = 1;
var y = 2;
var SwitchTimer = /** @class */ (function () {
function SwitchTimer(in_x, in_y) {
var _this = this;
this.innerThis = function () {
console.log(_this);
};
this.inner = function () {
console.log(_this.x + _this.y);
};
this.innerGlobal = function () {
console.log(x + y);
};
this.x = in_x;
this.y = in_y;
}
return SwitchTimer;
}());
(function () {
var timer1 = new SwitchTimer(3, 4);
var timer2 = new SwitchTimer(5, 6);
var timer3 = new SwitchTimer(7, 8);
setTimeout(timer1.innerThis, 1000); //1秒後にthisの内容を表示
setTimeout(timer2.inner, 3000); //3秒後に11を表示
setTimeout(timer3.innerGlobal, 5000); //5秒後に3を表示(グローバル変数の計算結果)
})();
「パターン6改定」と同様に_this
変数を用いた処理が自動で生成されています。
先程の15行目のinner = (): void =>{
で示した中身は、11行目のthis.inner = function () {
に変換され、その中の処理はconsole.log(this.x + this.y);
からconsole.log(_this.x + _this.y);
に変換されています。
このJavaScriptをChromeで実行した結果と、さらにDAZ Scriptで動くように調整した結果は次のようになります。
どちらとも想定通りの同じ結果が出力されています。
まとめ
DAZ ScriptはJavaScriptとほぼ同じ動作が期待できますが、主にブラウザ上で利用するJavaScriptと、DAZ Studio上でのみ利用するDAZ Scriptとでは、this
等の環境にアクセスする機構で若干挙動が異なります。 コールバック関数でthis
を使用したときという限定的なものですが、クリック処理やイベント処理で活用する機会も多いので、うまく実装しないと泥沼にハマりそうです。(まさに、この記事を書いていてハマりました…)
またTypeScriptを用いて、ややこしいバッドノウハウを自動で実装する方法も紹介しました。
尚、イベント処理を実装する方法はいくつかあり、以前の記事「DAZ Scriptにおける、イベント処理の実装方法」で説明しています。
単純にイベント処理を実装するだけなら、上記記事をご参照いただければと思います。
補足資料
DAZ ScriptはQtScriptの拡張版です。
そのため、公式サイトのDAZ Scriptのリファレンスではわからない箇所は、QtScriptのリファレンスを参照することが出来ます。
今回this
の挙動がよくわからず、藁をも掴む思いで参照したのですが、ブラウザ上のJavaScriptと特に比較して説明しているわけでもなく、あまり参考にはなりませんでした…。
this
についての説明は、下記にありました。
一応、下記に翻訳を載せておきます。
thisオブジェクト
When a Qt Script function is invoked from a script, the way in which it is invoked determines the this object when the function body is executed, as the following script example illustrates:
Qtスクリプト関数がスクリプトから呼び出されるとき、次の例が示すように、関数が実行されるときの方法により、thisオブジェクトが決まります。
You will get a reference error saying that 'a is not defined’ or, worse, two totally unrelated global variables a and b will be used to perform the computation, if they exist. Instead, the script should look like this:
「aが定義されていません」エラー、または関係のない2つのグローバル変数aとbが存在する場合は、それらを使用して参照エラーが発生します。このエラーを解消したスクリプトは次のようになります。
Accidentally omitting the this keyword is a typical source of error for programmers who are used to the scoping rules of C++ and Java.
誤ってthisキーワードを省略することは、C ++およびJavaのスコープ規則に慣れているプログラマーにとっての典型的なエラーの原因です。
脚注
↩1 | オブジェクト参照のためのthisの使用 |
---|---|
↩2 | 脚注1にも書いてあるとおり、JavaScriptのthis は、元々JavaScriptがHTML内に埋め込まれて使われる事を念頭に置いて用意されています。 |