I'm looking for a way to freeze native ES6 Maps.
Object.freeze and Object.seal don't seem to work:
let myMap = new Map([["key1", "value1"]]);
// Map { 'key1' => 'value1' }
Object.freeze(myMap);
Object.seal(myMap);
myMap.set("key2", "value2");
// Map { 'key1' => 'value1', 'key2' => 'value2' }
Is this intended behavior since freeze freezes properties of objects and maps are no objects or might this be a bug / not implemented yet?
And yes I know, I should probably use Immutable.js, but is there any way to do this with native ES6 Maps?
There is not, you could write a wrapper to do that. Object.freeze locks an object's properties, but while Map instances are objects, the values they store are not properties, so freezing has no effect on them, just like any other class that has internal state hidden away.
In a real ES6 environment where extending builtins is supported (not Babel), you could do this:
class FreezableMap extends Map {
set(...args){
if (Object.isFrozen(this)) return this;
return super.set(...args);
}
delete(...args){
if (Object.isFrozen(this)) return false;
return super.delete(...args);
}
clear(){
if (Object.isFrozen(this)) return;
return super.clear();
}
}
If you need to work in ES5 environments, you could easily make a wrapper class for a Map rather than extending the Map class.
#loganfsmyth, your answer gave me an idea, what about this:
function freezeMap(myMap){
if(myMap instanceof Map) {
myMap.set = function(key){
throw('Can\'t add property ' + key + ', map is not extensible');
};
myMap.delete = function(key){
throw('Can\'t delete property ' + key + ', map is frozen');
};
myMap.clear = function(){
throw('Can\'t clear map, map is frozen');
};
}
Object.freeze(myMap);
}
This works perfectly for me :)
Updated with points from #Bergi in the comments:
var mapSet = function(key){
throw('Can\'t add property ' + key + ', map is not extensible');
};
var mapDelete = function(key){
throw('Can\'t delete property ' + key + ', map is frozen');
};
var mapClear = function(){
throw('Can\'t clear map, map is frozen');
};
function freezeMap(myMap){
myMap.set = mapSet;
myMap.delete = mapDelete;
myMap.clear = mapClear;
Object.freeze(myMap);
}
Since Map and Set objects store their elements in internal slots, freezing them won't make them immutable. No matter the syntax used to extend or modify a Map object, its internal slots will still be mutable via Map.prototype.set. Therefore, the only way to protect a map is to not expose it directly to untrusted code.
Possible solution: Creating a read-only view for your map
You could create a new Map-like object that exposes a read-only view of your Map. For example:
function mapView (map) {
return Object.freeze({
get size () { return map.size; },
[Symbol.iterator]: map[Symbol.iterator].bind(map),
clear () { throw new TypeError("Cannot mutate a map view"); } ,
delete () { throw new TypeError("Cannot mutate a map view"); },
entries: map.entries.bind(map),
forEach (callbackFn, thisArg) {
map.forEach((value, key) => {
callbackFn.call(thisArg, value, key, this);
});
},
get: map.get.bind(map),
has: map.has.bind(map),
keys: map.keys.bind(map),
set () { throw new TypeError("Cannot mutate a map view"); },
values: map.values.bind(map),
});
}
A couple things to keep in mind about such an approach:
The view object returned by this function is live: changes in the original Map will be reflected in the view. This doesn't matter if you don't keep any references to the original map in your code, otherwise you might want to pass a copy of the map to the mapView function instead.
Algorithms that expect Map-like objects should work with a map view, provided they don't ever try to apply a Map.prototype method on it. Since the object is not an actual Map with internal slots, applying a Map method on it would throw.
The content of the mapView cannot be inspected in dev tools easily.
Alternatively, one could define MapView as a class with a private #map field. This makes debugging easier as dev tools will let you inspect the map's content.
class MapView {
#map;
constructor (map) {
this.#map = map;
Object.freeze(this);
}
get size () { return this.#map.size; }
[Symbol.iterator] () { return this.#map[Symbol.iterator](); }
clear () { throw new TypeError("Cannot mutate a map view"); }
delete () { throw new TypeError("Cannot mutate a map view"); }
entries () { return this.#map.entries(); }
forEach (callbackFn, thisArg) {
this.#map.forEach((value, key) => {
callbackFn.call(thisArg, value, key, this);
});
}
get (key) { return this.#map.get(key); }
has (key) { return this.#map.has(key); }
keys () { return this.#map.keys(); }
set () { throw new TypeError("Cannot mutate a map view"); }
values () { return this.#map.values(); }
}
Hack: Creating a custom FreezableMap
Instead of simply allowing the creation of a read-only view, we could instead create our own FreezableMap type whose set, delete, and clear methods only work if the object is not frozen.
This is, in my honest opinion, a terrible idea. It takes an incorrect assumption (that frozen means immutable) and tries to make it a reality, producing code that will only reinforce that incorrect assumption. But it's still a fun thought experiment.
Closure version:
function freezableMap(...args) {
const map = new Map(...args);
return {
get size () { return map.size; },
[Symbol.iterator]: map[Symbol.iterator].bind(map),
clear () {
if (Object.isSealed(this)) {
throw new TypeError("Cannot clear a sealed map");
}
map.clear();
},
delete (key) {
if (Object.isSealed(this)) {
throw new TypeError("Cannot remove an entry from a sealed map");
}
return map.delete(key);
},
entries: map.entries.bind(map),
forEach (callbackFn, thisArg) {
map.forEach((value, key) => {
callbackFn.call(thisArg, value, key, this);
});
},
get: map.get.bind(map),
has: map.has.bind(map),
keys: map.keys.bind(map),
set (key, value) {
if (Object.isFrozen(this)) {
throw new TypeError("Cannot mutate a frozen map");
}
if (!Object.isExtensible(this) && !map.has(key)) {
throw new TypeError("Cannot add an entry to a non-extensible map");
}
map.set(key, value);
return this;
},
values: map.values.bind(map),
};
}
Class version:
class FreezableMap {
#map;
constructor (...args) {
this.#map = new Map(...args);
}
get size () { return this.#map.size; }
[Symbol.iterator] () { return this.#map[Symbol.iterator](); }
clear () {
if (Object.isSealed(this)) {
throw new TypeError("Cannot clear a sealed map");
}
this.#map.clear();
}
delete (key) {
if (Object.isSealed(this)) {
throw new TypeError("Cannot remove an entry from a sealed map");
}
return this.#map.delete(key);
}
entries () { return this.#map.entries(); }
forEach (callbackFn, thisArg) {
this.#map.forEach((value, key) => {
callbackFn.call(thisArg, value, key, this);
});
}
get (key) { return this.#map.get(key); }
has (key) { return this.#map.has(key); }
keys () { return this.#map.keys(); }
set (key, value) {
if (Object.isFrozen(this)) {
throw new TypeError("Cannot mutate a frozen map");
}
if (!Object.isExtensible(this) && !this.#map.has(key)) {
throw new TypeError("Cannot add an entry to a non-extensible map");
}
this.#map.set(key, value);
return this;
}
values () { return this.#map.values(); }
}
I hereby release this code to the public domain. Note that it has not been tested much, and it comes with no warranty.
Happy copy-pasting.
If anyone is looking for a TypeScript version of the accepted answer:
export type ReadonlyMap<K,V> = Omit<Map<K,V>, "set"| "delete"| "clear">
export function freeze<K, V>(map: Map<K, V>): ReadonlyMap<K, V> {
if (map instanceof Map) {
map.set = (key: K) => {
throw new Error(`Can't set property ${key}, map is not extensible`);
};
map.delete = (key: K) => {
throw new Error(`Can't delete property ${key}, map is not extensible`);
};
map.clear = () => {
throw new Error("Can't clear map, map is frozen");
};
}
return Object.freeze(map);
}
Sorry, I am not able to comment. I just wanted to add my typescript variant
const mapSet = function (key: unknown) {
throw "Can't add property " + key + ', map is not extensible';
};
const mapDelete = function (key: unknown) {
throw "Can't delete property " + key + ', map is frozen';
};
const mapClear = function () {
throw 'Can\'t clear map, map is frozen';
};
function freezeMap<T extends Map<K, V>, K, V>(myMap: T) {
myMap.set = mapSet;
myMap.delete = mapDelete;
myMap.clear = mapClear;
Object.freeze(myMap);
return myMap;
}
So applying ES6 make the code looks clearer. From my perspective :)
class FreezeMap extends Map {
/**
* #param {Map<number, any>} OriginalMap
* #return {Map<number, any>}
*/
constructor(OriginalMap) {
super();
OriginalMap.set = this.set.bind(OriginalMap);
OriginalMap.delete = this.delete.bind(OriginalMap);
OriginalMap.clear = this.clear.bind(OriginalMap);
Object.freeze(OriginalMap);
return OriginalMap;
};
set(key) {
throw new Error(`Can't add property ${key}, map is not extensible`);
};
delete(key) {
throw new Error(`Can't delete property ${key}, map is frozen`);
};
clear() {
throw new Error(`Can't clear map, map is frozen`);
};
}
a way to immune Map from changes by others
in class:
class ValueSlider {
static #slMapAsFunc = (function(_privateMap) {
this.get = Map.prototype.get.bind(_privateMap);
this.set = this.delete = this.clear = () => {};
this.has = Map.prototype.has.bind(_privateMap);
this.entries = Map.prototype.entries.bind(_privateMap);
this.forEach = Map.prototype.forEach.bind(_privateMap);
this.keys = Map.prototype.keys.bind(_privateMap);
this.values = Map.prototype.values.bind(_privateMap);
});
static #_sliderMap = new Map() ; // for use internally in this class
static #slMapAsFuncInst = new this.#slMapAsFunc( this.#_sliderMap );
/* for consumers */
static get sliderMap() {
return this.#slMapAsFuncInst;
}
constructor() {
const statics = this.constructor;
statics.#_sliderMap.set( nameInit, this); // add instance
this.value = 9;
}
}
for consumer
function c() {
/* this is not possible
Map.prototype.clear.apply( ValueSlider.sliderMap._privateMap, [] );
Map.prototype.clear.apply( ValueSlider.sliderMap, [] );
*/
ValueSlider.sliderMap.forEach( (instance, key, map) => {
/* this works */
console.log(`value is ${instance.value}`;
}
}
Related
This question already has answers here:
How to extend Function with ES6 classes?
(12 answers)
Closed 17 days ago.
My attempt:
class SetOnceDict extends Function {
constructor() {
super('key', 'return this.get(key);')
}
items = {}
add(key, value) {
if (!this.items.hasOwnProperty(key)) {
this.items[key] = value;
} else {
throw new Error(`Duplicate key ${key}`);
}
}
get(key) {
return this.items[key];
}
}
let dict = new SetOnceDict();
dict.add('one', 'foo');
dict.add('two', 'bar');
console.log(dict.items);
console.log(dict('one'));
I'd expect this to log
{ one: 'foo', two: 'bar' }
foo
instead it errors
return this.get(key);
^
TypeError: this.get is not a function
This works:
console.log(dict.bind(dict)('one'));
But why would it have to be bound to itself when it should already have those properties?
Surprisingly (?) neither super.bind(this); nor this.bind(this); in the constructor fix the problem.
How can I make an extension of function using ES6+ class syntax that has custom behaviour when called?
Surprisingly (?) neither super.bind(this); nor this.bind(this); in the constructor fix the problem.
You can call bind in constructor. But it will create a new function so you need to:
Copy own properties, because it is a new object
Return the result.
class SetOnceDict extends Function {
constructor() {
super('key', 'return this.get(key);')
const out = this.bind(this) // create binded version
Object.assign(out, this) // copy own properties
return out // replace
}
items = {}
add(key, value) {
if (!this.items.hasOwnProperty(key)) {
this.items[key] = value;
} else {
throw new Error(`Duplicate key ${key}`);
}
}
get(key) {
return this.items[key];
}
}
let dict = new SetOnceDict();
dict.add('one', 'foo');
dict.add('two', 'bar');
console.log(dict.items);
console.log(dict('one'));
Also you can use a Proxy :)
class SetOnceDict extends Function {
constructor() {
super('key', 'return this.get(key);')
return new Proxy(this, {
apply(target, thisArg, args) {
return target.call(target, ...args)
}
})
}
items = {}
add(key, value) {
if (!this.items.hasOwnProperty(key)) {
this.items[key] = value;
} else {
throw new Error(`Duplicate key ${key}`);
}
}
get(key) {
return this.items[key];
}
}
let dict = new SetOnceDict();
dict.add('one', 'foo');
dict.add('two', 'bar');
console.log(dict.items);
console.log(dict('one'));
I came up with another ugly way that works using Object.assign and bind:
class SetOnceDict extends Function {
constructor() {
super('key', 'return this.get(key);');
return Object.assign(this.bind(this), this);
}
items = {}
add(key, value) {
if (!this.items.hasOwnProperty(key)) {
this.items[key] = value;
} else {
throw new Error(`Duplicate key ${key}`);
}
}
get(key) {
return this.items[key];
}
}
let dict = new SetOnceDict();
dict.add('one', 'foo');
dict.add('two', 'bar');
console.log(dict.items);
console.log(dict('one'));
this does not actually refer to the function object itself within function bodies. To do that you need arguments.callee, although this is deprecated.
class SetOnceDict extends Function {
constructor() {
super('key', 'return arguments.callee.get(key);')
}
items = {}
add(key, value) {
if (!this.items.hasOwnProperty(key)) {
this.items[key] = value;
} else {
throw new Error(`Duplicate key ${key}`);
}
}
get(key) {
return this.items[key];
}
}
let dict = new SetOnceDict();
dict.add('one', 'foo');
dict.add('two', 'bar');
console.log(dict.items);
console.log(dict('one'));
I have a class File, and constructor that accept fullname parameter. How can i prevent changing of this? I tried it with setter and getter, but it doesnt work
class File {
constructor(fullName) {
this.fullName = fullName;
}
get fullname() {
return this.fullname;
}
set fullname(newValue) {
if (newValue) {
newValue = this.fullName;
}
}
}
let example = new File("example.txt");
example.fullName = "modified.txt";
console.log(example.fullName); // should be example.txt
You can create a readonly property with Object.defineProperty:
class File {
constructor(fullName) {
Object.defineProperty(this, 'fullName', {
enumerable: true,
writable: false, // not necessary (because default) but more explicit,
value: fullName,
});
}
}
let example = new File("example.txt");
example.fullName = "modified.txt";
console.log(example.fullName);
Note that assigning to the property would throw an error in strict mode.
If you want to be in strict mode but also want to silently ignore the assignment, you could take the getter/setter approach ~but you will still have to store the real value somewhere on the object, which means it could still be accessed and be modified if one knows which property to access.~ but it's a bit more evolved. You'd basically create a getter and setter for every instance, thus avoiding to have to store the original value on the object itself:
"use strict";
class File {
constructor(fullName) {
Object.defineProperty(this, 'fullName', {
enumerable: true,
set: value => {}, // ignore
get: () => fullName,
});
}
}
let example = new File("example.txt");
example.fullName = "modified.txt";
console.log(example.fullName);
Private properties, which are a relatively new feature of JavaScript, might make this a bit "nicer" (subjective):
class File {
#fullName;
constructor(fullName) {
this.#fullName = fullName;
}
get fullName() {
return this.#fullName;
}
set fullName(newValue) {
// ignore new value
}
}
let example = new File("example.txt");
example.fullName = "modified.txt";
console.log(example.fullName);
Using ES2020+ syntax:
class File {
#fullName = null;
constructor(fullName) {
this.fullName = fullName;
}
get fullName() {
return this.#fullName;
}
set fullName(newValue) {
this.#fullName ??= newValue;
}
}
let example = new File("example.txt");
example.fullName = "modified.txt";
console.log(example.fullName); // should be example.txt
There are several places that you misspelled fullName to fullname, mind the cases matter in JavaScript.
const self = {
element: document.querySelector(selector),
html: () => self.element,
on: (event, callback) => {
self.element.addEventListener(event, callback);
},
style: {
alignContent: (property) => {
return (property === null) ? self.element.style.alignContent : self.element.style.alignContent = property;
}
}
}
I am trying to make it so I have quick access to all CSS style properties with jQuery like selectors it should work as: select('h1').style.alignContent('center'), but the problem is that I would have to make a seperate function for each style property in order for this method to work, is there a way to solve this problem without duplicating a lot of code?
//Duplication example
color: (property) => {
return (property === null) ? self.element.style.color : self.element.style.color = property;
}
One way to do this is with a Proxy (mdn):
let elemWrapper = selector => {
let element = document.querySelector(selector);
return {
element,
html: () => element,
on: (event, callback) => {
element.addEventListener(event, callback);
},
style: new Proxy({}, {
get: (obj, prop) => {
// The user called a function named "prop"
// We need to return a function that sets the style property named "prop"
return cssValue => element.style[prop] = cssValue;
}
})
};
};
let bodyElem = elemWrapper('body');
bodyElem.style.backgroundColor('cyan');
Here to prove the concept I've set the body element's background colour using a dynamically named function.
The big downside to this approach is the poor performance of Proxies (an excellent read on Proxy performance is available here).
This means it may be quicker to simply compile a list of all css property names, and define a function for each (never using Proxies). The following code compiles all css property names, to serve as a starting point:
console.log(Object.keys(document.body.style));
You can use a Proxy to intercept all attempts to get a property.
let selector = '#test';
const self = {
element: document.querySelector(selector),
html: () => self.element,
on: (event, callback) => {
self.element.addEventListener(event, callback);
},
style: new Proxy(Object.create(null), {
get(target, prop, receiver) {
if (self.element.style.hasOwnProperty(prop)) {
return val => {
if (val != null) {
self.element.style[prop] = val;
} else {
return self.element.style[prop];
}
}
}
throw Error("No such property exists: " + prop);
}
})
};
self.style.color('red')
console.log("Color:", self.style.color());
<div id="test">
This is a test
</div>
You can also wrap this into a general function like so:
const getEnhancedElement = arg => {
const element = /Element/.test(Object.prototype.toString.call(arg)) ? arg
: document.querySelector(arg);//accept a HTMLElement or a selector
return {
element,
html: () => element,
on: (event, callback) => {
element.addEventListener(event, callback);
},
style: new Proxy(Object.create(null), {
get(target, prop) {
if (element.style.hasOwnProperty(prop)) {
return val => {
if (val != null) {//set value
element.style[prop] = val;
} else {//get value
return element.style[prop];
}
}
}
throw Error("No such property exists: " + prop);
}
})
};
};
let test = getEnhancedElement("#test");
test.style.color('red')
console.log("Color:", test.style.color());
test.style.textAlign('center');
<div id="test">
This is a test
</div>
I would have something like this:
style: {
chnageStyle: (propertyName, propertyVal) => {
return (propertyName === null) ? self.element.style[propertyName] : self.element.style[propertyName] = propertyVal;
}
}
Then you can call this:
style.changeStyle('alignContent','center');
style.changeStyle('color','orange');
I'm messing around with decorators for a bit, having an Angular background i'm trying to wrap my head around the HostListener decorator.
This is how far i got:
class Demo {
counter = 0;
#Listen("mousemove") onMouseMove(e?) {
console.log(this);
this.counter++;
}
}
export function Listen(name) {
return (target, key, descriptor) => {
window.addEventListener(name, oldValue.bind(target));
return descriptor;
};
}
new Demo();
This is more or less the implementation only problem is passing the target/this reference as target is not initialised.
Solved, i'm using Vue so this might not be everyones answer, basically what i'm doing is calling the function just once in Vue you can add a mixin and inside this mixin the beforeMount hook will be called, thus allowing me here to call it once.
Decorator updated code:
export function Listen(name) {
return function (target, key, descriptor) {
if (!target.subscriptions) target.subscriptions = [];
add(target, "Listen", key);
if (process.client) {
const oldValue = descriptor.value;
descriptor.value = function() {
target.subscriptions.push(
target.$eventManager.add(window, name, oldValue.bind(this))
);
};
return descriptor;
}
return descriptor;
};
}
const add = (target, name, functionName) => {
if(!target.decorators) target.decorators = {};
if(!target.decorators[name]) target.decorators[name] = [];
target.decorators[name].push(functionName);
};
Vue mixin:
Vue.mixin({
beforeMount: function() {
if(this.decorators && this.decorators.Listen) {
this.decorators.Listen.forEach(key => this[key]());
}
},
destroyed: function () {
this.$subscriptionManager.remove(this.subscriptions);
}
});
I'm trying to Proxy an inheritance structure from within a node module and allow the client to instantiate a new Class A. Currently when trying to access class B's parent methods I get a.parentMethod is not a function
handler.js ->
module.exports = {
get(target, key, receiver) {
return target.getAttribute(key)
},
set(target, key, value, receiver) {
return target.setAttribute(key, value)
}
}
A.js ->
const handler = require('handler')
class B {
constructor(data) {
this.data = data
}
parentMethod() {
... do stuff
}
}
class A extends B {
constructor(data){
super(data)
}
}
module.exports = function(data) {
return new Proxy(new A(data), handler)
}
////
const A = require('A')
var a = new A
a.parentMethod()
Where am I going wrong with this structure? I'm new to Proxy!
Thanks
EDIT -
Further context:
I'm trying to keep sets of properties in sync based on a valueSchema I have defined. When I set Artwork.title I need Artwork['Artwork Title'] to be updated with the same value. Likewise when I retrieve Artwork.title I get the value of Artwork['Artwork Title']. Hopefully this helps a bit. I'm stuck at the above error so I can't be sure what I've written actually works yet! I'm trying to debug why the function can't be found first...
class Instance {
constructor(data) {
this._valueAttributes = {}
}
setAttribute(key, value) {
if (this._isValueAttribute(key)) {
return this._getSetValueAttribute(key, value)
}
throw Error('Cannot set invalid property '+key+' on instance.')
}
getAttribute(key) {
if (this._isValueAttribute(key)) {
return this._getSetValueAttribute(key)
}
}
_getSetValueAttribute(key, value) {
let schemaKey = this._getSchemaKey(key)
if (_.isFunction(schemaKey)) {
return alias(data)
}
if (value === undefined) {
return this._valueAttributes[schemaKey]
}
return this._valueAttributes[schemaKey] = value
}
_isValueAttribute(key) {
return _.keys(this._valueSchema).indexOf(key) === -1
}
}
class Artwork extends Instance {
constructor() {
this._valueSchema = {
medium: 'Artwork Medium',
title: 'Artwork Title'
}
}
}
///
a = new Artwork
a.title = 'thing'
a['Artwork Medium'] = 'medium';
I need
a.title == a['Artwork Title']
a['Artwork Medium'] == a.medium
It's very likely I've royally screwed it all up. I've assumed that I can access __valueSchema on the child from the parent. Is this not possible?