I am new to es6 and am wondering if this approach is OK.I do not want someone to set properties that I do not want on my object.
export default class User {
static getMembers() {
return ['username','email','fullname','birthdate'];
}
constructor(props) {
Object.keys(props).forEach((p)=>{
if (User.getMembers().includes(p)) this[p]=props[p]
});
Object.defineProperty(this,"username",{writable:false});
Object.defineProperty(this,"email",{writable:false});
}
any other approaches?
With your code as shown, you can't set any value for username or email. They'll always be undefined, as you've made them read-only and haven't assigned them a value. You also haven't done anything to prevent code with access to one of these objects setting a completely arbitrary property:
let u = new User();
x.anythingIWant = 42;
If you want to prevent code from adding properties to an object, you can do that with Object.seal (ref: MDN, spec), which prevents new properties being added and makes it impossible to re-define existing properties. So:
export default class User {
constructor() {
// ...create your desired properties
// Seal the object
Object.seal(this);
}
}
And if you also want to make username and email read-only, you'd also include the Object.defineProperty calls you have (before the Object.seal). Just make sure you assign them values first, as once you've done that defineProperty and Object.seal, you can't change them anymore.
I think I'd probably approach using that array of whitelisted properties slightly differently as well, by just looping through the array.
So:
export default class User {
static getMembers() {
return ['username','email','fullname','birthdate'];
}
constructor(props) {
// Create the properties
User.getMembers().forEach((name) => { this[name] = props[name]; });
// Make sure username and email have a value, because we won't
// be able to change it in a moment
// ...
// Make username and email read-only
Object.defineProperty(this, "username", {writeable: false});
Object.defineProperty(this, "email", {writeable: false});
// Seal the object
Object.seal(this);
}
}
Related
const student = {
// data property
firstName: 'Monica',
// accessor property(getter)
get getName() {
return this.firstName;
}
};
// accessing data property
console.log(student.firstName); // Monica
// accessing getter methods
console.log(student.getName); // Monica
// trying to access as a method
console.log(student.getName()); // error
I can't access the getName() method while using () 1st brackets. Why is the last line showing error?
You're using a getter:
the get syntax binds an object property to a function that will be
called when that property is looked up.
So what you have in your code is a getName property (and, for consistency, it should be called name) not a method.
It's the same of having:
Object.defineProperty(student, 'name', {
get() {
return this.firstName;
},
});
If you want to have the a method instead of a property, you should define it as such:
const student = {
// data property
firstName: 'Monica',
// method, not getter
getName() {
return this.firstName;
}
};
But since JS supports getter, you could just have as you wrote (just less confused code, removing the double "get get"):
const student = {
// data property
firstName: 'Monica',
// accessor property(getter)
get name() {
return this.firstName;
}
};
The reason is because it is a getter.
Getters (not to be confused with the getter design pattern in other languages) allow a property to be dynamic and are NOT functions. They are properties that have dynamic behavior. In order to achieve this dynamic behavior the implementation of getters require you to define them as functions.
Getters were introduced to close a capability gap where it was impossible to implement something like .innerHTML in javascript. Previously if you wanted a property that behaves like .innerHTML (that is, its value is dynamic) you would need to write it in C/C++ and create your own javascript interpreter (alternatively you could write a NaCl plugin in C/C++ for Chrome or Firefox, however most browsers these days have deprecated NaCl after they've removed support for Flash so you can't do that anymore).
So TLDR: it's because that's how it works: getters are NOT functions. They are variables that happen to have side-effects.
A getter binds an object property to a function[*]. So it isn't a function type by itself and can't be invoked as such.
However as a function is a valid type in javascript, you can make a getter return a function itself:
const student = {
// data property
firstName: 'Monica',
// accessor property(getter)
get getName() {
return function() {
return 'func result';
}
}
};
// accessing data property
console.log(student.firstName); // Monica
// accessing getter methods
console.log(student.getName); // Monica
// trying to access as a method
console.log(student.getName()); // error
Is there any point in repeating this pattern for every property in JavaScript?
class Thing {
get myProp() {
return this._myProp;
}
set myProp(value) {
this._myProp = value;
}
}
I understand that getters/setters can be useful if you're doing additional work in the methods, but recreating the basic functionality here seems like needless repetition. If I instantiate, I can still manipulate the backing property (._myProp) directly, so I feel like I could just as easily leave these out and perform my assignment and access in the more typical, ad-hoc fashion.
I suppose you could argue that defining the interface this way (with the underscore-prefixed property name) signals to users that it's not meant to manipulate the property, but that seems like a flimsy reason for potentially dozens of these.
In compiled languages, it's common for people to do this. This is because in those languages, assigning to a field and invoking a setter may be identical to the programmer, but they compile to two completely different operations.
If I wanted to add a side effect for setting a field in a C# class, for example, and that field was being set directly instead of through a setter? Changing it to a setter would cause some issues. I wouldn't have to rewrite any of the consuming code, but I would have to recompile all of it. This is, of course, a huge problem if your public releases are compiled.
JavaScript is subject to no such considerations, though, so making everything into a getter/setter prematurely is kind of silly. If you see someone doing this, more than likely you're dealing with someone who learned the convention from another language and carried it into JavaScript, without thinking a whole lot about why.
Using an accessor property in the fashion you describe (set and retrieve a "background" data property) is virtually semantically identical to using a data property directly. There are some differences: the accessor property will exist on instance's prototype, rather than on the instance directly (though the instance will have the "background" data property), but this won't really affect anything unless you are doing advanced introspection on your class instances.
The only advantage is ease of modifying the code if you want to introduce more sophisticated accessor behavior in the future. If you forsee a need to add accessor behavior, use this pattern to save yourself time in the future.
Property accessors are useful to provide side effects or change original behaviour:
class Thing {
get myProp() {
console.log('myProp was read');
return this._myProp;
}
set myProp(value) {
if (!value)
throw new Error('myProp cannot be falsy');
this._myProp = value;
}
}
There is no point in myProp getter/setter pure abstraction:
class Thing {
get myProp() {
return this._myProp;
}
set myProp(value) {
this._myProp = value;
}
}
If I instantiate, I can still manipulate the backing property
(._myProp) directly,
If private states are what you are looking for you can still use a weak map.
(function(scope) {
"use strict";
const prop = new WeakMap();
scope.Foo = class {
constructor() {
prop.set(this, {});
Object.seal(this);
}
get bar() {
return prop.get(this)._bar;
}
set bar(value) {
return prop.get(this)._bar = value;
}
}
}(this))
const f = new Foo;
f.bar = "bar";
f._bar = "_bar";
console.log(f.bar);
console.log(f._bar);
get and setters are also useful when implementing MVC, you can trigger events on property change.
(function(scope) {
"use strict";
const prop = new WeakMap();
scope.Foo = class {
constructor() {
prop.set(this, {});
prop.get(this)._target = new EventTarget
Object.seal(this);
}
get bar() {
return prop.get(this)._bar;
}
set bar(value) {
prop.get(this)._bar = value;
prop.get(this)._target.dispatchEvent(new CustomEvent('change', {
detail: value
}));
}
addEventListener(event, listener) {
prop.get(this)._target.addEventListener(event, listener)
}
}
}(this))
const f = new Foo;
f.addEventListener('change', function(event) {
console.log("changed!", event.detail);
});
f.bar = "bar";
I've recently came across the following situation and looking for an advice / elegant solution of pretty tricky misbehave, IMHO.
Let's assume that I've created the following custom element definition in file bible-reference.js:
class BibleReference extends HTMLElement {
constructor() {
super();
}
get value() { return this._value; }
set value(newValue) { this._value = newValue; }
}
customElements.define('bible-reference', BibleReference);
We've defined our new element, and we have a value property getter/setter to interact with this element's value. Pay attention, that the getter/setter will be found on the element's prototype object, which is okay in itself, to be sure.
Now, let's review the following HTML layout that happened to be in my case:
<bible-reference id='elem-1'></bible-reference>
<script src="starter.js"></script>
<script src="bible-reference.js"></bible-reference>
The trick is in that starter.js content. If this script contains code below, things get broken:
let br = document.getElementById('elem-1');
br.value = 'some value';
The value is set on the non-upgraded yet element, therefore it is not the getter/setter that are being invoked, but just a new property is added to the element. Having this property set on the object itself, even after the upgrade is done the value's getter/setter are not accessible for this element instance anymore, since there is an (overriding) value property available before getting to the prototype.
I've solved it meanwhile by adding delete this.value; to the constructor, but... what a hack!
Any insights appreciated.
Another way to handle the issue would be to define the value property in the closure of the constructor().
<bible-reference id='elem-1'></bible-reference>
<script>
let br = document.getElementById('elem-1');
br.value = 'Genesis 1:1';
class BibleReference extends HTMLElement {
constructor() {
super()
var _value = this.value
Reflect.defineProperty(this, 'value', {
get: () => _value,
set: newValue => _value = newValue
})
}
}
customElements.define('bible-reference', BibleReference);
console.log(br.value)
</script>
But I guess you'll say it's a bad hack ;-) Anyway I'd prefer your delete solution.
I'm rewriting some old Chrome extension code while simultaneously trying to learn new ES6 tricks, and I'm running into some design questions.
My goal is to provide a value storage (which is backed by the asynchronous chrome.storage for persistence, but that's outside the scope of the question). What I want is to associate some validation with the values. So, my Storage is a collection of Values, each associated with a validation function.
In my old version, I would just pass a validation function when I instantiate a value, something like this (simplified):
Storage["key1"] = new Value({
validator: ValidatorIsInteger, defaultValue: 0, /* ... */
});
Storage["key2"] = new Value({
validator: ValidatorEnum(["a", "b", "c"]), defaultValue: "a", /* ... */
});
However, I'm trying to rewrite this with Value being a class that can be extended with specific validators, which seemed to be a good idea at the time. Again, simplified:
class Value {
constructor(key, defaultValue) {
this.key = key;
this.defaultValue = defaultValue;
this.set(defaultValue);
}
set(newValue) {
var validationResult = this.validate(newValue);
if (validationResult.pass) {
this.value = newValue;
return newValue;
} else {
throw new RangeError(
`Value ${newValue} for ${this.key} failed validation: ${validationResult.message}`
);
}
}
get() { return this.value; }
// Overload in children
validate(value) {
return {pass: true};
}
}
class IntegerValue extends Value {
validate(value) {
if (Number.isInteger(value)) {
return {pass: true};
} else {
return {pass: false, message: "Value must be an integer"};
}
}
}
So far so good. However, I run into problems when trying to make a parametrized child class:
class EnumValue extends Value {
constructor(key, defaultValue, possibleValues) {
this.possibleValues = possibleValues; // NUH-UH, can't set that before super()
super(key, defaultValue);
}
// Will be called from parent constructor
validate(value) {
if (this.possibleValues.includes(value)) {
return {pass: true};
} else {
return {pass: false, message: `Value must be in [${this.possibleValues}]`};
}
}
}
The problem is in "setting up" the parametrized validator before .set(defaultValue) is called. I see several ways out of this, all of which seems lacking:
Resign, and not use the class-extension-based approach - I want to see if it can be fixed first.
Always trust the default value as a workaround to calling .set(defaultValue) - bad, because I don't want accidentally inconsistent data.
Make .set() asynchronous, giving the constructor a chance to finish before validation is performed - while the persistence backend is asynchronous, the purpose of Storage is, among other things, to provide a synchronous "cache".
Am I failing to see some obvious fix to this approach? If not, and this is simply a wrong tool for the job, how should I re-organize this?
This is the classic problem with calling overrideable methods (validate, via set) from constructors; discussed here (different language, same problem).
Your specific example lends itself to a couple of workarounds, but the general issue remains.
To solve the general problem, I'd set value directly, not via set, and use unit tests to ensure that I didn't create validators that have an invalid default value. That is, after all, a coding error, not a runtime error.
But if you want to keep calling set, a couple of options:
You could have the concept of a Value that has no default value, which might be useful in general anyway. That would solve the problem by letting you have a Value constructor that doesn't expect to receive a default value. You could give a Value a default value after construction via setDefaultValue or similar; that method would validate, but that's fine, because it would be called post-construction in the subclass.
You could give Value a "validating" and "non-validating" state, and have the constructor accept a flag for which state it should start out in. Subclasses would use false if they had special validation behavior, ensure that all their ducks are in a row, and then set the validation state (which would validate).
I'm trying to explore using ES6 classes instead of how we do it currently, using the Function.prototype means. Currently our API looks like:
var myclass = createClass('MyClass', {
test : function() {}
});
We iterate through the object and apply those properties onto the Function that we return, basically a prettier way than to do so that it's more inline with other programming languages of sorts:
function MyClass() {}
MyClass.prototype.test = function() {};
We also cache the class onto an object where name is the key and the function is the value for use throughout our application. The class name can be namespaced so you can have My.Cls and it will split by the period and then cache it onto the manager but it also can be retrieved via window.My.Cls.
Looking into ES6 classes, I don't see how I can keep the createClass function. Would love something like:
function createClass(name, config) {
return class name config;
}
I didn't expect it to work and it doesn't.
Two issues I have here:
How can I create a class using a variable as the class name?
How can I create a class and assign the properties via the config object argument?
Not sure this would be possible. We don't plan on keeping the createClass, we hope to keep it for now and upgrade our legacy "classes". I'd like to start using ES6 classes but not break the whole app for however long it'll take us to fully upgrade.
The only good upgrade route is to refactor the property hashes into proper classes. You can start that work and keep using your hash-based classes in the meantime, which will lighten the requirement to do it all at once.
If you have a limited number of "class" name:config pairs -- which you should for maintainability reasons -- then you can replace createClass with an implementation that does:
class Foo { ... }
class Bar { ... }
let classes = {'Foo': Foo, 'Bar': Bar};
function createClass(name, config) {
if (classes[name]) {
return classes[name];
}
// old impl
}
This will ignore the config if a "real" implementation exists, but keep using the legacy behavior if you haven't replaced the class. If it is, you can implement createClass more like:
function createClass(name, config) {
if (classes[name]) {
return new classes[name](config);
}
// old impl
}
and pass the config arguments into the class ctor. In this case, you may want to filter out function properties (methods) first, as the class probably implements them already. Something like:
function createClass(name, config) {
if (classes[name]) {
let fields = Object.keys(config).filter(key => {
return typeof config[key] !== 'function';
}).map(key => config[key]);
return new classes[name](fields);
}
// old impl
}