Skip to content

Better support for super constructor call within derived classes in Node API #60829

@ruochenjia

Description

@ruochenjia

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_instance function accept an additional napi_value argument for new.target. -- This could cause ABI issues with existing binaries.
  • Create a new API function napi_call_super to simulate the super() 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

No one assigned

    Labels

    feature requestIssues that request new features to be added to Node.js.node-apiIssues and PRs related to the Node-API.

    Type

    No type

    Projects

    Status

    Awaiting Triage

    Status

    Need Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions