How does Etherscan do this? It is my understanding that you have to call a function on each ERC20 contract to see the balance for a particular address. Is there some function in web3 that provides this information?
You are correct about the function, the ERC20 standard defines a function which all ERC20 compatible tokens must include as follows:
function balanceOf(address _owner) view returns (uint256 balance)
Calling this function on a particular token contract, for a particular address, will return how many of those tokens this particular address owns.
How does Etherscan know all the different tokens you have? I don't know for sure, but as the name of their website suggests, their system watches the blockchain, and sorts the data into a readable format (ie, the website). So presumably whenever a contract gets deployed, their system checks it to ask "is this an ERC20 token contract?" and if so, it takes note of that.
Remember that the blockchain is basically just a big database, so when you enter a wallet address it could just call
balanceOf on all the different ERC20 tokens it knows about, with your address, against its own copy of the blockchain, and display the ones that return greater than 0.
(Note: for efficiency they may actually just watch for changes and store all relevant data on their own private backend database - but the end result is the same).
To answer the final part of your question, using Web3, if we assume
token is your ERC20 token contract, you can get balances as follows:
const balance = await token.methods.balanceOf(someAddress).call();