-
Notifications
You must be signed in to change notification settings - Fork 1
JsObject and Properties
Roger Johansson edited this page Jan 14, 2026
·
1 revision
How JavaScript objects and property descriptors work in Asynkron.JsEngine.
flowchart TB
subgraph JsObject["JsObject"]
Storage["Storage<br/>(Dictionary)"]
Descriptors["Descriptors<br/>(Dictionary)"]
Proto["Prototype"]
end
subgraph PropertyAccess["Property Access"]
Get["TryGetProperty"]
Set["SetProperty"]
Define["DefineProperty"]
end
subgraph Chain["Prototype Chain"]
Obj((Object))
ObjProto((Object.prototype))
Null((null))
Obj --> ObjProto --> Null
end
PropertyAccess --> JsObject
JsObject --> Chain
File: JsTypes/JsObject.cs
public sealed class JsObject : IDictionary<string, object?>, IJsObjectLike, IAsJsValue
{
// Lazily allocated state
private JsObjectState? _state;
// Cached JsValue wrapper (avoids repeated struct creation)
private readonly JsValue _cachedJsValue;
// Prototype references
public JsObject? Prototype { get; private set; }
public IJsPropertyAccessor? PrototypeAccessor { get; private set; }
// Extensibility control
public bool IsExtensible { get; private set; } = true;
public bool IsFrozen { get; private set; }
public bool IsSealed { get; private set; }
}
// Internal state - only allocated when needed
private sealed class JsObjectState
{
public Dictionary<string, JsValue> Storage { get; } = new();
public Dictionary<string, PropertyDescriptor> Descriptors { get; } = new();
}-
Lazy state allocation -
_stateonly created when first property is set -
Cached JsValue -
_cachedJsValuecreated once in constructor, reused viaIAsJsValue -
Dual prototype references -
Prototype(JsObject) for fast path,PrototypeAccessor(interface) for flexibility
File: JsTypes/PropertyDescriptor.cs
public sealed class PropertyDescriptor
{
// Data descriptor properties
public JsValue JsValue { get; set; }
public bool Writable { get; set; } = true;
// Common properties
public bool Enumerable { get; set; } = true;
public bool Configurable { get; set; } = true;
// Accessor descriptor properties
public IJsCallable? Get { get; set; }
public IJsCallable? Set { get; set; }
// Tracking which properties were explicitly set
public bool HasValue { get; set; }
public bool HasWritable { get; set; }
public bool HasEnumerable { get; set; }
public bool HasConfigurable { get; set; }
public bool HasGet { get; set; }
public bool HasSet { get; set; }
}flowchart TB
Desc["PropertyDescriptor"]
Desc --> Data["Data Descriptor<br/>(HasValue or HasWritable)"]
Desc --> Accessor["Accessor Descriptor<br/>(HasGet or HasSet)"]
Desc --> Generic["Generic Descriptor<br/>(neither)"]
Data --> DataProps["value, writable<br/>enumerable, configurable"]
Accessor --> AccProps["get, set<br/>enumerable, configurable"]
public bool IsDataDescriptor => HasValue || HasWritable;
public bool IsAccessorDescriptor => HasGet || HasSet;
public bool IsGenericDescriptor => !IsAccessorDescriptor && !IsDataDescriptor;public bool TryGetProperty(string name, out JsValue value)
{
// Fast path: direct storage access (no descriptors, no virtual providers)
if (_virtualPropertyProvider is null &&
!(_state?.Descriptors.ContainsKey(name) ?? false))
{
if (TryGetJsValue(name, out value))
return true;
// Check prototype chain
if (Prototype is { } protoObj)
return protoObj.TryGetProperty(name, out value);
value = JsValue.Undefined;
return false;
}
// Slow path: full lookup with descriptors
return TryGetPropertyInternalJsValue(name, out value);
}public void SetProperty(string name, JsValue value)
{
// Check for accessor (getter/setter)
if (TryGetAccessor(name, out _, out var setter) && setter is not null)
{
setter.Invoke([value], this);
return;
}
// Check descriptor
var descriptor = GetOwnPropertyDescriptor(name);
if (descriptor is not null && !descriptor.Writable)
throw ThrowTypeError($"Cannot assign to read only property '{name}'");
// Set value
State.Storage[name] = value;
}public void DefineProperty(string name, PropertyDescriptor descriptor)
{
// Check extensibility
if (!IsExtensible && !HasOwnProperty(name))
throw ThrowTypeError("Cannot define property on non-extensible object");
// Validate descriptor changes
var existing = GetOwnPropertyDescriptor(name);
if (existing is not null && !existing.Configurable)
{
// Non-configurable: limited changes allowed
ValidateDescriptorChange(existing, descriptor);
}
// Apply descriptor
State.Descriptors[name] = descriptor;
if (descriptor.HasValue)
State.Storage[name] = descriptor.JsValue;
}flowchart TB
subgraph Lookup["Property Lookup: obj.foo"]
O1["Check obj own properties"]
O2["Check obj.[[Prototype]]"]
O3["Check prototype's [[Prototype]]"]
O4["null -> undefined"]
end
O1 -->|"not found"| O2
O2 -->|"not found"| O3
O3 -->|"not found"| O4
public void SetPrototype(IJsPropertyAccessor? candidate)
{
MarkMutated();
PrototypeAccessor = candidate;
Prototype = candidate as JsObject; // Fast path reference
}const parent = { foo: 1 };
const child = Object.create(parent);
child.bar = 2;
child.foo; // 1 (inherited from parent)
child.bar; // 2 (own property)
child.baz; // undefined (not found in chain)const obj = {
_value: 0,
get value() { return this._value; },
set value(v) { this._value = v; }
};// Define accessor property
obj.DefineProperty("value", new PropertyDescriptor
{
Get = getterCallable,
Set = setterCallable,
Enumerable = true,
Configurable = true
});public bool TryGetProperty(string name, JsValue receiver, out JsValue value)
{
if (TryGetAccessor(name, out var getter, out _) && getter is not null)
{
value = getter.Invoke([], receiver); // Call with receiver as 'this'
return true;
}
// ... data property lookup
}public void PreventExtensions()
{
MarkMutated();
IsExtensible = false;
}public void Seal()
{
PreventExtensions();
IsSealed = true;
// Make all properties non-configurable
foreach (var key in State.Storage.Keys)
{
if (State.Descriptors.TryGetValue(key, out var desc))
desc.Configurable = false;
else
State.Descriptors[key] = new PropertyDescriptor
{
Value = this[key],
Writable = true,
Enumerable = true,
Configurable = false
};
}
}public void Freeze()
{
Seal();
IsFrozen = true;
// Make all data properties non-writable
foreach (var desc in State.Descriptors.Values)
{
if (desc.IsDataDescriptor)
desc.Writable = false;
}
}Per ECMAScript spec:
- Integer indices (ascending numeric order)
- String keys (insertion order)
- Symbol keys (insertion order)
public IEnumerable<string> GetEnumerablePropertyNames()
{
// Own enumerable properties
foreach (var key in State.Storage.Keys)
{
var desc = GetOwnPropertyDescriptor(key);
if (desc?.Enumerable != false)
yield return key;
}
// Prototype chain (non-shadowed)
if (Prototype is not null)
{
foreach (var key in Prototype.GetEnumerablePropertyNames())
{
if (!HasOwnProperty(key))
yield return key;
}
}
}for (const key in obj) {
// Iterates enumerable properties (own + inherited)
}Object.keys(obj); // Own enumerable string keys
Object.values(obj); // Own enumerable values
Object.entries(obj); // Own enumerable [key, value] pairsSome objects have internal slots for special behavior:
// Promise slot
private JsPromise? _promiseSlot;
internal void SetPromiseSlot(JsPromise promise)
{
_promiseSlot = promise;
}
internal bool TryGetPromiseSlot(out JsPromise? promise)
{
promise = _promiseSlot;
return promise is not null;
}class Foo {
#privateField = 42;
}Private fields are stored with special key prefixes and brand checking.
For objects that compute properties dynamically:
internal interface IVirtualPropertyProvider
{
bool TryGetOwnProperty(string name, out JsValue value, out PropertyDescriptor? descriptor);
IEnumerable<string> GetOwnPropertyNames();
}Used by:
- Array (index properties)
- String (character access)
- TypedArray (element access)
- Arguments object
// Preferred - no boxing
obj.SetProperty("foo", jsValue);
obj.TryGetProperty("foo", out JsValue value);
// Avoid - causes boxing
obj["foo"] = someObject;
var val = obj["foo"];// Define multiple properties efficiently
obj.DefineProperties(new Dictionary<string, PropertyDescriptor>
{
["a"] = new PropertyDescriptor { Value = 1 },
["b"] = new PropertyDescriptor { Value = 2 },
["c"] = new PropertyDescriptor { Value = 3 }
});// Set prototype before adding properties
var obj = new JsObject();
obj.SetPrototype(prototype); // First
obj.SetProperty("foo", value); // Then add properties- JsValue System - Value representation
- Standard Library Architecture - Object constructor
- JsEnvironment & Slots - Scope objects