function deserialize(obj) {
        let deserialized = null;
        let queue = [{item: obj, target: null}];
        let objectMap = new Map();

        /* Instead of making this recursive, use a queue holding the objects to be
         * deserialized. Each item in the queue has the following properties:
         *
         * item - The input item to be deserialised.
         *
         * target - For members of a collection, a wrapper around the
         * output collection. This has a ``type`` field which is the
         * name of the collection type, and a ``value`` field which is
         * the actual output collection. For primitives, this is null.
         *
         * targetName - For object members, the property name on the
         * output object. For maps, "key" if the item is a key in the output map,
         * or "value" if it's a value in the output map.
         */
        while (queue.length > 0) {
            const {item, target, targetName} = queue.shift();
            const {type, value, objectId} = item;
            let result;
            let newTarget;
            if (objectId !== undefined && value === undefined) {
                result = objectMap.get(objectId);
            } else {
                switch(type) {
                case "undefined":
                    result = undefined;
                    break;
                case "null":
                    result = null;
                    break;
                case "string":
                case "boolean":
                    result = value;
                    break;
                case "number":
                    if (typeof value === "string") {
                        switch(value) {
                        case "NaN":
                            result = NaN;
                            break;
                        case "-0":
                            result = -0;
                            break;
                        case "+Infinity":
                            result = Number.POSITIVE_INFINITY;
                            break;
                        case "-Infinity":
                            result = Number.NEGATIVE_INFINITY;
                            break;
                        default:
                            throw new Error(`Unexpected number value "${value}"`);
                        }
                    } else {
                        result = value;
                    }
                    break;
                case "bigint":
                    result = BigInt(value);
                    break;
                case "function":
                    result = new Function("...args", `return (${value}).apply(null, args)`);
                    break;
                case "remoteobject":
                    let remote = new RemoteObject(value.type, value.objectId);
                    let local = remote.toLocal();
                    if (local !== null) {
                        result = local;
                    } else {
                        result = remote;
                    }
                    break;
                case "sendchannel":
                    result = new SendChannel(value);
                    break;
                case "regexp":
                    result = new RegExp(value.pattern, value.flags);
                    break;
                case "date":
                    result = new Date(value);
                    break;
                case "error":
                    // The item.value.type property is the name of the error constructor.
                    // If we have a constructor with the same name in the current realm,
                    // construct an instance of that type, otherwise use a generic Error
                    // type.
                    if (item.value.type in self &&
                        typeof self[item.value.type] === "function") {
                        result = new self[item.value.type](item.value.message);
                    } else {
                        result = new Error(item.value.message);
                    }
                    result.name = item.value.name;
                    result.lineNumber = item.value.lineNumber;
                    result.columnNumber = item.value.columnNumber;
                    result.fileName = item.value.fileName;
                    result.stack = item.value.stack;
                    break;
                case "array":
                    result = [];
                    newTarget = {type, value: result};
                    for (let child of value) {
                        queue.push({item: child, target: newTarget});
                    }
                    break;
                case "set":
                    result = new Set();
                    newTarget = {type, value: result};
                    for (let child of value) {
                        queue.push({item: child, target: newTarget});
                    }
                    break;
                case "object":
                    result = {};
                    newTarget = {type, value: result};
                    for (let [targetName, child] of Object.entries(value)) {
                        queue.push({item: child, target: newTarget, targetName});
                    }
                    break;
                case "map":
                    result = new Map();
                    newTarget = {type, value: result};
                    for (let [key, child] of value) {
                        queue.push({item: key, target: newTarget, targetName: "key"});
                        queue.push({item: child, target: newTarget, targetName: "value"});
                    }
                    break;
                default:
                    throw new TypeError(`Can't deserialize object of type ${type}`);
                }
                if (objectId !== undefined) {
                    objectMap.set(objectId, result);
                }
            }

            if (target === null) {
                if (deserialized !== null) {
                    throw new Error(`Tried to deserialized a non-root output value without a target`
                                    ` container object.`);
                }
                deserialized = result;
            } else {
                switch(target.type) {
                case "array":
                    target.value.push(result);
                    break;
                case "set":
                    target.value.add(result);
                    break;
                case "object":
                    target.value[targetName] = result;
                    break;
                case "map":
                    // For maps the same target wrapper is shared between key and value.
                    // After deserializing the key, set the `key` property on the target
                    // until we come to the value.
                    if (targetName === "key") {
                        target.key = result;
                    } else {
                        target.value.set(target.key, result);
                    }
                    break;
                default:
                    throw new Error(`Unknown target type ${target.type}`);
                }
            }
        }
        return deserialized;
    }
Deserialize an object from a JSON-compatible representation.

For details on the serialized representation see serialize().

@param {Object} obj - The value to be deserialized.
@returns {Any} - The deserialized value.