-
-
Notifications
You must be signed in to change notification settings - Fork 33.9k
Description
What is the problem this feature will solve?
When a class extends another class in JavaScript using the standard class syntax, the derived class must inherit the prototype chain from the super class and invoke the super class constructor within the derived constructor.
However classes defined with Node API behave as pre-ES6 classes (those declared with function). Class inheritance with pre-ES6 syntax is difficult, especially when invoking the super constructor, because there is no super() syntax. Workaround for this was to use SuperClass.apply(this, args) within the derived class constructor, but this only works if SuperClass is also declared with function and does not strictly check for new.target. If SuperClass is declared with the class syntax, this workaround would fail.
Reflect.construct was introduced in ES6 to address this issue, by accepting a custom value for new.target. However the Node API equivalent function napi_new_instance does not support this feature. Without new.target, everything works for the derived class but it also prevents other classes from extending it correctly.
Example Case:
var Base = ArrayBuffer;
function Derived(arg1, arg2) {
const _this = Reflect.construct(Base, [ arg1 ], new.target);
this.prop1 = arg2;
this.prop2 = 40;
return _this;
}
Object.setPrototypeOf(Derived.prototype, Base.prototype);
class SecondDerived extends Derived {
#id = "foo";
get id() { return this.#id; }
}
const obj = new SecondDerived(2, 3);
console.log(obj.byteLength, obj.prop2); // 2 40
console.log(obj.id); // `undefined` if `new.target` is unset in `Reflect.construct`The current workaround in Node API for derived classes is to manually get Reflect.construct from the global scope and invoke it with value returned by napi_get_new_target, but this approach is slow as it requires multiple Node API call and can be unstable if other JS code modified values on the global scope.
Example Workaround:
napi_value call_super(napi_env env, napi_value super, size_t argc, napi_value* argv, napi_callback_info info) noexcept {
napi_value _new_target;
napi_get_new_target(env, info, &_new_target);
if (_new_target == nullptr)
return nullptr;
napi_value _undefined;
napi_get_undefined(env, &_undefined);
napi_value _value;
napi_get_global(env, &_value);
napi_get_named_property(env, _value, "Reflect", &_value);
napi_get_named_property(env, _value, "construct", &_value);
napi_value args;
napi_create_array_with_length(env, argc, &args);
for (size_t i = 0; i < argc; i++)
napi_set_element(env, args, i, argv[i]);
napi_value reflect_args[] = {
super,
args,
_new_target
};
napi_call_function(env, _undefined, _value, 3, reflect_args, &_value);
return _value;
}
napi_value DerivedClass_constructor(napi_env env, napi_callback_info info) __THROW {
napi_value _this = call_super(env, SuperClass, argc, argv, info);
if (_this == nullptr) {
napi_throw_type_error(env, nullptr, "Class constructor invoked without 'new'.");
return nullptr;
}
// init
return _this;
}What is the feature you are proposing to solve the problem?
Create a new API function napi_new_instance_with_new_target to match the behavior of Reflect.construct in JS code.
What alternatives have you considered?
- Make the existing
napi_new_instancefunction accept an additionalnapi_valueargument fornew.target. -- This could cause ABI issues with existing binaries. - Create a new API function
napi_call_superto simulate thesuper()call in JS code. -- This might offer better performance but could also require some low-level hooks with the underlying engine, as classes defined with Node API are functions, not real classes.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status
Status