QA@IT

JavaScript超初心者です。JavaScriptの戻り値が返ってきません。

15513 PV

JavaScript超初心者です。JavaScriptの戻り値が返ってきません。以下のスクリプトを開発ツールのコンソールで実行したのですが、戻り値が返ってきません。
(undefinedとなる)コメントアウトした部分までは正常に動作するのですが、xhrStart('GET','http://www.yahoo.co.jp/')
を実行した結果が返ってきません。どうすればundefinedになりませんか?ご教授よろしくお願いします。

function xhrStart(method,url,data,RequestHeader){
    var xhr = new XMLHttpRequest();

    xhr.onload = function(){

        //console.log(xhr.response);
        return xhr.response;
    }   
    xhr.open(method,url);
    if(RequestHeader != null){
        for(key in RequestHeader){
            xhr.setRequestHeader(key,RequestHeader[key]);
        }
    }
    xhr.responseType = "document";
    if(method === 'GET')
        xhr.send(null);
    else
        xhr.send(data);
}

console.log(xhrStart('GET','http://www.yahoo.co.jp/'));
  • そもそものやりたいことは何なのですか? 上のコードを勉強していてこれを動くようにしたい? 非同期要求を出して応答を受け取りたいが方法はこだわらない? 何がしたいのかシナリオ的なことも含めて書いていただけると、回答者のほうも的を得た回答がしやすいのですが。 -
  • DOMを使いたくて同期要求だとHTMLのパースをサポートしてない(https://developer.mozilla.org/ja/docs/HTML_in_XMLHttpRequestのでどうにか非同期で出来ないかと思いまして、DOMParserやHTMLParserやjQuery.parseHTMLはよくわからないので… -
  • Web アプリを作っているのだと思いますが「開発ツール」とは何でしょう? Visual Studio とかですか? -
  • やりたいことはリクエストしてリクエストしたDOMを使って取得したHTMLから判断してまたリクエストをだすことです。 -
  • Webアプリではなくブックマークレットを作っているのですが、開発ツールはクロームのF12を押したら出てくるもののコンソールを使ってます。エディタはメモ帳です。 -
  • flied_onion さんの回答に書かれている「表示しているページと別のドメインへのXMLHttpRequest経由アクセスということでブロック」というところは理解されているでしょうか。どういうブックマークレットか分かりませんが、その問題でそもそもダメということはありませんか? -
  • クロスドメインではないので大丈夫だと思います。http://www.yahoo.co.jp/からやってます。クロスドメインのときはURLの先頭にhttp://allow-any-origin.appspot.com/を入れたりしたます。 -

回答

非同期モードなのでサーバーからレスポンスが戻る前に戻り値(document)を参照したた
めundefined(未定義値)が返って来たのだとおもいます。

そうではなくて、2 つめのコードが動かないのは、flied_onion さんが最初のレスで指摘されたように「xhr.onload のところに書いているreturnはあくまで xhr.onloadのreturnですのでxhrStartの戻り値にはなりません。」ということだからでしょう。

参考にされていた MDN の記事の一番最後のコードのように callback を利用してはいかがでしょう? 質問者さんのケースですと以下のような感じでしょうか? (検証してません。あくまで感じですので参考にとどめてください。)

function xhrStart(method, url, data, callback) {
    var xhr = new XMLHttpRequest();
    xhr.onload = function () {
        callback(xhr.response);
    }
    xhr.open(method, 'http://allow-any-origin.appspot.com/' + url);
    xhr.responseType = "document";
    xhr.send(data);
}

xhrStart('GET', 'http://www.yahoo.co.jp/', null, callback1);

function callback1(dom) {
    var element = dom.getElementById("toptxt");
    console.log(element.innerHTML);
    var childElement = element.getElementsByTagName("a");
    console.log(childElement[0].href);
    xhrStart('GET', childElement[0].href, null, callbaxk2);
}

function callback2(dom) {
    var innerElement = dom.getElementsByTagName("a");
    console.log(innerElement[Math.floor(Math.random() * innerElement.length)].href);
    xhrStart('GET', innerElement[Math.floor(Math.random() * innerElement.length)].href, null, callback3);
}

function callback3(dom) {
    var innerInnerElement = dom.getElementsByTagName("a");
    console.log(innerInnerElement[Math.floor(Math.random() * innerInnerElement.length)].href);
}

# allow-any-origin.appspot.com というのを調べましたが、個人が立てたプロキシです。応答ヘッダに Access-Control-Allow-Origin: * をつけて返してくれる仕組みになっているようです。いつサービスが終了するか分からないので、業務目的には使わないほうが良いかと思います。

編集 履歴 (0)
  • すばらしい回答ありがとうございます。callbackの存在自体知らなかったのでMDNの記事を読んでもほとんど分かりませんでした。もっとjavascriptを勉強すべきだと改めて痛感しました。あまりよく分からないですけどAccess-Control-Allow-Origin: *のようなプロキシを個人で立てることは出来るのでしょうか? -
  • プロキシは個人でも立てられると思いますが、立てるための費用・工数をどうするか、1 年 365 日 24 時間運用を続けられるのかなどを考えると容易ではないと思います。 -
  • コメントありがとうございます。プロキシ立てるの大変なのですね。最後に少し聞きたいのですが、flied_onionのvar callback1 = function(document)とSurferOnWwwのfunction callback1(dom)で試したところ同じ挙動をするのですが何か違いはありますでしょうか? -
  • あと、flied_onionさんはcallback2→callback1、SurferOnWwwさんはcallback1→callback2→callback3の順で定義されてますがどちらがいいとかっていうのはありますでしょうか?お答えいただけると幸いです。 -
  • flied_onion さんに聞いていただかないと分からないと思いますが、前者についてはクロージャーを意識していて、後者は好みの問題と思います。 -
  • クロージャーについてもっと調べてみようと思います。 -
  • 先に「後者は好みの問題」と書きましたけど、それは普通に function callback のような関数定義をした場合、その関数は前方参照が可能になるので、好きな順所で書けるということでした。flied_onion さんの書き方 var callback = finction の場合はそうはいかないです。 -

クロスドメインの問題はおいておきますが(これ自体はクロスドメインのjsonpの様に別サーバー挟むという手はあるにはありますけどね)、

とりあえず非同期と同期ではアプローチの仕方が異なります。
厳密にはいろいろあるのですが誤解を恐れず言えば、「戻り値を受け取る」と考えた時点でそれは同期処理です。

それでも非同期処理で戻り値を受け取りたいのであれば、戻り値となりうる値が手に入るまで待つ(たとえば単純にsleepなどして)というのが考えられ、
例えば以下の様な処理を思いつきます(が、以下は動きません)。

function xhrStart(method,url,data,RequestHeader){
    var xhr = new XMLHttpRequest();
    var response = undefined;
    var onload_processed = false;

    xhr.onload = function(){
        onload_processed = true;
        response = xhr.response;
    }   
    xhr.open(method,url);
    if(RequestHeader != null){
        for(key in RequestHeader){
            xhr.setRequestHeader(key,RequestHeader[key]);
        }
    }
    xhr.responseType = "document";
    if(method === 'GET')
        xhr.send(null);
    else
        xhr.send(data);

    while(!onload_processed){
        sleep(100);  // javascript に sleepはなく、busy waitさせるとajax 側に影響がでる場合もある
    }

    return response;
}

console.log(xhrStart('GET','http://www.yahoo.co.jp/'));

現状の javascriptはその場で待つ処理が苦手ですので、何か別の手立てで待つ必要があります。
例えば以下の様に連想配列を用意し、xhrStartでは格納予定のキーを返し、
xhrStartを呼び出した方はそのキーを元に連想配列が変化するかどうかを一定間隔で確認します(resposewait)

var resultHash = new Object();

function xhrStart(method,url,data,RequestHeader){
    var xhr = new XMLHttpRequest();

    var hashkey = new Date().getTime();
    xhr.onload = function(){
        resultHash[hashkey] = xhr.response;
    }

    xhr.open(method,url); // 非同期
    if(RequestHeader != null){
        for(key in RequestHeader){
            xhr.setRequestHeader(key,RequestHeader[key]);
        }
    }
    xhr.responseType = "document";

    if(method === 'GET')
        xhr.send(null);
    else
        xhr.send(data);

    return hashkey.toString();
}

function resposewait(hashkey){
    if(resultHash[hashkey] == undefined){
        setTimeout("resposewait(" + hashkey + ")", 500);
    }else{
        return resultHash[hashkey];
    }
}

var hashkey = xhrStart('GET','http://www.yahoo.co.jp/');
var doc = resposewait(hashkey);
console.log(doc);

例えばこれを使うと


var hashkey = xhrStart('GET','http://www.yahoo.co.jp/');
var document = resposewait(hashkey);
var element = document.getElementById("toptxt");
console.log(element.innerHTML);
var childElement = element.getElementsByTagName("a");
console.log(childElement[0].href);

hashkey = xhrStart('GET',childElement[0].href);
document = resposewait(hashkey);
var innerElement = document.getElementsByTagName("a");
console.log(innerElement[Math.floor(Math.random() * innerElement.length)].href);

// 以下略

といったコードがかけるかと思います。


別の方法として、現在は「xhrStartの結果を受け取って、メイン処理を継続する」という発想に立っていますが、「xhrStartに続きの処理も引き渡しておく」という考え方もあります。
個人的にはこちらの方がajaxらしく見える気もします(元々がコールバック関数を受け取るオブジェクトだからでしょうが)。

// 引数にコールバック関数(response後にやってほしい続きの処理)を増やす
function xhrStart(method,url,data,RequestHeader, callback){
    var xhr = new XMLHttpRequest();

    xhr.onload = function(){
        // 続きの処理を呼び出す
        callback(xhr.response);
    }

    xhr.open(method,url); // 非同期
    if(RequestHeader != null){
        for(key in RequestHeader){
            xhr.setRequestHeader(key,RequestHeader[key]);
        }
    }
    xhr.responseType = "document";

    if(method === 'GET')
        xhr.send(null);
    else
        xhr.send(data);
}

// callback1 で使うので先に書いてある。
var callback2 = function(document) {
    var innerElement = document.getElementsByTagName("a");
    console.log(innerElement[Math.floor(Math.random() * innerElement.length)].href);
}

var callback1 = function(document) {
    var element = document.getElementById("toptxt");  
    console.log(element.innerHTML);
    var childElement = element.getElementsByTagName("a");
    console.log(childElement[0].href);

    hashkey = xhrStart('GET',childElement[0].href, null,null, callback2);
}

// 最後の引数にコールバック関数を渡す
xhrStart('GET','http://www.yahoo.co.jp/',null,null,callback1);

上記の様な構成もとれる。ということで確認は簡単にしかしていませんが参考にどうぞ。

編集 履歴 (2)
  • すばらしい回答ありがとうございます。
    引数を増やしてコールバック関数を使って処理自体を渡せば戻り値を受け取らなくても実現出来るのですね。順次構造に慣れてる私からすると少し違和感ありますけど慣れれば大丈夫そうです。大変勉強になりました。もっとajaxの勉強頑張ります。
    -
function xhrStart(method,url,data){
    var xhr = new XMLHttpRequest();
    xhr.onload = function(){
        var document = xhr.response;
        var element = document.getElementById("toptxt");
        console.log(element.innerHTML);
        var childElement = element.getElementsByTagName("a");
        console.log(childElement[0].href);
        var innerXhr = new XMLHttpRequest();
        innerXhr.onload = function(){
            var innerDocument = innerXhr.response;
            var innerElement = innerDocument.getElementsByTagName("a");
            console.log(innerElement[Math.floor(Math.random() * innerElement.length)].href);
            var innerInnerXhr = new XMLHttpRequest();
            innerInnerXhr.onload = function(){
                var innerInnerDocument = innerInnerXhr.response;
                var innerInnerElement = innerInnerDocument.getElementsByTagName("a");
                console.log(innerInnerElement[Math.floor(Math.random() * innerInnerElement.length)].href);
                //
                //ネストが続く…
                //
            }
            innerInnerXhr.open('GET','http://allow-any-origin.appspot.com/' + innerElement[Math.floor(Math.random() * innerElement.length)].href);
            innerInnerXhr.responseType = "document";
            innerInnerXhr.send(data);
        }
        innerXhr.open('GET','http://allow-any-origin.appspot.com/' + childElement[0].href);
        innerXhr.responseType = "document";
        innerXhr.send(data);
    }
    xhr.open(method,url);
    xhr.responseType = "document";
    xhr.send(data);
}

xhrStart('GET','http://www.yahoo.co.jp/',null);

この質問は最初上記のように(一応、動きます(http://www.yahoo.co.jp/のドメイン下でコンソールを使ってください。))書くとネストばかりで可読性がすごく損なわれ、変数が衝突する可能性があるので関数を作ろうと思って以下のように(動きません)

function xhrStart(method,url,data){
    var xhr = new XMLHttpRequest();
    xhr.onload = function(){
        var document = xhr.response;
        return document;
    }
    xhr.open(method,'http://allow-any-origin.appspot.com/' + url);
    xhr.responseType = "document";
    xhr.send(data);
}

var document = xhrStart('GET','http://www.yahoo.co.jp/',null);
var element = document.getElementById("toptxt");
console.log(element.innerHTML);
var childElement = element.getElementsByTagName("a");
console.log(childElement[0].href);

var innerDocument = xhrStart('GET',childElement[0].href,null);
var innerElement = innerDocument.getElementsByTagName("a");
console.log(innerElement[Math.floor(Math.random() * innerElement.length)].href);

var innerInnerDocument = xhrStart('GET',innerElement[Math.floor(Math.random() * innerElement.length)].href,null);
var innerInnerElement = innerInnerDocument.getElementsByTagName("a");
console.log(innerInnerElement[Math.floor(Math.random() * innerInnerElement.length)].href);

書きたかったのですが、xhr.onload のところに書いているreturnはあくまで xhr.onloadのreturnですのでxhrStartの戻り値にはなりません。またDOMParserやJsonParserやjQuery.parseHTMLなどを使用するとデバイスに依存するかもしれないのでXMLHttpRequestだけで完結させたいのですが、もしこれを改善するならどのようにすればいいでしょうか?ご教授よろしくお願いします。

編集 履歴 (6)
  • 後半のソースに、innerInnerDocumentが 2つあって先の方の内容が、innerDocumentと後の方のinnerInnerDocumentのミックス版の様になっていますがこれは正しいですか?(細かいところまでは見てませんがパッと見で気になりました) -
  • ご指摘ありがとうございます。innerInnerDocumentは一つだけです。修正しておきます。 -
  • callback を使うという案を解答欄に書きました。あくまで案で、検証はしてませんので参考に留めてください。 -

ブックマークレットという形ではなく普通のページ上の JavaScript として試してみましたが、質問者さんが紹介されている MDN の記事の通り response プロパティで dom を取得できることを確認しました。

質問者さんのコードで取得できなかったのは、単純にメソッドが何も返していなかったためではないでしょうか?

確認に使ったのは以下のコードです。ただし、当然ですが、response プロパティをサポートしているブラウザでないとダメです。(自分が試した限り Firefox 32.0.3, Chrome 37.0.2062.124 m は OK、IE9 はダメでした)

クロスドメインは試していませんが XMLHttpRequest の仕様から考えてダメだと思います。取得できる方法があったら教えていただけると幸いです。

<%@ Page Language="C#" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<script runat="server">

</script>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <script type="text/javascript">
    //<![CDATA[
        function xhrStart() {
            var xhr = new XMLHttpRequest();
            xhr.open("GET", "0067-HTMLPage.htm");

            xhr.onreadystatechange = function () {
                if (xhr.readyState == 4) {
                    var label = document.getElementById("msg");
                    var dom = xhr.response;
                    var element = dom.getElementById("para1");
                    var innerHtml = element.innerHTML;
                    label.textContent = innerHtml;
                }
            }

            xhr.responseType = "document";
            xhr.send();
        }
    //]]>
    </script>
</head>
<body>
    <form id="form1" runat="server">
        <input type="button" value="Request" onclick="xhrStart();" />
        <hr />
        <span id="msg" />
    </form>
</body>
</html>

上のページから非同期要求する 0067-HTMLPage.htm は以下の通りです。応答は以下のテキストそのままの形で返ってきます(Fiddler2 で確認)。そのテキストから上のコードの xhr.response で dom が取得できるようです。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>XMLHttpRequest Test</title>
</head>
<body>
    <h1>XMLHttpRequest Test</h1>
    <p id="para1">
        para1 の InnerText
    </p>
</body>
</html>
編集 履歴 (1)
  • 親切に回答してくださってありがとうございます。コードで取得できなかったのは、単純にメソッドが何も返していなかったためだと考えております。やはりイベントに戻り値を入れるのはだめですよね。しかし、どうしても同期モードだとHTMLをパースしてくれません。もう一度回答するのでコメントしていただけると幸いです。 -

XMLHttpRequestの同期モードでのHTMLパース処理
あれから自分なりに考えた結果以下のようなスクリプトになりました。

//'GET'の場合'data'に'null'を代入、'requestHeader'は連想配列です。
function xhrStart(method,url,data,requestHeader){
    var xhr = new XMLHttpRequest();
    xhr.onload = function(){
        var response = xhr.responseText;
    }
    xhr.open(method,url,false);
    if(requestHeader !== undefined)
        for(key in requestHeader)
            xhr.setRequestHeader(key,requestHeader[key]);
    xhr.send(data);
    return response;
}

function DOMParserStart(text){
    var parser = new DOMParser();
    var document = parser.parseFromString(text,"text/html");
    return document;
}

var text = xhrStart('GET','http://www.yahoo.co.jp/',null);
console.log(DOMParserStart(text));

IEのDOMParserはHTMLをサポートしていません。(参照)
XMLHttpRequest
XMLHttpRequestの非同期モードでのHTMLパース処理
DOMParser

flied_onionさんSurferOnWwwさんこの質問に回答していただきありがとうございました。

編集 履歴 (1)
  • 確認してみましたが、質問者さんが紹介されている MDN の記事の通り response で dom が取得できるようです。詳細は解答欄に書きます。 -

どういう環境で実行されているかわかりませんが(Safariかモバイルのブラウザですかね?)、
最近のブラウザですと現状抱えている問題以外に、表示しているページと別のドメインへのXMLHttpRequest経由アクセスということでブロックされるように思います。

その問題を除いて考えた場合、現在xhrStartの戻り値がundefinedなのは、
xhrStart関数がそもそも何かを返すような関数になっていないからです。
(具体的に言えばなにもreturnしていないからです)

xhr.onload のところに書いているreturnはあくまで xhr.onloadのreturnですのでxhrStartの戻り値にはなりません。

じゃぁ、xhr.onloadの戻り値をxhrStartに引き継げればいいんだねという話になるんですが、
もしそういうことをするのであれば、現在xhrは非同期で実行されていますのでこれを同期実行にしなくてはなりません。
(非同期実行と同期実行の違いはまずは調べてみてください)

で、その辺の諸々を解決すると以下の様なコードになるとは思うんですが、

function xhrStart(method,url,data,RequestHeader){
    var xhr = new XMLHttpRequest();

    var response = undefined;  // 値を引き取るための変数

    xhr.onload = function(){

        //console.log(xhr.response);
        // return xhr.response;
        response = xhr.response;  // 親ブロック(xhrStart)のresponse変数に引き継ぐ
    }   
    xhr.open(method,url,false); // 第三引数を指定して同期実行に
    if(RequestHeader != null){
        for(key in RequestHeader){
            xhr.setRequestHeader(key,RequestHeader[key]);
        }
    }
    // xhr.responseType = "document";  // 同期実行では指定できないのでコメントアウト
    if(method === 'GET')
        xhr.send(null);
    else
        xhr.send(data);

    console.log(response); // 同期実行ではxhr.sendが終わった後にここ以降が実行される。
    return response;
}

console.log(xhrStart('GET','http://www.yahoo.co.jp/'));

ちょっと私のブラウザだと、No 'Access-Control-Allow-Origin' headerエラーが出てしまい、すいませんが動作確認できてません。

xhrでアクセスできるようなサイトに使うにしても前述の同期/非同期の問題も出てくると思いますが、とりあえずサンプルとして上記を提示しておきます。

編集 履歴 (0)
  • すばやい回答ありがとうございます。
    xhr.responseをDOMとして扱いたいのですが、同期実行だとパースしてくれないらしいです。(https://developer.mozilla.org/ja/docs/HTML_in_XMLHttpRequest)そのためDOMParserやHTMLParserやjQuery.parsを使わないといけなくなり、どうにかして非同期で出来ないかと思って…
    -
  • いるのですが、戻り値を受け取って次の処理をすれば同期処理みたいなのができないかと思っているのですがどうなのでしょうか?やはり非同期で戻り値を取得することは不可能でしょうか?(public変数とか?)わずらわしい質問ですがご教授お願いします -
ウォッチ

この質問への回答やコメントをメールでお知らせします。