28

I am wondering whether dot-abstraction methods (e.g. dog.bark) bind at runtime or at compile-time. My question concerns the following code, which throws an error:

(true ? ''.toLowerCase : ''.toUpperCase)()

But the following does not:

true ? ''.toLowerCase() : ''.toUpperCase()

Why is my string literal '' not getting resolved in the first example?

Simon Kuang
  • 3,870
  • 4
  • 27
  • 53
  • 2
    When you do `dog.bark()`, `this` inside `bark` will point to `dog`. But when you do `fn = dog.bark`, you are copying reference of function. So in `fn()`, `this` will point to `window` or `undefined` based on environment. – Rajesh Aug 24 '17 at 07:07
  • Related: https://stackoverflow.com/questions/13441307/how-does-the-this-keyword-in-javascript-act-within-an-object-literal/13441628#13441628 – slebetman Aug 24 '17 at 07:12
  • 6
    What do you mean "compile time".. JavaScript is not a compiled language. There is only runtime. – alexanderbird Aug 24 '17 at 13:57
  • @alexisking Thanks, I've been looking for a proper duplicate for ages. Don't we have anything better than [How to access the correct `this` / context inside a callback](https://stackoverflow.com/q/20279484/1048572) and [How does the “this” keyword work](https://stackoverflow.com/q/3127429/1048572)? There must be a question why JS methods don't work like Python's already! – Bergi Aug 25 '17 at 02:21
  • I also found [Javascript lost context when assigned to other variable](https://stackoverflow.com/q/23154778/1048572), [Why is apply not already bound to functions in Javascript?](https://stackoverflow.com/q/23090860/1048572) and [Nature of JS bound functions and function invocation operator](https://stackoverflow.com/q/25609927/1048572). – Bergi Aug 25 '17 at 02:23

5 Answers5

21
(true ? ''.toLowerCase : ''.toUpperCase)()

is equivalent to:

String.prototype.toLowerCase.call()
// or:
String.prototype.toLowerCase.call(undefined)

However,

true ? ''.toLowerCase() : ''.toUpperCase()

is equivalent to:

String.prototype.toLowerCase.call('')

In both cases, the first argument to call is converted to an object, which the this in String.prototype.toLowerCase will reference to.

undefined can't be converted to an object, but the empty string can:

function logThis () { console.log(this); }

logThis.call('');

The SO snippet console only shows {}, but it's actually the same thing that you get from new String(''). Read about the string wrapper on MDN.

PeterMader
  • 6,987
  • 1
  • 21
  • 31
  • I know it's quite old but where you first says "is equivalent to" is completely wrong. In the tertiary expression, `''.toLowerCase` will not call the function. It will only check if `toLowerCase` in `String.prototype` is not falsy. So the first line of that two liner should be removed... – marekful Jan 24 '18 at 08:47
  • @marekful No, the ternary expression will *return* `String.prototype.toLowerCase`. The parentheses at the end call that function. – PeterMader Jan 24 '18 at 14:41
  • Yes, and that's the only call to it but you say it translates to two calls to the function, `String.prototype.toLowerCase.call()` with empty arguments. This is exactly the same in both cases. No difference in the passed arguments as you suggested `''` vs nothing. Probably demonstrating that the empty string literal is cast to a String instance attracted the upvotes and that's nice really but people just looked away from the rest instead of critically evaluating. – marekful Jan 24 '18 at 22:36
  • I'm afraid I don't fully understand you. Did you regard the snippet as being translated to two seperate function calls? That is not what I meant and apparently what the other viewers understood. But it was a little ambigous; I hope the edited answer is clearer. – PeterMader Jan 25 '18 at 19:05
7

Because these methods apply on the this context, and in your example the this is undefined

One way to override this variable by using bind method:

(true ? ''.toLowerCase : ''.toUpperCase).bind('Hello')();

this will return hello

amd
  • 20,637
  • 6
  • 49
  • 67
7

This is actually quite simple once you get how methods work in javascript behind the scenes.

toUpperCase is a method. This is a function that operates on a specific object... usually via the this variable.

Javascript is a prototypal language... meaning that the functions attached to objects are just functions and can be copied. There is some work behind the scenes that makes sure this is set to the right thing when you call a method, but this work only happens when you call it as a method... as in the obj.method() form.

In other words: ''.toUpperCase() makes sure that this is set to the string '' when you call it.

When you call it as toUpperCase() this is not set to anything in particular (different environments do different things with this in this case)

What your code does could be rewritten as this:

var function_to_call;
 if (true) {
    function_to_call = ''.toLowerCase;
 } else {
    function_to_call = ''.toUpperCase;
 }

 function_to_call();

Because your function call: function_to_call() is not in the object.method() syntax, the thing that sets this to the correct object is not done, and your function call executes with this not set to what you want.

As other people have pointed out, you can use func.call(thing_to_make_this) or func.apply() to attach the correct thing to this explicitly.

I find it much more helpful to use .bind() - which is extremely under-used in my opinion. function_name.bind(this_object) gives you a new function that will always have this attached to the correct thing:

// assuming function_to_call is set
function_that_works = function_to_call.bind(my_object)

function_that_works(); // equivalent to my_object.function_to_call()

and this means you can pass around the function you get back from bind() as you would a normal function, and it will work on the object you want. This is especially useful in callbacks, as you can create an anonymous function that is bound to the object it was created in:

// this won't work because when this runs, 'this' doesn't mean what you think
setTimeout(function() { this.display_message('success'); }, 2000);

// this will work, because we have given setTimeout a pre-bound function.
setTimeout(function() { this.display_message('success'); }.bind(this), 2000); 

TL;DR: You can't call a method as a function and expect it to work, because it doesn't know what this should be. If you want to use that function, you have to use .call(), .apply() or .bind() to make sure this is set correctly by the time the function executes.

Hope that helps.

JayKuri
  • 839
  • 5
  • 10
  • 4
    TL;DR: Javascript is insane. – Reinstate Monica Aug 24 '17 at 20:00
  • 2
    Since sanity is largely based on norms among the population, the truth of that statement depends on whether you are in the population or outside of it. I think javascript is very sane and in fact very cool and flexible in ways not a lot of other languages are... but I don't expect javascript to be anything but javascript. If you come from a language with strict-references and/or one that is class-oriented, you can't use the same guidelines. They simply don't make sense. It's like getting into the pilot seat of a jet and asking where's the gear-shift, and the glove-box. – JayKuri Aug 24 '17 at 20:07
4

Because when you do (true ? ''.toLowerCase : ''.toUpperCase)() you are not calling the function that is bound to a string. You are simply calling the function without any context.

Consider the following example:

var obj = {
    objname: "objname",
    getName: function() {
        return this.objname;
    }
}

When you call it with obj.getName(), it correctly returns the value, but when you do something like this:

var fn = obj.getName
fn() // returns undefined because `fn` is not bound to `obj`
Saravana
  • 37,852
  • 18
  • 100
  • 108
0

In your first example the toLowerCase function is detached from its context (the empty string object) and then it's invoked. Since you don't reattach the function to anything it has undefined as its context.

This behavior exist to enable code reuse through mix-ins:

var obj1 = {
   name: "obj1",
   getName: function() { return this.name; }
}

var obj2 = {
   name: "obj2",
}

obj2.getName = obj1.getName //now obj2 has the getName method with correct context

console.log(obj2.getName())
Dmitry
  • 6,716
  • 14
  • 37
  • 39