Lock files are useful in production environments where you want to ensure the exact same package versions are being used that you tested with. A package.json file specifies package names and versions, but theres no guarantee your package registry will always serve the same content.
The lock file records the hash of each package and uses it to verify package integrity on future installs. If any of the hashes don't match up, the `npm clean-install` command[0] will throw an error.
Integrity checks are important, but an even more basic benefit obtains regarding version ranges. Typical package.json dependency entries specify _ranges_ of versions that will satisfy the dependency, rather than a single specific ("pinned") version. So it's by design, and fully to be expected, that over time, subsequent builds will yield different results as library authors publish newer versions which still satisfy the version ranges. Knowing when this has happened, and being able to diff the results in the dependency graph, is a key feature of lockfiles. A proper lockfile implementation (like yarn's) provides deterministic builds, so anyone building the project with the same lockfile gets precisely the same dependency graph. You get the benefits of version pinning without the downside.
The lock file records the hash of each package and uses it to verify package integrity on future installs. If any of the hashes don't match up, the `npm clean-install` command[0] will throw an error.
[0] https://docs.npmjs.com/cli/ci.html