جاوااسکریپت - تعیین پویا ویژگیهای شیء تو در تو


من یک شیء دارم که میتواند هر تعداد سطح عمیق داشته باشد و میتواند هر ویژگی موجودی داشته باشد. به عنوان مثال:

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

روی آن میخواهم خصوصیات را به این صورت تنظیم (یا بازنویسی) کنم:

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

جایی که رشته ویژگی میتواند هر عمقی داشته باشد و مقدار میتواند هر نوع/چیزی باشد.
اشیاء و آرایهها به عنوان مقادیر نیازی به ترکیب ندارند، اگر کلید ویژگی قبلاً وجود داشته باشد.

مثال قبلی شیء زیر را تولید میکرد:

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

چطور میتوانم چنین تابعی را پیادهسازی کنم؟

پاسخ شماره ۱

این تابع، با استفاده از آرگومانهایی که مشخص کردید، باید دادهها را در ظرف obj اضافه/بهروزرسانی کند. توجه داشته باشید که باید پیگیری کنید کدام عناصر در طرح obj ظرف هستند و کدام مقادیر (رشتهها، اعداد صحیح و غیره) هستند، وگرنه شروع به ایجاد استثنا خواهید کرد.

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');

پاسخ شماره ۲

لاداش یک متد _.set() دارد.

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

پاسخ شماره ۳

من فقط یک تابع کوچک با استفاده از ES6 و بازگشتی نوشتم تا به هدف برسم.

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');

من از آن زیاد در ریاکت برای بهروزرسانی وضعیت استفاده کردم، برای من خیلی خوب کار کرد.

پاسخ شماره ۴

کمی دیر شده اما این یک پاسخ ساده و بدون کتابخانه است:

/**
 * 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);
}

این تابعی که من ساختهام میتواند دقیقاً همان کاری را که شما نیاز دارید انجام دهد و کمی بیشتر.

فرض کنیم میخواهیم مقدار هدفی که بهطور عمیق در این شیء قرار دارد را تغییر دهیم:

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

پس ما تابع خود را به این صورت صدا میکنیم:

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

منجر به:

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

تنظیم کردن فلگ set recursively به true، اشیاء را در صورتی که وجود نداشته باشند تنظیم خواهد کرد.

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

این نتیجه خواهد داد:

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

پاسخ شماره ۵

ما میتوانیم از یک تابع بازگشتی استفاده کنیم:

/**
 * 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)
}

این سادهتر است!

پاسخ شماره ۶

الهام گرفته از پاسخ @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;
}

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' } }

پاسخ شماره ۷

من راهحل خودم را با استفاده از ES6 خالص و بازگشتدهی (recursion) ارائه دادم که شیء اصلی را تغییر نمیدهد.

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);

پاسخ شماره ۸

ES6 راهی واقعاً جالب برای انجام این کار هم دارد که از نام ویژگی محاسبهشده و پارامتر باقیمانده استفاده میکند.

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

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

اگر levelThree یک ویژگی پویا باشد، یعنی برای تنظیم هر یک از ویژگیها در levelTwo، میتوانید از [propertyName]: "I am now updated!" استفاده کنید که در آن propertyName نام ویژگی در levelTwo را نگه میدارد.