DAZ Scriptにおけるthisの挙動

2020年1月2日DAZ ScriptDAZ Script,DAZ Studio,TypeScript

最初に結論(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キーワードはありましたが、本来の目的は異なっていました。[ref]オブジェクト参照のためのthisの使用[/ref]しかし、使い方によってはオブジェクト指向のものと同じような使い方ができることがわかり、今日ではそちらの使われ方のほうが主流となっています。

ところが、そもそもクラスがないJavaScriptでは、thisは使用する場所や使い方によって、指定されるオブジェクトが大きく異なるという問題が発生してしまいました。これはバグの温床となり、JavaScript開発者の頭を悩ませました。

ES2015以降はかなり改善され、JavaScript界隈では問題視されることも少なくなりましたが、DAZ Scriptでは依然古いECMA Script(gen3)に準拠した言語仕様となっているため、この問題が避けて通れないものとなっています。

thisで指定される領域を保持する

パターン6の例では、「オブジェクトのメソッド」に処理を格納しています。
その後、その「オブジェクトのメソッド」をコールバック関数にしています。

パターン2で示したように、本来this.xSwitchTimer2.xを示し、xはグローバル変数のxを示します。 インスタンスを模すことができ、ここがまさにthisの本来の使い所なのですが、コールバック関数にすると、同じオブジェクト内のxyには、もはやアクセスすることができなくなってしまいました。

これは困りました。わざわざオブジェクトを用意した意味がありません。

そこで、コールバック関数を指定するときに同じオブジェクト内のプロパティをいったん_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独特の設計です。
[ref]脚注1にも書いてあるとおり、JavaScriptのthisは、元々JavaScriptがHTML内に埋め込まれて使われる事を念頭に置いて用意されています。[/ref]

それでは、アロー関数を用いた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についての説明は、下記にありました。

Qt Script – The this Object

一応、下記に翻訳を載せておきます。

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オブジェクトが決まります。

var o = { a: 1, b: 2, sum: function() { return a + b; } };
print(o.sum()); // reference error, or sum of global variables a and b!!
TypeScript

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が存在する場合は、それらを使用して参照エラーが発生します。このエラーを解消したスクリプトは次のようになります。

var o = { a: 1, b: 2, sum: function() { return this.a + this.b; } };
print(o.sum()); // 3
TypeScript

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のスコープ規則に慣れているプログラマーにとっての典型的なエラーの原因です。

Posted by lowpolysnow