Node.JS BaseObject
A frequently recurring situation is that a JavaScript object and a C++ object need to be tied together. BaseObject is the main abstraction for that in Node.js, and most classes that are associated with JavaScript objects are subclasses of it.
This is what you can see in the Node.js C++ documentation. In simple terms, BaseObject is the glue between C++ and JS lands.

Class Definition
Looking into header file, we can get some insights about the class:
enum InternalFields { kEmbedderType, kSlot, kInternalFieldCount };
// Associates this object with `object`. It uses the 1st internal field for
// that, and in particular aborts if there is no such field.
// This is the designated constructor.
BaseObject(Realm* realm, v8::Local<v8::Object> object);
// Convenient constructor for constructing BaseObject in the principal realm.
inline BaseObject(Environment* env, v8::Local<v8::Object> object);
~BaseObject() override;
BaseObject() = delete;
It has two constructors, one that takes a Realm* and another that takes an Environment*. The second one is a convenience constructor for constructing BaseObject in the principal realm.
The default constructor is deleted, it enforces the use of one of the two constructors above. It makes sense, creating a BaseObject without associating it with a JS object would be pointless.
Internal Fields
Each Object can have a number of internal fields. They can be used to store C++ pointers or other data and are not directly accessible from JavaScript.
It’s important to note that the number of internal fields must be set when creating the object template using SetInternalFieldCount. If it’s not set, the internal fields won’t be available.
You can see this when creating objects. If one creates an object on the fly, without an ObjectTemplate, it won’t have any internal fields.
v8::Local<v8::Object> obj = v8::Object::New(isolate);
std::cout << "On the Fly object's internal field count: "
        << object_on_thefly->InternalFieldCount() << std::endl;
// Output: On the Fly object's internal field count: 0
But if one creates an object from a template that has internal fields set, it will have them.
v8::Local<v8::ObjectTemplate> obj_template = v8::ObjectTemplate::New(isolate);
obj_template->SetInternalFieldCount(1); // Set 1 internal field
v8::Local<v8::Object> obj_from_template = obj_template->NewInstance(context).ToLocalChecked();
std::cout << "Template object's internal field count: "
        << obj_from_template->InternalFieldCount() << std::endl;
// Output: Template object's internal field count: 1
Once you have an object with internal fields, you can set them using SetInternalField and SetAlignedPointerInInternalField. The former is used to set a Data, while the latter is used to set a raw pointer.
Local<v8::ObjectTemplate> obj_tmpl = v8::ObjectTemplate::New(isolate);
obj_tmpl->SetInternalFieldCount(2);
auto obj_from_template = obj_tmpl->NewInstance(context).ToLocalChecked();
Local<String> message =
    String::NewFromUtf8(isolate, "InternalField0").ToLocalChecked();
obj_from_template->SetInternalField(0, message);
obj_from_template->SetAlignedPointerInInternalField(
    1, static_cast<void*>(const_cast<char*>("InternalField1")));
For retrieving the values, you can use GetInternalField and GetAlignedPointerFromInternalField.
auto v = obj_from_template->GetInternalField(0).As<v8::Value>();
std::string value = Utf8Value(isolate, v).ToString(); // This is a Node.js thing, just a helper
// std::string value = *String::Utf8Value(isolate, v); // This would work too; It is using V8 API v8::String::Utf8Value
std::cout << "Value in internal field 0: " << value << std::endl;
std::cout << "Value in internal field 1: "
        << static_cast<const char*>(
                obj_from_template->GetAlignedPointerFromInternalField(1))
        << std::endl;
// Output
// Value in internal field 0: InternalField0
// Value in internal field 1: InternalField1
NOTE: While I used ToLocalChecked() this is not recommended for Node.js codebase. You should use ToLocal() and check the return value.
BaseObject’s Internal Fields
From InternalFields enum we can see that BaseObject has at least two internal fields: kEmbedderType and kSlot.
enum InternalFields { kEmbedderType, kSlot, kInternalFieldCount };
// kEmbeddertype = 0
// kSlot = 1
// kInternalFieldCount = 2
In the constructor, we can see how it uses kSlot to store the pointer to the BaseObject instance.
BaseObject::BaseObject(Realm* realm, Local<Object> object)
    : persistent_handle_(realm->isolate(), object), realm_(realm) {
  CHECK_EQ(false, object.IsEmpty());
  // Ensure the object has enough internal fields.
  CHECK_GE(object->InternalFieldCount(), BaseObject::kInternalFieldCount);
  // Store the pointer to this BaseObject instance in the object's internal field.
  SetInternalFields(realm->isolate_data(), object, static_cast<void*>(this));
  realm->TrackBaseObject(this);
}
The SetInternalFields is defined at src/base_object-inl.h and sets the kEmbedderType and kSlot internal fields.
void BaseObject::SetInternalFields(IsolateData* isolate_data,
                                   v8::Local<v8::Object> object,
                                   void* slot) {
  TagBaseObject(isolate_data, object);
  object->SetAlignedPointerInInternalField(BaseObject::kSlot, slot);
}
The constructor that receives an Environment* just calls the one above, passing the principal realm.
BaseObject::BaseObject(Environment* env, v8::Local<v8::Object> object)
    : BaseObject(env->principal_realm(), object) {
}
How To Create a BaseObject
While you can create a BaseObject directly, there are helper functions to make it easier. The most used one is
MakeBaseObject, defined at src/base_object-inl.h.
You define your class like this:
class MyClass : public BaseObject {
    public:
        MyClass(Environment* env, v8::Local<v8::Object> object)
            : BaseObject(env, object) {
        }
        static BaseObjectPtr<MyClass> Create(Environment* env) {
            auto obj_tmpl = v8::ObjectTemplate::New(isolate);
            obj_tmpl->SetInternalFieldCount(2);
            auto obj_from_template = obj_tmpl->NewInstance(context).ToLocalChecked();
            return MakeBaseObject<MyClass>(env, obj_from_template);
        }
}
Then you can create an instance of MyClass like this:
BaseObjectPtr<MyClass> my_class_instance = MyClass::Create(env);
Notice that MakeBaseObject will create the MyClass instance and associate it with the JS object by storing the pointer in the internal field kSlot. Also, it returns a BaseObjectPtr<MyClass>, which acts like a smart pointer.
There is also a MakeWeakBaseObject function that creates a weak reference to the BaseObject. This is useful when you
want the C++ object to be garbage collected when there are no more references to it from JavaScript.
See more here.