Is there a way to assign properties of one object as references to the properties of another, and do so dynamically? Note that in the for loop, I've skipped any property that has the same name as the second object. I'm working on a framework that will cache JSON as objects with behaviors and allow ORM kind of behavior, where I can grab cached objects and collections as properties of other cached objects. I need to skip certain properties to avoid circular reference.
var obj1 = {
prop1: "hey",
obj2:"you",
prop2: "come over here"
}
var obj2 = {}
for(var prop in obj1){
if(prop != 'obj2'){
obj2[prop] = obj1[prop];
}
}
console.log(obj1);
console.log(obj2);
obj1.prop2 = "come on, man";
console.log(obj1);
console.log(obj2);
//obj1 is unchanged in output. I would like to be able to update it by mutating obj2's properties
fiddle: http://jsfiddle.net/6ncasLb0/1/
If this is not possible, is it possible to remove or mutate a property of a reference without mutating the original object? I know, probably not. Just a shot in the dark.
I guess the closest you can get to it, is to make sure the property you are changing it the same property you are getting on both objects, so you would need to do some work to make sure they "know" each other when they are instantiated (eg, clone from the original object)
As an example, you could use a simplified model like this, any properties marked in its creation would also update the original object, though new properties defined on the object should be fine). Note that enumrating and just referencing the properties wouldn't work, at least not with strings (objects would change when copied from 1 object to another)
;
(function(namespace) {
function addProperty(obj, property, valueholder) {
Object.defineProperty(obj, property, {
get: function() {
return valueholder[property];
},
set: function(val) {
valueholder[property] = val;
},
enumerable: true,
configurable: false
});
}
var model = namespace.model || function(options) {
if (typeof options === 'undefined') {
options = {};
}
var propHolder = options.container || {},
prop;
if (typeof options.props != null) {
for (prop in options.props) {
if (options.props.hasOwnProperty(prop)) {
addProperty(this, prop, propHolder);
propHolder[prop] = options.props[prop];
}
}
};
namespace.model.prototype.clone = function() {
var options = {
props: {},
container: propHolder
},
prop;
for (prop in this) {
if (this.hasOwnProperty(prop)) {
options.props[prop] = this[prop];
}
}
return new namespace.model(options);
};
namespace.model.prototype.toString = function() {
var prop, msg, props = [];
for (prop in propHolder) {
if (propHolder.hasOwnProperty(prop)) {
props.push(prop + ': "' + this[prop].toString() + '"');
}
}
return '[Model] {' + props.join(', ') + '}';
}
return this;
};
namespace.model = model;
}(window));
var obj1 = new model({
props: {
prop2: "come over here"
}
});
obj1.prop1 = 'Hey';
obj1.obj2 = 'You';
obj1.test = {
a: 10
};
var obj2 = obj1.clone();
console.log('-- before changes --');
console.log(obj1.toString());
console.log(obj2.toString());
obj2.prop2 = "come on, man";
obj2.prop1 = "won't change";
obj2.obj2 = "also not";
obj2.test.b = "both have this now";
console.log('-- after changes --');
console.log(obj1.toString());
console.log(obj2.toString());
console.log(obj1.test);
console.log(obj2.test);
Related
Given an object obj, I would like to define a read-only property 'prop' and set its value to val. Is this the proper way to do that?
Object.defineProperty( obj, 'prop', {
get: function () {
return val;
}
});
The result should be (for val = 'test'):
obj.prop; // 'test'
obj.prop = 'changed';
obj.prop; // still 'test' since it's read-only
This method works btw: http://jsfiddle.net/GHMjN/
I'm just unsure if this is the easiest / smoothest / most proper way to do it...
You could instead use the writable property of the property descriptor, which prevents the need for a get accessor:
var obj = {};
Object.defineProperty(obj, "prop", {
value: "test",
writable: false
});
As mentioned in the comments, the writable option defaults to false so you can omit it in this case:
Object.defineProperty(obj, "prop", {
value: "test"
});
This is ECMAScript 5 so won't work in older browsers.
In new browsers or node.js it is possible to use Proxy to create read-only object.
var obj = {
prop: 'test'
}
obj = new Proxy(obj ,{
setProperty: function(target, key, value){
if(target.hasOwnProperty(key))
return target[key];
return target[key] = value;
},
get: function(target, key){
return target[key];
},
set: function(target, key, value){
return this.setProperty(target, key, value);
},
defineProperty: function (target, key, desc) {
return this.setProperty(target, key, desc.value);
},
deleteProperty: function(target, key) {
return false;
}
});
You can still assign new properties to that object, and they would be read-only as well.
Example
obj.prop
// > 'test'
obj.prop = 'changed';
obj.prop
// > 'test'
// New value
obj.myValue = 'foo';
obj.myValue = 'bar';
obj.myValue
// > 'foo'
In my case I needed an object where we can set its properties only once.
So I made it throw an error when somebody tries to change already set value.
class SetOnlyOnce {
#innerObj = {}; // private field, not accessible from outside
getCurrentPropertyName(){
const stack = new Error().stack; // probably not really performant method
const name = stack.match(/\[as (\w+)\]/)[1];
return name;
}
getValue(){
const key = this.getCurrentPropertyName();
if(this.#innerObj[key] === undefined){
throw new Error('No global param value set for property: ' + key);
}
return this.#innerObj[key];
}
setValue(value){
const key = this.getCurrentPropertyName();
if(this.#innerObj[key] !== undefined){
throw new Error('Changing global parameters is prohibited, as it easily leads to errors: ' + key)
}
this.#innerObj[key] = value;
}
}
class GlobalParams extends SetOnlyOnce {
get couchbaseBucket() { return this.getValue()}
set couchbaseBucket(value){ this.setValue(value)}
get elasticIndex() { return this.getValue()}
set elasticIndex(value){ this.setValue(value)}
}
const _globalParams = new GlobalParams();
_globalParams.couchbaseBucket = 'some-bucket';
_globalParams.elasticIndex = 'some-index';
console.log(_globalParams.couchbaseBucket)
console.log(_globalParams.elasticIndex)
_globalParams.elasticIndex = 'another-index'; // ERROR is thrown here
console.log(_globalParams.elasticIndex)
Because of the old browsers (backwards compatibility) I had to come up with accessor functions for properties. I made it part of bob.js:
var obj = { };
//declare read-only property.
bob.prop.namedProp(obj, 'name', 'Bob', true);
//declare read-write property.
bob.prop.namedProp(obj, 'age', 1);
//get values of properties.
console.log(bob.string.formatString('{0} is {1} years old.', obj.get_name(), obj.get_age()));
//set value of read-write property.
obj.set_age(2);
console.log(bob.string.formatString('Now {0} is {1} years old.', obj.get_name(), obj.get_age()));
//cannot set read-only property of obj. Next line would throw an error.
// obj.set_name('Rob');
//Output:
//========
// Bob is 1 years old.
// Now Bob is 2 years old.
I hope it helps.
I tried and it Works ...
element.readOnly = "readOnly" (then .readonly-> true)
element.readOnly = "" (then .readonly-> false)
I have an object like
const obj = { field1: obj1, field2: obj2 }
and now I'd like to run a function when anything in obj was changed:
function objChanged() { ... }
// decorate obj somehow ...
obj.field3 = data; // objChanged should be called (Proxy can see it)
obj.field1.val = data; //objChanged should be called (Proxy can't see it?)
AFAIK there is a MutationObserver which works only for DOM and Proxy which intercepts only own properties, right?
I do not own obj1 so I can not change it. Is there a way to achieve this functionality?
Following the piece of code will listen to object property you can iterate over object properties to listen all. I am curious, what are you trying to achieve?
const dog = { bark: true };
function Observer(o, property) {
var _this = this;
this.observers = [];
this.Observe = function (notifyCallback) {
_this.observers.push(notifyCallback);
};
Object.defineProperty(o, property, {
set: function (val) {
_this.value = val;
for (var i = 0; i < _this.observers.length; i++) {
_this.observers[i](val);
}
},
get: function () {
return _this.value;
},
});
}
const observer = new Observer(dog, "bark");
observer.Observe(function (value) {
l("Barked");
});
dog.bark = true;
dog.bark = true;
dog.bark = true;
dog.bark = true;
Orgil's answer works only with a single property that needs to be known and encoded. I wanted a solution which works for all properties, including later added. Inspired by his idea to create an observing object, I created a dynamic Proxy that adds another Proxies when needed.
In the following code dog1 serves as proxy: setting its properties modifies the original dog object and logs the assigned value to console.
function AssignProxy(o, fn, path) {
var tree = {};
if(!path) path = "obj";
return new Proxy(o, {
get: (_, prop) => {
if(typeof o[prop] != "object") return o[prop];
if(tree[prop] === undefined) tree[prop] = AssignProxy(o[prop], fn, `${path}.${prop}`);
return tree[prop];
},
set: (_, prop, val) => fn(o[prop] = val, prop, o, path) || 1
});
}
/****** TEST *******/
const dog = {
sounds: {},
name: "Spike"
};
let callback = (val, prop, o, path) => console.log(`assigning ${path}.${prop} to ${val}`)
const dog1 = AssignProxy(dog, callback, "dog1");
dog1.name = "Tyke"; // overwrite property
dog1.age = 4; // create a property
dog1.sounds.howl = "hoooooowl"; // create a deep property
dog1.sounds.howl = {text: "hoowl", pitch: 5000}; // overwrite the deep property
var howl = dog1.sounds.howl; // access by reference
howl.pitch = 6000; // overwrite later added property
console.log(dog); // verify the original object
I would like to create a TypeScript decorator that can extend the logic of a property's getter/setter. I have tried to copy the original property under a symbol and call that when I redefine the property. The problem is it turns into an infinite loop.
//Find the latest version of 'attribute' getter setter in the prototype chain
let obj = _object;
while(obj && !(Object.getOwnPropertyDescriptor(obj, 'attribute'))){
obj = Object.getPrototypeOf(obj);
}
//Copy original 'attribute' logic under a symbol
const attributeDesc = Object.getOwnPropertyDescriptor(obj, 'attribute');
let id=Symbol('__attribute');
Object.defineProperty(obj, id, attributeDesc);
//Redefine 'attribute' logic
Object.defineProperty(_object, 'attribute', {
get: () => {
//call original
const attribute = obj[id]; //It crashes the page (probably infinite loop)
//extend original logic
attribute['extend'] = 'property';
return attribute;
},
enumerable: false,
configurable: true
});
If you could explain me why it ends up this way that would help me out. I thought the new getter function reference nothing to do with the original. Please suggest me a solution to achive this in JavaScript.
Thank you for your time and answers!
I don't quite see the error. In the repro you provided, it's logical that there is one: the getter for attribute property is calling itself on the line var attributes = obj[id], so there is an infinite loop. However if you edit your code to be like the snippet you provided in the question:
class A {
get attribute() {
return { a: 1 }
}
}
var _object = new A()
let obj = _object
while (obj && !Object.getOwnPropertyDescriptor(obj, 'attribute')) {
obj = Object.getPrototypeOf(obj)
}
const attributeDesc = Object.getOwnPropertyDescriptor(obj, 'attribute')
let id = Symbol('__attribute')
Object.defineProperty(obj, id, attributeDesc)
Object.defineProperty(obj, 'attribute', {
get: function () {
var attributes = obj[id]
attributes['extend'] = 'property'
return attributes
},
enumerable: false,
configurable: true,
})
console.log('result:', obj.attribute)
There is no error and it works as expected.
You don't really need the symbol though, you could do something like
function extendAttributes(_object) {
let obj = _object
while (obj && !Object.hasOwnProperty(obj, 'attributes')) {
obj = Object.getPrototypeOf(obj)
}
if(!obj) return;
const oldContainer = {}
const attributesDescriptor = Object.getOwnPropertyDescriptor(obj, 'attributes')
Object.defineProperty(oldContainer, 'attributes', attributesDescriptor)
Object.defineProperty(obj, 'attributes', {
get() {
const attribute = oldContainer.attributes;
//extend original logic
attribute['extend'] = 'property';
return attribute;
}
})
}
class A {
get attributes() { return {a: 1} }
}
const obj = new A()
extendAttributes(obj)
console.log(obj.attributes)
Which also works like expected
This question already has answers here:
How to set object property (of object property of..) given its string name in JavaScript?
(16 answers)
Closed 6 years ago.
I have an object called MyObject and a method called createEntry() which aims to create a new property for MyObject and sub-properties for the property or just skip it altogether if it already exists.
My code:
var MyObject = {
createEntry: function (val1, val2, val3, val4) {
this[val1] = this[val1] || {};
this[val1][val2] = this[val1][val2] || {};
this[val1][val2][val3] = val4;
}
};
MyObject.createEntry("val1", "val2", "val3", "val4");
As shown in the above function, I'm trying to create a new sub-object for each argument of the method createEntry() except for the last two, where val3 is a property or method and val4 is its value.
With my method in its current state, I can only reach up to level 3 with its subsequent ones requiring longer and longer code. I assume the above can be achieved with a while loop, but I haven't been able to figure it out yet.
Question: How can I create unlimited sub-objects based on the number of arguments passed on the above function in a tree fashion that looks like the following:
var MyObject = {
val1: {
val2 {
val3: val4
}
}
}
reduceRight seems perfect for this:
function createEntry(...args) {
return args.reduceRight(function(prev, curr) {
return {[curr]: prev};
});
}
console.log(createEntry("val1", "val2", "val3", "val4"));
Well, when creating unlimited sub-objects using parameters, I'd iterate each parameter, having the last object I entered as reference. This code is enough to understand.
Note: Object.assign is a new browser implementation, but it has polyfills already. I use this method to concatenate object with object when a entry name already exists in a object
Object['prototype']['createEntries'] = function() {
/* store last object location */
var lastObj = this
/**
* Note: each argument is a object that specify the following properties -->
* inner : the entry object
* *name : the entry property name
*/
for (var i = 0, arg, existent, len = arguments.length; i < len; ++i) {
if (typeof (arg = arguments[i]) === 'object' && (typeof arg.inner === 'object' ? true : arg.inner = {} ))
/* create new entry/keep existent entry and reserve it in lastObj */
lastObj = (typeof (existent = lastObj[arg.name]) === 'object' ?
lastObj[arg.name] = Object.assign(existent, arg.inner) :
lastObj[arg.name] = arg.inner)
}
return this
}
Then this is a basic usage
({}).createEntries({
name: "val1"
}, {
name: "val2"
})
/* {
val1: {
val2: {
}
}
}
*/
I hope this is what you want
Oriol gave a fantastic answer, but his function, due to the way it's built, is not really useful as it returns a new object instead of modifying MyObject.
So, in order to actually modify MyObject, instead of the first return, we have to do something else that will modify MyObject without overwriting it.
To do just that, a function that expands the MyObject must be built:
expand: function (object) {
for (var key in object) if (!!object[key]) {
if (key in this) throw new Error("`" + key + "` already exists.");
else this[key] = object[key];
}
}
Then, all we have to do is to use #Oriol's answer without the first return statement as an argument to expand():
createEntry: function () {
this.expand([].reduceRight.call(arguments, function(previous, current) {
return {[current]: previous};
}));
}
Full code:
var MyObject = {
createEntry: function() {
this.expand([].reduceRight.call(arguments, function(previous, current) {
return {
[current]: previous
};
}));
},
expand: function(object) {
for (var key in object)
if (!!object[key]) {
if (key in this) throw new Error("`" + key + "` already exists.");
else this[key] = object[key];
}
}
};
MyObject.createEntry("val1", "val2", "val3", "val4");
console.log(MyObject);
Thanks to everyone for the help!
I have an object that contains a getter.
myObject {
id: "MyId",
get title () { return myRepository.title; }
}
myRepository.title = "MyTitle";
I want to obtain an object like:
myResult = {
id: "MyId",
title: "MyTitle"
}
I don't want to copy the getter, so:
myResult.title; // Returns "MyTitle"
myRepository.title = "Another title";
myResult.title; // Should still return "MyTitle"
I've try:
$.extend(): But it doesn't iterate over getters. http://bugs.jquery.com/ticket/6145
Iterating properties as suggested here, but it doesn't iterate over getters.
As I'm using angular, using Angular.forEach, as suggested here. But I only get properties and not getters.
Any idea? Thx!
Update
I was setting the getter using Object.defineProperty as:
"title": { get: function () { return myRepository.title; }},
As can be read in the doc:
enumerable true if and only if this property shows up during
enumeration of the properties on the corresponding object. Defaults to
false.
Setting enumerable: true fix the problem.
"title": { get: function () { return myRepository.title; }, enumerable: true },
$.extend does exactly what you want. (Update: You've since said you want non-enumerable properties as well, so it doesn't do what you want; see the second part of this answer below, but I'll leave the first bit for others.) The bug isn't saying that the resulting object won't have a title property, it's saying that the resulting object's title property won't be a getter, which is perfect for what you said you wanted.
Example with correct getter syntax:
// The myRepository object
const myRepository = { title: "MyTitle" };
// The object with a getter
const myObject = {
id: "MyId",
get title() { return myRepository.title; }
};
// The copy with a plain property
const copy = $.extend({}, myObject);
// View the copy (although actually, the result would look
// the same either way)
console.log(JSON.stringify(copy));
// Prove that the copy's `title` really is just a plain property:
console.log("Before: copy.title = " + copy.title);
copy.title = "foo";
console.log("After: copy.title = " + copy.title);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
Syntax fixes:
Added missing variable declarations, =, and ;
Removed duplicate property title
Corrected the getter declaration syntax
If you want to include non-enumerable properties, you'll need to use Object.getOwnPropertyNames because they won't show up in a for-in loop, Object.keys, or $.extend (whether or not they're "getter" or normal properties):
// The myRepository object
const myRepository = { title: "MyTitle" };
// The object with a getter
const myObject = {
id: "MyId",
};
Object.defineProperty(myObject, "title", {
enumerable: false, // it's the default, this is just for emphasis,
get: function () {
return myRepository.title;
},
});
console.log("$.extend won't visit non-enumerable properties, so we only get id here:");
console.log(JSON.stringify($.extend({}, myObject)));
// Copy it
const copy = {};
for (const name of Object.getOwnPropertyNames(myObject)) {
copy[name] = myObject[name];
}
// View the copy (although actually, the result would look
// the same either way)
console.log("Our copy operation with Object.getOwnPropertyNames does, though:");
console.log(JSON.stringify(copy));
// Prove that the copy's `title` really is just a plain property:
console.log("Before: copy.title = " + copy.title);
copy.title = "foo";
console.log("After: copy.title = " + copy.title);
.as-console-wrapper {
max-height: 100% !important;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
First of all, fix your syntax, though it probably is good in your actual code:
myObject = {
id: "MyId",
get title () { return myRepository.title; }
}
Now, to the answer. :)
You can just use a for..in loop to get all the properties, then save them as-is:
var newObj = {};
for (var i in myObject) {
newObj[i] = myObject[i];
}
No jQuery, Angular, any other plugins needed!
I had the same issue but in TypeScript and the method mentioned by T.J. Crowder didnt work.
What did work was the following:
TypeScript:
function copyObjectIncludingGettersResult(originalObj: any) {
//get the properties
let copyObj = Object.getOwnPropertyNames(originalObj).reduce(function (result: any, name: any) {
result[name] = (originalObj as any)[name];
return result;
}, {});
//get the getters values
let prototype = Object.getPrototypeOf(originalObj);
copyObj = Object.getOwnPropertyNames(prototype).reduce(function (result: any, name: any) {
//ignore functions which are not getters
let descriptor = Object.getOwnPropertyDescriptor(prototype, name);
if (descriptor?.writable == null) {
result[name] = (originalObj as any)[name];
}
return result;
}, copyObj);
return copyObj;
}
Javascript version:
function copyObjectIncludingGettersResult(originalObj) {
//get the properties
let copyObj = Object.getOwnPropertyNames(originalObj).reduce(function (result, name) {
result[name] = originalObj[name];
return result;
}, {});
//get the getters values
let prototype = Object.getPrototypeOf(originalObj);
copyObj = Object.getOwnPropertyNames(prototype).reduce(function (result,name) {
let descriptor = Object.getOwnPropertyDescriptor(prototype, name);
if (descriptor?.writable == null) {
result[name] = originalObj[name];
}
return result;
}, copyObj);
return copyObj;
}