Unable to access private members when redefining method of class - javascript

When defining a private member using #, and then re-defining some member that uses this private member, you will find that you cannot use it anymore:
class Foo {
#secret = "Keyboard Cat";
method() {
console.log(this.#secret);
}
}
const target = Foo.prototype;
const key = "method";
const desc = Object.getOwnPropertyDescriptor(target, key);
const og = desc.value;
desc.value = function (...args) {
return og.apply(target, args);
};
Object.defineProperty(target, key, desc);
new Foo().method();
Uncaught TypeError: Cannot read private member #secret from an object whose class did not declare it
Why? All I have done is wrap around the original method in this case. Note that this example is a dramatic simplification of using decorators with TypeScript. How could I get around this while still being able to redefine and "change" the method?
Here's the same thing, but with a TypeScript decorator:
const Curse: MethodDecorator = (target, _, desc) => {
const og = desc.value as Function;
desc.value = function (...args: any[]) {
return og.apply(target, args);
} as any;
return desc;
};
class Foo {
#secret = "Keyboard Cat";
#Curse
method() {
console.log(this.#secret);
}
}
new Foo().method();
Playground

The mistake you made is to apply the method to the target, which is Foo.prototype, not to the new Foo instance via the this keyword:
class Foo {
#secret = "Keyboard Cat";
method() {
console.log(this.#secret);
}
}
const target = Foo.prototype;
const key = "method";
const desc = Object.getOwnPropertyDescriptor(target, key);
const orig = desc.value;
desc.value = function (...args) {
return orig.apply(this, args);
// ^^^^
};
Object.defineProperty(target, key, desc);
new Foo().method();
Foo.prototype does not have the #secret private field. You'll get the same error with
class Foo {
#secret = "Keyboard Cat";
method() {
console.log(this.#secret);
}
}
Foo.prototype.method();

Related

Is there a away to Bind a Proxy object to the ES6 class constructor?

In ES5^ I can create a proxy object and bind it to the other functions constructors, see what I want to do in the example below:
function X() {
this.x = `${this.anyAttr} it's a trap`;
}
function Y() {
this.y = `${this.anyAttr} do the barrel roll`;
}
function Z(...a) {
const proxy = new Proxy(this, {
get(target, key) {
if (target[key]) {
return Reflect.get(target, key);
}
return "FROM Z:";
}
});
X.apply(proxy, a);
Y.apply(proxy, a);
return proxy;
}
const obj = new Z();
console.log(obj.someAttr) // FROM Z:
console.log(obj.x) // FROM Z: it's a trap
console.log(obj.y) // FROM Z: do the barrel roll
I want to do the same thing, but with the ES6 Class syntax, but the apply method can't be used with ES6 classes, see the example below:
class X {
constructor() {
this.x = `${this.anyAttr} it's a trap`
}
}
class Y {
constructor() {
this.y = `${this.anyAttr} do the barrel roll`
}
}
function Z(...a) {
const proxy = new Proxy(this, {
get(target, key) {
if (target[key])
return Reflect.get(target, key)
return "FROM Z:"
}
})
X.apply(proxy, a) // Uncaught TypeError: Class constructor X cannot be invoked without 'new' at new Z
Y.apply(proxy, a) // Uncaught TypeError: Class constructor Y cannot be invoked without 'new' at new Z
return proxy;
}
const obj = new Z()
To be specific, I need to mix classes, but before construct the superclasses I need to link a proxy object to do some black magic, so...
Is there a way to do something similar to the first example but using ES6 Classes?
I'd recommend re-designing your mixins to be higher-order classes like this:
const X = (Base) => class extends Base {
constructor(...args) {
super(...args);
this.x = `${this.anyAttr} it's a trap`;
}
};
const Y = (Base) => class extends Base {
constructor(...args) {
super(...args);
this.y = `${this.anyAttr} do the barrel roll`;
}
};
const Z = Y(X(class {
constructor(...args) {
return new Proxy(this, {
get(target, key, receiver) {
if (Reflect.has(target, key, receiver))
return Reflect.get(target, key);
return "FROM Z:";
}
});
}
}));
const obj = new Z();
console.log(obj.someAttr); // FROM Z:
console.log(obj.x); // FROM Z: it's a trap
console.log(obj.y); // FROM Z: do the barrel roll

How to get getter/setter name in JavaScript/TypeScript?

Getting a function name is pretty straightforward:
const func1 = function() {}
const object = {
func2: function() {}
}
console.log(func1.name);
// expected output: "func1"
console.log(object.func2.name);
// expected output: "func2"
How can I get the string name of a getter/setter, though?
class Example {
get hello() {
return 'world';
}
}
const obj = new Example();
Important note:
I don't want to use a hard-coded string:
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(obj), 'hello')
But get the name, e.g.:
console.log(getGetterName(obj.hello))
// expected output: "hello"
That syntax sets the get function of the hello property descriptor so the name of the function will always be get you can check if the hello property has a get function on it's property descriptor with Object.getOwnPropertyDescriptor().
class Example {
get hello() {
return 'world';
}
}
/*
compiles to / runs as
var Example = (function () {
function Example() {
}
Object.defineProperty(Example.prototype, "hello", {
get: function () {
return 'world';
},
enumerable: true,
configurable: true
});
return Example;
}());
*/
const des = Object.getOwnPropertyDescriptor(Example.prototype, 'hello');
console.log(des.get.name); // get (will always be 'get')
// to check if 'hello' is a getter
function isGetter(name) {
const des = Object.getOwnPropertyDescriptor(Example.prototype, name);
return !!des && !!des.get && typeof des.get === 'function';
}
console.log(isGetter('hello')); // true
Sounds like this won't solve your ultimate issue but:
Object.getOwnPropertyDescriptor(Example.prototype, 'hello').get.name
100% answers the question "How to get getter/setter name in JavaScript/TypeScript?" and it will always be "get"
Edit:
Once you call obj.hello the getter is already called an all you have is the primitive result, but you may be able to use metadata on the property value its self.
function stringPropertyName() {
let _internal;
return (target, key) => {
Object.defineProperty(target, key, {
get: () => {
const newString = new String(_internal);
Reflect.defineMetadata('name', key, newString);
return newString;
},
set: value => {
_internal = value;
}
});
};
}
class Example1 {
#stringPropertyName()
hello = 'world';
}
const obj1 = new Example1();
console.log(Reflect.getMetadata('name', obj1.hello)); // hello
class Example2 {
_hello = 'world';
get hello() {
const newString = new String(this._hello);
Reflect.defineMetadata('name', 'hello', newString);
return newString;
}
set hello(value) {
this._hello = value;
}
}
const obj2 = new Example2();
console.log(Reflect.getMetadata('name', obj2.hello)); // hello
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.6.11/core.min.js"></script>

JS TS apply decorator to all methods / enumerate class methods

I would like to apply a decorator function to all methods within a class so I can replace:
class User {
#log
delete() {}
#log
create() {}
#log
update() {}
}
with
#log
class User {
delete() {}
create() {}
update() {}
}
Create a class decorator and enumerate the properties on the target's prototype.
For each property:
Get the property descriptor.
Ensure it's for a method.
Wrap the descriptor value in a new function that logs the information about the method call.
Redefine the modified property descriptor back to the property.
It's important to modify the property descriptor because you want to ensure your decorator will work well with other decorators that modify the property descriptor.
function log(target: Function) {
for (const propertyName of Object.keys(target.prototype)) {
const descriptor = Object.getOwnPropertyDescriptor(target.prototype, propertyName);
const isMethod = descriptor.value instanceof Function;
if (!isMethod)
continue;
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log("The method args are: " + JSON.stringify(args));
const result = originalMethod.apply(this, args);
console.log("The return value is: " + result);
return result;
};
Object.defineProperty(target.prototype, propertyName, descriptor);
}
}
Base Class Methods
If you want this to affect the base class methods as well, then you might want something along these lines:
function log(target: Function) {
for (const propertyName in target.prototype) {
const propertyValue = target.prototype[propertyName];
const isMethod = propertyValue instanceof Function;
if (!isMethod)
continue;
const descriptor = getMethodDescriptor(propertyName);
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log("The method args are: " + JSON.stringify(args));
const result = originalMethod.apply(this, args);
console.log("The return value is: " + result);
return result;
};
Object.defineProperty(target.prototype, propertyName, descriptor);
}
function getMethodDescriptor(propertyName: string): TypedPropertyDescriptor<any> {
if (target.prototype.hasOwnProperty(propertyName))
return Object.getOwnPropertyDescriptor(target.prototype, propertyName);
// create a new property descriptor for the base class' method
return {
configurable: true,
enumerable: true,
writable: true,
value: target.prototype[propertyName]
};
}
}
For whomever stumbles upon this in the future:
I took inspiration in David's answer and created my own version. I later made it into a npm package: https://www.npmjs.com/package/decorate-all
In OP's scenario, it would be used like this
#DecorateAll(log)
class User {
delete() {}
create() {}
update() {}
}
If you don't feel like pulling additional dependencies, here's a simplified version of #Papooch's implementation
function DecorateAll(decorator: MethodDecorator) {
return (target: any) => {
const descriptors = Object.getOwnPropertyDescriptors(target.prototype);
for (const [propName, descriptor] of Object.entries(descriptors)) {
const isMethod =
typeof descriptor.value == "function" &&
propName != "constructor";
if (!isMethod) {
continue;
}
decorator(target, propName, descriptor);
Object.defineProperty(target.prototype, propName, descriptor);
}
};
}
function Throttle(
target: any,
propertyKey: string | symbol,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function () {
console.log("throttle");
return original.call(this);
};
}
#DecorateAll(Throttle)
class SharedApiClient {
async fetch1() { }
async fetch2() { }
}

Class Decorator in Typescript

I'm trying to understand how class decorators work in Typescript when we wish to replace the constructor. I've seen this demo:
const log = <T>(originalConstructor: new(...args: any[]) => T) => {
function newConstructor(... args) {
console.log("Arguments: ", args.join(", "));
new originalConstructor(args);
}
newConstructor.prototype = originalConstructor.prototype;
return newConstructor;
}
#log
class Pet {
constructor(name: string, age: number) {}
}
new Pet("Azor", 12);
//Arguments: Azor, 12
Everything is understood but this line:
newConstructor.prototype = originalConstructor.prototype;
Why do we do that?
The classes like:
class Pet {
constructor(name: string, age: number) {}
dosomething() {
console.log("Something...");
}
}
Are compiled into functions when targeting ES5:
var Pet = (function () {
function Pet(name, age) {
}
Pet.prototype.dosomething = function () {
console.log("Something...");
};
return Pet;
}());
As you can seem when we use functions to define classes. The methods are added to the function's prototype.
This means that if you are going to create a new constructor (new function) you need to copy all the methods (the prototype) from the old object:
function logClass(target: any) {
// save a reference to the original constructor
const original = target;
// a utility function to generate instances of a class
function construct(constructor: any, args: any[]) {
const c: any = function () {
return constructor.apply(this, args);
};
c.prototype = constructor.prototype;
return new c();
}
// the new constructor behaviour
const newConstructor: any = function (...args: any[]) {
console.log("New: " + original.name);
return construct(original, args);
};
// copy prototype so intanceof operator still works
newConstructor.prototype = original.prototype;
// return new constructor (will override original)
return newConstructor;
}
You can learn more at "Decorators & metadata reflection in TypeScript: From Novice to Expert (Part I)"
Update
Please refer to https://github.com/remojansen/LearningTypeScript/tree/master/chapters/chapter_08 for a more recent version.

How to avoid hard coded this? in Decorators

I have read "How to implement a typescript decorator?" and multiple sources but there is something that i have nor been able to do with decorators.
class FooBar {
public foo(arg): void {
console.log(this);
this.bar(arg);
}
private bar(arg) : void {
console.log(this, "bar", arg);
}
}
If we invoke the function foo:
var foobar = new FooBar();
foobar.foo("test");
The object FooBar is logged in the console by console.log(this); in foo
The string "FooBar {foo: function, bar: function} bar test" is logged in the console by console.log(this, "bar", arg); in bar.
Now let's use a decorator:
function log(target: Function, key: string, value: any) {
return {
value: (...args: any[]) => {
var a = args.map(a => JSON.stringify(a)).join();
var result = value.value.apply(this, args); // How to avoid hard coded this?
var r = JSON.stringify(result);
console.log(`Call: ${key}(${a}) => ${r}`);
return result;
}
};
}
We use the same function but decorated:
class FooBar {
#log
public foo(arg): void {
console.log(this);
this.bar(arg);
}
#log
private bar(arg) : void {
console.log(this, "bar", arg);
}
}
And we invoke foo as we did before:
var foobarFoo = new FooBar();
foobarFooBar.foo("test");
The objectWindow is logged in the console by console.log(this); in foo
And bar is never invoked by foo because this.bar(arg); causes Uncaught TypeError: this.bar is not a function.
The problem is the hardcoded this inside the log decorator:
value.value.apply(this, args);
How can I conserve the original this value?
Don't use an arrow function. Use a function expression:
function log(target: Object, key: string, value: any) {
return {
value: function(...args: any[]) {
var a = args.map(a => JSON.stringify(a)).join();
var result = value.value.apply(this, args);
var r = JSON.stringify(result);
console.log(`Call: ${key}(${a}) => ${r}`);
return result;
}
};
}
That way it will use the function's this context instead of the value of this when log is called.
By the way, I would recommend editing the descriptor/value parameter and return that instead of overwriting it by returning a new descriptor. That way you keep the properties currently in the descriptor and won't overwrite what another decorator might have done to the descriptor:
function log(target: Object, key: string, descriptor: TypedPropertyDescriptor<any>) {
var originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
var a = args.map(a => JSON.stringify(a)).join();
var result = originalMethod.apply(this, args);
var r = JSON.stringify(result);
console.log(`Call: ${key}(${a}) => ${r}`);
return result;
};
return descriptor;
}
More details in this answer - See the "Bad vs Good" example under "Example - Without Arguments > Notes"
I believe you can use
var self = this;
in order to preserve the 'this' at that specific point. Then, just use self at the later point where you would have wanted that particular this

Categories

Resources