Posted at

Lightning LockerService と thisキーワード

More than 1 year has passed since last update.


Lightning LockerService

Lightning LockerServiceはSPAアプリケーション上で動作するLightningコンポーネントのセキュリティを向上させるための仕組みであり、既に多くのSalesforce組織で有効化されています。この仕組みについてよくご存じでない方は、各セールスフォース組織の「重要な更新」の「Lightning LockerService セキュリティの有効化」を参照してみて下さい。現時点で有効化されていない組織でも、2017年6月14日から自動有効化されるスケジュールとなっていることが分かります。

LockerService有効化による最も大きな影響は、コンポーネント自身に含まれるDOM要素にしかアクセスできなくなることです。これまではDOM階層をトラバースしたり、IDやクラス名でDOMツリーを検索することでドキュメント内にある任意のDOMに自由にアクセスできましたが、今後は他のコンポーネントやコンテナであるLEX環境が提供するDOM要素には一切アクセスできなくなります。また、暗黙的に"use strict"が有効化されES5の厳格モードが適用されるとか、WindowやDocumentといったグローバルオブジェクトはバーチャルなDOMに置き換わり利用が制限されるとか、より安全なコードが書けるようになる一方でコーディングの自由度は下がります。

ドキュメントには明示的に記載されていないのですが、実はthisキーワードの取り扱いもLockerService適用により変わります。この変更によって、コンポーネントの挙動が意図せずに変わってしまうことがあるので、今回はLockerService適用前と適用後でのthisキーワードの扱いがどのように変わるかを説明したいと思います。


Lightningコンポーネントでのthis


LockerService適用前

JavaScriptのthisキーワードは実行時の状況によってセットされる値が異なるため、注意して使う必要があります。LightningコンポーネントでのJavaScriptコードはコントローラーやヘルパーモジュール内にArray形式で定義していきます。そのためコード内でthisを使った場合、thisにはコントローラーやヘルパーオブジェクト自身がセットされており、直感的に使うことができます。

下記はLockerServiceを適用しない環境でコードを実行した結果です。変数helperとthisは同じオブジェクトを指していて、サーバーメソッド呼出後の無名関数内でもちゃんとthisはhelperを指してくれています。


helperクラスのサンプルコード(LockerService適用前)

({

textMethod10 : function(cmp, helper) {
var data1 = [3,4,5,1,2];
helper.sort(data1);
console.log('結果1: ' + data1);

var data2 = [3,4,5,1,2];
this.sort(data2); // #thisとhelperは同じ
console.log('結果2: ' + data2);

//サーバーメソッドの呼出し
var action = cmp.get("c.serverMethod");
action.setCallback(this, function(response){
var data1 = [3,4,5,1,2];
helper.sort(data1);
console.log('結果3: ' + data1);

var data2 = [3,4,5,1,2];
this.sort(data2); // #無名関数内でもthisはhelperを指している
console.log('結果4: ' + data2);
});
$A.enqueueAction(action);
},

sort : function(dataList){
console.log(' sort実行');
dataList.sort(function(a,b){
if( a < b ) return -1;
if( a > b ) return 1;
return 0;
});
},


実行結果:

sort実行
結果1: 1,2,3,4,5
sort実行
結果2: 1,2,3,4,5
sort実行
結果3: 1,2,3,4,5
sort実行
結果4: 1,2,3,4,5

分かりやすい結果です。


LockerService適用後

同じコードをLockerService適用後の環境で動かしてみます。

実行結果:

sort実行
結果1: 1,2,3,4,5
sort実行
結果2: 1,2,3,4,5
sort実行
結果3: 1,2,3,4,5
sort実行
結果4: 3,4,5,1,2 #データがソートされていない

無名関数内でthis.sort()を実行した結果(結果4)がLockerService適用前と異なっていて、ソートが実行されていないようです。ログを見る限りsort()メソッドは呼び出されており、また実行時エラーも発生しておらず、パッと見ではなぜソート処理が実行されていないのか分かりません。

helperとthisが同じオブジェクトかどうかを調べるために、下記のコードを入れて実行してみます。

console.log('helper===this =>' + helper===this)

すると、無名関数で実行した時にはthisとhelperが同じオブジェクトを指していないことが判明しました。


helper===this => true
sort実行
結果1: 1,2,3,4,5
sort実行
結果2: 1,2,3,4,5
helper===this => false #無名関数では thisとhelperが異なっている
sort実行
結果3: 1,2,3,4,5
sort実行
結果4: 3,4,5,1,2 #データがソートされていない
*/

無名関数内でのthisとhelperの状態をデバッガーで確認すると下記のようになっていました。両者は似ていますが、関数の仕様が微妙に異なっています。どうやらLockerServiceを適用すると、action.setCallback()で呼び出される無名関数内のthisはhelperと同じインターフェースを持つ別のオブジェクトがセットされているようです。this.sort()を呼び出すときちんとsort()関数自体は呼び出されているため、thisにセットされているのは恐らくラッパー的な働きをするオブジェクトと推測されます。


このラッパーオブジェクトは一体何をしているのでしょうか? デバッガーで実行時の状況を確認することでその答えが分かりました。

上記のコードは、helperに定義された各関数を呼び出した際に実行されるラッパーメソッドの一部です。value.apply()でターゲットとなる関数を呼び出していて、その前の処理でSecureObject.$filterEverything$という関数を呼び出して、関数の引数をディープコピーしています。 filteredArguments はディープコピーされた引数であり、元の引数とは別の値となっています。関数名から想像するに、関数をよりセキュアに実行させるために、引数を完全に値渡しとする仕組みといったところでしょうか。

thisの謎が解けたところで、サンプルコードをもう一度見てみます。sort関数は引数自身をソートする処理となっています。

    sort : function(dataList){

dataList.sort(function(a,b){
if( a < b ) return -1;
if( a > b ) return 1;
return 0;
});
},

引数dataListはhelper.sort()で呼び出された時は呼出元の変数と同じものを指していますが、this.sort()で呼び出された時はラッパー関数内でコピーされた別モノにすり替わっていますので、関数内でいくら処理を行っても呼出元の変数の状態は変わりません。


まとめ

LockerService適用後は、helper経由で関数を呼出す場合とthis経由で呼び出す場合では引数の取り扱いが変わります。この引数のすり替えは裏側でこっそり行われ、見た目は同じように処理が実行されているように見えますが、実際にはthis経由で呼び出されると引数に対する値の操作は全て無効になります。この問題が発生した場合、実際にエラーが発生する箇所は原因となっている関数とは別の箇所となるため、デバッグが難しいタイプの障害になります(実際、苦労しました)。

一般的なコーディングのお作法として、引数にセットされた値そのものを操作する処理はそれほど多くはないと思いますが、長い関数を複数の小さな関数に分割したいケースなど複数の関数で値を引き回したい時もあります。thisとhelperはどちらも同じだと考えていると思わぬところでトラブルに陥ることがありますので注意して下さい。

helper経由で関数を呼び出す場合は、このラッパー関数による引数の差し替えは起こりませんので、thisではなくhelperを使って関数を呼び出すように普段から心掛けておいた方が良さそうです。