18
14

More than 5 years have passed since last update.

VBAからJScriptのfunctionオブジェクトを使用する(64bit対応)

Last updated at Posted at 2017-12-14

はじめに

以前、こちらの記事で64bit環境のVBAでScriptControlを無理矢理使う方法を紹介しました。

しかし、特定用途(例:JScriptのfunctionオブジェクトをVBAからインスタンスする)であればより簡単かつ堅牢な方法があったため、備忘録も兼ねて記事にします。

functionオブジェクトのインスタンス用関数

コード

'Instance JScript Function object

'e.g.
    'Dim adder As Object: Set adder = JSFunc("a,b", "a+b") 'autoReturn = True
    'Debug.Print adder(2, 6) '->8

    'Dim inRange As Object
    'Set inRange = JSFunc("range,min,max", "v=range.Value;return min<=v&&v<=max;", False) 'autoReturn = False
    'Excel.ActiveCell.Value() = 150
    'Debug.Print inRange(Excel.ActiveCell, 100, 200) '->True

'Arguments
'args
    '`funcBody`内で使用する引数。
    '複数指定時はカンマ区切りで指定する。
    '参考:'[Function - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Function)
'funcBody
    '関数本文。
'autoReturn
    '省略可能。省略時True。
    'Trueのとき`funcBody`の先頭に`return `を追加する。

'Return
    'インスタンスされたJScriptのfunctionオブジェクト。

Function JSFunc( _
        args As String, _
        funcBody As String, _
        Optional autoReturn As Boolean = True) As Object

    Const EXEC_SCRIPT = _
            "this.createFunc=" & _
                "function(args,funcBody){" & _
                    "return new Function(args,funcBody);}"

    '各種初期化
    '関数オブジェクトのキャッシュ
    Static funcCache As Object 'As Scripting.Dictionary
    If funcCache Is Nothing Then Set funcCache = VBA.CreateObject("Scripting.Dictionary")

    'JScript実行環境。参照を保持しないとインスタンスしたfunctionオブジェクトも消える
    Static htmlDoc    As Object 'As MSHTML.HTMLDocument
    Static createFunc As Object 'JScript function

    If htmlDoc Is Nothing Then
        Call funcCache.RemoveAll
        Set htmlDoc = VBA.CreateObject("htmlfile")

        'JScriptのグローバル変数に関数を定義
        Call htmlDoc.parentWindow.execScript(EXEC_SCRIPT)

        '作成した関数を静的変数に保管(書き換え防止)
        Set createFunc = htmlDoc.parentWindow.createFunc
    End If


    'キャッシュ用に整形
    Dim trimedArgs As String, trimedBody As String
    trimedArgs = VBA.Trim$(args)
    If autoReturn Then
        trimedBody = "return " & VBA.Trim$(funcBody)
    Else
        trimedBody = VBA.Trim$(funcBody)
    End If

    Dim cacheKey As String
    cacheKey = trimedArgs & "|" & trimedBody


    'キャッシュに無ければ新規インスタンス
    If Not funcCache.Exists(cacheKey) Then
        Call funcCache.Add(cacheKey, createFunc(trimedArgs, trimedBody))
    End If

    Set JSFunc = funcCache.Item(cacheKey)

End Function

やっていること

JScript実行環境の用意

JScriptの実行環境としてScriptControlの代わりに、IEのエンジンであるMSHTML(Microsoft HTML Object Library)を使用しています。

ScriptControl は64bitVBAではインスタンスできませんが、MSHTMLはOfficeがインストールされていれば大体使用できるはずです(MS Office 2010 32bit・MS Office 2016 64bitで確認)。

関数作成関数の定義

MSHTML.HTMLWindow2オブジェクトのexecScriptメソッドを使用すれば任意のスクリプトを実行できますが、実行結果を直接受け取ることはできません。

そのため、代替手段として「JScriptのグローバル変数はルートオブジェクトのプロパティとなる」ことを利用します。

execScriptで必要なものを作成・グローバル変数に設定し、ルートオブジェクトのプロパティから作成したものを取得します。

'グローバル変数に定義
Call htmlDoc.parentWindow.execScript(EXEC_SCRIPT)
'ルートオブジェクトのプロパティから取得
Set createFunc = htmlDoc.parentWindow.createFunc
関数作成関数(EXEC_SCRIPT)
this.createFunc=
    function(args,funcBody){
        return new Function(args,funcBody);}

その他の部分は効率化などのために追加したものになります。

使用例

普通の計算
Sub FuncSample()

    Dim multiply As Object
    Set multiply = JSFunc("a,b", "a*b")

    Debug.Print multiply 'function anonymous(a,b) {return a*b}

    Debug.Print multiply(3, 5) '15
    Debug.Print multiply(7, 9) '63

End Sub
オブジェクトの操作
'Excel用 シート名の一覧を出力する
Sub FuncSample2()
    Dim getName As Object
    Set getName = JSFunc("o", "o.Name")

    Debug.Print getName 'function anonymous(o) {return o.Name}

    Dim s As Object
    For Each s In Excel.ThisWorkbook.Sheets
        Debug.Print getName(s)
    Next s

End Sub
列挙子を使用
Sub EnumeratorSample()

    '適当なコレクションを用意
    Dim col As VBA.Collection
    Set col = New VBA.Collection
    col.Add 1
    col.Add "ABC"
    col.Add Now

    Dim Enumerator As Object
    Set Enumerator = JSFunc("col", "new Enumerator(col)")

    'JScriptのEnumeratorオブジェクトをインスタンス
    Dim e As Object
    Set e = Enumerator(col)

    '`.atEnd()`だとfunctionオブジェクトが返るため、適当な引数を指定するか、CallByNameを使う
    Do While Not e.atEnd(Nothing)

        'メソッド名の大文字・小文字を区別するため、CallByNameを使う
        Debug.Print CallByName(e, "item", VbMethod)
        Call e.moveNext

    Loop

End Sub

参考:速度比較

'処理速度比較用
Sub SpeedTest()

    Dim startTime As Single
    Const LOOP_COUNT = 100000
    Dim i As Long, buf As String

    Debug.Print VBA.Format$(LOOP_COUNT, "0,0"); "回実行した結果"

    '通常のプロパティ参照
    startTime = VBA.Timer
    For i = 1 To LOOP_COUNT
        buf = Excel.ThisWorkbook.Name
    Next i
    Debug.Print "Normal", VBA.Timer - startTime

    'CallByName
    startTime = VBA.Timer
    For i = 1 To LOOP_COUNT
        buf = VBA.CallByName(Excel.ThisWorkbook, "Name", VbGet)
    Next i
    Debug.Print "CallByName", VBA.Timer - startTime

    'function
    startTime = VBA.Timer
    Dim getName As Object
    Set getName = JSFunc("o", "o.Name")
    For i = 1 To LOOP_COUNT
        buf = getName(Excel.ThisWorkbook)
    Next i
    Debug.Print "function", VBA.Timer - startTime

End Sub

作者環境で上のコードを実行すると以下の結果となりました。

100,000回実行した結果
Normal         0.109375
CallByName     0.625
function       1.289063

処理内容にもよりますが、速度の要求がそこまでで無ければアリな気はします。

関連・参考記事

[Excel/VBA] VBAでJavaScriptの関数オブジェクト・高階関数を使う - Qiita

64bit版VBAでScriptControlを使用する - Qiita

18
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
14