如果你还没忘的话,仔细回想一下,之前我们是如何将对象绑定到变量名上的。但当时我们只是全局绑定,在那时这种绑定是非常有用的。不过,有很多时候,本地绑定往往比全局绑定更合适,例如把变量限制在一个操作内部的时候。下面就让我们看看如果使用绑定函数 "let " 进行本地绑定。
=>id
java.lang.Exception: Unable to resolve symbol: id...
=>(let [id 1]
(println id))
1
nil
=>id
java.lang.Exception: Unable to resolve symbol: id...
正向你看到的南阳,通过使用"let" 操作,我们把1绑定到了”id“这个变量名上。然后我们又把它打印了出来。当这个操作执行完后,我们在外面查看”id“时却是无法解析的。这就证明了变量”id“只存在于操作内部(本地绑定类似于java中的方法局部变量)。
本地绑定可以将变量限制在某个操纵内,这样就不会造成对其他操作的变量污染。试想一下,如果没有本地绑定,一旦我们使用了id这个变量名后,我们就再也不能使用它来绑定其他对象了。使用本地绑定后,我们可以在某个操作内使用任何有意义的变量名而不用担心和其他相同名字的变量造成冲突,即使是全局变量:
=>(def id 0) ;;全局绑定
#'user/id
=>id
0
=>(let [id 1] ;;本地绑定
(println id))
1
nil
=>id ;;本地绑定对全局绑定没有任何影响
0
这种行为通常被称为词法作用域,我们可以保护变量不受污染,甚至是父操作也存在同样的变量名:
=>(let [id 1] ;;外层操作
(let [id 2] ;;内层操作
(println id)) ;;内层本地绑定的id
(println id)) ;;外层本地绑定的id
2
1
nil
我们再来举一个例子。上一篇最后我们写了一个”data-list"函数,这个函数最终返回给我们一个包含各个时间元素的列表。我们每调用一次,它都会返回给我们当前时间年、月、日、时、分、秒组成的一个列表:
=>(date-list)
("2013" "07" "10" "15" "02" "59")
=>(date-list)
("2013" "07" "10" "15" "03" "02")
现在呢,我们想要这么一个函数 run-report,通过这个函数,我们能只打印出"时"和"分"这两个元素。这个简单我们可以向下面这样去实现它:
=>(defn run-report []
(str "report ran: " (nth (date-list) 3) ":" (nth (date-list) 4)))
#'user/run-report
=>(run-report)
"report ran: 15:04"
上面这个函数有什么问题吗?聪明的你就会发现data-list这个函数被调用了两次。一次我们用来获取小时,一次我们用来获取分钟。这样做的话有两个坏处。第一个是,这两次调用返回的时间是不一样的(函数在快也需要时间执行),我们很可能得到非常错误的结果。假如第一次调用恰好是15:59:59,到了接近16点的临界点。第二次调用变成了16:00:00。这两个组合就变成了 15:00 。第二个坏处就是,排除第一个错误的话,如果data-list执行时间比较长,多次调用势必影响函数效率。
更好的做法就是我们只调用一次data-list,然后把调用后的结果绑定到一个本地变量上:
=>(defn run-report [ ]
(let [date (date-list)] ;;data-list是全局变量。date是本地变量
(str "report ran " (nth date 3) ":" (nth date 4))))
#'user/run-report
=>(run-report)
"report ran: 15:09"
通过上面的做法,之前的两个问题都不复存在了。
下面是另一种做法:
=>(defn run-report [date]
(str "report ran: " (nth date 3) ":" (nth date 4)))
#'user/run-report
=>(run-report (date-list)) ;; 传入参数就是一种隐式的本地绑定
"report ran: 15:10"
我们给run-raport 函数添加了一个参数date,参数对函数来说就是一个隐式的本地绑定。当函数被执行时,用实参替换形参的时候,本地绑定就自动的和隐式的进行了。
关于词法作用域可以参考这篇文章,虽然是关于javascript的,但道理是一样的。