Đặt động thuộc tính của đối tượng lồng nhau


84

Tôi có một đối tượng có thể là bất kỳ số tầng sâu nào và có thể có bất kỳ thuộc tính nào hiện có. Ví dụ:

var obj = {
    db: {
        mongodb: {
            host: 'localhost'
        }
    }
};

Trên đó tôi muốn đặt (hoặc ghi đè) các thuộc tính như vậy:

set('db.mongodb.user', 'root');
// or:
set('foo.bar', 'baz');

Trong đó chuỗi thuộc tính có thể có bất kỳ độ sâu nào và giá trị có thể là bất kỳ loại / thứ nào.
Các đối tượng và mảng dưới dạng giá trị không cần phải được hợp nhất, nếu khóa thuộc tính đã tồn tại.

Ví dụ trước sẽ tạo ra đối tượng sau:

var obj = {
    db: {
        mongodb: {
            host: 'localhost',
            user: 'root'
        }
    },
    foo: {
        bar: baz
    }
};

Làm thế nào tôi có thể nhận ra một chức năng như vậy?


Kết quả phải là gì set('foo', 'bar'); set('foo.baz', 'qux');, nơi foođầu tiên giữ a Stringsau đó trở thành an Object? Điều gì xảy ra với 'bar'?
Jonathan Lonowski 21/09/13


có thể nhờ trợ giúp này: stackoverflow.com/questions/695050/…
Rene M.

1
Nếu bạn loại bỏ set()phương thức và chỉ cần obj.db.mongodb.user = 'root';bạn có chính xác những gì bạn dường như muốn?
adeneo 21/09/13

@JonathanLonowski Lonowski barbị ghi đè bởi Object. @adeneo và @rmertins Thật vậy :) Nhưng tiếc là tôi phải quấn một số logic khác xung quanh. @Robert Levy tôi thấy một điều đó và đã làm việc truy cập, nhưng đặt nó dường như nhiều hơn nữa phức tạp ...
John B.

Câu trả lời:


92

Hàm này, sử dụng các đối số bạn đã chỉ định, sẽ thêm / cập nhật dữ liệu trong vùng objchứa. Lưu ý rằng bạn cần theo dõi xem phần tử nào trong objlược đồ là vùng chứa và đâu là giá trị (chuỗi, số nguyên, v.v.) nếu không bạn sẽ bắt đầu ném các ngoại lệ.

obj = {};  // global object

function set(path, value) {
    var schema = obj;  // a moving reference to internal objects within obj
    var pList = path.split('.');
    var len = pList.length;
    for(var i = 0; i < len-1; i++) {
        var elem = pList[i];
        if( !schema[elem] ) schema[elem] = {}
        schema = schema[elem];
    }

    schema[pList[len-1]] = value;
}

set('mongo.db.user', 'root');

2
@ bpmason1 bạn có thể giải thích tại sao bạn sử dụng var schema = objthay vì chỉ objở mọi nơi không?
sman591

3
@ sman591 schemalà một con trỏ được di chuyển xuống đường dẫn schema = schema[elem]. Vì vậy, sau vòng lặp for, hãy schema[pList[len - 1]]trỏ đến mongo.db.user trong obj.
webjay

đã giải quyết được vấn đề của tôi, cảm ơn, không thể tìm thấy điều này trong tài liệu MDN. Nhưng tôi có một nghi ngờ khác, nếu toán tử gán cung cấp một tham chiếu đến các đối tượng bên trong thì làm thế nào để tạo một đối tượng2 riêng biệt với đối tượng1 để những thay đổi được thực hiện trên đối tượng2 sẽ không phản ánh trên đối tượng1.
Onix

@Onix Bạn có thể sử dụng cloneDeepchức năng lodash cho việc này.
Aakash Thakur

@Onix const clone = JSON.parse (JSON.stringify (obj))
Mike Makuch

72

Lodash có phương thức _.set () .

_.set(obj, 'db.mongodb.user', 'root');
_.set(obj, 'foo.bar', 'baz');

1
Nó có thể được sử dụng để đặt giá trị cho khóa không? nếu có, bạn có thể chia sẻ một ví dụ. Cảm ơn bạn
poudel hiền

Điều này thật tuyệt, nhưng bạn sẽ theo dõi / xác định đường đi như thế nào?
Tom

19

Hơi muộn nhưng đây là một câu trả lời không phải thư viện, đơn giản hơn:

/**
 * Dynamically sets a deeply nested value in an object.
 * Optionally "bores" a path to it if its undefined.
 * @function
 * @param {!object} obj  - The object which contains the value you want to change/set.
 * @param {!array} path  - The array representation of path to the value you want to change/set.
 * @param {!mixed} value - The value you want to set it to.
 * @param {boolean} setrecursively - If true, will set value of non-existing path as well.
 */
function setDeep(obj, path, value, setrecursively = false) {
    path.reduce((a, b, level) => {
        if (setrecursively && typeof a[b] === "undefined" && level !== path.length){
            a[b] = {};
            return a[b];
        }

        if (level === path.length){
            a[b] = value;
            return value;
        } 
        return a[b];
    }, obj);
}

Chức năng này tôi đã thực hiện có thể làm chính xác những gì bạn cần và hơn thế nữa.

giả sử chúng tôi muốn thay đổi giá trị đích được lồng sâu trong đối tượng này:

let myObj = {
    level1: {
        level2: {
           target: 1
       }
    }
}

Vì vậy, chúng tôi sẽ gọi hàm của chúng tôi như vậy:

setDeep(myObj, ["level1", "level2", "target1"], 3);

sẽ cho kết quả:

myObj = {level1: {level2: {target: 3}}}

Việc đặt cờ đệ quy thành true sẽ thiết lập các đối tượng nếu chúng không tồn tại.

setDeep(myObj, ["new", "path", "target"], 3, true);

sẽ dẫn đến điều này:

obj = myObj = {
    new: {
         path: {
             target: 3
         }
    },
    level1: {
        level2: {
           target: 3
       }
    }
}

1
Đã sử dụng mã này, sạch sẽ và đơn giản. Thay vì tính toán, leveltôi đã sử dụng reduceđối số thứ ba.
Juan Lanus

Tôi tin rằng điều đó levelcần phải là +1 hoặc path.length-1
ThomasReggi.

12

Chúng ta có thể sử dụng một hàm đệ quy:

/**
 * Sets a value of nested key string descriptor inside a Object.
 * It changes the passed object.
 * Ex:
 *    let obj = {a: {b:{c:'initial'}}}
 *    setNestedKey(obj, ['a', 'b', 'c'], 'changed-value')
 *    assert(obj === {a: {b:{c:'changed-value'}}})
 *
 * @param {[Object]} obj   Object to set the nested key
 * @param {[Array]} path  An array to describe the path(Ex: ['a', 'b', 'c'])
 * @param {[Object]} value Any value
 */
export const setNestedKey = (obj, path, value) => {
  if (path.length === 1) {
    obj[path] = value
    return
  }
  return setNestedKey(obj[path[0]], path.slice(1), value)
}

Nó đơn giản hơn!


3
có vẻ tốt! chỉ cần kiểm tra tham số obj để đảm bảo rằng nó không phải là falsey, sẽ tạo ra lỗi nếu bất kỳ đạo cụ nào trong chuỗi không tồn tại.
C Smith

2
bạn chỉ có thể sử dụng path.slice (1);
Marcos Pereira

1
Câu trả lời xuất sắc, một giải pháp hay và ngắn gọn.
chim

Tôi tin vào câu lệnh if đó, lẽ ra nó phải có obj[path[0]] = value;pathnó luôn thuộc loại string[], ngay cả khi chỉ còn 1 chuỗi.
Valorad

Các đối tượng Javascript sẽ hoạt động bằng cách sử dụng obj[['a']] = 'new value'. Kiểm tra mã: jsfiddle.net/upsdne03
Hemã Vidal

11

Tôi chỉ viết một hàm nhỏ bằng cách sử dụng đệ quy ES6 + để đạt được mục tiêu.

updateObjProp = (obj, value, propPath) => {
    const [head, ...rest] = propPath.split('.');

    !rest.length
        ? obj[head] = value
        : this.updateObjProp(obj[head], value, rest.join('.'));
}

const user = {profile: {name: 'foo'}};
updateObjProp(user, 'fooChanged', 'profile.name');

Tôi đã sử dụng nó rất nhiều khi phản ứng với trạng thái cập nhật, nó hoạt động khá tốt đối với tôi.


1
điều này rất tiện lợi, tôi phải đặt một toString () trên proPath để làm cho nó hoạt động với các thuộc tính lồng nhau, nhưng sau đó nó hoạt động tốt. const [head, ... rest] = propPath.toString (). split ('.');
WizardsOfWor

1
@ user738048 @ Bruno-Joaquim dòng this.updateStateProp(obj[head], value, rest);phải làthis.updateStateProp(obj[head], value, rest.join());
ma.mehralian

8

ES6 có một cách khá hay để làm điều này bằng cách sử dụng Tên thuộc tính tính toánTham số phần còn lại .

const obj = {
  levelOne: {
    levelTwo: {
      levelThree: "Set this one!"
    }
  }
}

const updatedObj = {
  ...obj,
  levelOne: {
    ...obj.levelOne,
    levelTwo: {
      ...obj.levelOne.levelTwo,
      levelThree: "I am now updated!"
    }
  }
}

Nếu levelThreelà một thuộc tính động, tức là để đặt bất kỳ thuộc tính nào trong đó levelTwo, bạn có thể sử dụng [propertyName]: "I am now updated!"nơi propertyNamechứa tên của thuộc tính đó levelTwo.


7

Lodash có một phương pháp gọi là cập nhật thực hiện chính xác những gì bạn cần.

Phương thức này nhận các tham số sau:

  1. Đối tượng cần cập nhật
  2. Đường dẫn của thuộc tính cần cập nhật (thuộc tính có thể được lồng sâu)
  3. Một hàm trả về giá trị để cập nhật (được cung cấp giá trị ban đầu làm tham số)

Trong ví dụ của bạn, nó sẽ như thế này:

_.update(obj, 'db.mongodb.user', function(originalValue) {
  return 'root'
})

7

Lấy cảm hứng từ câu trả lời của @ bpmason1:

function leaf(obj, path, value) {
  const pList = path.split('.');
  const key = pList.pop();
  const pointer = pList.reduce((accumulator, currentValue) => {
    if (accumulator[currentValue] === undefined) accumulator[currentValue] = {};
    return accumulator[currentValue];
  }, obj);
  pointer[key] = value;
  return obj;
}

Thí dụ:

const obj = {
  boats: {
    m1: 'lady blue'
  }
};
leaf(obj, 'boats.m1', 'lady blue II');
leaf(obj, 'boats.m2', 'lady bird');
console.log(obj); // { boats: { m1: 'lady blue II', m2: 'lady bird' } }

2

Tôi đã tạo ý chính để đặt và nhận các giá trị obj theo chuỗi dựa trên câu trả lời đúng. Bạn có thể tải xuống hoặc sử dụng dưới dạng gói npm / sợi.

// yarn add gist:5ceba1081bbf0162b98860b34a511a92
// npm install gist:5ceba1081bbf0162b98860b34a511a92
export const DeepObject = {
  set: setDeep,
  get: getDeep
};

// https://stackoverflow.com/a/6491621
function getDeep(obj: Object, path: string) {
  path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
  path = path.replace(/^\./, '');           // strip a leading dot
  const a = path.split('.');
  for (let i = 0, l = a.length; i < l; ++i) {
    const n = a[i];
    if (n in obj) {
      obj = obj[n];
    } else {
      return;
    }
  }

  return obj;
}

// https://stackoverflow.com/a/18937118
function setDeep(obj: Object, path: string, value: any) {
  let schema = obj;  // a moving reference to internal objects within obj
  const pList = path.split('.');
  const len = pList.length;
  for (let i = 0; i < len - 1; i++) {
    const elem = pList[i];
    if (!schema[elem]) {
      schema[elem] = {};
    }
    schema = schema[elem];
  }

  schema[pList[len - 1]] = value;
}

// Usage
// import {DeepObject} from 'somePath'
//
// const obj = {
//   a: 4,
//   b: {
//     c: {
//       d: 2
//     }
//   }
// };
//
// DeepObject.set(obj, 'b.c.d', 10); // sets obj.b.c.d to 10
// console.log(DeepObject.get(obj, 'b.c.d')); // returns 10

1

Nếu bạn chỉ cần thay đổi các đối tượng lồng nhau sâu hơn, thì một phương thức khác có thể là tham chiếu đối tượng. Vì các đối tượng JS được xử lý bởi các tham chiếu của chúng, bạn có thể tạo một tham chiếu đến một đối tượng mà bạn có quyền truy cập khóa chuỗi.

Thí dụ:

// The object we want to modify:
var obj = {
    db: {
        mongodb: {
            host: 'localhost',
            user: 'root'
        }
    },
    foo: {
        bar: baz
    }
};

var key1 = 'mongodb';
var key2 = 'host';

var myRef = obj.db[key1]; //this creates a reference to obj.db['mongodb']

myRef[key2] = 'my new string';

// The object now looks like:
var obj = {
    db: {
        mongodb: {
            host: 'my new string',
            user: 'root'
        }
    },
    foo: {
        bar: baz
    }
};

1

Một cách tiếp cận khác là sử dụng đệ quy để tìm hiểu đối tượng:

(function(root){

  function NestedSetterAndGetter(){
    function setValueByArray(obj, parts, value){

      if(!parts){
        throw 'No parts array passed in';
      }

      if(parts.length === 0){
        throw 'parts should never have a length of 0';
      }

      if(parts.length === 1){
        obj[parts[0]] = value;
      } else {
        var next = parts.shift();

        if(!obj[next]){
          obj[next] = {};
        }
        setValueByArray(obj[next], parts, value);
      }
    }

    function getValueByArray(obj, parts, value){

      if(!parts) {
        return null;
      }

      if(parts.length === 1){
        return obj[parts[0]];
      } else {
        var next = parts.shift();

        if(!obj[next]){
          return null;
        }
        return getValueByArray(obj[next], parts, value);
      }
    }

    this.set = function(obj, path, value) {
      setValueByArray(obj, path.split('.'), value);
    };

    this.get = function(obj, path){
      return getValueByArray(obj, path.split('.'));
    };

  }
  root.NestedSetterAndGetter = NestedSetterAndGetter;

})(this);

var setter = new this.NestedSetterAndGetter();

var o = {};
setter.set(o, 'a.b.c', 'apple');
console.log(o); //=> { a: { b: { c: 'apple'}}}

var z = { a: { b: { c: { d: 'test' } } } };
setter.set(z, 'a.b.c', {dd: 'zzz'}); 

console.log(JSON.stringify(z)); //=> {"a":{"b":{"c":{"dd":"zzz"}}}}
console.log(JSON.stringify(setter.get(z, 'a.b.c'))); //=> {"dd":"zzz"}
console.log(JSON.stringify(setter.get(z, 'a.b'))); //=> {"c":{"dd":"zzz"}}

1

Tôi cần đạt được điều tương tự, nhưng trong Node.js ... Vì vậy, tôi đã tìm thấy mô-đun tuyệt vời này: https://www.npmjs.com/package/nested-property

Thí dụ:

var mod = require("nested-property");
var obj = {
  a: {
    b: {
      c: {
        d: 5
      }
    }
  }
};
console.log(mod.get(obj, "a.b.c.d"));
mod.set(obj, "a.b.c.d", 6);
console.log(mod.get(obj, "a.b.c.d"));

cách giải quyết cho các đối tượng lồng nhau phức tạp. `` const x = {'một': 1, 'hai': 2, 'ba': {'một': 1, 'hai': 2, 'ba': [{'một': 1}, { 'one': 'ONE'}, {'one': 'I'}]}, 'four': [0, 1, 2]}; console.log (np.get (x, 'ba.three [0] .one')); ``
Sumukha HS

1

Tôi đã đưa ra giải pháp của riêng mình bằng cách sử dụng es6 thuần túy và đệ quy không làm thay đổi đối tượng ban đầu.

const setNestedProp = (obj = {}, [first, ...rest] , value) => ({
  ...obj,
  [first]: rest.length
    ? setNestedProp(obj[first], rest, value)
    : value
});

const result = setNestedProp({}, ["first", "second", "a"], 
"foo");
const result2 = setNestedProp(result, ["first", "second", "b"], "bar");

console.log(result);
console.log(result2);


Bạn có thể loại bỏ khối if đầu tiên bằng cách khai báo "obj" với giá trị mặc định setNestedProp = (obj = {}, các khóa, giá trị) => {
BlindChicken

1
Vâng, tốt. Nhìn lại cũng có thể phá hủy đối số khóa tại chỗ và lưu một dòng mã khác
Henry Ing-Simmons

Về cơ bản bây giờ là một lớp lót 👍
Henry Ing-Simmons

0

Nếu bạn muốn một hàm yêu cầu các thuộc tính trước đó tồn tại, thì bạn có thể sử dụng một cái gì đó như thế này, nó cũng sẽ trả về một cờ cho biết liệu nó có quản lý để tìm và đặt thuộc tính lồng nhau hay không.

function set(obj, path, value) {
    var parts = (path || '').split('.');
    // using 'every' so we can return a flag stating whether we managed to set the value.
    return parts.every((p, i) => {
        if (!obj) return false; // cancel early as we havent found a nested prop.
        if (i === parts.length - 1){ // we're at the final part of the path.
            obj[parts[i]] = value;          
        }else{
            obj = obj[parts[i]]; // overwrite the functions reference of the object with the nested one.            
        }   
        return true;        
    });
}

0

Lấy cảm hứng từ ClojureScript's assoc-in( https://github.com/clojure/clojurescript/blob/master/src/main/cljs/cljs/core.cljs#L5280 ), sử dụng đệ quy:

/**
 * Associate value (v) in object/array (m) at key/index (k).
 * If m is falsy, use new object.
 * Returns the updated object/array.
 */
function assoc(m, k, v) {
    m = (m || {});
    m[k] = v;
    return m;
}

/**
 * Associate value (v) in nested object/array (m) using sequence of keys (ks)
 * to identify the path to the nested key/index.
 * If one of the values in the nested object/array doesn't exist, it adds
 * a new object.
 */
function assoc_in(m={}, [k, ...ks], v) {
    return ks.length ? assoc(m, k, assoc_in(m[k], ks, v)) : assoc(m, k, v);
}

/**
 * Associate value (v) in nested object/array (m) using key string notation (s)
 * (e.g. "k1.k2").
 */
function set(m, s, v) {
    ks = s.split(".");
    return assoc_in(m, ks, v);
}

Ghi chú:

Với cách triển khai được cung cấp,

assoc_in({"a": 1}, ["a", "b"], 2) 

trả lại

{"a": 1}

Tôi muốn nó tạo ra một lỗi trong trường hợp này. Nếu muốn, bạn có thể thêm kiểm tra assocđể xác minh mlà một đối tượng hay một mảng và nếu không sẽ gặp lỗi.


0

Tôi đã cố gắng viết phương pháp thiết lập này trong ngắn hạn , nó có thể giúp ích cho ai đó!

function set(obj, key, value) {
 let keys = key.split('.');
 if(keys.length<2){ obj[key] = value; return obj; }

 let lastKey = keys.pop();

 let fun = `obj.${keys.join('.')} = {${lastKey}: '${value}'};`;
 return new Function(fun)();
}

var obj = {
"hello": {
    "world": "test"
}
};

set(obj, "hello.world", 'test updated'); 
console.log(obj);

set(obj, "hello.world.again", 'hello again'); 
console.log(obj);

set(obj, "hello.world.again.onece_again", 'hello once again');
console.log(obj);


Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.