|
@@ -1444,19 +1444,10 @@ func (r *Runtime) ClearInterrupt() {
|
|
|
ToValue converts a Go value into a JavaScript value of a most appropriate type. Structural types (such as structs, maps
|
|
|
and slices) are wrapped so that changes are reflected on the original value which can be retrieved using Value.Export().
|
|
|
|
|
|
-WARNING! There are two very important caveats to bear in mind when modifying wrapped Go structs, maps and
|
|
|
-slices.
|
|
|
+WARNING! These wrapped Go values do not behave in the same way as native ECMAScript values. If you plan to modify
|
|
|
+them in ECMAScript, bear in mind the following caveats:
|
|
|
|
|
|
-1. If a slice is passed by value (not as a pointer), resizing the slice does not reflect on the original
|
|
|
-value. Moreover, extending the slice may result in the underlying array being re-allocated and copied.
|
|
|
-For example:
|
|
|
-
|
|
|
- a := []interface{}{1}
|
|
|
- vm.Set("a", a)
|
|
|
- vm.RunString(`a.push(2); a[0] = 0;`)
|
|
|
- fmt.Println(a[0]) // prints "1"
|
|
|
-
|
|
|
-2. If a regular JavaScript Object is assigned as an element of a wrapped Go struct, map or array, it is
|
|
|
+1. If a regular JavaScript Object is assigned as an element of a wrapped Go struct, map or array, it is
|
|
|
Export()'ed and therefore copied. This may result in an unexpected behaviour in JavaScript:
|
|
|
|
|
|
m := map[string]interface{}{}
|
|
@@ -1468,7 +1459,74 @@ Export()'ed and therefore copied. This may result in an unexpected behaviour in
|
|
|
`)
|
|
|
fmt.Println(m["obj"].(map[string]interface{})["test"]) // prints "false"
|
|
|
|
|
|
-Non-addressable structs, slices and arrays get copied (as if they were passed as a function parameter, by value).
|
|
|
+2. Be careful with nested non-pointer compound types (structs, slices and arrays) if you modify them in
|
|
|
+ECMAScript. Better avoid it at all if possible. One of the fundamental differences between ECMAScript and Go is in
|
|
|
+the former all Objects are references whereas in Go you can have a literal struct or array. Consider the following
|
|
|
+example:
|
|
|
+
|
|
|
+ type S struct {
|
|
|
+ Field int
|
|
|
+ }
|
|
|
+
|
|
|
+ a := []S{{1}, {2}} // slice of literal structs
|
|
|
+ vm.Set("a", &a)
|
|
|
+ vm.RunString(`
|
|
|
+ let tmp = {Field: 1};
|
|
|
+ a[0] = tmp;
|
|
|
+ a[1] = tmp;
|
|
|
+ tmp.Field = 2;
|
|
|
+ `)
|
|
|
+
|
|
|
+In ECMAScript one would expect a[0].Field and a[1].Field to be equal to 2, but this is really not possible
|
|
|
+(or at least non-trivial without some complex reference tracking).
|
|
|
+
|
|
|
+To cover the most common use cases and to avoid excessive memory allocation, the following 'copy-on-change' mechanism
|
|
|
+is implemented (for both arrays and structs):
|
|
|
+
|
|
|
+* When a nested compound value is accessed, the returned ES value becomes a reference to the literal value.
|
|
|
+This ensures that things like 'a[0].Field = 1' work as expected and simple access to 'a[0].Field' does not result
|
|
|
+in copying of a[0].
|
|
|
+
|
|
|
+* The original container ('a' in our case) keeps track of the returned reference value and if a[0] is reassigned
|
|
|
+(e.g. by direct assignment, deletion or shrinking the array) the old a[0] is copied and the earlier returned value
|
|
|
+becomes a reference to the copy:
|
|
|
+
|
|
|
+ let tmp = a[0]; // no copy, tmp is a reference to a[0]
|
|
|
+ tmp.Field = 1; // a[0].Field === 1 after this
|
|
|
+ a[0] = {Field: 2}; // tmp is now a reference to a copy of the old value (with Field === 1)
|
|
|
+ a[0].Field === 2 && tmp.Field === 1; // true
|
|
|
+
|
|
|
+* Array value swaps caused by in-place sort (using Array.prototype.sort()) do not count as re-assignments, instead
|
|
|
+the references are adjusted to point to the new indices.
|
|
|
+
|
|
|
+* Assignment to an inner compound value always does a copy (and sometimes type conversion):
|
|
|
+
|
|
|
+ a[1] = tmp; // a[1] is now a copy of tmp
|
|
|
+ tmp.Field = 3; // does not affect a[1].Field
|
|
|
+
|
|
|
+3. Non-addressable structs, slices and arrays get copied. This sometimes may lead to a confusion as assigning to
|
|
|
+inner fields does not appear to work:
|
|
|
+
|
|
|
+ a1 := []interface{}{S{1}, S{2}}
|
|
|
+ vm.Set("a1", &a1)
|
|
|
+ vm.RunString(`
|
|
|
+ a1[0].Field === 1; // true
|
|
|
+ a1[0].Field = 2;
|
|
|
+ a1[0].Field === 2; // FALSE, because what it really did was copy a1[0] set its Field to 2 and immediately drop it
|
|
|
+ `)
|
|
|
+
|
|
|
+An alternative would be making a1[0].Field a non-writable property which would probably be more in line with
|
|
|
+ECMAScript, however it would require to manually copy the value if it does need to be modified which may be
|
|
|
+impractical.
|
|
|
+
|
|
|
+Note, the same applies to slices. If a slice is passed by value (not as a pointer), resizing the slice does not reflect on the original
|
|
|
+value. Moreover, extending the slice may result in the underlying array being re-allocated and copied.
|
|
|
+For example:
|
|
|
+
|
|
|
+ a := []interface{}{1}
|
|
|
+ vm.Set("a", a)
|
|
|
+ vm.RunString(`a.push(2); a[0] = 0;`)
|
|
|
+ fmt.Println(a[0]) // prints "1"
|
|
|
|
|
|
Notes on individual types:
|
|
|
|
|
@@ -1629,10 +1687,6 @@ Note that the underlying type is not lost, calling Export() returns the original
|
|
|
reflect based types.
|
|
|
*/
|
|
|
func (r *Runtime) ToValue(i interface{}) Value {
|
|
|
- return r.toValue(i, reflect.Value{})
|
|
|
-}
|
|
|
-
|
|
|
-func (r *Runtime) toValue(i interface{}, origValue reflect.Value) Value {
|
|
|
switch i := i.(type) {
|
|
|
case nil:
|
|
|
return _null
|
|
@@ -1722,44 +1776,18 @@ func (r *Runtime) toValue(i interface{}, origValue reflect.Value) Value {
|
|
|
if i == nil {
|
|
|
return _null
|
|
|
}
|
|
|
- obj := &Object{runtime: r}
|
|
|
- a := &objectGoSlice{
|
|
|
- baseObject: baseObject{
|
|
|
- val: obj,
|
|
|
- },
|
|
|
- data: &i,
|
|
|
- }
|
|
|
- obj.self = a
|
|
|
- a.init()
|
|
|
- return obj
|
|
|
+ return r.newObjectGoSlice(&i).val
|
|
|
case *[]interface{}:
|
|
|
if i == nil {
|
|
|
return _null
|
|
|
}
|
|
|
- obj := &Object{runtime: r}
|
|
|
- a := &objectGoSlice{
|
|
|
- baseObject: baseObject{
|
|
|
- val: obj,
|
|
|
- },
|
|
|
- data: i,
|
|
|
- }
|
|
|
- obj.self = a
|
|
|
- a.init()
|
|
|
- return obj
|
|
|
+ return r.newObjectGoSlice(i).val
|
|
|
}
|
|
|
|
|
|
- if !origValue.IsValid() {
|
|
|
- origValue = reflect.ValueOf(i)
|
|
|
- } else {
|
|
|
- // If origValue was a result of an Index(), or Field(), or such, its Kind may be Interface:
|
|
|
- // a := []interface{}{(*S)(nil)}
|
|
|
- // a0 := reflect.ValueOf(a).Index(0) // a0.Kind() is reflect.Interface
|
|
|
- // a1 := reflect.ValueOf(a[0]) // a1.Kind() is reflect.Ptr
|
|
|
- // Need to "dereference" it to make it consistent with plain value being passed.
|
|
|
- for origValue.Kind() == reflect.Interface {
|
|
|
- origValue = origValue.Elem()
|
|
|
- }
|
|
|
- }
|
|
|
+ return r.reflectValueToValue(reflect.ValueOf(i))
|
|
|
+}
|
|
|
+
|
|
|
+func (r *Runtime) reflectValueToValue(origValue reflect.Value) Value {
|
|
|
value := origValue
|
|
|
for value.Kind() == reflect.Ptr {
|
|
|
value = reflect.Indirect(value)
|
|
@@ -1824,7 +1852,7 @@ func (r *Runtime) toValue(i interface{}, origValue reflect.Value) Value {
|
|
|
obj.self = a
|
|
|
return obj
|
|
|
case reflect.Func:
|
|
|
- name := unistring.NewFromString(runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name())
|
|
|
+ name := unistring.NewFromString(runtime.FuncForPC(value.Pointer()).Name())
|
|
|
return r.newNativeFunc(r.wrapReflectFunc(value), nil, name, nil, value.Type().NumIn())
|
|
|
}
|
|
|
|