1. ホーム
  2. ジャバスクリプト

[解決済み】ドット記法のJavaScript文字列をオブジェクト参照に変換する

2022-03-31 16:39:28

質問

JavaScriptのオブジェクトを指定します。

var obj = { a: { b: '1', c: '2' } }

と文字列

"a.b"

文字列をドット表記に変換して、次のように入力します。

var val = obj.a.b

もし、この文字列がただの 'a' を使用することができます。 obj[a] . しかし、これはより複雑です。何か簡単な方法があるのでしょうが、今のところ私にはわかりません。

解決方法は?

<ブロッククオート

最近のメモ この回答が多くのupvoteを得たことは喜ばしいことですが、私は少々恐ろしくもあります。もし、"x.a.b.c"のようなドット注記の文字列を参照に変換する必要があるなら、それは(多分)何か非常に間違ったことが起こっている兆候かもしれません(多分、何かおかしなデシリアライズを行っているのでなければですが)。

<ブロッククオート

つまり、この答えにたどり着いた初心者は、「"なぜ私はこんなことをしているのだろう?

<ブロッククオート

もちろん、ユースケースが小さく、パフォーマンスの問題が発生しないのであれば、この方法で問題ないでしょう。実際、もしこれでコードの複雑さが軽減され、物事がシンプルになるのなら、あなたは すべき おそらく、OPが求めているようなことを先にやってしまうのでしょう。しかし、そうでない場合は、これらのいずれかに当てはまるかどうかを検討してください。

<ブロッククオート

ケース1 : データを扱う主要な方法として(例えば、オブジェクトを渡したり参照したりするアプリのデフォルトの形式として)。例えば、「文字列から関数名や変数名を調べるにはどうしたらいいですか?

<ブロッククオート
  • これは悪いプログラミングのやり方です(特に不必要なメタプログラミングで、関数の副作用のないコーディングスタイルに違反しているようなもので、パフォーマンスが低下します)。このケースに当てはまる初心者は、代わりに配列表現、例えば ['x','a','b','c'] や、可能ならもっと直接的/単純/直接的なものを検討すべきです。そもそも参照自体を見失わない(クライアントサイドだけ、あるいはサーバーサイドだけなら最も理想的)、などです(既存のユニークIDは追加するには不便ですが、仕様がその存在を必要としているなら使うことができます)。
<ブロッククオート

ケース2 : シリアル化されたデータ、またはユーザーに表示されるデータを扱うこと。例えば、日付を Date オブジェクトではなく、文字列 "1999-12-30" として使用します(注意しないと、タイムゾーンのバグやシリアライズの複雑さを引き起こす可能性があります)。あるいは、自分が何をしているのか分かっているのか。

<ブロッククオート
  • これは多分大丈夫です。サニタイズされた入力断片の中にドット文字列 "." がないように注意してください。
<ブロッククオート

もし、この答えをいつも使っていて、文字列と配列の間を行ったり来たりしているようなら、悪いケースかもしれませんので、代替案を検討する必要があります。

他の解決策より10倍短いエレガントなワンライナーを紹介します。

function index(obj,i) {return obj[i]}
'a.b.etc'.split('.').reduce(index, obj)

[編集] あるいはECMAScript 6で。

'a.b.etc'.split('.').reduce((o,i)=> o[i], obj)

(他の人が言うように eval が常に悪いとは思いませんが(普通はそうですが)、この方法が eval を使わないので、そういう人は喜ぶでしょう)。上記は obj.a.b.etc 与えられた obj という文字列と "a.b.etc" .)

を使うのがまだ怖いという人に向けて。 reduce ECMA-262標準(第5版)にあるにもかかわらず、2行の再帰的な実装を紹介します。

function multiIndex(obj,is) {  // obj,['1','2','3'] -> ((obj['1'])['2'])['3']
    return is.length ? multiIndex(obj[is[0]],is.slice(1)) : obj
}
function pathIndex(obj,is) {   // obj,'1.2.3' -> multiIndex(obj,['1','2','3'])
    return multiIndex(obj,is.split('.'))
}
pathIndex('a.b.etc')

JSコンパイラの最適化によっては、ネストされた関数が通常の方法(クロージャ、オブジェクト、グローバル名前空間への配置)で呼び出されるたびに再定義されないようにすることができます。

編集 :

コメント欄の興味深い質問にお答えします。

<ブロッククオート

これをセッターにするにはどうすればいいのでしょうか?パスで値を返すだけでなく、新しい値が関数に送られたらそれを設定するのですか?- Swader 6月28日21時42分

(補足: 悲しいことに、セッターでオブジェクトを返すことはできません。それは呼び出し規約に違反することになるからです。 index(obj,"a.b.etc", value) して obj.a.b.etc = value .)

reduce のスタイルはあまり適していませんが、再帰的な実装を修正することは可能です。

function index(obj,is, value) {
    if (typeof is == 'string')
        return index(obj,is.split('.'), value);
    else if (is.length==1 && value!==undefined)
        return obj[is[0]] = value;
    else if (is.length==0)
        return obj;
    else
        return index(obj[is[0]],is.slice(1), value);
}

デモの様子

> obj = {a:{b:{etc:5}}}

> index(obj,'a.b.etc')
5
> index(obj,['a','b','etc'])   #works with both strings and lists
5

> index(obj,'a.b.etc', 123)    #setter-mode - third argument (possibly poor form)
123

> index(obj,'a.b.etc')
123

...個人的には、別の関数を作ることをお勧めします。 setIndex(...) . 最後に余談ですが、元の質問者はインデックスの配列を扱うことができる(はず? .split しかし、便利な関数であることに間違いはありません。


コメント欄からの質問です。

配列の場合はどうなんでしょうか?-アレックス

Javascriptは非常に奇妙な言語で、一般的にオブジェクトはプロパティのキーとして文字列しか持つことができません。 x のような一般的なオブジェクトでした。 x={} であれば x[1] になります。 x["1"] ... 読んだ通りだ...そうだ...。

Javascriptの配列(それ自体がObjectのインスタンス)は、特に整数のキーを推奨しており、次のようなことが可能です。 x=[]; x["puppy"]=5; .

しかし、一般的には(例外もありますが)。 x["somestring"]===x.somestring (許される場合。 x.123 ).

(使用しているJSコンパイラが、仕様に違反しないことを証明できれば、これらをより健全な表現にコンパイルすることを選択するかもしれないことを心に留めておいてください)。

つまり、質問の答えは、これらのオブジェクトが(問題領域の制限により)整数しか受け入れないと仮定しているか、そうでないかによって変わってくるということです。そうでないと仮定しましょう。その場合、有効な式は基本識別子といくつかの .identifier に加えて、いくつかの ["stringindex"] s.

もちろん、文法上では他にも以下のようなことが正当に行えるということは、ちょっと無視しましょう。 identifier[0xFA7C25DD].asdf[f(4)?.[5]+k][false][null][undefined][NaN] 整数は(それほど)「特別」ではないのです。

この場合、コメンテーターの発言は次のようになります。 a["b"][4]["c"]["d"][1][2][3] をサポートする必要があります。 a.b["c\"validjsstringliteral"][3] . を確認する必要がありますね。 文字列リテラルに関する ecmascript の文法セクション をクリックすると、有効な文字列リテラルをパースする方法を見ることができます。技術的には、(私の最初の回答とは異なり)あなたはまた、以下のことを確認したいと思うでしょう。 a は有効な javascriptの識別子 .

簡単な答えですが。 文字列にカンマや括弧が含まれていない場合 にない長さ1以上の文字列をマッチさせるだけです。 , または [ または ] :

> "abc[4].c.def[1][2][\"gh\"]".match(/[^\]\[.]+/g)
// ^^^ ^  ^ ^^^ ^  ^   ^^^^^
["abc", "4", "c", "def", "1", "2", ""gh""]

文字列がエスケープ文字を含まない場合や " 文字 そして、IdentifierNamesはStringLiteralsのサブ言語であるため(確か???)、ドットを[]に変換することができます。

> var R=[], demoString="abc[4].c.def[1][2][\"gh\"]";
> for(var match,matcher=/^([^\.\[]+)|\.([^\.\[]+)|\["([^"]+)"\]|\[(\d+)\]/g; 
      match=matcher.exec(demoString); ) {
  R.push(Array.from(match).slice(1).filter(x=> x!==undefined)[0]);
  // extremely bad code because js regexes are weird, don't use this
}
> R

["abc", "4", "c", "def", "1", "2", "gh"]

もちろん、常に注意して、決してデータを信用してはいけません。また、一部のユースケースで有効かもしれない悪い方法もいくつかあります。

// hackish/wrongish; preprocess your string into "a.b.4.c.d.1.2.3", e.g.: 
> yourstring.replace(/]/g,"").replace(/\[/g,".").split(".")
"a.b.4.c.d.1.2.3"  //use code from before


2018年特別編集。

このまま一周して、思いつく限り最も非効率的で、恐ろしくオーバーメタな解決策を実行しましょう...構文的な観点で <ストライク 純度 のハムフィステルです。ES6のProxyオブジェクトで!... また、不適切に書かれたライブラリを破壊する可能性のあるプロパティも定義しておきましょう。もし、あなたがパフォーマンスや正気(あなたや他の人の)、仕事などを気にしているのなら、これを使うのは注意すべきかもしれません。

// [1,2,3][-1]==3 (or just use .slice(-1)[0])
if (![1][-1])
    Object.defineProperty(Array.prototype, -1, {get() {return this[this.length-1]}}); //credit to caub

// WARNING: THIS XTREME™ RADICAL METHOD IS VERY INEFFICIENT,
// ESPECIALLY IF INDEXING INTO MULTIPLE OBJECTS,
// because you are constantly creating wrapper objects on-the-fly and,
// even worse, going through Proxy i.e. runtime ~reflection, which prevents
// compiler optimization

// Proxy handler to override obj[*]/obj.* and obj[*]=...
var hyperIndexProxyHandler = {
    get: function(obj,key, proxy) {
        return key.split('.').reduce((o,i)=> o[i], obj);
    },
    set: function(obj,key,value, proxy) {
        var keys = key.split('.');
        var beforeLast = keys.slice(0,-1).reduce((o,i)=> o[i], obj);
        beforeLast[keys[-1]] = value;
    },
    has: function(obj,key) {
        //etc
    }
};
function hyperIndexOf(target) {
    return new Proxy(target, hyperIndexProxyHandler);
}

デモの様子

var obj = {a:{b:{c:1, d:2}}};
console.log("obj is:", JSON.stringify(obj));

var objHyper = hyperIndexOf(obj);
console.log("(proxy override get) objHyper['a.b.c'] is:", objHyper['a.b.c']);
objHyper['a.b.c'] = 3;
console.log("(proxy override set) objHyper['a.b.c']=3, now obj is:", JSON.stringify(obj));

console.log("(behind the scenes) objHyper is:", objHyper);

if (!({}).H)
    Object.defineProperties(Object.prototype, {
        H: {
            get: function() {
                return hyperIndexOf(this); // TODO:cache as a non-enumerable property for efficiency?
            }
        }
    });

console.log("(shortcut) obj.H['a.b.c']=4");
obj.H['a.b.c'] = 4;
console.log("(shortcut) obj.H['a.b.c'] is obj['a']['b']['c'] is", obj.H['a.b.c']);

出力します。

objは。{"a":{"b":{"c":1,"d":2}}} のようになります。

<ブロッククオート

(プロキシ オーバーライド get) objHyper['a.b.c'] is: 1

<ブロッククオート

(プロキシ オーバーライド セット) objHyper['a.b.c']=3, now obj is: {"a":{"b":{"c":3,"d":2}}} となりました。

<ブロッククオート

(裏)objHyperは。プロキシ {a: {...}}

<ブロッククオート

(ショートカット) obj.H['a.b.c']=4

<ブロッククオート

(ショートカット) obj.H['a.b.c'] is obj['a']['b']['c'] is: 4

非効率的なアイデア: 上記を修正して、入力引数に基づいてディスパッチすることができます。 .match(/[^\]\[.]+/g) メソッドでサポートします。 obj['keys'].like[3]['this'] または、もし instanceof Array のように配列を入力として受け取ります。 keys = ['a','b','c']; obj.H[keys] .


未定義のインデックスを「よりソフトな」NaNスタイルで処理したいのではないかという提案につき、(例えば index({a:{b:{c:...}}}, 'a.x.c') はキャッチされないTypeErrorではなく、undefinedを返す)...:

  1. これは、1次元のインデックスの状況({})['e.g.']==undefinedでは、"エラーを投げるよりも未定義を返すべき"という観点から、N次元の状況でも意味をなします。

  2. これは ない という観点では意味がありません。 x['a']['x']['c'] この場合、上記の例ではTypeErrorで失敗します。

とはいえ、還元関数をどちらかに置き換えることで、うまくいくでしょう。

(o,i)=> o===undefined?undefined:o[i] または (o,i)=> (o||{})[i] .

(これをより効率的にするには、for ループを使用して、次にインデックスを作成するサブ結果が未定義になるたびに中断/リターンするか、そのような失敗が十分にまれであると期待するならば try-catch を使用します)。