When arguments are mutable

I’ve found this weird behavior of the arguments inside a function while playing and experimenting with some code some days ago. Normally it would not be interesting since all the code we use is usually built-transpiled and put into strict mode, but my sandbox was not - and this is an exact case when omitting strict mode can cause weird side effects.

Arguments

Lets start with the famous arguments object found in almost* every function in JavaScript.

Quickly go over what you can and cannot do with arguments.

You can

You cannot

Looks pretty clear, right? Now, let’s get down the rabbit hole…

Tracking and mutating

Focus on our mutable arguments object right now. Take a look at this piece of code, and try to guess what is written to the console:


        
var bar = function (a) {
  a = 'foo'
  console.log(arguments[0])
}

bar(2)
    

Just by looking at the code, we think it should log 2. But here comes the strange quirk, it will log “foo”. If you don’t believe me, try it out yourself.

What just happened?! Turns out there is a maintained relationship between the arguments of a function, and the arguments object, when certain conditions apply:

When a non-strict function does not contain rest, default, or destructured parameters, then the values in the arguments object do track the values of the arguments (and vice versa).

This works vice-versa, altering either the named argument or an entry in the arguments object, will track the changes to the other one:


        
// non-strict function, not containing rest,
// defaults or destructured parameters
var fn1 = function (a) {
  a = "foo"
  console.log(arguments[0])
}

// argument `a` tracks arguments[0]
fn1(2) // "foo"

var fn2 = function (a) {
  arguments[0] = "bar"
  console.log(a)
}

// arguments[0] tracks argument `a`
fn2(2) // "bar"

    

Using strict mode, or working with rest, defaults or destructured arguments will prevent this weird behavior


        
// using strict mode
var fn3 = function (a) {
  "use strict";
  a = "foo"
  console.log(arguments[0])
}

// arguments, and the arguments object no longer track each other
fn3(2) // 2

// containing rest, defaults or destructured parameters
var fn4 = function (a = 0) {
  arguments[0] = "bar"
  console.log(a)
}

fn4(2) // 2

var fn5 = function (...args) {
  arguments[0] = "bar"
  console.log(args[0])
}

fn5(2) // 2

var fn6 = function () {
  const [a] = arguments
  arguments[0] = "bar"
  console.log(a)
}

fn6(2) // 2
    

Check out these examples as real code in this bin.

Quirks like this can cause side effects, and severe bugs later in your code, so be safe! The following tricks can help you, to keep your code sane:


PS: this is totally different, when you assign an argument to a variable inside the function, like here


        
var bar = function (a) {
  var aa = arguments[0]
  a = 'foo'
  console.log(aa)
}

//this works as expected
bar(2) // 2