preface
A classic question from 2ality.com/try-finally.
What is the output of the following code?
var count = 0;
function foo() {
try {
return count;
} finally{ count++; }}console.log(foo());
console.log(count);
Copy the code
Output:
0
1
Copy the code
Thus the author asserts:
- The
finally
clause is always executed, no matter what happens inside thetry
Clause (return, exception, break, normal exit).”Finally is always executed, no matter what operation is performed inside the try statement, such as return, throw, break, exit normally“ - However, it is executed afterThe return statement.”However finally is executed after return“
The first is true. The second thing that it looks like from the result is that we did return 0, and then count++, and count becomes 1. Shouldn’t a return always be the last statement a function executes? Because once a function exits the call stack is destroyed, it is impossible to execute any code inside the function. This is the underlying mechanism of the function, not something that can be violated by the syntax layer. If finally is executed first and changes the value of count, why does it have no effect on the count of the final return? Both look like a count from the JS code level!
Finally is not executed after a return, and says the value that hold will return, so the return value is still 0, as if a snapshot had been taken.
That’s what we’re going to argue. What’s the order? What holds the return value 0, and whether the two counts are the same.
What Hold the return
ed Value
The first thing to remember is that JS code probably doesn’t end up executing what you see. What your see is not What the V8 actually executed! Because JS is an interpreted language, the actual running code will experience
JavaScript Code => V8 Bytecode => Machine Code
This famous picture:
fromdailyjs/understanding-v8s-bytecode.
So what I’m going to do is I’m going to break this up in bytecode form and see what’s going on underneath? Whether the two counts are the same or not.
Change the code slightly to make it easier to observe bytecode:
let count = 13;
function foo() {
try {
count += 7;
return count;
}
finally {
count += 10; }}console.log('return:', foo()); // 13 + 7 = 20
console.log('count:', count); // 13 + 7 + 10 = 30
Copy the code
return
return: 20
count: 30
Copy the code
It’s really consistent with what we saw before. To bytecode:
node --print-bytecode --print-bytecode-filter=foo finally-countpp.js
Copy the code
Output: please swipe right at ➡️
[generated bytecode for function: foo]
Parameter count 1
Register count 4
Frame size 32
29 E> 0x1477b9ae18d6@ 0:a5 StackCheck
0x1477b9ae18d7@ 1:27ff f9 Mov <context>, r2
46 S> 0x1477b9ae18da@ 4:1a 04 LdaCurrentContextSlot [4]
0x1477b9ae18dc@ 6:aa 00 ThrowReferenceErrorIfHole [0]
0x1477b9ae18de@ 8:40 07 00AddSmi [7], [0]
0x1477b9ae18e1@ 11:26f8 Star r3
0x1477b9ae18e3@ 13:1a 04 LdaCurrentContextSlot52 [4]E> 0x1477b9ae18e5@ 15:aa 00 ThrowReferenceErrorIfHole [0]
0x1477b9ae18e7@ 17:25f8 Ldar r3
0x1477b9ae18e9@ 19:1d 04 StaCurrentContextSlot[4], 63S> 0x1477b9ae18eb@ 21:1a 04 LdaCurrentContextSlot [4]
0x1477b9ae18ed@ 23:aa 00 ThrowReferenceErrorIfHole [0]
0x1477b9ae18ef@ 25, 26fa Star r1
0x1477b9ae18f1@ 27:0c 01 LdaSmi [1]
0x1477b9ae18f3@ 29:26fb Star r0
0x1477b9ae18f5@ 31:8b 07 Jump [7] (0x1477b9ae18fc @ 38)
0x1477b9ae18f7@ 33:26fa Star r1
0x1477b9ae18f9@ 35:0b LdaZero
0x1477b9ae18fa@ 36:26fb Star r0
0x1477b9ae18fc@ 38:0f LdaTheHole
0x1477b9ae18fd@ 39:a6 SetPendingMessage
0x1477b9ae18fe@ 40:26f9 Star r2
97 S> 0x1477b9ae1900@ 42:1a 04 LdaCurrentContextSlot [4]
0x1477b9ae1902@ 44:aa 00 ThrowReferenceErrorIfHole [0]
0x1477b9ae1904@ 46:40 0a 01 AddSmi [10], [1]
0x1477b9ae1907@ 49:26f8 Star r3
0x1477b9ae1909@ 51:1a 04 LdaCurrentContextSlot[4], 103E> 0x1477b9ae190b@ 53:aa 00 ThrowReferenceErrorIfHole [0]
0x1477b9ae190d@ 55:25f8 Ldar r3
0x1477b9ae190f@ 57:1d 04 StaCurrentContextSlot [4]
0x1477b9ae1911@ 59:25f9 Ldar r2
0x1477b9ae1913@ 61:a6 SetPendingMessage
0x1477b9ae1914@ 62:25fb Ldar r0
0x1477b9ae1916@ 64:9f01 00 02SwitchOnSmiNoFeedback [1], [2], [0] { 0: @70.1: @73 }
0x1477b9ae191a @ 68 : 8b 08 Jump [8] (0x1477b9ae1922 @ 76)
0x1477b9ae191c @ 70 : 25 fa Ldar r1
0x1477b9ae191e @ 72 : a8 ReThrow
0x1477b9ae191f @ 73 : 25 fa Ldar r1
114 S> 0x1477b9ae1921 @ 75 : a9 Return
0x1477b9ae1922 @ 76 : 0d LdaUndefined
114 S> 0x1477b9ae1923 @ 77 : a9 Return
Constant pool (size = 3)
Handler Table (size = 16)
from to hdlr (prediction, data)
( 4.33) - >33 (prediction=0, data=2)
return: 20
count: 30
Copy the code
How to read bytecode
The V8 Ignition interpreter uses the Register Machine architecture. Register or stack This is the architecture selection for almost all virtual machines, why V8 uses register architecture is not expanded yet. V8 Ignition uses ordinary registers R0, R1, R2… And an accumulator register. The accumulator register is almost the same as an ordinary register, but is generally used as a temporary variable storage. We deliberately omit it when writing instructions, because almost all bytecode instructions operate the accumulator register. Makes bytecode compact and saves memory. Add R1, for example, adds the values in register R1 to the accumulator and places them in the accumulator, making the code shorter by omitting the accumulator.
From v8. Dev/blog/igniti…
Most bytecodes begin with Lda or Sta, in which the A represents accumulator, an accumulator register. For example, LdaSmi [42] loaded the Small INTEGER (Smi) 42 into the accumulator register. 42 = > a. Star r0 Stores the value currently in the accumulator in register R0, a => r0.
fromdailyjs/understanding-v8s-bytecode
For our example, we need to understand more bytecode, and we need to turn to the V8 source interpreter/interpreter-generator.cc, which has detailed annotations and is moderately difficult to read.
Start reading
Read by adding comments and extract key bytecode paragraphs.
step1: count += 7
Please ➡ ️ right and smooth
46 S> 0x1477b9ae18da @ 4 : 1a 04 LdaCurrentContextSlot [4] // Load count 13 from the current context into the accumulator (a=13).0x1477b9ae18de @ 8 : 40 07 00 AddSmi [7], [0] // 🔥 1️ corresponding count += 7; And put 20 into the accumulator (a=20)
0x1477b9ae18e1 @ 11 : 26 f8 Star r3 // Place the accumulator values in R3. (r3 = 20).0x1477b9ae18e7 @ 17 : 25 f8 Ldar r3 // Load the values in R3 into the accumulator, where a=20
0x1477b9ae18e9 @ 19 : 1d 04 StaCurrentContextSlot [4] // Stores the values in the accumulator into the context, that is, save the context (context[4]=20) in preparation for switching the context to finally
63 S> 0x1477b9ae18eb @ 21 : 1a 04 LdaCurrentContextSlot [4] // a=20
0x1477b9ae18ed @ 23 : aa 00 ThrowReferenceErrorIfHole [0]
0x1477b9ae18ef @ 25 : 26 fa Star r1 // 🔥 r1=20, remember the r1 register
Copy the code
acc | r0 | r1 | r2 | r3 | CurrentContextSlot |
---|---|---|---|---|---|
13 + 7 = 20 | 20 (remember R1) | 20 | 20 |
step 2: count += 10
Please ➡ ️ right and smooth
0x1477b9ae18f5 @ 31 : 8b 07 Jump [7] (0x1477b9ae18fc @ 38) // Skip to line 38, finally.0x1477b9ae18fd @ 39 : a6 SetPendingMessage // keep context alive.97 S> 0x1477b9ae1900 @ 42 : 1a 04 LdaCurrentContextSlot [4] // select a from a=20.0x1477b9ae1904 @ 46 : 40 0a 01 AddSmi [10], [1] // 🔥 2️ corresponding 'count += 10' a=30
0x1477b9ae1907 @ 49 : 26 f8 Star r3 // r3=a=30.0x1477b9ae190d @ 55 : 25 f8 Ldar r3 // a=r3=30
0x1477b9ae190f @ 57 : 1d 04 StaCurrentContextSlot [4] // 🔥 3️ retail context, context[4]=30
Copy the code
acc | r0 | r1 | r2 | r3 | CurrentContextSlot |
---|---|---|---|---|---|
13 + 7 = 20 | 20 (remember R1) | 20 | 20 | ||
20 + 10 = 30 | 20 (remember R1) | 30 | 30 |
step 3: return count
Please ➡ ️ right and smooth
// throw jumps to 70, otherwise to 73
0x1477b9ae1916 @ 64 : 9f 01 02 00 SwitchOnSmiNoFeedback [1], [2], [0] { 0: @70.1: @73 }
0x1477b9ae191a @ 68 : 8b 08 Jump [8] (0x1477b9ae1922 @ 76)
0x1477b9ae191c @ 70 : 25 fa Ldar r1
0x1477b9ae191e @ 72 : a8 ReThrow
0x1477b9ae191f @ 73 : 25 fa Ldar r1 // 🔥 4️ one = R1 =20
114 S> 0x1477b9ae1921 @ 75 : a9 Return // 🔥 returns 20
0x1477b9ae1922 @ 76 : 0d LdaUndefined
114 S> 0x1477b9ae1923 @ 77 : a9 Return
Copy the code
acc | r0 | r1 | r2 | r3 | CurrentContextSlot |
---|---|---|---|---|---|
13 + 7 = 20 | 20 (remember R1) | 20 | 20 | ||
20 + 10 = 30 | 20 (remember R1) | 30 | 30 |
R1 is returned, so 20 is eventually returned, but the context count has changed to 30, so 30 is printed.
return: 20
count: 30
Copy the code
Full annotated version: right swipe at ➡️
[generated bytecode for function: foo]
Parameter count1 // A total of 1 arguments, that is, implicitthis, the parameter is 0Register count4 // Involves four common registersr0 r1 r2 r3
Frame size 32
29 E> 0x1477b9ae18d6@ 0:a5 StackCheck// Check whether the stack overflows 0x1477b9ae18d7@ 1:27ff f9 Mov <context>, r2 // r2=context= 13 46S> 0x1477b9ae18da@ 4:1a 04 LdaCurrentContextSlot[4] // Place the current contextcount13 Load to accumulator (a0 = 13)x1477b9ae18dc@ 6:aa 00 ThrowReferenceErrorIfHoleBecause [0] / /count 是 letDeclared, so need to determine whether the accumulatorholeIf it is, it is a mistake, which is called "temporary dead zone".TDZCheck, ifvarThere is no need to check 0x1477b9ae18de@ 8:40 07 00AddSmi[7], [0] // 1️ onecount+ = 7; And the results20Put in the accumulator (a=20)0x1477b9ae18e1 @ 11 : 26 f8 Star r3 // Place the accumulator values in R3. (r3 = 20)
0x1477b9ae18e3 @ 13 : 1a 04 LdaCurrentContextSlot [4] // Load the context count 13 into the accumulator (a=13)
52 E> 0x1477b9ae18e5 @ 15 : aa 00 ThrowReferenceErrorIfHole [0] / / TDZ examination
0x1477b9ae18e7 @ 17 : 25 f8 Ldar r3 // Load the values in R3 into the accumulator, where a=20
0x1477b9ae18e9 @ 19 : 1d 04 StaCurrentContextSlot [4] // Stores the values in the accumulator into the context, that is, save the context (context[4]=20) in preparation for switching the context to finally
63 S> 0x1477b9ae18eb @ 21 : 1a 04 LdaCurrentContextSlot [4] // a=20
0x1477b9ae18ed @ 23 : aa 00 ThrowReferenceErrorIfHole [0]
0x1477b9ae18ef @ 25 : 26 fa Star r1 // r1=20
0x1477b9ae18f1 @ 27 : 0c 01 LdaSmi [1] // a=1
0x1477b9ae18f3 @ 29 : 26 fb Star r0 // r0=1
0x1477b9ae18f5 @ 31 : 8b 07 Jump [7] (0x1477b9ae18fc @ 38) // Skip to line 38, finally
0x1477b9ae18f7 @ 33 : 26 fa Star r1
0x1477b9ae18f9 @ 35 : 0b LdaZero
0x1477b9ae18fa @ 36 : 26 fb Star r0
0x1477b9ae18fc @ 38 : 0f LdaTheHole // a=
hole is undefined but slightly different
0x1477b9ae18fd @ 39 : a6 SetPendingMessage // keep context alive
0x1477b9ae18fe @ 40 : 26 f9 Star r2 // r2=<the_hole>
97 S> 0x1477b9ae1900 @ 42 : 1a 04 LdaCurrentContextSlot [4] // select a from a=20
0x1477b9ae1902 @ 44 : aa 00 ThrowReferenceErrorIfHole [0]
0x1477b9ae1904 @ 46 : 40 0a 01 AddSmi [10], [1] // 2️ corresponding 'count += 10' A =30
0x1477b9ae1907 @ 49 : 26 f8 Star r3 // r3=a=30
0x1477b9ae1909 @ 51 : 1a 04 LdaCurrentContextSlot [4] // a=20
103 E> 0x1477b9ae190b @ 53 : aa 00 ThrowReferenceErrorIfHole [0]
0x1477b9ae190d @ 55 : 25 f8 Ldar r3 // a=r3=30
0x1477b9ae190f @ 57 : 1d 04 StaCurrentContextSlot [4] // 3️ retail context, context[4]=30
0x1477b9ae1911 @ 59 : 25 f9 Ldar r2 // a=<the_hole>
0x1477b9ae1913 @ 61 : a6 SetPendingMessage // a=<pending_message>
0x1477b9ae1914 @ 62 : 25 fb Ldar r0 // a=r0=1
// throw jumps to 70, otherwise to 73
0x1477b9ae1916 @ 64 : 9f 01 02 00 SwitchOnSmiNoFeedback [1], [2], [0] { 0: @70.1: @73 }
0x1477b9ae191a @ 68 : 8b 08 Jump [8] (0x1477b9ae1922 @ 76)
0x1477b9ae191c @ 70 : 25 fa Ldar r1
0x1477b9ae191e @ 72 : a8 ReThrow / / throw exceptions, V8: https://v8docs.nodesource.com/node-0.8/d4/dc6/classv8_1_1_try_catch.html
0x1477b9ae191f @ 73 : 25 fa Ldar r1 / / 4 ️ ⃣ a = r1 = 20
114 S> 0x1477b9ae1921 @ 75 : a9 Return // Returns 20
0x1477b9ae1922 @ 76 : 0d LdaUndefined
114 S> 0x1477b9ae1923 @ 77 : a9 Return
Constant pool (size = 3)
Handler Table (size = 16)
from to hdlr (prediction, data)
( 4.33) - >33 (prediction=0, data=2)
return: 20
count: 30
Copy the code
Comments:
- SetPendingMessage: Sets the pending message to the value in the accumulator, And returns the previous pending message in the Accumulator. Pending message causes context to remain alive. Pending Exception messages are V8’s try-catch mechanism.
- The result of the function will be stored in the accumulator, generally
Return
It was executed beforeLda
Then add the values in the accumulatorreturn
.
Execution sequence and code comparison diagram:
Therefore, the execution sequence is:
let count = 13;
function foo() {
try {
count += 7; // 1️ temporary storage to R1
return count; // 3️ return value of R1
}
finally {
count += 10; // 2️ modify the value of context}}Copy the code
Therefore, the count in return is not the same as the count in finally. The former is R1, and the latter is the count in context.
conclusion
- Finally is executed before return.
- Not the same count.
return
In thecount
Temporary storage in registersr1
,finally
All that changes is the context. You can do that because you have registersfinally
The modification does not affect the final return value becausereturn
The return value is already stored in a register as if it had been taken as a snapshot.
This is just how finally works. Finally must be executed before a return ina try to ensure that some resource must be released.
Finally extended
Something you thought you had mastered, and then you come across it again and it turns out to be different from what you thought it was, which I like to call “Schrodinger learning.”
Keep reading the comments:
If the finally doesn’t return or throw, then the function returns the try’s return value.
However, the finally can override that return value with it’s own return value or the finally can stop any return value from being returned by throwing.
The proof is logically the same as what a previous commenter wrote:
It says that “the finally can override that return value”, that is, if there is a return in finally, its return shall prevail. However, because of this non-intuitive writing method, Eslint has a rule specifically to disallow return rules/ no-safe-finally in finally.
JavaScript suspends the control flow statements of
try
andcatch
blocks until the execution offinally
block finishes. So, whenreturn
,throw
,break
, orcontinue
is used infinally
, control flow statements insidetry
andcatch
are overwritten, which is considered as unexpected behavior. Such as:
var count = 0; function foo() { try { return count + 100; }finally { return ++count;} } console.log(foo()); console.log(count); Copy the code
Output:
1 1 Copy the code
Articles to write:
- Why does V8 choose Register Machine over Stack Machine
- Does let const have variable promotion at all? The conclusion exists first
- Bytecode see let var performance
reference
This all the way through too much information, a lot of information is very valuable, so record it.
- Medium.com/dailyjs/und…
- V8. Dev/blog/igniti…
- Github.com/v8/v8/blob/…
- V8docs.nodesource.com/node-0.8/d4…
- 2 ality.com/2013/03/try…
- Eslint.org/docs/rules/…
- www.coderbridge.com/series/817c…
- Stackoverflow.com/questions/6…
Recruitment of Alipay Experience Technology Department, wechat Legend80s