Przeglądaj źródła

Export() and ExportTo() now support circular objects. Fixes #162, closes #188.

Dmitry Panov 5 lat temu
rodzic
commit
536f9d9465
18 zmienionych plików z 342 dodań i 93 usunięć
  1. 4 1
      README.md
  2. 6 3
      array.go
  3. 6 2
      array_sparse.go
  4. 1 1
      date.go
  5. 2 2
      func.go
  6. 75 6
      object.go
  7. 6 2
      object_args.go
  8. 1 1
      object_gomap.go
  9. 4 2
      object_gomap_reflect.go
  10. 2 3
      object_goreflect.go
  11. 1 1
      object_goslice.go
  12. 1 2
      object_goslice_reflect.go
  13. 2 2
      object_lazy.go
  14. 117 1
      object_test.go
  15. 1 1
      proxy.go
  16. 104 61
      runtime.go
  17. 1 1
      typedarrays.go
  18. 8 1
      value.go

+ 4 - 1
README.md

@@ -109,7 +109,10 @@ Exporting Values from JS
 ------------------------
 A JS value can be exported into its default Go representation using Value.Export() method.
 
-Alternatively it can be exported into a specific Go variable using Runtime.ExportTo() method.
+Alternatively it can be exported into a specific Go variable using [Runtime.ExportTo()](https://godoc.org/github.com/dop251/goja#Runtime.ExportTo) method.
+
+Within a single export operation the same Object will be represented by the same Go value (either the same map, slice or
+a pointer to the same struct). This includes circular objects and makes it possible to export them.
 
 Calling JS functions from Go
 ----------------------------

+ 6 - 3
array.go

@@ -490,14 +490,17 @@ func (a *arrayObject) deleteIdx(idx valueInt, throw bool) bool {
 	return a.baseObject.deleteStr(idx.string(), throw)
 }
 
-func (a *arrayObject) export() interface{} {
+func (a *arrayObject) export(ctx *objectExportCtx) interface{} {
+	if v, exists := ctx.get(a); exists {
+		return v
+	}
 	arr := make([]interface{}, a.length)
+	ctx.put(a, arr)
 	for i, v := range a.values {
 		if v != nil {
-			arr[i] = v.Export()
+			arr[i] = exportValue(v, ctx)
 		}
 	}
-
 	return arr
 }
 

+ 6 - 2
array_sparse.go

@@ -434,11 +434,15 @@ func (a *sparseArrayObject) sortLen() int64 {
 	return 0
 }
 
-func (a *sparseArrayObject) export() interface{} {
+func (a *sparseArrayObject) export(ctx *objectExportCtx) interface{} {
+	if v, exists := ctx.get(a); exists {
+		return v
+	}
 	arr := make([]interface{}, a.length)
+	ctx.put(a, arr)
 	for _, item := range a.items {
 		if item.value != nil {
-			arr[item.idx] = item.value.Export()
+			arr[item.idx] = exportValue(item.value, ctx)
 		}
 	}
 	return arr

+ 1 - 1
date.go

@@ -106,7 +106,7 @@ func (d *dateObject) toPrimitive() Value {
 	return d.toPrimitiveString()
 }
 
-func (d *dateObject) export() interface{} {
+func (d *dateObject) export(*objectExportCtx) interface{} {
 	if d.isSet() {
 		return d.time()
 	}

+ 2 - 2
func.go

@@ -32,7 +32,7 @@ type boundFuncObject struct {
 	wrapped *Object
 }
 
-func (f *nativeFuncObject) export() interface{} {
+func (f *nativeFuncObject) export(*objectExportCtx) interface{} {
 	return f.f
 }
 
@@ -164,7 +164,7 @@ func (f *funcObject) call(call FunctionCall, newTarget Value) Value {
 	return vm.pop()
 }
 
-func (f *funcObject) export() interface{} {
+func (f *funcObject) export(*objectExportCtx) interface{} {
 	return f.Call
 }
 

+ 75 - 6
object.go

@@ -180,6 +180,12 @@ func (p *PropertyDescriptor) complete() {
 	}
 }
 
+type objectExportCacheItem map[reflect.Type]interface{}
+
+type objectExportCtx struct {
+	cache map[objectImpl]interface{}
+}
+
 type objectImpl interface {
 	sortable
 	className() string
@@ -227,7 +233,7 @@ type objectImpl interface {
 	preventExtensions(throw bool) bool
 	enumerate() iterNextFunc
 	enumerateUnfiltered() iterNextFunc
-	export() interface{}
+	export(ctx *objectExportCtx) interface{}
 	exportType() reflect.Type
 	equal(objectImpl) bool
 	ownKeys(all bool, accum []Value) []Value
@@ -262,7 +268,7 @@ type primitiveValueObject struct {
 	pValue Value
 }
 
-func (o *primitiveValueObject) export() interface{} {
+func (o *primitiveValueObject) export(*objectExportCtx) interface{} {
 	return o.pValue.Export()
 }
 
@@ -946,13 +952,18 @@ func (o *baseObject) swap(i, j int64) {
 	o.val.self.setOwnIdx(jj, x, false)
 }
 
-func (o *baseObject) export() interface{} {
-	m := make(map[string]interface{})
-	for _, itemName := range o.ownKeys(false, nil) {
+func (o *baseObject) export(ctx *objectExportCtx) interface{} {
+	if v, exists := ctx.get(o); exists {
+		return v
+	}
+	keys := o.ownKeys(false, nil)
+	m := make(map[string]interface{}, len(keys))
+	ctx.put(o, m)
+	for _, itemName := range keys {
 		itemNameStr := itemName.String()
 		v := o.val.self.getStr(itemName.string(), nil)
 		if v != nil {
-			m[itemNameStr] = v.Export()
+			m[itemNameStr] = exportValue(v, ctx)
 		} else {
 			m[itemNameStr] = nil
 		}
@@ -1449,3 +1460,61 @@ func (o *guardedObject) deleteStr(name unistring.String, throw bool) bool {
 	}
 	return res
 }
+
+func (ctx *objectExportCtx) get(key objectImpl) (interface{}, bool) {
+	if v, exists := ctx.cache[key]; exists {
+		if item, ok := v.(objectExportCacheItem); ok {
+			r, exists := item[key.exportType()]
+			return r, exists
+		} else {
+			return v, true
+		}
+	}
+	return nil, false
+}
+
+func (ctx *objectExportCtx) getTyped(key objectImpl, typ reflect.Type) (interface{}, bool) {
+	if v, exists := ctx.cache[key]; exists {
+		if item, ok := v.(objectExportCacheItem); ok {
+			r, exists := item[typ]
+			return r, exists
+		} else {
+			if reflect.TypeOf(v) == typ {
+				return v, true
+			}
+		}
+	}
+	return nil, false
+}
+
+func (ctx *objectExportCtx) put(key objectImpl, value interface{}) {
+	if ctx.cache == nil {
+		ctx.cache = make(map[objectImpl]interface{})
+	}
+	if item, ok := ctx.cache[key].(objectExportCacheItem); ok {
+		item[key.exportType()] = value
+	} else {
+		ctx.cache[key] = value
+	}
+}
+
+func (ctx *objectExportCtx) putTyped(key objectImpl, typ reflect.Type, value interface{}) {
+	if ctx.cache == nil {
+		ctx.cache = make(map[objectImpl]interface{})
+	}
+	v, exists := ctx.cache[key]
+	if exists {
+		if item, ok := ctx.cache[key].(objectExportCacheItem); ok {
+			item[typ] = value
+		} else {
+			m := make(objectExportCacheItem, 2)
+			m[key.exportType()] = v
+			m[typ] = value
+			ctx.cache[key] = m
+		}
+	} else {
+		m := make(objectExportCacheItem)
+		m[typ] = value
+		ctx.cache[key] = m
+	}
+}

+ 6 - 2
object_args.go

@@ -127,12 +127,16 @@ func (a *argumentsObject) defineOwnPropertyStr(name unistring.String, descr Prop
 	return a.baseObject.defineOwnPropertyStr(name, descr, throw)
 }
 
-func (a *argumentsObject) export() interface{} {
+func (a *argumentsObject) export(ctx *objectExportCtx) interface{} {
+	if v, exists := ctx.get(a); exists {
+		return v
+	}
 	arr := make([]interface{}, a.length)
+	ctx.put(a, arr)
 	for i := range arr {
 		v := a.getIdx(valueInt(int64(i)), nil)
 		if v != nil {
-			arr[i] = v.Export()
+			arr[i] = exportValue(v, ctx)
 		}
 	}
 	return arr

+ 1 - 1
object_gomap.go

@@ -160,7 +160,7 @@ func (o *objectGoMapSimple) ownKeys(_ bool, accum []Value) []Value {
 	return accum
 }
 
-func (o *objectGoMapSimple) export() interface{} {
+func (o *objectGoMapSimple) export(*objectExportCtx) interface{} {
 	return o.data
 }
 

+ 4 - 2
object_gomap_reflect.go

@@ -19,7 +19,8 @@ func (o *objectGoMapReflect) init() {
 }
 
 func (o *objectGoMapReflect) toKey(n Value, throw bool) reflect.Value {
-	key, err := o.val.runtime.toReflectValue(n, o.keyType)
+	key := reflect.New(o.keyType).Elem()
+	err := o.val.runtime.toReflectValue(n, key, &objectExportCtx{})
 	if err != nil {
 		o.val.runtime.typeErrorResult(throw, "map key conversion error: %v", err)
 		return reflect.Value{}
@@ -95,7 +96,8 @@ func (o *objectGoMapReflect) getOwnPropIdx(idx valueInt) Value {
 }
 
 func (o *objectGoMapReflect) toValue(val Value, throw bool) (reflect.Value, bool) {
-	v, err := o.val.runtime.toReflectValue(val, o.valueType)
+	v := reflect.New(o.valueType).Elem()
+	err := o.val.runtime.toReflectValue(val, v, &objectExportCtx{})
 	if err != nil {
 		o.val.runtime.typeErrorResult(throw, "map value conversion error: %v", err)
 		return reflect.Value{}, false

+ 2 - 3
object_goreflect.go

@@ -216,12 +216,11 @@ func (o *objectGoReflect) _put(name string, val Value, throw bool) (has, ok bool
 				o.val.runtime.typeErrorResult(throw, "Cannot assign to a non-addressable or read-only property %s of a host object", name)
 				return true, false
 			}
-			vv, err := o.val.runtime.toReflectValue(val, v.Type())
+			err := o.val.runtime.toReflectValue(val, v, &objectExportCtx{})
 			if err != nil {
 				o.val.runtime.typeErrorResult(throw, "Go struct conversion error: %v", err)
 				return true, false
 			}
-			v.Set(vv)
 			return true, true
 		}
 	}
@@ -404,7 +403,7 @@ func (o *objectGoReflect) ownKeys(_ bool, accum []Value) []Value {
 	return accum
 }
 
-func (o *objectGoReflect) export() interface{} {
+func (o *objectGoReflect) export(*objectExportCtx) interface{} {
 	return o.origValue.Interface()
 }
 

+ 1 - 1
object_goslice.go

@@ -318,7 +318,7 @@ func (o *objectGoSlice) ownKeys(_ bool, accum []Value) []Value {
 	return accum
 }
 
-func (o *objectGoSlice) export() interface{} {
+func (o *objectGoSlice) export(*objectExportCtx) interface{} {
 	return *o.data
 }
 

+ 1 - 2
object_goslice_reflect.go

@@ -104,12 +104,11 @@ func (o *objectGoSliceReflect) putIdx(idx int, v Value, throw bool) bool {
 		}
 		o.grow(idx + 1)
 	}
-	val, err := o.val.runtime.toReflectValue(v, o.value.Type().Elem())
+	err := o.val.runtime.toReflectValue(v, o.value.Index(idx), &objectExportCtx{})
 	if err != nil {
 		o.val.runtime.typeErrorResult(throw, "Go type conversion error: %v", err)
 		return false
 	}
-	o.value.Index(idx).Set(val)
 	return true
 }
 

+ 2 - 2
object_lazy.go

@@ -235,10 +235,10 @@ func (o *lazyObject) enumerate() iterNextFunc {
 	return obj.enumerate()
 }
 
-func (o *lazyObject) export() interface{} {
+func (o *lazyObject) export(ctx *objectExportCtx) interface{} {
 	obj := o.create(o.val)
 	o.val.self = obj
-	return obj.export()
+	return obj.export(ctx)
 }
 
 func (o *lazyObject) exportType() reflect.Type {

+ 117 - 1
object_test.go

@@ -1,6 +1,9 @@
 package goja
 
-import "testing"
+import (
+	"reflect"
+	"testing"
+)
 
 func TestArray1(t *testing.T) {
 	r := &Runtime{}
@@ -125,6 +128,119 @@ func TestObjectAssign(t *testing.T) {
 	testScript1(TESTLIB+SCRIPT, _undefined, t)
 }
 
+func TestExportCircular(t *testing.T) {
+	vm := New()
+	o := vm.NewObject()
+	o.Set("o", o)
+	v := o.Export()
+	if m, ok := v.(map[string]interface{}); ok {
+		if reflect.ValueOf(m["o"]).Pointer() != reflect.ValueOf(v).Pointer() {
+			t.Fatal("Unexpected value")
+		}
+	} else {
+		t.Fatal("Unexpected type")
+	}
+
+	res, err := vm.RunString(`var a = []; a[0] = a;`)
+	if err != nil {
+		t.Fatal(err)
+	}
+	v = res.Export()
+	if a, ok := v.([]interface{}); ok {
+		if reflect.ValueOf(a[0]).Pointer() != reflect.ValueOf(v).Pointer() {
+			t.Fatal("Unexpected value")
+		}
+	} else {
+		t.Fatal("Unexpected type")
+	}
+}
+
+type test_s struct {
+	S *test_s1
+}
+type test_s1 struct {
+	S *test_s
+}
+
+func TestExportToCircular(t *testing.T) {
+	vm := New()
+	o := vm.NewObject()
+	o.Set("o", o)
+	var m map[string]interface{}
+	err := vm.ExportTo(o, &m)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	type K string
+	type T map[K]T
+	var m1 T
+	err = vm.ExportTo(o, &m1)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	type A []A
+	var a A
+	res, err := vm.RunString("var a = []; a[0] = a;")
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = vm.ExportTo(res, &a)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if &a[0] != &a[0][0] {
+		t.Fatal("values do not match")
+	}
+
+	o = vm.NewObject()
+	o.Set("S", o)
+	var s test_s
+	err = vm.ExportTo(o, &s)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if s.S.S != &s {
+		t.Fatalf("values do not match: %v, %v", s.S.S, &s)
+	}
+
+	type test_s2 struct {
+		S  interface{}
+		S1 *test_s2
+	}
+
+	var s2 test_s2
+	o.Set("S1", o)
+
+	err = vm.ExportTo(o, &s2)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if m, ok := s2.S.(map[string]interface{}); ok {
+		if reflect.ValueOf(m["S"]).Pointer() != reflect.ValueOf(m).Pointer() {
+			t.Fatal("Unexpected m.S")
+		}
+	} else {
+		t.Fatalf("Unexpected s2.S type: %T", s2.S)
+	}
+	if s2.S1 != &s2 {
+		t.Fatal("Unexpected s2.S1")
+	}
+
+	o1 := vm.NewObject()
+	o1.Set("S", o)
+	o1.Set("S1", o)
+	err = vm.ExportTo(o1, &s2)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if s2.S1.S1 != s2.S1 {
+		t.Fatal("Unexpected s2.S1.S1")
+	}
+}
+
 func BenchmarkPut(b *testing.B) {
 	v := &Object{}
 

+ 1 - 1
proxy.go

@@ -794,7 +794,7 @@ func (p *proxyObject) exportType() reflect.Type {
 	return proxyType
 }
 
-func (p *proxyObject) export() interface{} {
+func (p *proxyObject) export(*objectExportCtx) interface{} {
 	return Proxy{
 		proxy: p,
 	}

+ 104 - 61
runtime.go

@@ -1515,17 +1515,19 @@ func (r *Runtime) wrapReflectFunc(value reflect.Value) func(FunctionCall) Value
 			// actual set of variadic Go arguments. if that succeeds, break
 			// out of the loop.
 			if typ.IsVariadic() && len(call.Arguments) == nargs && i == nargs-1 {
-				if v, err := r.toReflectValue(a, typ.In(n)); err == nil {
+				v := reflect.New(typ.In(n)).Elem()
+				if err := r.toReflectValue(a, v, &objectExportCtx{}); err == nil {
 					in[i] = v
 					callSlice = true
 					break
 				}
 			}
-			var err error
-			in[i], err = r.toReflectValue(a, t)
+			v := reflect.New(t).Elem()
+			err := r.toReflectValue(a, v, &objectExportCtx{})
 			if err != nil {
-				panic(r.newError(r.global.TypeError, "Could not convert function call parameter %v to %v", a, t))
+				panic(r.newError(r.global.TypeError, "could not convert function call parameter %v to %v", a, t))
 			}
+			in[i] = v
 		}
 
 		var out []reflect.Value
@@ -1566,99 +1568,133 @@ func (r *Runtime) wrapReflectFunc(value reflect.Value) func(FunctionCall) Value
 	}
 }
 
-func (r *Runtime) toReflectValue(v Value, typ reflect.Type) (reflect.Value, error) {
+func (r *Runtime) toReflectValue(v Value, dst reflect.Value, ctx *objectExportCtx) error {
+	typ := dst.Type()
 	switch typ.Kind() {
 	case reflect.String:
-		return reflect.ValueOf(v.String()).Convert(typ), nil
+		dst.Set(reflect.ValueOf(v.String()).Convert(typ))
+		return nil
 	case reflect.Bool:
-		return reflect.ValueOf(v.ToBoolean()).Convert(typ), nil
+		dst.Set(reflect.ValueOf(v.ToBoolean()).Convert(typ))
+		return nil
 	case reflect.Int:
 		i, _ := toInt64(v)
-		return reflect.ValueOf(int(i)).Convert(typ), nil
+		dst.Set(reflect.ValueOf(int(i)).Convert(typ))
+		return nil
 	case reflect.Int64:
 		i, _ := toInt64(v)
-		return reflect.ValueOf(i).Convert(typ), nil
+		dst.Set(reflect.ValueOf(i).Convert(typ))
+		return nil
 	case reflect.Int32:
 		i, _ := toInt64(v)
-		return reflect.ValueOf(int32(i)).Convert(typ), nil
+		dst.Set(reflect.ValueOf(int32(i)).Convert(typ))
+		return nil
 	case reflect.Int16:
 		i, _ := toInt64(v)
-		return reflect.ValueOf(int16(i)).Convert(typ), nil
+		dst.Set(reflect.ValueOf(int16(i)).Convert(typ))
+		return nil
 	case reflect.Int8:
 		i, _ := toInt64(v)
-		return reflect.ValueOf(int8(i)).Convert(typ), nil
+		dst.Set(reflect.ValueOf(int8(i)).Convert(typ))
+		return nil
 	case reflect.Uint:
 		i, _ := toInt64(v)
-		return reflect.ValueOf(uint(i)).Convert(typ), nil
+		dst.Set(reflect.ValueOf(uint(i)).Convert(typ))
+		return nil
 	case reflect.Uint64:
 		i, _ := toInt64(v)
-		return reflect.ValueOf(uint64(i)).Convert(typ), nil
+		dst.Set(reflect.ValueOf(uint64(i)).Convert(typ))
+		return nil
 	case reflect.Uint32:
 		i, _ := toInt64(v)
-		return reflect.ValueOf(uint32(i)).Convert(typ), nil
+		dst.Set(reflect.ValueOf(uint32(i)).Convert(typ))
+		return nil
 	case reflect.Uint16:
 		i, _ := toInt64(v)
-		return reflect.ValueOf(uint16(i)).Convert(typ), nil
+		dst.Set(reflect.ValueOf(uint16(i)).Convert(typ))
+		return nil
 	case reflect.Uint8:
 		i, _ := toInt64(v)
-		return reflect.ValueOf(uint8(i)).Convert(typ), nil
+		dst.Set(reflect.ValueOf(uint8(i)).Convert(typ))
+		return nil
 	}
 
 	if typ == typeCallable {
 		if fn, ok := AssertFunction(v); ok {
-			return reflect.ValueOf(fn), nil
+			dst.Set(reflect.ValueOf(fn))
+			return nil
 		}
 	}
 
 	if typ == typeValue {
-		return reflect.ValueOf(v), nil
+		dst.Set(reflect.ValueOf(v))
+		return nil
 	}
 
 	if typ == typeObject {
 		if obj, ok := v.(*Object); ok {
-			return reflect.ValueOf(obj), nil
+			dst.Set(reflect.ValueOf(obj))
+			return nil
 		}
 	}
 
 	et := v.ExportType()
 	if et == nil || et == reflectTypeNil {
-		return reflect.Zero(typ), nil
+		dst.Set(reflect.Zero(typ))
+		return nil
 	}
 	if et.AssignableTo(typ) {
-		return reflect.ValueOf(v.Export()), nil
+		dst.Set(reflect.ValueOf(exportValue(v, ctx)))
+		return nil
 	} else if et.ConvertibleTo(typ) {
-		return reflect.ValueOf(v.Export()).Convert(typ), nil
+		dst.Set(reflect.ValueOf(exportValue(v, ctx)).Convert(typ))
+		return nil
 	}
 
 	if typ == typeTime && et.Kind() == reflect.String {
 		tme, ok := dateParse(v.String())
 		if !ok {
-			return reflect.Value{}, fmt.Errorf("Could not convert string %v to %v", v, typ)
+			return fmt.Errorf("could not convert string %v to %v", v, typ)
 		}
-		return reflect.ValueOf(tme), nil
+		dst.Set(reflect.ValueOf(tme))
+		return nil
 	}
 
 	switch typ.Kind() {
 	case reflect.Slice:
 		if o, ok := v.(*Object); ok {
 			if o.self.className() == classArray {
+				if v, exists := ctx.getTyped(o.self, typ); exists {
+					dst.Set(reflect.ValueOf(v))
+					return nil
+				}
 				l := int(toLength(o.self.getStr("length", nil)))
-				s := reflect.MakeSlice(typ, l, l)
-				elemTyp := typ.Elem()
+				if dst.IsNil() || dst.Len() != l {
+					dst.Set(reflect.MakeSlice(typ, l, l))
+				}
+				s := dst
+				ctx.putTyped(o.self, typ, s.Interface())
 				for i := 0; i < l; i++ {
 					item := o.self.getIdx(valueInt(int64(i)), nil)
-					itemval, err := r.toReflectValue(item, elemTyp)
+					err := r.toReflectValue(item, s.Index(i), ctx)
 					if err != nil {
-						return reflect.Value{}, fmt.Errorf("Could not convert array element %v to %v at %d: %s", v, typ, i, err)
+						return fmt.Errorf("could not convert array element %v to %v at %d: %w", v, typ, i, err)
 					}
-					s.Index(i).Set(itemval)
 				}
-				return s, nil
+				return nil
 			}
 		}
 	case reflect.Map:
 		if o, ok := v.(*Object); ok {
-			m := reflect.MakeMap(typ)
+			if v, exists := ctx.getTyped(o.self, typ); exists {
+				dst.Set(reflect.ValueOf(v))
+				return nil
+			}
+			if dst.IsNil() {
+				dst.Set(reflect.MakeMap(typ))
+			}
+			m := dst
+			ctx.putTyped(o.self, typ, m.Interface())
 			keyTyp := typ.Key()
 			elemTyp := typ.Elem()
 			needConvertKeys := !reflect.ValueOf("").Type().AssignableTo(keyTyp)
@@ -1666,9 +1702,10 @@ func (r *Runtime) toReflectValue(v Value, typ reflect.Type) (reflect.Value, erro
 				var kv reflect.Value
 				var err error
 				if needConvertKeys {
-					kv, err = r.toReflectValue(itemName, keyTyp)
+					kv = reflect.New(keyTyp).Elem()
+					err = r.toReflectValue(itemName, kv, ctx)
 					if err != nil {
-						return reflect.Value{}, fmt.Errorf("Could not convert map key %s to %v", itemName.String(), typ)
+						return fmt.Errorf("could not convert map key %s to %v", itemName.String(), typ)
 					}
 				} else {
 					kv = reflect.ValueOf(itemName.String())
@@ -1676,9 +1713,10 @@ func (r *Runtime) toReflectValue(v Value, typ reflect.Type) (reflect.Value, erro
 
 				ival := o.get(itemName, nil)
 				if ival != nil {
-					vv, err := r.toReflectValue(ival, elemTyp)
+					vv := reflect.New(elemTyp).Elem()
+					err := r.toReflectValue(ival, vv, ctx)
 					if err != nil {
-						return reflect.Value{}, fmt.Errorf("Could not convert map value %v to %v at key %s", ival, typ, itemName.String())
+						return fmt.Errorf("could not convert map value %v to %v at key %s", ival, typ, itemName.String())
 					}
 					m.SetMapIndex(kv, vv)
 				} else {
@@ -1686,11 +1724,17 @@ func (r *Runtime) toReflectValue(v Value, typ reflect.Type) (reflect.Value, erro
 				}
 
 			}
-			return m, nil
+			return nil
 		}
 	case reflect.Struct:
 		if o, ok := v.(*Object); ok {
-			s := reflect.New(typ).Elem()
+			t := reflect.PtrTo(typ)
+			if v, exists := ctx.getTyped(o.self, t); exists {
+				dst.Set(reflect.ValueOf(v).Elem())
+				return nil
+			}
+			s := dst
+			ctx.putTyped(o.self, t, s.Addr().Interface())
 			for i := 0; i < typ.NumField(); i++ {
 				field := typ.Field(i)
 				if ast.IsExported(field.Name) {
@@ -1706,35 +1750,34 @@ func (r *Runtime) toReflectValue(v Value, typ reflect.Type) (reflect.Value, erro
 					}
 
 					if v != nil {
-						vv, err := r.toReflectValue(v, field.Type)
+						err := r.toReflectValue(v, s.Field(i), ctx)
 						if err != nil {
-							return reflect.Value{}, fmt.Errorf("Could not convert struct value %v to %v for field %s: %s", v, field.Type, field.Name, err)
-
+							return fmt.Errorf("could not convert struct value %v to %v for field %s: %w", v, field.Type, field.Name, err)
 						}
-						s.Field(i).Set(vv)
 					}
 				}
 			}
-			return s, nil
+			return nil
 		}
 	case reflect.Func:
 		if fn, ok := AssertFunction(v); ok {
-			return reflect.MakeFunc(typ, r.wrapJSFunc(fn, typ)), nil
+			dst.Set(reflect.MakeFunc(typ, r.wrapJSFunc(fn, typ)))
+			return nil
 		}
 	case reflect.Ptr:
-		elemTyp := typ.Elem()
-		v, err := r.toReflectValue(v, elemTyp)
-		if err != nil {
-			return reflect.Value{}, err
+		if o, ok := v.(*Object); ok {
+			if v, exists := ctx.getTyped(o.self, typ); exists {
+				dst.Set(reflect.ValueOf(v))
+				return nil
+			}
 		}
-
-		ptrVal := reflect.New(v.Type())
-		ptrVal.Elem().Set(v)
-
-		return ptrVal, nil
+		if dst.IsNil() {
+			dst.Set(reflect.New(typ.Elem()))
+		}
+		return r.toReflectValue(v, dst.Elem(), ctx)
 	}
 
-	return reflect.Value{}, fmt.Errorf("could not convert %v to %v", v, typ)
+	return fmt.Errorf("could not convert %v to %v", v, typ)
 }
 
 func (r *Runtime) wrapJSFunc(fn Callable, typ reflect.Type) func(args []reflect.Value) (results []reflect.Value) {
@@ -1748,7 +1791,11 @@ func (r *Runtime) wrapJSFunc(fn Callable, typ reflect.Type) func(args []reflect.
 		res, err := fn(_undefined, jsArgs...)
 		if err == nil {
 			if typ.NumOut() > 0 {
-				results[0], err = r.toReflectValue(res, typ.Out(0))
+				v := reflect.New(typ.Out(0)).Elem()
+				err = r.toReflectValue(res, v, &objectExportCtx{})
+				if err == nil {
+					results[0] = v
+				}
 			}
 		}
 
@@ -1771,18 +1818,14 @@ func (r *Runtime) wrapJSFunc(fn Callable, typ reflect.Type) func(args []reflect.
 }
 
 // ExportTo converts a JavaScript value into the specified Go value. The second parameter must be a non-nil pointer.
+// Exporting to an interface{} results in a value of the same type as Export() would produce.
 // Returns error if conversion is not possible.
 func (r *Runtime) ExportTo(v Value, target interface{}) error {
 	tval := reflect.ValueOf(target)
 	if tval.Kind() != reflect.Ptr || tval.IsNil() {
 		return errors.New("target must be a non-nil pointer")
 	}
-	vv, err := r.toReflectValue(v, tval.Elem().Type())
-	if err != nil {
-		return err
-	}
-	tval.Elem().Set(vv)
-	return nil
+	return r.toReflectValue(v, tval, &objectExportCtx{})
 }
 
 // GlobalObject returns the global object.

+ 1 - 1
typedarrays.go

@@ -848,7 +848,7 @@ func (o *arrayBufferObject) exportType() reflect.Type {
 	return arrayBufferType
 }
 
-func (o *arrayBufferObject) export() interface{} {
+func (o *arrayBufferObject) export(*objectExportCtx) interface{} {
 	return ArrayBuffer{
 		buf: o,
 	}

+ 8 - 1
value.go

@@ -721,7 +721,7 @@ func (o *Object) baseObject(*Runtime) *Object {
 }
 
 func (o *Object) Export() interface{} {
-	return o.self.export()
+	return o.self.export(&objectExportCtx{})
 }
 
 func (o *Object) ExportType() reflect.Type {
@@ -951,6 +951,13 @@ func (s *valueSymbol) hash(*maphash.Hash) uint64 {
 	return uint64(s.h)
 }
 
+func exportValue(v Value, ctx *objectExportCtx) interface{} {
+	if obj, ok := v.(*Object); ok {
+		return obj.self.export(ctx)
+	}
+	return v.Export()
+}
+
 func newSymbol(s valueString) *valueSymbol {
 	r := &valueSymbol{
 		desc: asciiString("Symbol(").concat(s).concat(asciiString(")")),