Consider the following code:
function foo() {
console.log(this.bar);
}
const obj = {
bar: "foobar",
myFunction: foo
}
obj.myFunction(); // foobar
obj["myFunction"](); // also `foobar`
In the above, the this in the function foo gets bound to the calling context of foo, in the case above, that is the object obj because that's where the function myFunction is being invoked from, and so this inside of the function foo will refer to obj.
In your case, arguments is an object in a similar way to how obj above is an objct. It has a bunch of properties such as its length as well as numeric properties to represent indexes for the arguments passed into the function. For example, you could (roughly) make your own arguments object like so:
function foo() {
console.log(this.length);
}
const myArguments = {
0: foo,
length: 2
};
// myArguments.0() is syntactically invalid, so we can only use bracket-notation like below
myArguments[0](); // 2
Notice that the above myArguments[0]() is using the same idea of calling a function from the first code snippet (obj.myFunction() & obj["myFunction"]()), as we are using bracket notation (arguments[0]) (rather than dot-notation (arguments.0)) to access and call the function stored at the property 0 from the object myArguments. Thus, the calling context for foo in this case is myArguments, and as a result that is what this gets bound to within the function foo.
The same idea above applies to your code. When you call fn() without an explicit context (as you're not inovking it from an object by having it prefixed with someObject., or using methods such as .bind(), .call() or .apply()) the this within your foo() function will be undefined (if in strict-mode), thus causing your error:
TypeError: Cannot read property 'length' of undefined
However, when using arguments[0], the this gets bound to the arguments object, much like in the above two examples.