แปลงสตริง JavaScript ในรูปแบบจุดเป็นการอ้างอิงวัตถุ


214

รับวัตถุ JS

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

และสตริง

"a.b"

ฉันจะแปลงสตริงให้เป็นสัญกรณ์ดอทเพื่อให้ฉันไปได้อย่างไร

var val = obj.a.b

หากสตริงเป็นเพียงฉันสามารถใช้'a' obj[a]แต่มันซับซ้อนกว่านี้ ฉันคิดว่ามีวิธีการที่ตรงไปตรงมา แต่มันก็หนีออกมาได้ในปัจจุบัน


25
@Andrey evalเป็นสิ่งที่ชั่วร้าย; อย่าใช้มัน
kevinji

5
FYI: นี่คือการทดสอบความเร็วที่น่าสนใจที่ฉันเพิ่งทำ: jsperf.com/dereference-object-property-path-from-string
James Wilkins

ถ้า perf เป็นการพิจารณาที่จริงจังและคุณกำลังใช้เส้นทางเดียวกันซ้ำอีกมากมาย (เช่นภายในฟังก์ชันตัวกรองอาร์เรย์) ให้ใช้ตัวสร้างฟังก์ชันตามที่อธิบายไว้ในคำตอบของฉันด้านล่าง เมื่อมีการใช้เส้นทางเดียวกันหลายพันครั้งวิธีการของฟังก์ชั่นอาจเร็วกว่าถึง 10 เท่าในการวิเคราะห์หรือแยกและลดเส้นทางในทุกการตรวจสอบ
Kevin Crumley


คำตอบ:


437

หมายเหตุเมื่อไม่นานมานี้:ในขณะที่ฉันรู้สึกปลื้มใจที่คำตอบนี้ได้รับการโหวตมากมาย แต่ฉันก็ค่อนข้างตกใจ ถ้าใครต้องการแปลงสตริงดอทสัญกรณ์อย่าง "xabc" เป็นการอ้างอิงอาจเป็นสัญญาณว่ามีบางอย่างผิดปกติเกิดขึ้น (เว้นแต่คุณอาจกำลังแสดงท่าทีแปลกประหลาด)

กล่าวคือสามเณรที่หาทางไปสู่คำตอบนี้ต้องถามตัวเองว่า "ทำไมฉันถึงทำอย่างนี้?"

เป็นเรื่องปกติที่จะทำเช่นนี้หากกรณีการใช้งานของคุณมีขนาดเล็กและคุณจะไม่พบปัญหาเกี่ยวกับประสิทธิภาพและคุณไม่จำเป็นต้องสร้างสิ่งที่เป็นนามธรรมเพื่อให้ซับซ้อนขึ้นในภายหลัง ในความเป็นจริงถ้าสิ่งนี้จะลดความซับซ้อนของรหัสและทำให้สิ่งต่าง ๆ เป็นเรื่องง่ายคุณควรจะไปข้างหน้าและทำสิ่งที่ OP ขอ อย่างไรก็ตามหากไม่ใช่กรณีดังกล่าวให้พิจารณาว่าสิ่งเหล่านี้มีผลบังคับใช้หรือไม่:

กรณีที่ 1 : เป็นวิธีหลักในการทำงานกับข้อมูลของคุณ (เช่นรูปแบบเริ่มต้นของแอปในการส่งวัตถุไปรอบ ๆ และยกเลิกการลงทะเบียน) เช่นเดียวกับการถาม "ฉันจะค้นหาฟังก์ชันหรือชื่อตัวแปรจากสตริงได้อย่างไร"

  • นี่เป็นวิธีปฏิบัติในการเขียนโปรแกรมที่ไม่ดี (โดยเฉพาะการใช้โปรแกรมไม่จำเป็นโดยเฉพาะและชนิดของการละเมิดรูปแบบการเขียนโปรแกรมฟังก์ชั่นที่ไม่มีผลข้างเคียง สามเณรที่พบว่าตัวเองในกรณีนี้ควรพิจารณาการทำงานกับการเป็นตัวแทนอาร์เรย์เช่น ['x', 'a', 'b', 'c'] หรือแม้กระทั่งสิ่งที่ตรงไปตรงมา / ง่าย / ตรงไปตรงมาถ้าเป็นไปได้: เหมือนไม่แพ้ ติดตามการอ้างอิงตัวเองในสถานที่แรก (เหมาะที่สุดถ้าเป็นเพียงฝั่งไคลเอ็นต์หรือฝั่งเซิร์ฟเวอร์เท่านั้น) ฯลฯ (รหัสเฉพาะที่มีอยู่ก่อนจะไม่เหมาะสมที่จะเพิ่ม แต่สามารถใช้ได้ถ้าข้อมูลจำเพาะต้องการเป็นอย่างอื่น การดำรงอยู่โดยไม่คำนึงถึง)

กรณีที่ 2 : การทำงานกับข้อมูลที่ต่อเนื่องกันหรือข้อมูลที่จะแสดงต่อผู้ใช้ เช่นการใช้วันที่เป็นสตริง "1999-12-30" แทนที่จะเป็นวัตถุ Date (ซึ่งอาจทำให้เกิดข้อบกพร่องของเขตเวลาหรือเพิ่มความซับซ้อนในการทำให้เป็นอนุกรมหากไม่ระวัง) หรือคุณรู้ว่าคุณกำลังทำอะไร

  • อาจจะไม่เป็นไร ระวังว่าไม่มีสตริงจุด "." ในชิ้นส่วนอินพุตที่ถูกสุขลักษณะของคุณ

หากคุณพบว่าตัวเองใช้คำตอบนี้อยู่ตลอดเวลาและแปลงไปมาระหว่างสตริงและอาร์เรย์คุณอาจอยู่ในสถานการณ์ที่ไม่ดีและควรพิจารณาทางเลือกอื่น

นี่คือหนึ่งซับที่หรูหราสั้นลง 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) นี่คือการใช้งานแบบเรียกซ้ำสองบรรทัด:

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 28 มิ.ย. เวลา 21:42 น

(sidenote: น่าเศร้าที่ไม่สามารถส่งคืนวัตถุด้วย Setter ได้เนื่องจากจะเป็นการละเมิดข้อตกลงการเรียก; commenter ดูเหมือนจะอ้างถึงฟังก์ชัน setter-style ทั่วไปที่มีผลข้างเคียงเช่นindex(obj,"a.b.etc", value)ทำobj.a.b.etc = value)

reduceสไตล์ไม่ได้จริงๆเหมาะกับที่ แต่เราสามารถปรับเปลี่ยนการดำเนินงาน recursive:

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) มากกว่าสตริง; แม้ว่าจะไม่มีอะไรผิดปกติกับฟังก์ชั่นอำนวยความสะดวก


ผู้วิจารณ์ถามว่า:

แล้วอาร์เรย์ล่ะ บางอย่างเช่น "ab [4] .cd [1] [2] [3]"? -AlexS

Javascript เป็นภาษาที่แปลกมาก ในวัตถุทั่วไปสามารถมีสตริงเป็นคีย์คุณสมบัติของพวกเขาดังนั้นตัวอย่างเช่นถ้าxเป็นวัตถุทั่วไปเช่นx={}นั้นx[1]จะกลายเป็นx["1"]... คุณอ่านถูกต้องว่า ... yup ...

Javascript Arrays (ซึ่งเป็นอินสแตนซ์ของ Object) เป็นตัวส่งเสริมคีย์จำนวนเต็มโดยเฉพาะแม้ว่าคุณสามารถทำอะไรบางอย่างx=[]; x["puppy"]=5;ได้

แต่โดยทั่วไป (และมีข้อยกเว้น) x["somestring"]===x.somestring(เมื่อได้รับอนุญาตคุณไม่สามารถทำได้x.123 )

(โปรดทราบว่าสิ่งใดที่ผู้รวบรวม JS ที่คุณใช้อาจเลือกรวบรวมคอมไพล์เหล่านี้ให้เป็นตัวแทนของ saner ถ้ามันสามารถพิสูจน์ได้ว่ามันจะไม่ละเมิด spec)

ดังนั้นคำตอบสำหรับคำถามของคุณจะขึ้นอยู่กับว่าคุณสมมติว่าวัตถุเหล่านั้นยอมรับเฉพาะจำนวนเต็ม (เนื่องจากข้อ จำกัด ในโดเมนปัญหาของคุณ) หรือไม่ สมมติว่าไม่ จากนั้นการแสดงออกที่ถูกต้องเป็นกำหนดการตัวระบุฐานบวกบาง.identifiers บวกบาง["stringindex"]s

นี่จะเทียบเท่ากับa["b"][4]["c"]["d"][1][2][3]แม้ว่าเราควรจะสนับสนุนa.b["c\"validjsstringliteral"][3]เช่นกัน คุณต้องตรวจสอบส่วนของไวยากรณ์ ecmascript บนตัวอักษรสตริงเพื่อดูวิธีการวิเคราะห์สตริงตัวอักษรที่ถูกต้อง ในทางเทคนิคคุณยังต้องการที่จะตรวจสอบ (เหมือนในคำตอบแรกของฉัน) ที่aเป็นที่ถูกต้องระบุจาวาสคริปต์

คำตอบง่ายๆสำหรับคำถามของคุณหากสตริงของคุณไม่มีเครื่องหมายจุลภาคหรือเครื่องหมายวงเล็บจะเป็นการจับคู่ความยาว 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:

Let 's go แบบเต็มรูปแบบวงกลมและจะไม่มีประสิทธิภาพมากที่สุดวิธีการแก้ปัญหาอย่างน่ากลัว-overmetaprogrammed เราสามารถขึ้นมา ... ในความสนใจของการสร้างประโยคบริสุทธิ์ hamfistery ด้วยออบเจ็กต์ ES6 Proxy! ... ลองกำหนดคุณสมบัติบางอย่างที่ (imho นั้นดีและยอดเยี่ยม แต่) อาจทำลายไลบรารี่ที่เขียนไม่ถูกต้อง คุณอาจจะระมัดระวังในการใช้สิ่งนี้หากคุณสนใจในเรื่องของการแสดงความมีสติ (ของคุณหรือของผู้อื่น) งานของคุณ ฯลฯ

// [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}}}}

(การลบล้างพร็อกซีรับ) objHyper ['abc'] คือ: 1

(ชุดการแทนที่พร็อกซี) objHyper ['abc'] = 3 ตอนนี้ obj คือ: {"a": {"b": {"c": 3, "d": 2}}}

(เบื้องหลัง) objHyper คือ: พร็อกซี {a: {…}}

(ทางลัด) obj.H ['abc'] = 4

(ทางลัด) obj.H ['abc'] is obj ['a'] ['b'] ['c'] คือ: 4

แนวคิดที่ไม่มีประสิทธิภาพ: คุณสามารถแก้ไขข้อมูลข้างต้นเพื่อจัดส่งตามอาร์กิวเมนต์อินพุตได้ ทั้งใช้.match(/[^\]\[.]+/g)วิธีการในการสนับสนุนobj['keys'].like[3]['this']หรือหากinstanceof Arrayแล้วก็ยอมรับอาร์เรย์เป็น input keys = ['a','b','c']; obj.H[keys]เช่น


ตามคำแนะนำที่คุณอาจต้องการจัดการกับดัชนีที่ไม่ได้กำหนดในรูปแบบ 'นุ่ม' NaN สไตล์ (เช่นindex({a:{b:{c:...}}}, 'a.x.c')กลับไม่ได้กำหนดมากกว่าที่ไม่ได้ระบุ TypeError) ... :

1) สิ่งนี้สมเหตุสมผลจากมุมมองของ "เราควรกลับไม่ได้กำหนดมากกว่าโยนข้อผิดพลาด" ในสถานการณ์ดัชนี 1 มิติ ({}) ['เช่น'] == ไม่ได้กำหนดดังนั้น "เราควรกลับไม่ได้กำหนดมากกว่าโยน ข้อผิดพลาด "ในสถานการณ์ N-Dim

2) สิ่งนี้ไม่สมเหตุสมผลจากมุมมองที่เราทำx['a']['x']['c']ซึ่งจะล้มเหลวด้วย TypeError ในตัวอย่างด้านบน

ที่กล่าวว่าคุณจะใช้งานได้โดยเปลี่ยนฟังก์ชั่นการลดลงของคุณด้วย:

(o,i)=>o===undefined?undefined:o[i](o,i)=>(o||{})[i]หรือ

(คุณสามารถทำให้สิ่งนี้มีประสิทธิภาพมากขึ้นโดยใช้ for for loop และ break / return เมื่อใดก็ตามที่ subresult ที่คุณต้องการจัดทำดัชนีถัดไปนั้นไม่ได้กำหนดไว้หรือใช้ try-catch ถ้าคุณคาดว่าความล้มเหลวดังกล่าวจะหายากพอสมควร)


4
reduceไม่รองรับเบราว์เซอร์ที่ใช้อยู่ในปัจจุบันทั้งหมด
Ricardo Tomasi

14
@ Ricardo: Array.reduceเป็นส่วนหนึ่งของมาตรฐาน ECMA-262 หากคุณต้องการสนับสนุนเบราว์เซอร์ที่ล้าสมัยจริงๆคุณสามารถกำหนดArray.prototype.reduceให้กับตัวอย่างการติดตั้งที่ให้ไว้ที่ไหนสักแห่ง (เช่นdeveloper.mozilla.org/en/JavaScript/Reference/Global_Objects/ … )
ninjagecko

2
ใช่ แต่มันง่ายพอที่จะทำให้ทั้งสองบรรทัดเป็นฟังก์ชั่น var setget = function( obj, path ){ function index( robj,i ) {return robj[i]}; return path.split('.').reduce( index, obj ); }
nevf

3
ฉันชอบตัวอย่างที่สง่างามนี้ขอบคุณ ninjagecko ฉันได้ขยายมันเพื่อจัดการสัญกรณ์สไตล์อาเรย์เช่นเดียวกับสตริงว่าง - ดูตัวอย่างของฉันที่นี่: jsfiddle.net/sc0ttyd/q7zyd
Sc0ttyD

2
@ninjagecko คุณจะเปลี่ยนมันให้เป็น setter ได้อย่างไร? ไม่เพียง แต่คืนค่าตามเส้นทาง แต่ยังตั้งค่าหากมีการส่งค่าใหม่เข้าสู่ฟังก์ชัน
Swader

64

หากคุณสามารถใช้lodashมีฟังก์ชั่นซึ่งทำสิ่งนั้นอย่างแน่นอน:

_.get (วัตถุเส้นทาง [ค่าเริ่มต้น])

var val = _.get(obj, "a.b");

4
หมายเหตุ: _.get(object, path)ไม่แตกหากไม่พบเส้นทาง 'a.b.etc'.split('.').reduce((o,i)=>o[i], obj)ทำ. สำหรับกรณีเฉพาะของฉัน - ไม่ใช่สำหรับทุกกรณี - สิ่งที่ฉันต้องการ ขอบคุณ!
นาย B.

1
@ Mr.B รุ่นล่าสุดของ Lodash defaultValueมีสามตัวเลือกสำหรับการโต้แย้ง _.get()วิธีการผลตอบแทนที่คุ้มค่าเริ่มต้นถ้า_.get()มีมติให้undefinedตั้งจึงสิ่งที่คุณต้องการและนาฬิกาสำหรับค่าที่คุณตั้งไว้
steampowered

7
_.set(object, path, value)สำหรับทุกคนที่สงสัยว่านอกจากนี้ยังสนับสนุน
Jeffrey Roosendaal

19

ตัวอย่างที่เกี่ยวข้องอีกเล็กน้อยกับการเรียกซ้ำ

function recompose(obj,string){
    var parts = string.split('.');
    var newObj = obj[parts[0]];
    if(parts[1]){
        parts.splice(0,1);
        var newString = parts.join('.');
        return recompose(newObj,newString);
    }
    return newObj;
}


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

alert(recompose(obj,'a.d.a.b')); //blah

19

คุณยังสามารถใช้lodash.get

คุณเพิ่งติดตั้งแพ็กเกจนี้ (npm i - บันทึก lodash.get) จากนั้นใช้งานดังนี้:

const get = require('lodash.get');

const myObj = { user: { firstName: 'Stacky', lastName: 'Overflowy' }, id: 123 };

console.log(get(myObj, 'user.firstName')); // prints Stacky
console.log(get(myObj, 'id')); //prints  123

//You can also update values
get(myObj, 'user').firstName = John;

10

หากคุณคาดว่าจะอ้างอิงเส้นทางเดียวกันหลาย ๆ ครั้งการสร้างฟังก์ชั่นสำหรับแต่ละเส้นทางสัญกรณ์ดอทจะมีประสิทธิภาพที่ดีที่สุดเท่าที่เคยมีมา

var path = 'a.b.x';
var getter = new Function("obj", "return obj." + path + ";");
getter(obj);

การใช้ตัวสร้างฟังก์ชั่นมีข้อเสียบางอย่างเช่นเดียวกับ eval () ในแง่ของความปลอดภัยและประสิทธิภาพที่แย่ที่สุด แต่ IMO เป็นเครื่องมือที่ใช้งานไม่ดีสำหรับกรณีที่คุณต้องการการผสมผสานของพลังและประสิทธิภาพที่สูง ฉันใช้วิธีการนี้เพื่อสร้างฟังก์ชั่นการกรองอาเรย์และเรียกพวกมันภายในวงย่อยของ AngularJS โปรไฟล์ของฉันแสดงขั้นตอน array.filter () ที่ใช้เวลาน้อยกว่า 1 มิลลิวินาทีในการตรวจสอบและกรองวัตถุที่ซับซ้อนประมาณ 2,000 รายการโดยใช้เส้นทางที่กำหนดแบบไดนามิก 3-4 ระดับลึก

วิธีการที่คล้ายกันสามารถใช้ในการสร้างฟังก์ชั่น setter ได้แน่นอน:

var setter = new Function("obj", "newval", "obj." + path + " = newval;");
setter(obj, "some new val");

1
หากคุณต้องการแยกเส้นทางเดียวกันออกเป็นระยะเวลานานลิงก์ jsperf.com ด้านบนจะแสดงตัวอย่างวิธีบันทึกและค้นหาฟังก์ชั่นในภายหลัง การเรียกใช้ตัวสร้างฟังก์ชั่นค่อนข้างช้ารหัสที่สมบูรณ์แบบสูงควรบันทึกผลลัพธ์เพื่อหลีกเลี่ยงการทำซ้ำหากเป็นไปได้
Kevin Crumley

9

หลายปีตั้งแต่โพสต์ต้นฉบับ ขณะนี้มีห้องสมุดที่ยอดเยี่ยมที่เรียกว่า 'object-path' https://github.com/mariocasciaro/object-path

มีอยู่ใน NPM และ BOWER https://www.npmjs.com/package/object-path

ง่ายเหมือน:

objectPath.get(obj, "a.c.1");  //returns "f"
objectPath.set(obj, "a.j.0.f", "m");

และทำงานสำหรับคุณสมบัติและอาร์เรย์ที่ซ้อนกันอย่างล้ำลึก


7

ฉันแนะนำให้แบ่งเส้นทางและวนซ้ำและลดวัตถุที่คุณมี ข้อเสนอนี้ใช้ได้กับค่าเริ่มต้นสำหรับคุณสมบัติที่ขาดหายไป

const getValue = (object, keys) => keys.split('.').reduce((o, k) => (o || {})[k], object);

console.log(getValue({ a: { b: '1', c: '2' } }, 'a.b'));
console.log(getValue({ a: { b: '1', c: '2' } }, 'foo.bar.baz'));


6

หมายเหตุหากคุณใช้Lodashอยู่แล้วคุณสามารถใช้propertyหรือgetฟังก์ชั่น

var obj = { a: { b: '1', c: '2' } };
_.property('a.b')(obj); // => 1
_.get(obj, 'a.b'); // => 1

ขีดล่างมีpropertyฟังก์ชั่น แต่ไม่รองรับเครื่องหมายจุด


1
ดูเหมือนว่า Underscore ไม่สนับสนุนเครื่องหมายจุด อัปเดตคำตอบแล้ว
Tamlyn

5

ข้อเสนออื่น ๆ เป็นความลับเล็ก ๆ น้อย ๆ ดังนั้นฉันคิดว่าฉันมีส่วนร่วม:

Object.prop = function(obj, prop, val){
    var props = prop.split('.')
      , final = props.pop(), p 
    while(p = props.shift()){
        if (typeof obj[p] === 'undefined')
            return undefined;
        obj = obj[p]
    }
    return val ? (obj[final] = val) : obj[final]
}

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

// get
console.log(Object.prop(obj, 'a.c')) // -> 2
// set
Object.prop(obj, 'a.c', function(){})
console.log(obj) // -> { a: { b: '1', c: [Function] } }

5

คุณสามารถใช้ไลบรารีที่มีอยู่ในเวลา npm ซึ่งจะทำให้กระบวนการนี้ง่ายขึ้น https://www.npmjs.com/package/dot-object

 var dot = require('dot-object');

var obj = {
 some: {
   nested: {
     value: 'Hi there!'
   }
 }
};

var val = dot.pick('some.nested.value', obj);
console.log(val);

// Result: Hi there!

1
ขอขอบคุณที่แจ้งให้เราทราบ ดูมีประโยชน์มาก
nevf

และนี่คือเหตุผลที่ทุกโครงการฉีด 10,000 พึ่งพา :-)
Marvin

4
var a = { b: { c: 9 } };

function value(layer, path, value) {
    var i = 0,
        path = path.split('.');

    for (; i < path.length; i++)
        if (value != null && i + 1 === path.length)
            layer[path[i]] = value;
        layer = layer[path[i]];

    return layer;
};

value(a, 'b.c'); // 9

value(a, 'b.c', 4);

value(a, 'b.c'); // 4

นี่เป็นรหัสจำนวนมากเมื่อเทียบกับevalวิธีที่ง่ายกว่าในการทำเช่นนั้น แต่อย่างที่ Simon Willison พูดว่าคุณไม่ควรใช้ evalEVAL

นอกจากนี้JSFiddle


เยี่ยมมากมันใช้งานได้ดีและสั้นกว่า CD Sanchez ขอบคุณ
nevf

ค่า (a, 'bbbbb') ไม่ส่งคืนค่าที่ไม่ได้กำหนด
frumbert

4

ฉันได้ขยายคำตอบที่สวยงามโดย ninjagecko เพื่อให้ฟังก์ชั่นการจัดการทั้งการอ้างอิงสไตล์และ / หรืออาเรย์สไตล์อาร์เรย์และเพื่อให้สตริงที่ว่างเปล่าทำให้วัตถุแม่จะกลับมา

ไปเลย:

string_to_ref = function (object, reference) {
    function arr_deref(o, ref, i) { return !ref ? o : (o[ref.slice(0, i ? -1 : ref.length)]) }
    function dot_deref(o, ref) { return ref.split('[').reduce(arr_deref, o); }
    return !reference ? object : reference.split('.').reduce(dot_deref, object);
};

ดูตัวอย่าง jsFiddle ที่ทำงานของฉันได้ที่นี่: http://jsfiddle.net/sc0ttyd/q7zyd/


ทางออกที่ดีจริงๆ มีเพียงปัญหาเดียวเท่านั้นมันถือว่า[]เป็นสัญกรณ์เสมอสำหรับอาร์เรย์ อาจมีวัตถุคีย์ที่แสดงเช่นนั้นเช่นobj['some-problem/name'].list[1]ในการแก้ไขปัญหานี้ฉันต้องอัปเดตarr_derefฟังก์ชั่นเช่นนี้ javascript function arr_deref(o, ref, i) { return !ref ? o : (o[(ref.slice(0, i ? -1 : ref.length)).replace(/^['"]|['"]$/g, '')]); }
Serkan Yersen

1
แม้ว่าทุกวันนี้ฉันจะไม่ทำเช่นนี้ ฉันจะใช้ Lodash: lodash.com/docs#get
Sc0ttyD

2
var find = function(root, path) {
  var segments = path.split('.'),
      cursor = root,
      target;

  for (var i = 0; i < segments.length; ++i) {
   target = cursor[segments[i]];
   if (typeof target == "undefined") return void 0;
   cursor = target;
  }

  return cursor;
};

var obj = { a: { b: '1', c: '2' } }
find(obj, "a.b"); // 1

var set = function (root, path, value) {
   var segments = path.split('.'),
       cursor = root,
       target;

   for (var i = 0; i < segments.length - 1; ++i) {
      cursor = cursor[segments[i]] || { };
   }

   cursor[segments[segments.length - 1]] = value;
};

set(obj, "a.k", function () { console.log("hello world"); });

find(obj, "a.k")(); // hello world

ขอบคุณสำหรับทุกคำตอบที่รวดเร็ว ไม่ชอบโซลูชัน eval () โพสต์นี้และโพสต์ที่คล้ายกันนั้นดูดีที่สุด อย่างไรก็ตามฉันยังคงมีปัญหา ฉันพยายามตั้งค่า obj.ab = ค่าใหม่ เพื่อความแม่นยำของค่า b คือฟังก์ชั่นดังนั้นฉันต้องใช้ obj.ab (new_value) มีการเรียกใช้ฟังก์ชัน แต่ไม่ได้ตั้งค่า ฉันคิดว่ามันเป็นปัญหาขอบเขต แต่ฉันยังคงขุด ฉันรู้ว่าสิ่งนี้อยู่นอกขอบเขตของคำถามเดิม รหัสของฉันใช้ Knockout.js และ b เป็น ko.observable
nevf

@nevf: ฉันได้เพิ่มฟังก์ชั่นที่สองที่ฉันคิดว่าทำในสิ่งที่คุณต้องการ คุณสามารถปรับแต่งมันตามความชอบของคุณขึ้นอยู่กับพฤติกรรมที่คุณต้องการ (เช่นควรสร้างวัตถุหากไม่มีอยู่หรือไม่ ฯลฯ )
Cristian Sanchez

@nevf แต่ฉันทำมันด้วยฟังก์ชั่นเดียว ; D
McKayla

ขอบคุณสำหรับการอัปเดตที่ฉันสามารถใช้งานได้ @tylermwashburn - และขอขอบคุณสำหรับการใช้งานที่สั้นลงซึ่งทำงานได้ดี มี w / e ที่ยอดเยี่ยมทั้งหมด
nevf

2

คุณสามารถรับค่าของสมาชิกวัตถุด้วยเครื่องหมายจุดด้วยรหัสบรรทัดเดียว:

new Function('_', 'return _.' + path)(obj);

ในกรณีที่คุณ:

var obj = { a: { b: '1', c: '2' } }
var val = new Function('_', 'return _.a.b')(obj);

เพื่อให้ง่ายคุณสามารถเขียนฟังก์ชันดังนี้:

function objGet(obj, path){
    return new Function('_', 'return _.' + path)(obj);
}

คำอธิบาย:

ตัวสร้างฟังก์ชั่นสร้างวัตถุฟังก์ชั่นใหม่ ใน JavaScript ทุกฟังก์ชั่นเป็นวัตถุของจริง ไวยากรณ์ในการสร้างฟังก์ชั่นอย่างชัดเจนด้วยตัวสร้างฟังก์ชั่นคือ:

new Function ([arg1[, arg2[, ...argN]],] functionBody)

โดยarguments(arg1 to argN)ต้องเป็นสตริงที่สอดคล้องกับตัวระบุ javaScript ที่ถูกต้องและfunctionBodyเป็นสตริงที่มีคำสั่ง javaScript ซึ่งประกอบไปด้วยฟังก์ชั่นการกำหนด

ในกรณีของเราเราใช้ประโยชน์จากฟังก์ชั่นของสตริงเพื่อดึงสมาชิกวัตถุด้วยเครื่องหมายจุด

หวังว่ามันจะช่วย


คุณช่วยอธิบายได้อย่างแม่นยำว่าสิ่งนี้กำลังทำอะไรอยู่? และคุณจะส่ง 'ab' เป็นต้นในฐานะพารามิเตอร์ฟังก์ชันได้อย่างไร
nevf

1
ผมให้นี้ +1 แต่ JSLint เตือนว่า"ฟังก์ชั่นคอนสตรัคเป็นรูปแบบของ EVAL เป็น"
gabe

2

คำตอบGET / SETที่ใช้งานได้กับเนทีฟแบบโต้ตอบ (คุณไม่สามารถกำหนดให้กับObject.prototypeปัจจุบันได้):

Object.defineProperty(Object.prototype, 'getNestedProp', {
    value: function(desc) {
        var obj = this;
        var arr = desc.split(".");
        while(arr.length && (obj = obj[arr.shift()]));
        return obj;
    },
    enumerable: false
});

Object.defineProperty(Object.prototype, 'setNestedProp', {
    value: function(desc, value) {
        var obj = this;
        var arr = desc.split(".");
        var last = arr.pop();
        while(arr.length && (obj = obj[arr.shift()]));
        obj[last] = value;
    },
    enumerable: false
});

การใช้งาน:

var a = { values: [{ value: null }] };
var b = { one: { two: 'foo' } };

a.setNestedProp('values.0.value', b.getNestedProp('one.two'));
console.log(a.values[0].value); // foo

1

ฉันคัดลอกสิ่งต่อไปนี้จากคำตอบของ Ricardo Tomasiและแก้ไขเพื่อสร้างวัตถุย่อยที่ยังไม่มีอยู่ตามความจำเป็น มันมีประสิทธิภาพน้อยกว่าเล็กน้อย (มากกว่าifและการสร้างของวัตถุที่ว่างเปล่า) แต่ควรจะค่อนข้างดี

นอกจากนี้ยังช่วยให้เราสามารถทำสิ่งObject.prop(obj, 'a.b', false)ที่เราไม่เคยทำมาก่อน น่าเสียดายที่มันยังไม่ยอมให้เรากำหนดundefined... ยังไม่แน่ใจว่าจะทำยังไงดี

/**
 * Object.prop()
 *
 * Allows dot-notation access to object properties for both getting and setting.
 *
 * @param {Object} obj    The object we're getting from or setting
 * @param {string} prop   The dot-notated string defining the property location
 * @param {mixed}  val    For setting only; the value to set
 */
 Object.prop = function(obj, prop, val){
   var props = prop.split('.'),
       final = props.pop(),
       p;

   for (var i = 0; i < props.length; i++) {
     p = props[i];
     if (typeof obj[p] === 'undefined') {
       // If we're setting
       if (typeof val !== 'undefined') {
         // If we're not at the end of the props, keep adding new empty objects
         if (i != props.length)
           obj[p] = {};
       }
       else
         return undefined;
     }
     obj = obj[p]
   }
   return typeof val !== "undefined" ? (obj[final] = val) : obj[final]
 }

1

หากคุณต้องการแปลงวัตถุใด ๆ ที่มีจุดคีย์โน้ตเป็นรุ่น Arrayedของคีย์ที่คุณสามารถใช้วิธีนี้


สิ่งนี้จะแปลงสิ่งที่ชอบ

{
  name: 'Andy',
  brothers.0: 'Bob'
  brothers.1: 'Steve'
  brothers.2: 'Jack'
  sisters.0: 'Sally'
}

ถึง

{
  name: 'Andy',
  brothers: ['Bob', 'Steve', 'Jack']
  sisters: ['Sally']
}

convertDotNotationToArray(objectWithDotNotation) {

    Object.entries(objectWithDotNotation).forEach(([key, val]) => {

      // Is the key of dot notation 
      if (key.includes('.')) {
        const [name, index] = key.split('.');

        // If you have not created an array version, create one 
        if (!objectWithDotNotation[name]) {
          objectWithDotNotation[name] = new Array();
        }

        // Save the value in the newly created array at the specific index 
        objectWithDotNotation[name][index] = val;
        // Delete the current dot notation key val
        delete objectWithDotNotation[key];
      }
    });

}

ไม่ประมวลผลค่าด้วย "brothers.0.one" เช่น JSON
Karthick

คุณมีข้อเสนอแนะใด ๆ สำหรับการประมวลผล JSON ที่ซับซ้อนยิ่งขึ้นด้วยการรวบรวมอาเรย์
Karthick

0

evalนี่คือรหัสของฉันโดยไม่ต้องใช้ มันเข้าใจง่ายเกินไป

function value(obj, props) {
  if (!props) return obj;
  var propsArr = props.split('.');
  var prop = propsArr.splice(0, 1);
  return value(obj[prop], propsArr.join('.'));
}

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

console.log(value(obj, 'a.d.a.b')); //returns blah

0

ใช่มันถูกถามเมื่อ 4 ปีก่อนและใช่การขยายต้นแบบพื้นฐานไม่ใช่ความคิดที่ดี แต่ถ้าคุณเก็บส่วนขยายทั้งหมดไว้ในที่เดียวมันอาจจะมีประโยชน์
ดังนั้นนี่คือวิธีของฉันในการทำเช่นนี้

   Object.defineProperty(Object.prototype, "getNestedProperty", {
    value     : function (propertyName) {
        var result = this;
        var arr = propertyName.split(".");

        while (arr.length && result) {
            result = result[arr.shift()];
        }

        return result;
    },
    enumerable: false
});

ตอนนี้คุณจะสามารถรับทรัพย์สินซ้อนกันได้ทุกที่โดยไม่ต้องนำเข้าโมดูลที่มีฟังก์ชั่นหรือฟังก์ชั่นคัดลอก / วาง

UPD.Example:

{a:{b:11}}.getNestedProperty('a.b'); //returns 11

UPD 2. ส่วนขยายต่อไปพังทลายพังพอนในโครงการของฉัน นอกจากนี้ฉันได้อ่านว่ามันอาจทำลาย jquery ดังนั้นอย่าทำด้วยวิธีต่อไป

 Object.prototype.getNestedProperty = function (propertyName) {
    var result = this;
    var arr = propertyName.split(".");

    while (arr.length && result) {
        result = result[arr.shift()];
    }

    return result;
};

0

นี่คือการดำเนินการของฉัน

การใช้งาน 1

Object.prototype.access = function() {
    var ele = this[arguments[0]];
    if(arguments.length === 1) return ele;
    return ele.access.apply(ele, [].slice.call(arguments, 1));
}

การใช้งาน 2 (ใช้การลดอาร์เรย์แทนการแบ่ง)

Object.prototype.access = function() {
    var self = this;
    return [].reduce.call(arguments,function(prev,cur) {
        return prev[cur];
    }, self);
}

ตัวอย่าง:

var myobj = {'a':{'b':{'c':{'d':'abcd','e':[11,22,33]}}}};

myobj.access('a','b','c'); // returns: {'d':'abcd', e:[0,1,2,3]}
myobj.a.b.access('c','d'); // returns: 'abcd'
myobj.access('a','b','c','e',0); // returns: 11

นอกจากนี้ยังสามารถจัดการวัตถุภายในอาร์เรย์ได้ด้วย

var myobj2 = {'a': {'b':[{'c':'ab0c'},{'d':'ab1d'}]}}
myobj2.access('a','b','1','d'); // returns: 'ab1d'

0

นี่คือโซลูชันเพิ่มเติมของฉันที่เสนอโดย: ninjagecko

สำหรับฉันสัญกรณ์สตริงที่เรียบง่ายไม่เพียงพอดังนั้นรุ่นด้านล่างจึงสนับสนุนสิ่งต่าง ๆ เช่น:

ดัชนี (obj, 'data.accounts [0] .address [0] .postcode');

/**
 * Get object by index
 * @supported
 * - arrays supported
 * - array indexes supported
 * @not-supported
 * - multiple arrays
 * @issues:
 *  index(myAccount, 'accounts[0].address[0].id') - works fine
 *  index(myAccount, 'accounts[].address[0].id') - doesnt work
 * @Example:
 * index(obj, 'data.accounts[].id') => returns array of id's
 * index(obj, 'data.accounts[0].id') => returns id of 0 element from array
 * index(obj, 'data.accounts[0].addresses.list[0].id') => error
 * @param obj
 * @param path
 * @returns {any}
 */
var index = function(obj, path, isArray?, arrIndex?){

    // is an array
    if(typeof isArray === 'undefined') isArray = false;
    // array index,
    // if null, will take all indexes
    if(typeof arrIndex === 'undefined') arrIndex = null;

    var _arrIndex = null;

    var reduceArrayTag = function(i, subArrIndex){
        return i.replace(/(\[)([\d]{0,})(\])/, (i) => {
            var tmp = i.match(/(\[)([\d]{0,})(\])/);
            isArray = true;
            if(subArrIndex){
                _arrIndex =  (tmp[2] !== '') ? tmp[2] : null;
            }else{
                arrIndex =  (tmp[2] !== '') ? tmp[2] : null;
            }
            return '';
        });
    }

    function byIndex(obj, i) {
        // if is an array
        if(isArray){
            isArray = false;
            i = reduceArrayTag(i, true);
            // if array index is null,
            // return an array of with values from every index
            if(!arrIndex){
                var arrValues = [];
                _.forEach(obj, (el) => {
                    arrValues.push(index(el, i, isArray, arrIndex));
                })
                return arrValues;
            }
            // if array index is specified
            var value = obj[arrIndex][i];
            if(isArray){
                arrIndex = _arrIndex;
            }else{
                arrIndex = null;
            }
            return value;
        }else{
            // remove [] from notation,
            // if [] has been removed, check the index of array
            i = reduceArrayTag(i, false);
            return obj[i]
        }
    }

    // reduce with byIndex method
    return path.split('.').reduce(byIndex, obj)
}

0

ในความเสี่ยงของการตีม้าตาย ... ฉันคิดว่าสิ่งนี้มีประโยชน์มากที่สุดในการสำรวจวัตถุที่ซ้อนกันเพื่ออ้างอิงตำแหน่งที่คุณอยู่ด้วยความเคารพต่อวัตถุฐานหรือวัตถุที่คล้ายกันที่มีโครงสร้างเดียวกัน ด้วยเหตุนี้จึงมีประโยชน์เมื่อใช้ฟังก์ชันการเคลื่อนที่ของวัตถุที่ซ้อนกัน โปรดทราบว่าฉันใช้อาร์เรย์เพื่อเก็บเส้นทาง มันจะไม่สำคัญที่จะแก้ไขสิ่งนี้เพื่อใช้ทั้งเส้นทางของสตริงหรืออาเรย์ นอกจากนี้โปรดทราบว่าคุณสามารถกำหนด "undefined" ให้กับค่าซึ่งแตกต่างจากการใช้งานอื่น ๆ

/*
 * Traverse each key in a nested object and call fn(curObject, key, value, baseObject, path)
 * on each. The path is an array of the keys required to get to curObject from
 * baseObject using objectPath(). If the call to fn() returns falsey, objects below
 * curObject are not traversed. Should be called as objectTaverse(baseObject, fn).
 * The third and fourth arguments are only used by recursion.
 */
function objectTraverse (o, fn, base, path) {
    path = path || [];
    base = base || o;
    Object.keys(o).forEach(function (key) {
        if (fn(o, key, o[key], base, path) && jQuery.isPlainObject(o[key])) {
            path.push(key);
            objectTraverse(o[key], fn, base, path);
            path.pop();
        }
    });
}

/*
 * Get/set a nested key in an object. Path is an array of the keys to reference each level
 * of nesting. If value is provided, the nested key is set.
 * The value of the nested key is returned.
 */
function objectPath (o, path, value) {
    var last = path.pop();

    while (path.length && o) {
        o = o[path.shift()];
    }
    if (arguments.length < 3) {
        return (o? o[last] : o);
    }
    return (o[last] = value);
}



0

ไม่กี่ปีต่อมาฉันพบสิ่งนี้ที่จัดการขอบเขตและอาร์เรย์ เช่นa['b']["c"].d.etc

function getScopedObj(scope, str) {
  let obj=scope, arr;

  try {
    arr = str.split(/[\[\]\.]/) // split by [,],.
      .filter(el => el)             // filter out empty one
      .map(el => el.replace(/^['"]+|['"]+$/g, '')); // remove string quotation
    arr.forEach(el => obj = obj[el])
  } catch(e) {
    obj = undefined;
  }

  return obj;
}

window.a = {b: {c: {d: {etc: 'success'}}}}

getScopedObj(window, `a.b.c.d.etc`)             // success
getScopedObj(window, `a['b']["c"].d.etc`)       // success
getScopedObj(window, `a['INVALID']["c"].d.etc`) // undefined

-1

ยังไม่ชัดเจนว่าคำถามของคุณคืออะไร ให้วัตถุของคุณobj.a.bจะให้ "2" ตามที่เป็นอยู่ หากคุณต้องการจัดการสตริงเพื่อใช้วงเล็บคุณสามารถทำได้ดังนี้

var s = 'a.b';
s = 'obj["' + s.replace(/\./g, '"]["') + '"]';
alert(s); // displays obj["a"]["b"]

1
สิ่งนี้ใช้ไม่ได้a.b.cและไม่บรรลุสิ่งที่พวกเขาต้องการจริงๆ .. พวกเขาต้องการคุณค่าไม่ใช่evalเส้นทาง
McKayla

ตอนนี้ผมคงมันจึงทำงานร่วมกับa.b.cแต่คุณมีสิทธิที่เห็นได้ชัดว่าเขาต้องการที่จะได้รับ / obj.a.bชุดมูลค่าของทรัพย์สินที่ คำถามที่ได้รับการทำให้เกิดความสับสนกับผมเพราะเขาบอกว่าเขาอยากจะ "แปลงสตริง" ....
มาร์ค Eirich

ทำได้ดีมาก :) มันคลุมเครือเล็กน้อย คุณทำได้ดีมากในการแปลง
McKayla

-1

นี่คือ 10 เซ็นต์ของฉันกับกรณีฟังก์ชั่นการร้องจะได้รับ / ตั้งอยู่บนเส้นทางที่ให้ .. แน่ใจว่าคุณสามารถปรับปรุงได้ลบ || และแทนที่ด้วยObject.hasOwnPropertyหากคุณใส่ใจกับค่าเท็จโดยไม่ตั้งใจ

ฉันทดสอบด้วย a.b.cและ ab2.c{a:{b:[0,1,{c:7}]}}และการทำงานสำหรับทั้งการตั้งค่าและรับ :)

cheerz

function helper(obj, path, setValue){
  const l = String(path).split('.');
  return l.reduce((o,i, idx)=>{
   if( l.length-idx===1)  { o[i] = setValue || o[i];return setValue ? obj : o[i];}
  o[i] = o[i] || {};
   return o[i];
  }, x)
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.