Reputation: 63
I have the following code here:
class ColorPrinter {
constructor(sourceFile, colors) {
this.sourceFile = sourceFile
this.colors = colors
}
showColors() {
this.colors.forEach(this.printColor);
}
printColor(colorObj) {
console.log(this.sourceFile, colorObj.name, colorObj.hex);
}
}
const colors = [{name: "red", hex: "#FF0000"}, {name: "yellow", hex: "#FFFF00"}, {name: "cyan", hex: "#0000FF"}];
const cp = new ColorPrinter('colors.csv', colors);
cp.showColors();
However when run I run this, I get the run time error of :
"TypeError: Cannot read property 'sourceFile' of undefined"
I don't understand why JS isn't happy. I am not trying to read from a file, but it won't take the string as a parameter.
Upvotes: 0
Views: 552
Reputation: 27245
Change it to:
this.colors.forEach((x) => this.printColor(x));
The reason your original code doesn't work as you expected has to do with the way scope works in javascript, specifically how the value of this
is bound.
The main thing to understand is that (in general) when you invoke a function, this
is set to the object on which the method was invoked.
wookie.eatBanana(); // inside the eatBanana method, this === wookie
But if you invoke the method separately from the object, this
ends up being undefined
:
const detachedFn = wookie.eatBanana;
// same function, but now this === undefined
detachedFn();
And what you're doing when you pass this.printColor
to forEach
is passing the function itself, which ends up getting invoked without being bound to your object:
const detachedFn = this.printColor;
this.colors.forEach((x) => detachedFn(x)); // this === undefined inside detachedFn, because it's invoked standalone
Inside the forEach
implementation it's just invoking the function it was given. Effectively:
// pseudocode
function forEach(fn) {
fn(); // no reference to your class instance; fn is just a regular function.
}
Defining a new function that invokes this.printColor()
preserves the scope:
this.colors.forEach((x) => this.printColor(x));
function forEach(fn) {
fn(); // inside this function your method is called *on your object*, preserving the 'this' binding.
}
Arrow functions auto-bind to the parent scope:
An arrow function does not have its own this. The this value of the enclosing lexical scope is used; arrow functions follow the normal variable lookup rules. So while searching for this which is not present in current scope, an arrow function ends up finding the this from its enclosing scope.
Arrow function auto-binding also comes in handy to solve these problems if you declare the methods as arrow functions to begin with. (This is experimental and might require the babel class properties plugin).
// declaring this method as an arrow function causes it to bind to
// the parent scope (the class instance) which means you can invoke
// it independently of the instance.
printColor = (colorObj) => {
console.log(this.sourceFile, colorObj.name, colorObj.hex);
}
// now this is fine because printColor is already bound to 'this'
this.colors.forEach(this.printColor);
Or, as palaѕн pointed out, with an explicit call to bind:
// make a copy of printColor that's explicitly bound to 'this'.
const explicitlyBoundFn = this.printColor.bind(this);
// works
this.colors.forEach(explicitlyBoundFn);
You can also accomplish this via call or apply, which both allow you to pass in a scope (although there's no reason to do so in this case).
// no reason to do this, but it works.
const detachedFn = this.printColor;
this.colors.forEach((x) => detachedFn.call(this, x));
Hope this helps. Happy to update it if anything needs clarification.
Upvotes: 2
Reputation: 73886
You can bind the current this
also like:
this.colors.forEach(this.printColor.bind(this));
class ColorPrinter {
constructor(sourceFile, colors) {
this.sourceFile = sourceFile
this.colors = colors
}
showColors() {
this.colors.forEach(this.printColor.bind(this));
}
printColor(colorObj) {
console.log(this.sourceFile, colorObj.name, colorObj.hex);
}
}
const colors = [{name: "red", hex: "#FF0000"}, {name: "yellow", hex: "#FFFF00"}, {name: "cyan", hex: "#0000FF"}];
const cp = new ColorPrinter('colors.csv', colors);
cp.showColors();
The bind()
method creates a new function that, when called, has its this
keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.
So, without using bind
this
inside printColor
method is undefined and by using bind
we are just telling the printColor
method that this
here should refer to ColorPrinter
class so that we can access this.sourceFile
,
Upvotes: 0