3

I'm trying to process some json and write pieces of data to a few files.

So I have a function to create folder/files based on the json data and some literals. The filenames are defined by an object containing different categories:

const folders = {
    category1: {
        fileName: 'default',
        path : '/path',
        subpath : () => `/subpath/${this.fileName}${this.uniqueId}`
    }
}

This is where half of you will jump ahead and tell me that arrow functions can't see this of their own objects etc. I know that and that's intentional, since I'll be getting the necessary data later on.

The main function follows this pattern:

function readSave() {
    //suppose readFileSync would return a string 'filename' and an int 1029

    this.fileName = 'filename'; 
    this.uniqueId = 1029;

    let filePath = folders.category1.path + folders.category1.subpath();

    // I'd go on to write stuff to that file but for now let's just return
    return filePath;
}

readSave() 
// returns '/path/subpath/undefinedundefined'
// expected '/path/subpath/filename1029'

I also know that I could just pass fileName and uniqueId as args but that's not the point. This post is not exactly an attempt to find a solution but to understand why it's not working.

The confusion here is the use of this inside readSave. According to MDN this inside a regular function is the same object where the function is called. Since I'm calling it on a plain js file on node, it's global.

All good for now. If I inspect the run inside the function call, this is global and the properties are set without problems.

The problem is folders.category1.subpath() evaluates to undefined.

The debugger shows when it goes to evaluate () => /subpath/${this.fileName}${this.uniqueId}, this is not global anymore, instead it's an empty Object. This doc makes me think that the arrow function should inherit the this of the scope it's being called, which is readSave, which means this should be global.

To increase my confusion, setting the properties outside the function works flawlessly:

function readSave2() {
   let filePath = folders.category1.path + folders.category1.subpath();
   return filePath;
}

this.fileName = 'filename'; 
this.uniqueId = 1029;

readSave() 
// returns '/path/subpath/filename1029'

Inspecting the code above, everything is pretty much the same up until when it goes to evaluate () => /subpath/${this.fileName}${this.uniqueId}. Now the previously empty Object has two properties as they were set.

Finally, this also works exactly the same as the previous example:

const readSave3 = () => {
    this.fileName = 'filename'; 
    this.uniqueId = 1029;
    let filePath = folders.category1.path + folders.category1.subpath();
    return filePath;
}

I've browsed and read about this for hours but am still really confused as to why some methods work and some don't.

Thanks in advance

Update: Turns out I made some wrong assumptions on what Node's root object is in a given file or function call. Wasn't really aware of the module wrapper.

dalmo3
  • 368
  • 1
  • 8
  • In the const defination it is not valid object you are missing , after filename. Could you check that is not the problem or is it just typo in your question. – Jitesh Manglani Aug 08 '19 at 06:56
  • Simple rule. NEVER use `=>` to define methods on an object. It will give you the lexical `this`, not the object `this`. – jfriend00 Aug 08 '19 at 08:16

2 Answers2

1

This doc makes me think that the arrow function should inherit the this of the scope it's being called, which is readSave, which means this should be global.

No the this of the arrow function is determined at the time the arrow function is created:

[...] No matter what, foo's this is set to what it was when it was created (in the example above, the global object). The same applies to arrow functions created inside other functions: their this remains that of the enclosing lexical context [...].

So this in the arrow function refers to what this is here:

console.dir(this) // <----  refers to `exports`
const folders = {
    category1: {
        fileName: 'default'
        path : '/path',
        subpath : () => `/subpath/${this.fileName}${this.uniqueId}`
    }
}

And the this for those two code block refer to the same object for this exact same reason:

console.dir(this) // the `this` in the arrow function below is the same as here
                  // and `this` refers to `exports`
const readSave3 = () => {
    this.fileName = 'filename'; 
    this.uniqueId = 1029;

    // ...
}

readSave3()
function readSave() {
   // ...
}

this.fileName = 'filename'; 
this.uniqueId = 1029;

readSave() 

On load the content of a node file is wrapped into: (The module wrapper)

(function(exports, require, module, __filename, __dirname) {
   /*... filecontent ... */
})

That function is then called passing the corresponding values as arguments and that function is called on the object passed as exports to the function What is the root object in Node.js.

So for /subpath/${this.fileName}${this.uniqueId} the this refers to exports the same is with readSave3 and your last code. For your readSave (the first one) the this refers to the global object.

So for node your code would look like this:

var moduleSetup = function(exports, require, module, __filename, __dirname) {
  // here `this` is exports 

  const folders = {
    category1: {
      fileName: 'default',
      path: '/path',
      // `this`referes to exports due to arrow function
      subpath: () => `/subpath/${this.fileName}${this.uniqueId}`
    }
  }


  function readSave1() {
    // here `this` refers to `global` because `readSave1` is not called on an object

    //suppose readFileSync would return a string 'filename' and an int 1029

    this.fileName = 'filename';
    this.uniqueId = 1029;

    let filePath = folders.category1.path + folders.category1.subpath();

    // I'd go on to write stuff to that file but for now let's just return
    return filePath;
  }

  readSave1()


  function readSave2() {
    let filePath = folders.category1.path + folders.category1.subpath();
    return filePath;
  }

  // `this` refers to `exports`
  this.fileName = 'filename';
  this.uniqueId = 1029;

  readSave2()


  const readSave3 = () => {
    // `this` refers to `exports` due to arrow function
    this.fileName = 'filename';
    this.uniqueId = 1029;

    let filePath = folders.category1.path + folders.category1.subpath();
    return filePath;
  }
  readSave3()
}

// and that's roughly how node would invoce that function:
var module = {
  exports: {}
}

moduleSetup.call(module.exports, // moduleSetup called on module.exports
                 // with these arguments:
                 module.exports, require, module, theFileName, theDirname)
t.niese
  • 32,069
  • 7
  • 56
  • 86
  • Thanks for pointing out the "when created" part. After some testing now I get it why options 2 and 3 work. However for the first option, where I set `this.fileName` inside readSave, I still need to study a bit. It seems straightforward on a browser, but on node there's also the global and the exports objects... Anyway the locus of the question now has shifted to how `this` and scope in general is inherited by a regular function. I should probably write a separate question. – dalmo3 Aug 08 '19 at 13:00
  • @dalmo3 update my answer to contain this information – t.niese Aug 08 '19 at 13:14
  • That wraps it up, marking it as solution. Just one thing, your comment under `readSave3`... `//this refers [to what?] due to arrow function`. I'm guessing `exports`. – dalmo3 Aug 08 '19 at 23:10
  • @dalmo3 yes `exports` was missing there. – t.niese Aug 09 '19 at 06:33
0

this always point the current context. when the arrow function going to execute this change to its current context which is in your case is the arrow function. so the this inside the arrow function and outside of the arrow functions are not same they are in different context this

mak rony
  • 21
  • 3
  • `[...]change to its current context which is in your case is the arrow function[...]` that does not make to much sense. – t.niese Aug 08 '19 at 06:49