WSH JScriptでJavaScriptのお勉強(関数定義、クロージャ、this)

ラベル: ,

前の関連記事:WSH JScriptでJavaScriptの学習環境を整える


WSH JScriptでデバッグもできるようになったのでJavaScript リファレンス - JavaScript | MDNの例をいくつかやってみます。といっても今回やるのは関数定義の方法、クロージャ、thisの使い方、の3つです。

関数の作り方3つの方法


JavaScriptの関数の作り方には、汎用コンストラクタFunctionを使う方法、function文を使う方法、function演算子を使う方法の3つがあります。(Function コンストラクタか関数宣言か関数式か)

いずれの方法でもFunctionオブジェクトが生成されます。

関数の引数は値渡しなので関数内で値を変更しても呼び出し元には反映されません。

汎用コンストラクタFunctionを使う方法(Function - JavaScript | MDN)
new Function ([引数1[, 引数2[, ... 引数N]],] 関数本体)
Functionで定義された関数は関数名を持ちません。

関数本体の文字列が毎回解析されて速度が遅いので、なるべく使用をさけるべきとされています。

function文を使う方法(function - JavaScript | MDN)
function 関数名([引数1] [,引数2] [...,引数N]) {関数本体}
この方法を関数宣言といいます。

他の二つの方法と異なり、この関数宣言で定義された関数は、関数定義の前に使用する事ができます。

関数宣言は式の一部になったときや入れ子(関数本体の中では関数宣言のまま。)になったときは関数宣言ではなく、次に示す関数式になります。

function演算子を使う方法
function [関数名]([引数1] [,引数2] [...,引数N]) {関数本体}
関数名は省略できます。

関数名を省略できること以外の書式はfunction文と同じですが、function演算子は式の中で使われます。

クロージャのお勉強


Python(5)クロージャ(Closure)のお勉強でPythonのクロージャについてお勉強しましたがまだ実際に使う機会がなくて使いこなせていません。

使いこなせていないのにまた勉強するのは関数の解説のところにでてくるからという理由からだけです。

入れ子の関数とクロージャに例とともに解説されています。
function counter() {
    var n = 0
    function inc() {
        n += 1
        return n;
    }
    return inc;
}
count = counter();
WScript.Echo(count());
WScript.Echo(count());
WScript.Echo(count());
WScript.Echo(count());
1
2
3
4
続行するには何かキーを押してください . . .

とりあえずPythonと同じ例をやってみたらちゃんとうまくいきました。

Pythonと違って内側の関数は外側の関数のスコープにアクセスできるので、Pythonのようなnonlocal文は不要です。
function counter() {
    var n = 0
    function inc() {
        return n + 1;
    }
    return inc;
}
count = counter();
WScript.Echo(count());
WScript.Echo(count());
WScript.Echo(count());
WScript.Echo(count());
return文では値が保持されないのはPythonのときと同じです。

count()を何回呼び出しても1しか返ってきません。

this演算子に何が入るのかは呼び出し方によって決定される


Pythonの「self」と違ってJavaScriptの「this」が何のオブジェクトを指すのかは、thisを書いているときには決められません。

thisに何が入るのかはその呼び出し元によって変わるからです。

this演算子へのオブジェクトの入り方は3通りあります。

オブジェクトのメソッド呼び出し時にはそのオブジェクトがthisに入ります。
object.method( 引数 )
このときthisにはobjectが入ります。

Functionオブジェクトのcallメソッドを使うとthisにいれるオブジェクトを明示的に指定できます。
function.call(object, 引数1, 引数2, . . ., 引数N )
こうやって呼び出されたfunctionのthisにはcallメソッドの先頭の引数であるobjectが入ります。

2番目以降の引数はfunctionの引数になります。

applyメソッドcallメソッドの関数に渡す引数部分を配列にしただけです。
function.apply(object, 引数の配列 )
この3つの方法をみるとthisが指すものを理解するのは容易に思えます。

でもthisに入るオブジェクトにはこの3つの方法で入れられたもの以外にグローバルオブジェクトというものが入る場合があります。

グローバルオブジェクトがthisに入るのは、上記の3つの方法以外でthisが呼び出されたときです、、、this - JavaScript | MDNにはそう書いてあるのですが、それ以外にもthisにオブジェクトが入る方法があります。

new演算子でコンストラクタ関数をインスタンス化したときは、その生成された新しいオブジェクト(インスタンス)がthisに入ります。(new - JavaScript | MDN)

グローバルオブジェクトは何かというとグローバルスコープでthisが呼ばれたときにthisに入るオブジェクトのことです。(Global Objects - JavaScript | MDN

このグローバルオブジェクトが曲者で、とりあえず上記の3通り(とnew演算子でコンストラクタ関数を呼び出したとき)以外の方法でthisが呼び出されたときはグローバルオブジェクトが入るというのを知らないと思わぬ結果を出してしまいます。

thisの中身をVisual Studioでみてみる


this - JavaScript | MDNに載っている例をVisual StudioのWSH JScriptでデバッグしてthisの内容をみてみます。
function Car(brand) {
    this.brand = brand;
}
Car.prototype.getBrand = function () {
    return this.brand;
}
var foo = new Car("toyota");
WScript.Echo("foo.getBrand()の結果 " + foo.getBrand());

var brand = "not a car";
var bar = foo.getBrand;
WScript.Echo("bar()の結果 " + bar());
foo.getBrand()の結果 toyota
bar()の結果 not a car
続行するには何かキーを押してください . . .

foo.getBrand()の結果がtoyotaであるのはわかるのですが、bar()の結果がnot a carであるのは理解しにくいですよね。


デバッガを起動するとまず4行目で止まります。


このときローカルウィンドウでみると、thisにはCar()メソッド(つまり関数)をもつオブジェクトが入っていることがわかります。

このthisは上記の3ついずれかの方法で呼び出されたものではないので入っているのはグローバルオブジェクトです。

他にbarやbrand、fooといった未定義のものを含めた名前がすべて含まれておりthisがグローバルオブジェクトであることが理解できます。

ステップインすると次の7行目に移動しますがthisが呼び出される訳ではないのでthisの中身はまだ変わりません。


4行目の定義通りにCarオブジェクト(Car()コンストラクタ関数)のprototypeにgetBrand()メソッドが追加されました。


さらにステップインすると7行目でnew演算子でコンストラクタ関数Car()が"toyota"を引数にして新しいオブジェクトが生成される(インスタンス化)のでCar()の中である2行目に移動し、thisの中身がゴロっと変わります。


このbrandというのはthis.brandではなくCar()スコープのローカル変数brandになります。

このときのthisには7行目のnew演算子の働きによりCar()の新しいオブジェクト(インスタンス)が入ります。


ステップインして3行目に移動すると新しいオブジェクトにbrandプロパティが追加され,
その値が"toyota"あることがわかります。


ステップインして8行目にくると新しいオブジェクトが7行目でfooに代入されてfooにgetBrand()メソッドとbandプロパティをもつオブジェクトが入っていることがわかります。


8行目でfoo.getBrand()が実行されるとCar.prototype.getBrandの5行目に飛びます。


fooのメソッドでthisが呼び出されたのでこのときのthisにはfooが入ります。

なので5行目が実行されるとfoo.brandが返されて、"toyota"が返ってきます。

まあここまでは理解が容易です。

問題は10行目以降です。

10行目にステップインするとグローバルスコープなのでthisにはグローバルオブジェクトが入っていることがわかります。


10行目が実行されると変数brandに"not a car"が代入されますが、brandはグローバルスコープで宣言された変数なので、グローバルオブジェクト、つまりthisのbrandプロパティにも"not a car"が代入されていることがわかります。


11行目でbarにfoo.getBrandが代入されます。

このbarもグローバルスコープで宣言されているのでグローバルオブジェクトthisのメソッドに入ります。

ローカルウィンドウの最終行のbarをみるとbar自体は「オブジェクト」としか定義されておらず、fooのgetBrand()メソッドがコピーされて入っているわけではなく、、、オブジェクトは参照渡しになるので、barにはfoo.getBrandという関数オブジェクトへの参照が入っています。


12行目を実行してbar()が関数として呼び出されるとこれはグローバルオブジェクトのbar()メソッドが呼び出されことになり、barはfoo.getBrandの参照なのでfoo.getBrand()メソッドが呼び出されて、5行目に飛びます。

このときのthisはグローバルオブジェクトなのでthis.brandには"not a car"が入っており、12行目の実行結果では"toyota"ではなく"not a car"が出力されることになります。

結局3つの方法とnew演算子のとき以外のthisはすべてグローバルオブジェクトと思えばよいのですね。

参考にしたサイト


JavaScript リファレンス - JavaScript | MDN
MDN(Mozilla Developer Network)によるJavaScriptリファレンス。

次の関連記事:auto_rec_rename.jsをいろいろ作り変える

PR

0 件のコメント:

コメントを投稿