How to "override" a defined (get-)property on a prototype?
You can change Getter behavior by defining a Setter.
1. Define foo
as a prototype for new instance objects
Setters/Getters (accessor properties) will get copied to new created objects of their prototype.
Unlike accessor properties, value properties are always set on the object itself, not on a prototype.
The value of
bar
is 'hidden' behind a Symbol property. This prevents direct manipulation.
2. Create instances of foo
and overwrite set bar()
:
Option A:
➜ Create new
foo
.➜ Use
set bar()
to change the default.Option B:
➜ Create new
foo
with redefinedset bar()
.➜ Accessor property
set bar()
is overwritten by value propertybar
.Option C:
➜ Create new
foo
andObject.assign()
a new default.➜ This invoces
set bar()
.It uses [[Get]] on the source and [[Set]] on the target, so it will invoke getters and setters.
// Freeze foo. (Prevent accidental manipulation of the prototype)
const foo = Object.freeze(
// Create new objecct with NO prototype: null
// Change null ➜ {} for inheritance of standard object methods
Object.create(null, {
// 'bar' set/get
bar: {
get() {
// Check: Symbol existence || random number
return this[Symbol.for('bar')] ?? (Math.random() * 6) | 1;
},
set(val) {
console.log(`setter 'bar' = ${val}`);
// Set 'bar' val and hide behind a Symbol key (non enumerable)
this[Symbol.for('bar')] = val;
}
}
})
);
console.log('Option A: Call setter (post create) =================');
const x = Object.create(foo);
console.log(x.bar); // rnd
x.bar = 4; // setter 'bar' = 4
console.log(x.bar); // 4
x.bar = null; // setter 'bar' = null
console.log(x.bar); // rnd
console.log('Option B: Overwrite Setter (on create) ==============');
// Create new foo obj, overwrite 'bar' Setter/Getter
const create = obj => Object.create(foo, Object.getOwnPropertyDescriptors(obj));
const y = create({bar: 4});
console.log(y.bar); // 4
y.bar = 5; // overwrite prop 'bar' = 4
console.log(y.bar); // 5
y.bar = null; // overwrite prop 'bar' = null
console.log(y.bar); // null (no fallback)
console.log('Option C: Obj.assign invoces Setter (on create) ====');
// create new foo Obj, assign default
// ➜ Setter gets called on new target
const createFrom = obj => Object.assign(Object.create(foo), obj);
const z = createFrom({bar: 4});
console.log(z.bar); // 4
z.bar = 6; // setter 'bar' = 6
console.log(z.bar); // 6
z.bar = null; // setter 'bar' = null
console.log(z.bar); // rnd
As T.J. Crowder said, using defineProperty
again does the trick. You may consider the following variation, where the setter itself overrides the property:
Foo = function () {}
Foo.prototype = {
// computes, but only knows odd die sides
get bar() {
console.log("getter invoked")
return (Math.random() * 6) | 1
},
// fix it
set bar(value) {
console.log("setter invoked")
Object.defineProperty(
this, 'bar',
{writable: true, enumerable: true, configurable: true}
)
this.bar = value
}
}
var x = new Foo
console.log(x.bar) // => eg. 5
x.bar = 4 // by fair dice roll
console.log(x.bar) // => 4
x.bar = 2 // no setter, no getter
console.log(x.bar)
I hope you'll pardon me to rewrite in a slightly different syntax. It does not change anything to the trick. I was actually just looking for a way to override an inherited getter
when I came to this post.
By using Object.defineProperty
on x
:
var foo = {}
Object.defineProperty(foo, "bar", {
// only returns odd die sides
get: function () { return (Math.random() * 6) | 1; }
});
var x = Object.create(foo);
display(x.bar); // E.g. 5
(function() {
var bar;
var proto = Object.getPrototypeOf(x); // Or just use foo
Object.defineProperty(x, "bar", {
get: function () { return typeof bar !== "undefined" ? bar : proto.bar; },
set: function(value) { bar = value; }
});
})();
display(x.bar); // Still odd
x.bar = 4; // By fair dice roll
display(x.bar); // Shows 4
function display(msg) {
document.body.insertAdjacentHTML("beforeend", "<p>" + msg + "</p>");
}
I am looking for if there is a way to stop the behavior of a property in the [prototype] and turn "bar" back into a normal/ad-hoc property.
Okay, that's slightly different, but still uses Object.defineProperty
:
var foo = {}
Object.defineProperty(foo, "bar", {
// only returns odd die sides
get: function () { return (Math.random() * 6) | 1; }
});
var x = Object.create(foo);
display(x.bar); // E.g. 5
Object.defineProperty(x, "bar", {
value: undefined,
writable: true,
enumerable: true // Or leave off if you want it non-enumerable
});
display(x.bar); // undefined
x.bar = 4; // By fair dice roll
display(x.bar); // Shows 4
function display(msg) {
document.body.insertAdjacentHTML("beforeend", "<p>" + msg + "</p>");
}
It's not as simple as just Object.defineProperty.
You need to get the property-descriptor first (with Object.getOwnPropertyDescriptor
), and then check if the property is configurable.
If it is NOT configurable, you cannot override it.
Also, you need to take into consideration that a property can have a getter/setter XOR a VALUE.
So you cannot set desc.value
& desc.writable
if you have a getter/setter and vice-versa.
let desc = Object.getOwnPropertyDescriptor(self.constructor.prototype, key);
if (desc != null)
{
// We can only redefine configurable properties !
if (!desc.configurable)
{
console.log("AUTOTRACE-WARNING: Property \"" + key + "\" not configurable ! (" + self.constructor.name + ")");
continue;
}
let g = desc.get != null;
let s = desc.set != null;
if (g || s)
{
let newDescriptor: PropertyDescriptor = {};
newDescriptor.enumerable = desc.enumerable;
newDescriptor.configurable = desc.configurable;
// Argh !
// newDescriptor.value= desc.value;
// newDescriptor.writable = desc.writable;
if (g)
newDescriptor.get = getLoggableFunction(desc.get.bind(self), "Property", "get_" + key)
if (s)
newDescriptor.set = getLoggableFunction(desc.set.bind(self), "Property", "set_" + key)
Object.defineProperty(self, key, newDescriptor);
continue; // if it's a property, it can't be a function
} // End if (g || s)
} // End if (desc != null)
Here's how I do it (notice the autobind&autotrace-call in the constructor of 'TestClass'):
import { autoBind, autoTrace } from "./autobind_autotrace.js";
class TestClass
{
constructor()
{
autoBind(this);
autoTrace(this);
}
get bar(): boolean
{
return this._bar;
}
set bar(value: boolean)
{
this._bar = value;
}
public hello()
{
console.log("hello", "this", this);
}
public world(x, y)
{
console.log("world", "this", this);
}
}
AutoBind/AutoTrace (TypeScript):
export function autoBind(self: any): any
{
for (const key of Object.getOwnPropertyNames(self.constructor.prototype))
{
if (key !== 'constructor')
{
// console.log(key);
let desc = Object.getOwnPropertyDescriptor(self.constructor.prototype, key);
if (desc != null)
{
// We can only redefine configurable properties !
if (!desc.configurable)
{
console.log("AUTOBIND-WARNING: Property \"" + key + "\" not configurable ! (" + self.constructor.name + ")");
continue;
}
let g = desc.get != null;
let s = desc.set != null;
if (g || s)
{
let newGetter = null;
let newSetter = null;
if (g)
//desc.get = desc.get.bind(self);
newGetter = desc.get.bind(self);
if (s)
// desc.set = desc.set.bind(self);
newSetter = desc.set.bind(self);
if (newGetter != null && newSetter == null)
{
Object.defineProperty(self, key, {
get: newGetter
, enumerable: desc.enumerable
, configurable: desc.configurable
// , value: desc.value
// , writable: desc.writable
});
}
else if (newSetter != null && newGetter == null)
{
Object.defineProperty(self, key, {
set: newSetter
, enumerable: desc.enumerable
, configurable: desc.configurable
// , value: desc.value
// , writable: desc.writable
});
}
else // at least one is set, but none of the above cases, so two are set
{
Object.defineProperty(self, key, {
get: newGetter
, set: newSetter
, enumerable: desc.enumerable
, configurable: desc.configurable
// , value: desc.value
// , writable: desc.writable
});
}
// Object.defineProperty(self.constructor.prototype, key, desc);
// Object.defineProperty(self.constructor.prototype, key, desc);
continue; // if it's a property, it can't be a function
} // End if (g || s)
} // End if (desc != null)
if (typeof (self[key]) === 'function')
{
let val = self[key];
self[key] = val.bind(self);
}
} // End if (key !== 'constructor' && typeof val === 'function')
} // Next key
return self;
} // End Function autoBind
export function autoTrace(self: any): any
{
function getLoggableFunction_old(func: any, type: any, name: any)
{
return function (...args:any[])
{
let logText = name + '(';
for (var i = 0; i < args.length; i++)
{
if (i > 0)
{
logText += ', ';
}
logText += args[i];
}
logText += ');';
console.log(type + " " + logText);
return func.apply(self, args);
};
}
function getLoggableFunction(func: any, type: any, name: any)
{
return function (...args: any[])
{
let logText = name + '(';
for (var i = 0; i < args.length; i++)
{
if (i > 0)
{
logText += ', ';
}
logText += args[i];
}
logText += ')';
console.log("Pre " + type + " " + logText + "; ");
let res = func.apply(self, args);
console.log("Post " + type + " " + logText + ":", res);
return res;
};
}
for (const key of Object.getOwnPropertyNames(self.constructor.prototype))
{
if (key !== 'constructor')
{
// console.log(key);
let desc = Object.getOwnPropertyDescriptor(self.constructor.prototype, key);
if (desc != null)
{
// We can only redefine configurable properties !
if (!desc.configurable)
{
console.log("AUTOTRACE-WARNING: Property \"" + key + "\" not configurable ! (" + self.constructor.name + ")");
continue;
}
let g = desc.get != null;
let s = desc.set != null;
if (g || s)
{
let newGetter = null;
let newSetter = null;
if (g)
//desc.get = getLoggableFunction(desc.get.bind(self), "Property", "get_" + key)
newGetter = getLoggableFunction(desc.get.bind(self), "Property", "get_" + key)
if (s)
// desc.set = getLoggableFunction(desc.set.bind(self), "Property", "set_" + key)
newSetter = getLoggableFunction(desc.set.bind(self), "Property", "set_" + key)
if (newGetter != null && newSetter == null)
{
Object.defineProperty(self, key, {
get: newGetter
, enumerable: desc.enumerable
, configurable: desc.configurable
// , value: desc.value
// , writable: desc.writable
});
}
else if (newSetter != null && newGetter == null)
{
Object.defineProperty(self, key, {
set: newSetter
, enumerable: desc.enumerable
, configurable: desc.configurable
// , value: desc.value
// , writable: desc.writable
});
}
else // at least one is set, but none of the above cases, so two are set
{
Object.defineProperty(self, key, {
get: newGetter
, set: newSetter
, enumerable: desc.enumerable
, configurable: desc.configurable
// , value: desc.value
// , writable: desc.writable
});
}
continue; // if it's a property, it can't be a function
} // End if (g || s)
} // End if (desc != null)
// if it's not a property, it can only be a function or not a function
if (typeof (self[key]) === 'function')
{
let val = self[key];
self[key] = getLoggableFunction(val.bind(self), "Function", key);
} // End if (typeof (self[key]) === 'function')
} // End if (key !== 'constructor' && typeof val === 'function')
} // Next key
return self;
} // End Function autoTrace
This transpiles down to plain JavaScript:
"use strict";
function autoBind(self)
{
for (var _i = 0, _a = Object.getOwnPropertyNames(self.constructor.prototype); _i < _a.length; _i++)
{
var key = _a[_i];
if (key !== 'constructor')
{
var desc = Object.getOwnPropertyDescriptor(self.constructor.prototype, key);
if (desc != null)
{
if (!desc.configurable)
{
console.log("AUTOBIND-WARNING: Property \"" + key + "\" not configurable ! (" + self.constructor.name + ")");
continue;
}
var g = desc.get != null;
var s = desc.set != null;
if (g || s)
{
var newGetter = null;
var newSetter = null;
if (g)
newGetter = desc.get.bind(self);
if (s)
newSetter = desc.set.bind(self);
if (newGetter != null && newSetter == null)
{
Object.defineProperty(self, key, {
get: newGetter
, enumerable: desc.enumerable
, configurable: desc.configurable
// , writable: desc.writable
// , value: desc.value
});
}
else if (newSetter != null && newGetter == null)
{
Object.defineProperty(self, key, {
set: newSetter
, enumerable: desc.enumerable
, configurable: desc.configurable
// , writable: desc.writable
// , value: desc.value
});
}
else
{
Object.defineProperty(self, key, {
get: newGetter
, set: newSetter
, enumerable: desc.enumerable
, configurable: desc.configurable
// , writable: desc.writable
// , value: desc.value
});
}
continue;
}
}
if (typeof (self[key]) === 'function')
{
var val = self[key];
self[key] = val.bind(self);
}
}
}
return self;
}
function autoTrace(self)
{
function getLoggableFunction_old(func, type, name)
{
return function ()
{
var args = [];
for (var _i = 0; _i < arguments.length; _i++)
{
args[_i] = arguments[_i];
}
var logText = name + '(';
for (var i = 0; i < args.length; i++)
{
if (i > 0)
{
logText += ', ';
}
logText += args[i];
}
logText += ');';
console.log(type + " " + logText);
return func.apply(self, args);
};
}
function getLoggableFunction(func, type, name)
{
return function ()
{
var args = [];
for (var _i = 0; _i < arguments.length; _i++)
{
args[_i] = arguments[_i];
}
var logText = name + '(';
for (var i = 0; i < args.length; i++)
{
if (i > 0)
{
logText += ', ';
}
logText += args[i];
}
logText += ')';
console.log("Pre " + type + " " + logText + "; ");
var res = func.apply(self, args);
console.log("Post " + type + " " + logText + ":", res);
return res;
};
}
for (var _i = 0, _a = Object.getOwnPropertyNames(self.constructor.prototype); _i < _a.length; _i++)
{
var key = _a[_i];
if (key !== 'constructor')
{
var desc = Object.getOwnPropertyDescriptor(self.constructor.prototype, key);
if (desc != null)
{
if (!desc.configurable)
{
console.log("AUTOTRACE-WARNING: Property \"" + key + "\" not configurable ! (" + self.constructor.name + ")");
continue;
}
var g = desc.get != null;
var s = desc.set != null;
if (g || s)
{
var newGetter = null;
var newSetter = null;
if (g)
newGetter = getLoggableFunction(desc.get.bind(self), "Property", "get_" + key);
if (s)
newSetter = getLoggableFunction(desc.set.bind(self), "Property", "set_" + key);
if (newGetter != null && newSetter == null)
{
Object.defineProperty(self, key, {
get: newGetter
, enumerable: desc.enumerable
, configurable: desc.configurable
// , writable: desc.writable
// , value: desc.value
});
}
else if (newSetter != null && newGetter == null)
{
Object.defineProperty(self, key, {
set: newSetter
, enumerable: desc.enumerable
, configurable: desc.configurable
// , writable: desc.writable
// , value: desc.value
});
}
else
{
Object.defineProperty(self, key, {
get: newGetter
, set: newSetter
, enumerable: desc.enumerable
, configurable: desc.configurable
// , writable: desc.writable
// , value: desc.value
});
}
continue;
}
}
if (typeof (self[key]) === 'function')
{
var val = self[key];
self[key] = getLoggableFunction(val.bind(self), "Function", key);
}
}
}
return self;
}
// exports and module are globals of "require" - delete if usage without require
exports = exports || {}; // prevent error if no module
exports.autoBind = autoBind;
exports.autoTrace = autoTrace;