瀏覽代碼

LibJS: Implement Function.prototype.bind() according to the spec :^)

Linus Groh 3 年之前
父節點
當前提交
898ad7c682

+ 24 - 10
Userland/Libraries/LibJS/Runtime/BoundFunction.cpp

@@ -11,21 +11,35 @@
 
 namespace JS {
 
-BoundFunction::BoundFunction(GlobalObject& global_object, FunctionObject& bound_target_function, Value bound_this, Vector<Value> bound_arguments, i32 length)
-    : FunctionObject(*global_object.function_prototype())
+// 10.4.1.3 BoundFunctionCreate ( targetFunction, boundThis, boundArgs ), https://tc39.es/ecma262/multipage/ordinary-and-exotic-objects-behaviours.html#sec-boundfunctioncreate
+ThrowCompletionOr<BoundFunction*> BoundFunction::create(GlobalObject& global_object, FunctionObject& target_function, Value bound_this, Vector<Value> bound_arguments)
+{
+    // 1. Let proto be ? targetFunction.[[GetPrototypeOf]]().
+    auto* prototype = TRY(target_function.internal_get_prototype_of());
+
+    // 2. Let internalSlotsList be the list-concatenation of « [[Prototype]], [[Extensible]] » and the internal slots listed in Table 33.
+    // 3. Let obj be ! MakeBasicObject(internalSlotsList).
+    // 4. Set obj.[[Prototype]] to proto.
+    // 5. Set obj.[[Call]] as described in 10.4.1.1.
+    // 6. If IsConstructor(targetFunction) is true, then
+    //    a. Set obj.[[Construct]] as described in 10.4.1.2.
+    // 7. Set obj.[[BoundTargetFunction]] to targetFunction.
+    // 8. Set obj.[[BoundThis]] to boundThis.
+    // 9. Set obj.[[BoundArguments]] to boundArgs.
+    auto* object = global_object.heap().allocate<BoundFunction>(global_object, global_object, target_function, bound_this, move(bound_arguments), prototype);
+
+    // 10. Return obj.
+    return object;
+}
+
+BoundFunction::BoundFunction(GlobalObject& global_object, FunctionObject& bound_target_function, Value bound_this, Vector<Value> bound_arguments, Object* prototype)
+    : FunctionObject(global_object, prototype)
     , m_bound_target_function(&bound_target_function)
     , m_bound_this(bound_this)
     , m_bound_arguments(move(bound_arguments))
+    // FIXME: Non-standard and redundant, remove.
     , m_name(String::formatted("bound {}", bound_target_function.name()))
-    , m_length(length)
-{
-}
-
-void BoundFunction::initialize(GlobalObject& global_object)
 {
-    auto& vm = this->vm();
-    Base::initialize(global_object);
-    define_direct_property(vm.names.length, Value(m_length), Attribute::Configurable);
 }
 
 BoundFunction::~BoundFunction()

+ 4 - 3
Userland/Libraries/LibJS/Runtime/BoundFunction.h

@@ -6,6 +6,7 @@
 
 #pragma once
 
+#include <LibJS/Runtime/Completion.h>
 #include <LibJS/Runtime/FunctionObject.h>
 
 namespace JS {
@@ -14,8 +15,9 @@ class BoundFunction final : public FunctionObject {
     JS_OBJECT(BoundFunction, FunctionObject);
 
 public:
-    BoundFunction(GlobalObject&, FunctionObject& target_function, Value bound_this, Vector<Value> bound_arguments, i32 length);
-    virtual void initialize(GlobalObject&) override;
+    static ThrowCompletionOr<BoundFunction*> create(GlobalObject&, FunctionObject& target_function, Value bound_this, Vector<Value> bound_arguments);
+
+    BoundFunction(GlobalObject&, FunctionObject& target_function, Value bound_this, Vector<Value> bound_arguments, Object* prototype);
     virtual ~BoundFunction();
 
     virtual ThrowCompletionOr<Value> internal_call(Value this_argument, MarkedVector<Value> arguments_list) override;
@@ -37,7 +39,6 @@ private:
     Vector<Value> m_bound_arguments;                     // [[BoundArguments]]
 
     FlyString m_name;
-    i32 m_length { 0 };
 };
 
 }

+ 23 - 6
Userland/Libraries/LibJS/Runtime/FunctionPrototype.cpp

@@ -16,6 +16,7 @@
 #include <LibJS/Runtime/FunctionPrototype.h>
 #include <LibJS/Runtime/GlobalObject.h>
 #include <LibJS/Runtime/NativeFunction.h>
+#include <LibJS/Runtime/ShadowRealm.h>
 
 namespace JS {
 
@@ -58,13 +59,19 @@ JS_DEFINE_NATIVE_FUNCTION(FunctionPrototype::apply)
 }
 
 // 20.2.3.2 Function.prototype.bind ( thisArg, ...args ), https://tc39.es/ecma262/#sec-function.prototype.bind
+// 3.1.2.1 Function.prototype.bind ( thisArg, ...args ), https://tc39.es/proposal-shadowrealm/#sec-function.prototype.bind
 JS_DEFINE_NATIVE_FUNCTION(FunctionPrototype::bind)
 {
-    auto* this_object = TRY(vm.this_value(global_object).to_object(global_object));
-    if (!this_object->is_function())
-        return vm.throw_completion<TypeError>(global_object, ErrorType::NotAnObjectOfType, "Function");
-    auto& this_function = static_cast<FunctionObject&>(*this_object);
-    auto bound_this_arg = vm.argument(0);
+    auto this_argument = vm.argument(0);
+
+    // 1. Let Target be the this value.
+    auto target_value = vm.this_value(global_object);
+
+    // 2. If IsCallable(Target) is false, throw a TypeError exception.
+    if (!target_value.is_function())
+        return vm.throw_completion<TypeError>(global_object, ErrorType::NotAFunction, target_value.to_string_without_side_effects());
+
+    auto& target = static_cast<FunctionObject&>(target_value.as_object());
 
     Vector<Value> arguments;
     if (vm.argument_count() > 1) {
@@ -72,7 +79,17 @@ JS_DEFINE_NATIVE_FUNCTION(FunctionPrototype::bind)
         arguments.remove(0);
     }
 
-    return TRY(this_function.bind(bound_this_arg, move(arguments)));
+    // 3. Let F be ? BoundFunctionCreate(Target, thisArg, args).
+    auto* function = TRY(BoundFunction::create(global_object, target, this_argument, move(arguments)));
+
+    // 4. Let argCount be the number of elements in args.
+    auto arg_count = vm.argument_count() - 1;
+
+    // 5. Perform ? CopyNameAndLength(F, Target, "bound", argCount).
+    TRY(copy_name_and_length(global_object, *function, target, "bound"sv, arg_count));
+
+    // 6. Return F.
+    return function;
 }
 
 // 20.2.3.3 Function.prototype.call ( thisArg, ...args ), https://tc39.es/ecma262/#sec-function.prototype.call

+ 15 - 1
Userland/Libraries/LibJS/Tests/builtins/Function/Function.prototype.bind.js

@@ -33,6 +33,20 @@ describe("basic behavior", () => {
             )
         ).toBe(12);
     });
+
+    test("name has 'bound' prefix", () => {
+        function foo() {}
+        const boundFoo = foo.bind(123);
+        expect(foo.name).toBe("foo");
+        expect(boundFoo.name).toBe("bound foo");
+    });
+
+    test("prototype is inherited from target function", () => {
+        function foo() {}
+        Object.setPrototypeOf(foo, Array.prototype);
+        const boundFoo = Function.prototype.bind.call(foo, 123);
+        expect(Object.getPrototypeOf(boundFoo)).toBe(Array.prototype);
+    });
 });
 
 describe("bound function arguments", () => {
@@ -144,6 +158,6 @@ describe("errors", () => {
     test("does not accept non-function values", () => {
         expect(() => {
             Function.prototype.bind.call("foo");
-        }).toThrowWithMessage(TypeError, "Not an object of type Function");
+        }).toThrowWithMessage(TypeError, "foo is not a function");
     });
 });