Defining commands that are scoped to a particular environment

You can save the definitions in internal macros and then expose them locally within the foo environment.

\documentclass{article}

\newcommand{\asdf}{do something}
\newcommand{\zzz}{zzz}

%\usepackage{foo}
\makeatletter
\long\def\foo{%
\par FOO\par\hrule
\the\foo@defs}

\newtoks\foo@defs

\def\newfoocommand#1{%
\addto@hook\foo@defs{\foo@let#1}%
\expandafter\newcommand\csname foo\string#1\endcsname}

\def\foo@let#1{%
\expandafter\let\expandafter#1\csname foo\string#1\endcsname}

%\makeatother

% doesn't conflict with the above \asdf command because it's in a different
% namespace.
% this actually does \newcommand{\@foo@cmd@asdf}{do something else}
\newfoocommand{\asdf}{do something else}
\newfoocommand{\jkl}{blahblah}


\begin{document}

% expands to "do something"
\asdf
% expands to "zzz"
\zzz
% causes: ERROR: Undefined control sequence.
\jkl

\begin{foo}
  % is interpreted as \@foo@cmd@asdf and expands to "do something else"
\show\asdf
  \asdf
  % expands to "zzz" because \@foo@cmd@zzz isn't defined
  \zzz
  % is interpreted as \@foo@cmd@jkl and expands to "blahblah"
  \jkl
\end{foo}

\end{document}

With this implementation any command can have different meanings in environments which have \checkenvcommands in their "begin" part:

\documentclass{article}
\usepackage{xparse}
\ExplSyntaxOn
\NewDocumentCommand{\newenvcommand}{ m m } % #1 = env name, #2 = command name
  {
   \cs_if_exist:cF { g_envc_#1_list_tl } { \tl_new:c { g_envc_#1_list_tl } }
   \tl_gput_right:cn { g_envc_#1_list_tl } { #2 }
   \exp_after:wN \newcommand \cs:w envc_#1_\cs_to_str:N #2 \cs_end:
  }
\NewDocumentCommand{\checkenvcommands}{ }
  {
   \cs_if_exist:cT { g_envc_\use:c {@currenvir} _list_tl }
     {
      \tl_map_inline:cn { g_envc_\use:c {@currenvir} _list_tl }
        { \cs_set_eq:Nc ##1 { envc_\use:c {@currenvir} _\cs_to_str:N ##1 } }
     }
  }
\ExplSyntaxOff

\newcommand{\asdf}[1]{OUTER DEF - arg is #1}
\newenvcommand{foo}{\asdf}[1]{FOO INNER DEF - arg is #1}
\newenvcommand{baz}{\asdf}[1]{BAZ INNER DEF - arg is #1}

\newenvironment{foo}{\checkenvcommands}{}
\newenvironment{baz}{\checkenvcommands}{}

\begin{document}
\show\asdf

\begin{foo}
\show\asdf
\end{foo}

\show\asdf

\begin{baz}
\show\asdf
\end{baz}

\end{document}

The output is

> \asdf=\long macro:
#1->OUTER DEF - arg is #1.
l.28 \show\asdf

? 
> \asdf=\long macro:
#1->FOO INNER DEF - arg is #1.
l.31 \show\asdf

? 
> \asdf=\long macro:
#1->OUTER DEF - arg is #1.
l.34 \show\asdf

? 
> \asdf=\long macro:
#1->BAZ INNER DEF - arg is #1.
l.37 \show\asdf

The implementation is similar to David's, but it doesn't require to define a \<env>@let macro for each environment where we want "local" meanings. Only a single \checkenvcommands is required in those environments.

The "local" commands are stored in a different token list variable for each environment (in the example \g_envc_foo_list_tl and \g_envc_baz_list_tl), and \checkenvcommands does a mapping so that the "private" version of a command, say \envc_foo_asdf, is made equivalent to \asdf. If the list doesn't exist, nothing is done.