Objects and arrays
editCreate objects and arrays
realm.CreateObject creates an empty object, and realm.CreateArray an array of values. Set their properties with SetProperty, or define them with DefineProperty to choose their attributes:
using var engine = new JsEngine();
var realm = engine.CreateRealm();
using (engine.Enter())
{
var settings = realm.CreateObject();
settings.SetProperty("theme", "dark");
settings.SetProperty("fontSize", 14);
settings.SetProperty("languages", realm.CreateArray(["en", "fr"]));
// A read-only property, which the scripts do not enumerate
settings.DefineProperty("version", JsPropertyDescriptor.Data("1.0", writable: false, enumerable: false, configurable: false));
realm.GlobalObject.SetProperty("settings", settings);
}
Assert.Equal("theme,fontSize,languages", realm.Execute("Object.keys(settings).join()").AsString());
Assert.Equal("TypeError", realm.Execute("'use strict'; try { settings.version = '2.0'; } catch (e) { e.name }").AsString());
Assert.Equal("1.0", realm.Execute("settings.version").AsString());- SetProperty assigns a property, as
obj.name = valuedoes. It returns false when the assignment fails, for instance on a read-only property. - DefineProperty takes a JsPropertyDescriptor:
JsPropertyDescriptor.Data(value, writable, enumerable, configurable)for a value,JsPropertyDescriptor.Accessor(getter, setter, enumerable, configurable)for a getter and a setter, which are functions (see Functions). It returns false when the property cannot be defined. CreateObject(prototype)creates an object with another prototype, or with none (null).realm.GlobalObjectis an object like the others: its properties are the global variables of the scripts.
Read the objects of a script
The objects returned by a script are read the same way:
using var engine = new JsEngine();
var realm = engine.CreateRealm();
var user = realm.Execute("({ name: 'Ada', roles: ['admin', 'dev'], [Symbol.for('id')]: 1 })").AsObject();
using (engine.Enter())
{
Assert.Equal("Ada", user.GetProperty("name").AsString());
// An array is a JsArray: its elements are the properties "0", "1"...
var roles = (JsArray)user.GetProperty("roles").AsObject();
Assert.Equal(2u, roles.Length);
Assert.Equal("dev", roles.GetProperty("1").AsString());
// The keys are strings or symbols
Assert.Equal(["name", "roles"], user.GetOwnPropertyKeys().Where(key => key.IsString).Select(key => key.AsString()));
// A missing property is undefined
Assert.True(user.GetProperty("email").IsUndefined);
Assert.False(user.ContainsProperty("email"));
}- GetProperty reads a property through the prototype chain, as
obj.namedoes: it calls getters, and the traps of aProxy. A missing property isundefined. - TryGetOwnProperty reads the descriptor of an own property without calling its getter.
- GetOwnPropertyKeys returns the keys of the own properties in the order of JavaScript: the array indexes, then the strings in creation order, then the symbols. It includes the properties that are not enumerable. The list is a snapshot: adding or deleting properties later does not change it.
- ContainsProperty tests a property of the object or of its prototypes, as
key in objdoes. - An array is a JsArray: Length is its length, and its elements are the properties
"0","1"... - DeleteProperty, GetPrototype, SetPrototype and PreventObjectExtensions do what
delete,Object.getPrototypeOf,Object.setPrototypeOfandObject.preventExtensionsdo.
Read and write the elements of an array
The indexer of JsArray reads and writes an element as array[index] does, and ToArray copies the elements:
using var engine = new JsEngine();
var realm = engine.CreateRealm();
var array = (JsArray)realm.Execute("const a = [10, , 30]; Object.defineProperty(a, 3, { get() { return this[0] * 4; }, enumerable: true }); a").AsObject();
using (engine.Enter())
{
// The indexer reads and writes the elements as array[index] does: a hole reads the prototype chain, a getter runs
Assert.Equal(10, array[0].AsNumber());
Assert.True(array[1].IsUndefined);
Assert.Equal(40, array[3].AsNumber());
// Writing past the end extends the array
array[5] = "six";
Assert.Equal(6u, array.Length);
// ToArray copies the elements: the holes are undefined
var elements = array.ToArray();
Assert.Equal("10,undefined,30,40,undefined,six", string.Join(',', elements.Select(element => element.ToString())));
}- The getter is [[Get]]: a hole, or an index past the end, reads the prototype chain, which usually gives
undefined. Getters and proxies run, so the indexer is used inside the scope of the engine. - The setter assigns as strict code does: it extends the array, and throws a
TypeError(JsException) when the element cannot be written, for instance in a frozen array. The largest index is 4,294,967,294. - ToArray reads the length once, then each element in order. A getter that changes the array during the copy changes the elements that are read next, not the number of elements.
Create properties on first use
A host that exposes many objects, few of which a script uses, can create them on first access:
using var engine = new JsEngine();
var realm = engine.CreateRealm();
var created = 0;
using (engine.Enter())
{
// The value is created the first time a script reads the property
realm.GlobalObject.DefineLazyDataProperty("catalog", () =>
{
created++;
return realm.CreateArray(["apple", "pear"]);
}, writable: false, enumerable: false, configurable: true);
}
realm.Execute("1 + 1");
Assert.Equal(0, created);
Assert.Equal("apple,pear", realm.Execute("catalog.join()").AsString());
Assert.Equal(1, created);DefineLazyAccessorProperty does the same for a getter and a setter. Each method has an overload that passes a state to a static factory, so the properties do not allocate a closure each. When you define hundreds of properties on an object, call ReserveProperties first.
Exchange data as JSON
To pass a whole tree of data, such as a .NET object serialized with System.Text.Json, call JSON.parse and JSON.stringify of the realm:
using var engine = new JsEngine();
var realm = engine.CreateRealm();
realm.Execute("""
function summarize(order) {
return { id: order.id, count: order.items.length, total: order.items.reduce((sum, item) => sum + item.price, 0) };
}
""");
using (engine.Enter())
{
var json = realm.GlobalObject.GetProperty("JSON").AsObject();
var parse = json.GetProperty("parse");
var stringify = json.GetProperty("stringify");
// .NET to JavaScript: parse a JSON text, e.g. the output of System.Text.Json
var order = engine.Call(parse, json, """{ "id": "A-12", "items": [{ "price": 5 }, { "price": 7.5 }] }""");
var summary = engine.Call(realm.GlobalObject.GetProperty("summarize"), JsValue.Undefined, order);
// JavaScript to .NET: a JSON text
Assert.Equal("""{"id":"A-12","count":2,"total":12.5}""", engine.Call(stringify, json, summary).AsString());
}A script can replace JSON or its methods: when the scripts are not trusted, read JSON.parse and JSON.stringify once, right after you create the realm and before any script runs.