Skip to content

JsObject and Properties

Roger Johansson edited this page Jan 14, 2026 · 1 revision

JsObject and Properties

How JavaScript objects and property descriptors work in Asynkron.JsEngine.


Overview

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
Loading

JsObject Structure

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();
}

Key Design Points

  1. Lazy state allocation - _state only created when first property is set
  2. Cached JsValue - _cachedJsValue created once in constructor, reused via IAsJsValue
  3. Dual prototype references - Prototype (JsObject) for fast path, PrototypeAccessor (interface) for flexibility

Property Descriptor

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; }
}

Descriptor Types

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"]
Loading
public bool IsDataDescriptor => HasValue || HasWritable;
public bool IsAccessorDescriptor => HasGet || HasSet;
public bool IsGenericDescriptor => !IsAccessorDescriptor && !IsDataDescriptor;

Property Access

Get Property

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);
}

Set Property

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;
}

Define Property

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;
}

Prototype Chain

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
Loading

Setting Prototype

public void SetPrototype(IJsPropertyAccessor? candidate)
{
    MarkMutated();
    PrototypeAccessor = candidate;
    Prototype = candidate as JsObject;  // Fast path reference
}

Inheritance Example

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)

Accessor Properties

Getters and Setters

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
});

Invoking Accessors

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
}

Extensibility Control

Object.preventExtensions()

public void PreventExtensions()
{
    MarkMutated();
    IsExtensible = false;
}

Object.seal()

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
            };
    }
}

Object.freeze()

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;
    }
}

Property Enumeration

Enumeration Order

Per ECMAScript spec:

  1. Integer indices (ascending numeric order)
  2. String keys (insertion order)
  3. 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-in Loop

for (const key in obj) {
    // Iterates enumerable properties (own + inherited)
}

Object.keys() / Object.values() / Object.entries()

Object.keys(obj);     // Own enumerable string keys
Object.values(obj);   // Own enumerable values
Object.entries(obj);  // Own enumerable [key, value] pairs

Special Slots

Internal Slots

Some 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;
}

Private Fields

class Foo {
    #privateField = 42;
}

Private fields are stored with special key prefixes and brand checking.


Virtual Property Providers

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

Performance Tips

1. Use JsValue Methods

// Preferred - no boxing
obj.SetProperty("foo", jsValue);
obj.TryGetProperty("foo", out JsValue value);

// Avoid - causes boxing
obj["foo"] = someObject;
var val = obj["foo"];

2. Batch Property Definitions

// 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 }
});

3. Prototype Assignment Order

// Set prototype before adding properties
var obj = new JsObject();
obj.SetPrototype(prototype);  // First
obj.SetProperty("foo", value);  // Then add properties

See Also

Clone this wiki locally