Extend Function in JavaScript with ES6+ class syntax [duplicate] - javascript

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

Related

How can I make a subclass access private fields from the superclass in javascript?

I am trying to make a stack and queue classes, but I cant make the data field private without not being able to use inheritance.
I get an Uncaught SyntaxError: Private field '#data' must be declared in an enclosing class error every time I try.
how can I have the subclasses inherit the private field? code below:
class Datalist {
#data
constructor() {
this.#data = Array.from(arguments)
return this.#data
}
valueOf() {
return this.#data
}
get size() {
return this.#data.length
}
peek() {
if (this.size > 0) {
return this.#data[0]
} else {
return null
}
}
}
class Queue extends Datalist {
constructor() {
super(arguments)
}
enqueue() {
this.#data = this.#data.concat(arguments)
}
dequeue() {
return this.#data.shift()
}
}
class Stack extends Datalist {
constructor() {
super(arguments)
this.#data = this.#data.reverse()
}
push() {
this.#data = this.#data.splice(0, 0, Array.from(...arguments).reverse)
}
pop() {
return this.#data.shift()
}
}
A possible workaround which keeps the approach of extended classes and prototypal methods together with private fields and protected data could be based on WeakMap because one can take advantage of working with a single shared reference for each instantiation regardless of the actual inheritance.
const privateListLookup = new WeakMap;
const getPrivateList = reference =>
privateListLookup.get(reference);
const getClone = value =>
(typeof structuredClone === 'function')
? structuredClone(value)
: JSON.parse(JSON.stringify(value));
// const getCopy = value => [...value];
class DataList {
constructor(...list) {
// enable shared privacy via an instance's
// `this` reference and a weak map.
privateListLookup.set(this, list);
}
valueOf() {
// ensure data protection by not exposing
// the private `list` reference directly.
return getClone(getPrivateList(this));
// // make a decision, clone or shallow copy.
// return getCopy(getPrivateList(this));
}
toString() {
return String(getPrivateList(this));
}
// toJSON() {
// return JSON.stringify(getPrivateList(this));
// }
get size() {
return getPrivateList(this).length;
}
peek() {
return (this.size > 0)
// ? getPrivateList(this).at(0)
? getPrivateList(this)[0]
: null;
}
}
class Queue extends DataList {
constructor(...args) {
super(...args);
}
enqueue(...args) {
getPrivateList(this).push(...args);
}
dequeue() {
return getPrivateList(this).shift();
}
}
class Stack extends DataList {
constructor(...args) {
super(...args);
getPrivateList(this).reverse();
}
push(...args) {
getPrivateList(this).push(...args);
}
pop() {
return getPrivateList(this).pop();
}
}
const queue = new Queue(...['the', 'quick', 'brown', 'fox']);
const stack = new Stack('jumps', 'over', 'the', 'lazy', 'dog');
console.log({
queue: queue.valueOf(),
stack: stack.valueOf(),
});
console.log({
queue: queue.toString(),
stack: stack.toString(),
});
console.log(
'queue.enqueue(stack.pop()) ...'
);
queue.enqueue(stack.pop());
console.log({
queue: queue.toString(),
stack: stack.toString(),
});
console.log(
'queue.enqueue(stack.pop(), stack.pop()) ...'
);
queue.enqueue(stack.pop(), stack.pop());
console.log({
queue: queue.toString(),
stack: stack.toString(),
});
console.log(
'stack.peek() ...', stack.peek()
)
console.log(
'stack.push(queue.dequeue(), queue.dequeue()) ...'
);
stack.push(queue.dequeue(), queue.dequeue());
console.log({
queue: queue.toString(),
stack: stack.toString(),
});
console.log({
queue: queue.valueOf(),
stack: stack.valueOf(),
});
.as-console-wrapper { min-height: 100%!important; top: 0; }

For ... in not yielding methods

Given the following:
export class MyClass {
public dataA = 0
private dataB = 123
public myMethod(): any {
return {
test: 'true'
}
}
constructor() {
for (const propOrMethod in this) {
console.log({propOrMethod})
}
}
}
const myInst = new MyClass()
I run this with ts-node index.ts and all i get is:
{ propOrMethod: 'dataA' }
{ propOrMethod: 'dataB' }
With no reference to myMethod. I would like to iterate over all the methods of my class, but they don't appear to exist
for..in iterates over all enumerable properties of the instance and up the prototype chain. But normal methods in a class are not enumerable:
class MyClass {
myMethod() {
return {
test: 'true'
};
}
}
console.log(Object.getOwnPropertyDescriptor(MyClass.prototype, 'myMethod').enumerable);
So it doesn't get iterated over.
If you want to iterate over non-enumerable properties as well, use Object.getOwnPropertyNames (which iterates over the object's own property names, so you'll need to do so recursively if you want all property names anywhere in the prototype chain):
const recurseLog = obj => {
for (const name of Object.getOwnPropertyNames(obj)) {
console.log(name);
}
const proto = Object.getPrototypeOf(obj);
if (proto !== Object.prototype) recurseLog(proto);
};
class MyClass {
dataA = 0;
dataB = 123;
constructor() {
recurseLog(this);
}
myMethod() {
return {
test: 'true'
};
}
}
const myInst = new MyClass();
You could also make the method enumerable:
class MyClass {
dataA = 0;
dataB = 123;
constructor() {
for (const propOrMethod in this) {
console.log({propOrMethod})
}
}
myMethod() {
return {
test: 'true'
};
}
}
Object.defineProperty(MyClass.prototype, 'myMethod', { enumerable: true, value: MyClass.prototype.myMethod });
const myInst = new MyClass();
Or assign the method after the class definition:
class MyClass {
dataA = 0;
dataB = 123;
constructor() {
for (const propOrMethod in this) {
console.log({propOrMethod})
}
}
}
MyClass.prototype.myMethod = () => ({ test: 'true' });
const myInst = new MyClass();
Or assign it to the instance in the constructor:
class MyClass {
dataA = 0;
dataB = 123;
constructor() {
this.myMethod = this.myMethod;
for (const propOrMethod in this) {
console.log({propOrMethod})
}
}
myMethod() {
return {
test: 'true'
};
}
}
const myInst = new MyClass();

How to define the getter function of an array in javascript Object.defineProperty?

I expect that I can get the list by the a.list, but this code does not work except using original value return.
function myclass() {
this._v = {
list: []
};
}
Object.defineProperty(myclass.prototype, 'list', {
get: function() {
// return this._v.list;
return this._v.list.map(val => {
console.log('val', val);
return val;
});
}
});
var a = new myclass();
a.list.push('abc');
console.log(a.list);
The getter is always returning a new empty array when called. When you push to it, there are no observable effects, because the array that's being changed isn't the array on the object.
You need to get a reference to the _v.list array so that you can push to it, which you can do either by reference it directly
function myclass() {
this._v = {
list: []
};
}
Object.defineProperty(myclass.prototype, 'list', {
get: function() {
return this._v.list.map(val => {
return val;
});
}
});
var a = new myclass();
a._v.list.push('abc');
console.log(a.list);
Or add a different method that returns the _v.list array:
function myclass() {
this._v = {
list: []
}
}
myclass.prototype.getArr = function() {
return this._v.list;
}
Object.defineProperty(myclass.prototype, 'list', {
get: function() {
return this._v.list.map(val => {
return val;
});
}
});
var a = new myclass();
a.getArr().push('abc');
console.log(a.list);
Maybe you wanted to push the elements in the _v.list and retrieve them from new list on myclass.
function myclass() {
this._v = {
list: []
};
}
Object.defineProperty(myclass.prototype, 'list', {
get: function() {
// return this._v.list;
return this._v.list.map(val => {
return val;
})
}
});
var a = new myclass();
a._v.list.push('abc');
console.log(a.list)
In your code you have overridden get method for list which is taking all its elements from _v.list but while pushing elements to list you are not pushing them to _v.list. That's why you are getting empty array which is _v.list not list.
'use strict';
function myclass() {
this._v = {
list: []
};
}
Object.defineProperty(myclass.prototype, 'list', {
get: function() {
// return this._v.list;
console.log('list', this._v.list);
return this._v.list.map(val => {
return val;
});
},
set: function(newVal) {
this._v.list.push(newVal);
}
});
const a = new myclass();
console.log(a);
a.list = 'abc';
console.log(a.list);

Is there a way to freeze an ES6 Map?

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}`;
}
}

Is it possible to Proxy an extended class in javascript

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?

Categories