The difference is demonstrated by the following code snippet, where I've changed only the scoping construct. But let's first define a simple function:
f[x_]:=x+a
Now we first look at `Block`:
Block[{a=b}, Print[a]; a=3; f[x]+a]+foo[a,b]
(*
Prints: b
--> 6 + x + foo[a, b]
*)
As you see, the global value of a is temporarily assigned to b, then inside changed to 3. That is, not only the `a` inside the `Block`, but also the value of `a` in the function call from the `Block` is modified. Outside the block, the change is undone, including any change done inside the block (the `a=3`).
Let's now look at `Module`:
Module[{a=b}, Print[a]; a=3; f[x]+a]+foo[a,b]
(*
Prints: b
--> 3 + a + x + foo[a, b]
*)
As you see, this time the a in the function does *not* evaluate to b. Indeed, the `a` inside the `Module` is replaced by a temporary variable, as can be seen by not assigning a value:
Module[{a},a]
(*
--> a$84
*)
Finally, `With`:
With[{a=b}, Print[a]; a=3; f[x]+a]+foo[a,b]
(*
Prints: b
--> 3 + a + x + foo[a, 3]
*)
As you can see, the `a=3` now globally assigns 3 to `b`! That's because `With` actually replaces `a` with the "assigned" value, i.e. `b` in the whole body. That is, whereever the body contains `a` it's as if there was written `b` instead. But again, the value of `a` in the called function `f` is not affected.