There are two ways to invoke a function in a smart contract:
- You can send a transaction. A transaction gets sent to the network and gets mined as part of a block. It's verified by all the nodes in the network.
- You can call a function. This does not send anything to the rest of the network; a call is executed locally on the node you're connected to. You get back the return value of the function, but otherwise the call has no side effects. If it transferred ether or changed a state variable, those changes are just thrown out. The advantage of a call is that it's fast and free (no need to pay gas).
Remix decides whether to send a transaction or make a call based on whether the function is marked as mutating state or not. view/constant tells the compiler and then Remix that you're not mutating state, so this function can be called instead of sending a transaction.
This explains why Remix treats the function differently depending on whether you use the constant modifier. The final piece of the puzzle is that transactions do not have return values. If you invoke getAge as a transaction, the code will run, but you won't get back the return value.
Because of these differences, it's a best practice to always mark functions that don't modify state as view (a better alias for constant). That makes the function fast, free, and able to return a value to you.