Learning JavaScript: Call By Sharing, Parameter Passing

Chidume Nnamdi 🔥💻🎵🎮
Bits and Pieces
Published in
9 min readSep 13, 2018

--

There are so many misconceptions and debate on the internet about how JavaScript passes values to functions. Some people write JavaScript uses call-by-value for primitive values and uses call-by-reference for data types like arrays, objects, and functions.

But the answer lies in the fact that JavaScript uses call-by-value for all data-types. It uses call-by-value for Arrays and Objects but in what is called call-by-sharing or a copy of reference.

In this article, we will look into the memory model of JavaScript during its execution of functions to see and understand what really happens.

What will we gain from this article? We will have a deep knowledge of how JS passes parameters to functions. With this knowledge, we can write a highly optimized JavaScript code.

Build faster with Bit

Bit helps your team build faster by sharing components between apps. Collaborate, suggest updates, sync changes and build amazing apps. It’s free and open source, give it a try.

Memory Model

JavaScript has three portions of memory assigned to a program during execution: Code Area, Call Stack and Heap. These combined together is known as the Address Space of the program.

Code Area: This is the area the JS code to be executed is stored.

Call Stack: This area keeps track of currently executing functions, perform computation and store local variables. The variables are stored in the stack in a LIFO method. The last one in is the first out. Value data types are stored here.

For example:

var corn = 95
let lion = 100

Here, corn and lion values are stored in the stack during execution.

Heap: This is where JavaScript reference data types like objects are allocated. Unlike stack, memory allocation is randomly placed, no LIFO policy. And to prevent memory “holes” in the Heap, the JS engine has memory managers that prevent them from occurring.

class Animal {}// stores `new Animal()` instance on memory address 0x001232
// tiger has 0x001232 as value in stack
const tiger = new Animal()
// stores `new Object()` instance on memory address 0x000001
// `lion` has 0x000001 as value on stack
let lion = {
strength: "Very Strong"
}

Here, lion and tiger are reference types, their values are stored in the Heap and they are pushed to the stack. Their values in stack hold the memory address of the location in Heap.

Activation Record, Parameter Passing

We have seen the memory model of a JS program. Now, let’s see what happens when a function is called in JavaScript.

// example1.jsfunction sum(num1,num2) {
var result = num1 + num2
return result
}
var a = 90
var b = 100
sum(a, b)

Whenever a function is called in JS, all the information needed to execute the function is put on the stack. This information is what is called Activation Record.

The information on an Activation Record contains the following:

  • SP Stack Pointer: This current position of the stack pointer before a method was called.
  • RA Return Address: This is the address execution continue from when the function execution is done with.
  • RV Return Value: This is optional, a function may or may not return a value.
  • Parameters: The parameters needed by the function is pushed to the stack.
  • Local variables: Variable used by the function is pushed to the stack.

We have to know this, the codes we write in js files are compiled to Machine language by the JS engine(e.g V8, Rhino, SpiderMonkey etc) before being executed.

So code like this:

let shark = "Sea Animal"

will be compiled to something like this:

01000100101010
01010101010101

The above code is the machine-level equivalence of our js code. There is a language in-between machine code and JS, it is the Assembly Language. The code-generator in a JS engine compiles the js code to Assembly code before finally to machine code.

In order to understand what really happens and how Activation Records are pushed to the stack during function calls, we have to see how our program is represented in Assembly.

To trace how parameters are passed in JS during a function call, we will represent our example in Assembly and trace its flow of execution.

Using example1,

// example1.jsfunction sum(num1,num2) {
var result = num1 + num2
return result
}
var a = 90
var b = 100
var s = sum(a, b)

we see that sum function have two parameters num1 and num2. The function is called passing in a and b each with value 90 and 100 respectively.

Remember: Value data types contains values while reference data types contain memory addresses.

Before the call of the sum function, its parameters are pushed to the stack

ESP->[......] 
ESP->[ 100 ]
[ 90 ]
[.......]

Then, it pushes the return address to the stack. The return address is stored in the EIP register:

ESP->[Old EIP]
[ 100 ]
[ 90 ]
[.......]

Next, it saves the base pointer

ESP->[Old EBP]
[Old EIP]
[ 100 ]
[ 90 ]
[.......]

Then the EBP is changed and caller-save registers are pushed to the stack.

ESP->[Old ESI]
[Old EBX]
[Old EDI]
EBP->[Old EBP]
[Old EIP]
[ 100 ]
[ 90 ]
[.......]

Space is allocated for the local variable:

ESP->[       ]
[Old ESI]
[Old EBX]
[Old EDI]
EBP->[Old EBP]
[Old EIP]
[ 100 ]
[ 90 ]
[.......]

Here the addition is performed.

mov ebp+4, eax ; 100
add ebp+8, eax ; eax = eax + (ebp+8)
mov eax, ebp+16
ESP->[ 190 ]
[Old ESI]
[Old EBX]
[Old EDI]
EBP->[Old EBP]
[Old EIP]
[ 100 ]
[ 90 ]
[.......]

Our return value is 190. We move it into EAX.

mov ebp+16, eax

Then, all the register values are restored.

[   190 ] DELETED
[Old ESI] DELETED
[Old EBX] DELETED
[Old EDI] DELETED
[Old EBP] DELETED
[Old EIP] DELETED
ESP->[ 100 ]
[ 90 ]
EBP->[.......]

And control is returned to the calling function. The parameters pushed to the stack are cleaned up.

[   190 ] DELETED
[Old ESI] DELETED
[Old EBX] DELETED
[Old EDI] DELETED
[Old EBP] DELETED
[Old EIP] DELETED
[ 100 ] DELETED
[ 90 ] DELETED
[ESP, EBP]->[.......]

The calling function now retrieves the return value from the EAX register to the memory location of s.

mov eax, 0x000002 ; location of `s` variable in memory

We have seen what happens in the memory and how parameters are passed to functions in near-machine code level.

Parameters are pushed onto the stack by the caller before calling the function. Therefore, it will be right to say the parameters are copied by value in JS. If the callee function should change the value of the parameter it wouldn’t affect the original because it is stored elsewhere, it was only dealing with a copy.

function sum(num1) {
num1 = 30
}
let n = 90
sum(n)
// `n` still 90

Let’s look at what happens when we pass a reference data type.

function sum(num1) {
num1 = { number:30 }
}
let n = { number:90 }
sum(n)
// `n` remains `{ number:90 }`

This is the assembly code rep:

data: 
n -> 0x002233
Heap: Stack:
002254 012222
... 012223 0x002233
002240 012224
002239 012225
002238
002237
002236
002235
002234
002233 { number: 90 }
002232
002231 { number: 30 }
Code:
...
000233 main: // entry point
000234 push n // `n` contains `002233` which points to `{ number: 90 }` in Heap. `n` is pushed to stack at `0x12223`.
000235 ; save all registers
...
000239 call sum ; jumps to `sum` sub-routine function in memory
000240
...
000270 sum:
000271 ; creates new Object() `{ number: 30 }` in Heap at 0x002231.
000271 mov 0x002231, (ebp+4) ; moves the value at 0x002231 `{ number: 30 }` to stack (ebp+4). (ebp+4) is 0x12223 `n` containing the address `0x002233`, the location of Object `{number: 90}` in Heap. Here, the stack location is overridden with value 0x002231. Now, num1 points to another memory address.
000272 ; clean up stack.
...
000275 ret ; return to caller. jmp to 000240.

We see here the variable n holds the memory address that points to its value in the Heap. On sum function execution, the parameter is pushed to the stack, to be received by the sum function. The sum function creates another Object {number: 30} which is stored in another memory address 002231 and places it in the parameter location on the stack. This replaces the memory address of Object {number: 90} that was previously at the parameter location on the stack with the memory address of the newly created Object {number: 30}.

This leaves n untouched and unchanged. So the copy-of-reference policy is true. The variable n was pushed to the stack, thus becoming a copy of n on sum execution.

This statement num1 = { number: 30} created a new Object in Heap and assigned the memory address of the new Object to the parameter num1. Note, before that num1 pointed to n, let's test to verify:

// example1.js
let n = { number: 90 }
function sum(num1) {
log(num1 === n)
num1 = { number: 30 }
log(num1 === n)
}
sum(n)
$ node example1
true
false

Yes, we are right. Just like we saw on the assembly code. Initially, num1 refers to the same memory address as n because n was pushed to the stack.

Then after the Object creation, num1 was reassigned to the memory address of the Object instance.

Let’s modify our example1 further:

function sum(num1) {
num1.number = 30
}
let n = { number: 90 }
sum(n)
// `n` becomes `{ number: 30 }`

This will have almost the same memory model and assembly as the prev one. Only a few things will change here. Inside the sum function implementation, there’s no new object creation. The parameter was directly affected.

...
000270 sum:
000271 mov (ebp+4), eax ; copies parameter value to eax register. eax now contains `0x002233`.
000271 mov 30, [eax]; move `30` to the address pointed to by `eax`.
...; rest of the code

num1 is (ebp+4) containing the address of n. The value is copied into eax and 30 is copied into the memory pointed to by eax. The braces [] on any registers tells the CPU to not use the value found in the register but get the value of the memory address number corresponding to its value. Therefore, {number: 90} value of 0x002233 is retrieved.

Look at this SO answer:

Primitives are passed by value, Objects are passed by “copy of a reference”.

Specifically, when you pass an object (or array) you are (invisibly) passing a reference to that object, and it is possible to modify the contents of that object, but if you attempt to overwrite the reference it will not affect the copy of the reference held by the caller — i.e. the reference itself is passed by value:

function replace(ref) {
ref = {}; // this code does _not_ affect the object passed
}
function update(ref) {
ref.key = 'newvalue'; // this code _does_ affect the _contents_ of the object
}
var a = { key: 'value' };
replace(a); // a still has its original value - it's unmodfied
update(a); // the _contents_ of 'a' are changed

From what we have seen in our Assembly code and memory model. This answer is 100 percent correct. Inside the replace function, it creates a new Object in the Heap and assigns it to the ref parameter. The ref containing the memory address of a object is overridden.

The update function refers to the memory address in the ref parameter and changes the key property of the object stored in the memory address.

Conclusion

With what we have seen above, we can say that copies of primitive and reference data types are passed as parameters to functions. The difference is that in primitives they are only referenced by their actual value. JS doesn’t allow us to get their memory addresses unlike in C/C++. Reference data types refer to their memory addresses.

If you have any question regarding this, feel free to comment! Thanks 😄

--

--

JS | Blockchain dev | Author of “Understanding JavaScript” and “Array Methods in JavaScript” - https://app.gumroad.com/chidumennamdi 📕