Thursday 24 November 2016

Javascript: softbind

Excerpted from You Don't Know JS: this & Object Prototypes

I couldn't fully understand the code below.
  
if (!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
        var fn = this,
            curried = [].slice.call( arguments, 1 ),
            bound = function bound() {
                return fn.apply(
                    (!this ||
                        (typeof window !== "undefined" &&
                            this === window) ||
                        (typeof global !== "undefined" &&
                            this === global)
                    ) ? obj : this,
                    curried.concat.apply( curried, arguments )
                );
            };
        bound.prototype = Object.create( fn.prototype );
        return bound;
    };
}

function foo() {
   console.log("name: " + this.name);
}

var obj = { name: "obj" },
    obj2 = { name: "obj2" },
    obj3 = { name: "obj3" };

var fooOBJ = foo.softBind( obj );

fooOBJ(); // name: obj

obj2.foo = foo.softBind(obj);
obj2.foo(); // name: obj2   <---- look!!!

fooOBJ.call( obj3 ); // name: obj3   <---- look!

setTimeout( obj2.foo, 10 ); // name: obj   <---- falls back to soft-binding
  1. What does [].slice.call(...) do? 
  2. Are the two 'this' referring to the same thing? 
  3. What does bound.prototype = Object.create(fn.prototype) do?
Let's go through the code step by step by inserting some console output code. And for the sake of demonstrating the purpose of curried, I also rewrote the foo() function.
if (!Function.prototype.softBind) {
    Function.prototype.softBind = function(obj) {
        var fn = this;
        console.log("Outer this = "+ fn);
        var curried = [].slice.call( arguments, 1 );
        console.log("Outer curry = "+curried);
        var bound = function bound() {
            console.log("Inner this = " + this);
            console.log("Inner curry = "+curried.concat.apply( curried, arguments ));
            return fn.apply(
                (!this ||
                    (typeof window !== "undefined" &&
                        this === window) ||
                    (typeof global !== "undefined" &&
                         this === global)
                ) ? obj : this,
                curried.concat.apply( curried, arguments )
            );
        };
        bound.prototype = Object.create( fn.prototype );
        console.log("return bound");
        return bound;
    };
}

function foo(a1, a2) {
   console.log(a1 + ' ' +a2 + " name: " + this.name);
}

var obj = { name: "obj" },
    obj2 = { name: "obj2" },
    obj3 = { name: "obj3" };

var fooOBJ = foo.softBind( obj, 'A1');
fooOBJ('A2'); // name: obj

//obj2.foo = foo.softBind(obj, 'A1');
//obj2.foo('A2'); // name: obj2   <---- look!!!

//fooOBJ.call( obj3 ); // name: obj3   <---- look!
Here is the output.
Outer this = function foo(a1, a2) {
   console.log(a1 + ' ' +a2 + " name: " + this.name);
}
Outer curry = A
return bound
Inner this = [object Object]
Inner curry = A,A2
A A2 name: obj2

[].slice.call(arguments, 1)
is semantically equal to
arguments.slice(1)
except the latter is syntactically incorrect because the variable arguments is not a real array. It's an 'array-like' object.

The outer this refers to the foo function while the inner this refers to the window (global) variable. It's worth noting that by the time foo.softBind(obj) completes execution, the bound() function hasn't been invoked. It's only invoked when the line fooOBJ('A2') is executed.

bound.prototype = Object.create( fn.prototype );
copies all the properties of foo to bound, essentially ensuring that after softbind the function foo doesn't lose any properties.

No comments:

Post a Comment