I am writing code in assembly language (AT89s51). When I see a standard program, the ports are typically defined as inputs or outputs. But, without defining them it seems to work fine. How is this possible?
With the the DATA direction register bits when clear (like after reset) configure the port as an input, in this state the output register bits control the pull-up, giving you in effect a very weak output drive. if you don't need a strong drive (eg you just want to drive the base of an NPN transistor) then in this state the port can be used as an output.
But if you need more than a few hundered microamps you'll need to set the direction register to output.
The ancient 8051 series, at least models that cleave to the original architecture, do not have data-direction registers for the GPIO pins.
The pins are "pseudo-bidirectional" (all the time) and the pins cannot source much current at DC. To use them as inputs you just set them to "1". The weak pullup MOSFET is easily overpowered by an external N-channel MOSFET output.
If you set them to "0" the strong N-channel will be pulling the "input" down towards 0V and thus fighting an external output that tries to drive the pin high and may or may not win under all conditions.
More information in this answer.
Just in general terms, you should always study the datasheet for the MCU that you are programming. In the case of the 8051 you might have to look at older data. Many modern MCUs have datasheets that are in the 2,000 page range, plus additional information from the core supplier, plus all the info on the middleware and drivers. The 8051 is a very simple part and it's a good idea to get into this habit or you will have problems working with modern parts.
The ports are accessed through the SFRs (Special Function Registers).
The port latch registers have reset values of 0xFF, which pulls the pins high through internal pull-ups, and can then be read as inputs. At this point, the output of the pin will be high, but if pulled low externally, the change in value will be registered by the SFR. Therefore, by default you can read the inputs by reading their corresponding SFRs.
When a port's SFR is written to, the port becomes an output and the SFR data is forwarded to the port latch.
Note that, although the same SFR is used, internally there are two distinct operations carried out, which are executed entirely in hardware, preventing you from having to keep track of the direction in which the data is moved.