Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

confusion over how "softBind" function works

I'm currently studying javascript by following the "you dont know js" series.

In section "this & object prototype", the author came up with an way to soft bind this.

However, I am extremely confused by the code. So I was wondering if someone could kindly explain it to me, steps by steps, what the code really does?

//step 1: if "softBind" property does not exist on `Function.prototye`

if (!Function.prototype.softBind) {

    //step 2: create a property named "softBind" on "Function.prototype" and assign to "softBind" the following function 

    Function.prototype.softBind = function(obj) {

        //step 3: what is the point of assigning "this" to the variable "fn"?
        //what does "this" represent at this point in time?

        var fn = this,

            //step 4: I understand that "arguments" is an array-like object, i.e. "arguments" is not a true array. 
            //But you can convert "arguments" to an true array by using "[].slice.call(arguments)". 
            //The thing I dont understand here is, why the 1? 
            //I understand it tells "slice" method to start slicing at index 1, but why 1? 
            //And what is the purpose of "curried" variable? 

            curried = [].slice.call( arguments, 1 ),

            bound = function bound() {

                //step 5: I understand what "apply" function does

                return fn.apply(

                    //step 6: I dont really understand how "!this" works. 

                    (!this ||

                        //step 7: utterly confused...   
                         
                        (typeof window !== "undefined" &&
                            this === window) ||
                        (typeof global !== "undefined" &&
                            this === global)

                    //step 8: if the above statements evaluates to "true", then use "obj", 
                    //otherwise, use "this"

                    ) ? obj : this,

                    //step 9: can't I write "curried.concat(arguments)" instead?
                    //why the convoluted syntax?

                    curried.concat.apply( curried, arguments )
                );
            };

        //step 10: Why assign the "fn.prototype" as the prototype to "bound.prototype"?

        bound.prototype = Object.create( fn.prototype );
        return bound;
    };
}

I'm very sorry for the long question, but I thought instead of dividing the question into several posts, it is more convenient if they are put into one place.

like image 761
Thor Avatar asked Jul 12 '17 01:07

Thor


1 Answers

step 3: what is the point of assigning "this" to the variable "fn"?

The value of this holds a pointer to the currently executing function object. Only function objects can be held, so only things actually created through new() or equivalent notation. This can be useful for passing a reference to an outer object to an inner object created within said outer object.

Here's a minimal example:

function fn1() {
    var x = this;  // x = fn1.
    this.u = 5;
    function fn2() {
        this.u = 10;
        console.log(this.u); // Prints the member of fn2.
        console.log(x.u); // Prints the member of fn1.
    };
    var W = new fn2();
}
var V = new fn1();

The output should be:

10
5

First, an object of type fn1 is created called V. It has a member variable u holding the value 5. Then, we create an object of type fn2 called W within fn1. It also has a member variable u, but here it holds the value 10. If we wanted to print the value of V.u within W, then we need a pointer to V. Calling this.u within W would output its u value (10), which is not what we want. So we define a variable x within the scope of the class fn1, holding the this pointer for us. Now it's possible to access the members of fn1 within fn2.

Step 4

The first argument is the object being bound to. You don't want to pass that to the function being bound, that would break its functionality, for it does not expect an extra argument prepended to its normal list of arguments. So, the first argument has to be removed.

step 6: I dont really understand how "!this" works.

!this is simply a way of checking whether this is defined. If it is not, then the value will be true. Otherwise, since this would be an object (which evaluate to true when cast to a boolean), then it is false.

step 7: utterly confused...

Here, the original author checks if this is equal to either window, or global. Note; In modern browsers, checking for just window is enough, but IE exists (as do non-browser javascript environments). So, the full statement evaluates to this thing:

If I am not called from within an object, or if I'm called from the object window or global, then return the object softbind was created with. Otherwise, return the object I was called from

Note that this is exactly what the author of the original article wants. When a library function is called with this special binding, then we can be sure that whatever the library does; it can't access the global context through the use of the this variable. But, it can access any other object, allowing you to interface with the library.

step 9: can't I write "curried.concat(arguments)" instead?

Curried holds all arguments the original softbind function was called with, except for the first argument. arguments , at this point, is not equal to arguments in the earlier call. Here, it refers to the arguments the bound function is called with, not those it was bound with. This line integrates the two sets of arguments, allowing you to provide default arguments. The trick used here is to concatenate the arguments, e.g. Suppose your function has default arguments [1,2,3,4] and you supply [5,6]:

[1,2,3,4].concat([5,6]) produces [1,2,3,4,5,6].

Why not simply concatenate, and use the prototype? Arrays are passed by reference in javascript, so this will keep curried the same, while concatenating arguments to the call. Equivalently, you could write this:

curried2 = curried.concat(arguments);
return fn.apply(
(.....)
curried2);

Admittedly, the terseness does not help the understandability of this example. Simply re-naming arguments to calledArguments and curried (an advanced math term not relevant to the explanation) to be defaultArguments and using a simple for loop over each argument would be far easier to understand, if a little more verbose. I guess the author wanted to be fancy.

step 10: Why assign the "fn.prototype" as the prototype to "bound.prototype"?

Go up a bit on the article to the part where the author talks about the default bind function and how it works: basically, the end result of replacing the prototype back with the default prototype during the function call means that when your softbind enabled function is called with the new operator this will be set to itself, rather than the default bound object. prototype will not work when simply calling the bound function.

It also enables inheritance, meaning that creating things for a softbind enabled function using its prototype will not have that prototype be overruled by that of softbind when it is bound. (That would make softbind incompatible with prototypes). Instead, both prototypes are used.

Also see this reddit post.

A word of warning

We're extending the language with new features here. Features that aren't exactly necessary, and are largely concerned with semantics. If you're just interested in learning the language, this really goes way too far, you don't exactly need special binding semantics. Worse, it can be confusing if this does not behave in a way you expect it to.

A simpler alternative

Enable strict mode. Now this will default to undefined whenever it points to the global object. Prevents the problem this convoluted code is trying to solve (by usually resulting in errors from functions trying to access member variables or functions of undefined), while at the same time being far easier to use, and at the same time it will complain about a lot of syntax that is valid regular javascript, but a bug in any normal use-case. Also see the MDN article about it. It will catch a lot of potential errors for you instead of silently doing nonsensical things.

Another alternative

bind tries to resolve 'losing' the object when you pass a member function of it to another function, such as setTimeout. Another way of doing it is by using an anonymous function. Instead of using (soft) binding, assuming obj is an object holding a function fn being passed a parameter param;

setTimeout(obj.fn(param), 500);

You can use:

setTimeout(function(param){obj.fn(param);}, 500);

Which avoids the problem through a layer of indirection by passing an anonymous function. Also see this question.

like image 51
aphid Avatar answered Oct 08 '22 23:10

aphid