1

I know one of the difference is that instance variables of type Function automatically bind to the class. For example:

class Dog {
  sound = 'woof'
  bark() {
    console.log(this)
  }
  boundBark = () => {
    console.log(this)
  }
}

const fido = new Dog()
fido.bark() // woof
fido.boundBark() // woof
const bark = fido.bark
bark() // undefined
const boundBark = fido.boundBark
boundBark() // woof
Dog { sound: 'woof', boundBark: [Function: boundBark] }
Dog { sound: 'woof', boundBark: [Function: boundBark] }
undefined
Dog { sound: 'woof', boundBark: [Function: boundBark] }

Why is this and are there other difference between these two ways of writing an instance function?

Tim
  • 6,265
  • 5
  • 29
  • 24
  • 1
    Calling an arrow function does not establish a `this` binding. – Pointy Jan 29 '21 at 18:50
  • Also your code as posted throws an exception and does not do what you indicate. – Pointy Jan 29 '21 at 18:51
  • 3
    `bark` is a method that exists on the prototype of `Dog` and is shared between all instances. `boundBark` is an *instance property* that contains an arrow function and each instance has one. – VLAZ Jan 29 '21 at 18:52
  • 2
    "_instance variables of type Function automatically bind to the class_" If "_instance variable_" here is an own property, this is true only when the method is defined with an arrow function directly in the class. – Teemu Jan 29 '21 at 18:57
  • @Pointy whoops. Thanks for pointing that out. I run that in ts-node (I threw it into a typescript project to test, figured why not). Funny that typescript didn't throw an error when `this` was undefined. I've updated the code to make sure the question is accurate. – Tim Jan 29 '21 at 19:00
  • 1
    @Pointy Also, I believe your explanation is not accurate. The arrow function is the only one that actually preserves the this binding. – Tim Jan 29 '21 at 19:01
  • @Teemu good point. So this wouldn't work if I, for example, wrote `boundBark = null` in the class and then put `this.boundBark = ()=>...` in the constructor, right? – Tim Jan 29 '21 at 19:02
  • 2
    @Tim no, Pointy is entirely correct. Refer to [How does the “this” keyword work?](https://stackoverflow.com/q/3127429) for fuller explanation but a *non-arrow* function will determine the value of `this` at call time. That's not true for arrow functions - calling them will not alter `this`. – VLAZ Jan 29 '21 at 19:03
  • 1
    See the output in [this playground](https://www.typescriptlang.org/play?target=1&ssl=13&ssc=1&pln=13&pc=14#code/MYGwhgzhAEAiD2BzaBvAUNaF4FcB2AJtALzQDkA7vPAGZkbQBGYATgNYAUAlKg5sPDzYQAUwB0IJBwAuACwCWELgG4GAXwaNchAEKs2JaNxIA+Xpn6Dh4yYhkKl6tBrQCh06EVJ4RFOFJU0AjFmdm4gkO0CPTCuIA), you can see the difference in how `bark` and `boundBark` are attached to the class when transpiled. – Phix Jan 29 '21 at 19:03
  • @VLAZ Didn't get a chance to finish my reply. I had forgotten that instance methods are defined as, basically, static methods with an injected `this` parameter. That's the bottom line here, right? – Tim Jan 29 '21 at 19:04
  • 1
    @Tim yea I didn't notice the `=>` function being set up as an instance variable at first, apologies. – Pointy Jan 29 '21 at 19:05
  • @VLAZ but you can see in the output of my code that the arrow function is the only one that consistently respects `this`. It's the non-arrow function that ends up having `this` being `undefined` – Tim Jan 29 '21 at 19:05
  • @VLAZ `const bark = fido.bark` \n `bark() // undefined` bark is the non-arrow function – Tim Jan 29 '21 at 19:06
  • 1
    Instance variables are basically treated as lines of code in the constructor, like `this.foo = bar`, so in that context (the constructor) there **is** a value for `this` that gets "inherited" (not really, but I can't think of a better word) by the `=>` function. (Personally I find class instance variable syntax to violate the principle of least surprise, but there are good reasons nobody asks me my opinion on things like that.) – Pointy Jan 29 '21 at 19:07
  • 1
    "*instance methods are defined as, basically, static methods with an injected this parameter*" It's a bit of a weird phrasing but it's correct. I'd avoid calling them "instance methods" as they are technically on the prototype. I understand you mean "methods *used* by instances" but the phrasing can also be taken as "methods *defined* on instances" and that would be a different thing. – VLAZ Jan 29 '21 at 19:07
  • 1
    An own property will shadow a property with the same name in the prototype. All the engines don't have support for other than methods as class members, though, so you can't asssign `null` to `boundBark` in the class. It's all about how `this` keyword is assigned its value, arrow functions get that value from where they are defined, functions get `this` when they're called. There are ways to change `this` for functions, but not for arrow functions. – Teemu Jan 29 '21 at 19:09
  • @VLAZ Yea, that's exactly what I meant. And me thinking of them as "methods defined on instances" (even though I know it's not true if I think about it) is the ultimate cause of this confusion. – Tim Jan 29 '21 at 19:10
  • Not exactly a closure, but "lexical environment record", which is somewhat similar to closures in the context of objects. – Teemu Jan 29 '21 at 19:14
  • 1
    Thank you all for your help. I'm a pro js dev with 10 years of experience so I understand the nature of this and all that. I just needed to be reminded of a few facts to put it all together. I'll formalize all this with proper examples and post an answer for future users. Thanks again! – Tim Jan 29 '21 at 19:15
  • @Teemu Perhaps my semantics are wrong here. I thought closure was "using variables in the scope in which the function was originally called inside the function itself". Arrow functions don't really *have* a `this`. They're just using the one from where they were defined (in the constructor in this case) – Tim Jan 29 '21 at 19:16
  • @Teemu Yea, I just double checked. I've got closure correctly defined in my head. This would be a closure, wouldn't it? – Tim Jan 29 '21 at 19:17
  • That last sentence (_They're just using ..._) is exactly what is going on in your example. Closure is something you hardcode to a script file, that is, that functions can see variables defined in the outer scopes, then accessing an outerscope variable creates a closure. – Teemu Jan 29 '21 at 19:20
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/228016/discussion-between-tim-and-teemu). – Tim Jan 29 '21 at 19:31

1 Answers1

2

You can check what these ways are acting on the Dog.prototype object:

Method definition:

class Dog {
  bark() {
    console.log(this) // *this* referss to the actual instance
  }
}

console.log(Dog.prototype.bark); // function bark

Public class field [MDN]:

class Dog {
  bark = () => {
    console.log(this); // also, *this* refers to the actual instance
  }
}

console.log(Dog.prototype.bark); // undefined

In the first case you define a function in the class prototype, while in the latter you define the variable in the instance at "constructor time", as for any other variable.

The latter is the same as doing:

class Dog {
  constructor() {
    
    this.bark = () => {
      // this is the reason why *this* is actually available
      // and refers to the actual instance
      console.log(this);
    }
    
    /* The rest of defined constructor */
  }
}

console.log(Dog.prototype.bark); // undefined

Remember that the Public class fields are still not introduced in the ECMAs standards, so many JS environments could not support them, you should use some tools like Babel in order to achieve back compatibility. Some behaviours are still application dependant for this reason (like definition precedence).

DDomen
  • 1,808
  • 1
  • 7
  • 17