Browse Source

Do not create an object if a native constructor is called without 'new'. Closes #228.

Dmitry Panov 4 years ago
parent
commit
44400d2d22
5 changed files with 90 additions and 33 deletions
  1. 2 28
      README.md
  2. 2 1
      func.go
  3. 1 0
      object.go
  4. 46 4
      runtime.go
  5. 39 0
      runtime_test.go

+ 2 - 28
README.md

@@ -212,34 +212,8 @@ There are two standard mappers: [TagFieldNameMapper](https://godoc.org/github.co
 Native Constructors
 Native Constructors
 -------------------
 -------------------
 
 
-In order to implement a constructor function in Go:
-```go
-func MyObject(call goja.ConstructorCall) *Object {
-    // call.This contains the newly created object as per http://www.ecma-international.org/ecma-262/5.1/index.html#sec-13.2.2
-    // call.Arguments contain arguments passed to the function
-
-    call.This.Set("method", method)
-
-    //...
-
-    // If return value is a non-nil *Object, it will be used instead of call.This
-    // This way it is possible to return a Go struct or a map converted
-    // into goja.Value using runtime.ToValue(), however in this case
-    // instanceof will not work as expected.
-    return nil
-}
-
-runtime.Set("MyObject", MyObject)
-
-```
-
-Then it can be used in JS as follows:
-
-```js
-var o = new MyObject(arg);
-var o1 = MyObject(arg); // same thing
-o instanceof MyObject && o1 instanceof MyObject; // true
-```
+In order to implement a constructor function in Go use `func (goja.ConstructorCall) *goja.Object`.
+See [Runtime.ToValue()](https://godoc.org/github.com/dop251/goja#Runtime.ToValue) documentation for more details.
 
 
 Regular Expressions
 Regular Expressions
 -------------------
 -------------------

+ 2 - 1
func.go

@@ -215,7 +215,7 @@ func (f *baseFuncObject) hasInstance(v Value) bool {
 	return false
 	return false
 }
 }
 
 
-func (f *nativeFuncObject) defaultConstruct(ccall func(ConstructorCall) *Object, args []Value) *Object {
+func (f *nativeFuncObject) defaultConstruct(ccall func(ConstructorCall) *Object, args []Value, newTarget *Object) *Object {
 	proto := f.getStr("prototype", nil)
 	proto := f.getStr("prototype", nil)
 	var protoObj *Object
 	var protoObj *Object
 	if p, ok := proto.(*Object); ok {
 	if p, ok := proto.(*Object); ok {
@@ -227,6 +227,7 @@ func (f *nativeFuncObject) defaultConstruct(ccall func(ConstructorCall) *Object,
 	ret := ccall(ConstructorCall{
 	ret := ccall(ConstructorCall{
 		This:      obj,
 		This:      obj,
 		Arguments: args,
 		Arguments: args,
+		NewTarget: newTarget,
 	})
 	})
 
 
 	if ret != nil {
 	if ret != nil {

+ 1 - 0
object.go

@@ -295,6 +295,7 @@ type FunctionCall struct {
 type ConstructorCall struct {
 type ConstructorCall struct {
 	This      *Object
 	This      *Object
 	Arguments []Value
 	Arguments []Value
+	NewTarget *Object
 }
 }
 
 
 func (f FunctionCall) Argument(idx int) Value {
 func (f FunctionCall) Argument(idx int) Value {

+ 46 - 4
runtime.go

@@ -520,11 +520,22 @@ func (r *Runtime) newNativeConstructor(call func(ConstructorCall) *Object, name
 	}
 	}
 
 
 	f.f = func(c FunctionCall) Value {
 	f.f = func(c FunctionCall) Value {
-		return f.defaultConstruct(call, c.Arguments)
+		thisObj, _ := c.This.(*Object)
+		if thisObj != nil {
+			res := call(ConstructorCall{
+				This:      thisObj,
+				Arguments: c.Arguments,
+			})
+			if res == nil {
+				return _undefined
+			}
+			return res
+		}
+		return f.defaultConstruct(call, c.Arguments, nil)
 	}
 	}
 
 
-	f.construct = func(args []Value, proto *Object) *Object {
-		return f.defaultConstruct(call, args)
+	f.construct = func(args []Value, newTarget *Object) *Object {
+		return f.defaultConstruct(call, args, newTarget)
 	}
 	}
 
 
 	v.self = f
 	v.self = f
@@ -1270,7 +1281,38 @@ Nil is converted to null.
 Functions
 Functions
 
 
 func(FunctionCall) Value is treated as a native JavaScript function. This increases performance because there are no
 func(FunctionCall) Value is treated as a native JavaScript function. This increases performance because there are no
-automatic argument and return value type conversions (which involves reflect).
+automatic argument and return value type conversions (which involves reflect). Attempting to use
+the function as a constructor will result in a TypeError.
+
+func(ConstructorCall) *Object is treated as a native constructor, allowing to use it with the new
+operator:
+
+ func MyObject(call goja.ConstructorCall) *goja.Object {
+    // call.This contains the newly created object as per http://www.ecma-international.org/ecma-262/5.1/index.html#sec-13.2.2
+    // call.Arguments contain arguments passed to the function
+
+    call.This.Set("method", method)
+
+    //...
+
+    // If return value is a non-nil *Object, it will be used instead of call.This
+    // This way it is possible to return a Go struct or a map converted
+    // into goja.Value using runtime.ToValue(), however in this case
+    // instanceof will not work as expected.
+    return nil
+ }
+
+ runtime.Set("MyObject", MyObject)
+
+Then it can be used in JS as follows:
+
+ var o = new MyObject(arg);
+ var o1 = MyObject(arg); // same thing
+ o instanceof MyObject && o1 instanceof MyObject; // true
+
+When a native constructor is called directory (without the new operator) its behavior depends on
+this value: if it's an Object, it is passed through, otherwise a new one is created exactly as
+if it was called with the new operator. In either case call.NewTarget will be nil.
 
 
 Any other Go function is wrapped so that the arguments are automatically converted into the required Go types and the
 Any other Go function is wrapped so that the arguments are automatically converted into the required Go types and the
 return value is converted to a JavaScript value (using this method).  If conversion is not possible, a TypeError is
 return value is converted to a JavaScript value (using this method).  If conversion is not possible, a TypeError is

+ 39 - 0
runtime_test.go

@@ -1713,6 +1713,45 @@ func TestNativeCtorNewTarget(t *testing.T) {
 	testScript1(SCRIPT, valueTrue, t)
 	testScript1(SCRIPT, valueTrue, t)
 }
 }
 
 
+func TestNativeCtorNonNewCall(t *testing.T) {
+	vm := New()
+	vm.Set(`Animal`, func(call ConstructorCall) *Object {
+		obj := call.This
+		obj.Set(`name`, call.Argument(0).String())
+		obj.Set(`eat`, func(call FunctionCall) Value {
+			self := call.This.(*Object)
+			return vm.ToValue(fmt.Sprintf("%s eat", self.Get(`name`)))
+		})
+		return nil
+	})
+	v, err := vm.RunString(`
+
+	function __extends(d, b){
+		function __() {
+			this.constructor = d;
+		}
+		d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+	}
+
+	var Cat = (function (_super) {
+		__extends(Cat, _super);
+		function Cat() {
+			return _super.call(this, "cat") || this;
+		}
+		return Cat;
+	}(Animal));
+
+	var cat = new Cat();
+	cat instanceof Cat && cat.eat() === "cat eat";
+	`)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if v != valueTrue {
+		t.Fatal(v)
+	}
+}
+
 /*
 /*
 func TestArrayConcatSparse(t *testing.T) {
 func TestArrayConcatSparse(t *testing.T) {
 function foo(a,b,c)
 function foo(a,b,c)