一周一章前端书·第3周:《你不知道的JavaScript(上)》
2017-10-11 本文已影响16人
梁同学de自言自语
第3章:函数作用域和块级作用域
3.1 函数中的作用域
- JavaScript变量的查找规则是由内到外的,阅读如下代码:
function foo(a){
var b = 2;
function bar(){
//...
}
var c = 3;
}
bar(); //失败
console.log(a,b,c); //失败
-
foo()
函数作用域声明了变量a
、b
、c
以及函数bar
,如果在外部使用这些函数内的变量或函数都会失败。 - 函数作用域的含义就是说,函数内的变量只允许在函数范围内使用(也包括在内部嵌套函数)。
3.2 隐藏内部实现
- 既然函数中变量的活动范围只限制在函数内,反过来其实也可以这么理解:
- 我们挑选一个代码片段,用函数作用域将其包装起来了,而 函数内的具体细节(变量)对外不可见,被“隐藏”起来了。
- 这种做法就是 软件设计中的
最小授权
或最小暴露原则
。 - 如果所有变量和函数都声明到全局作用域中,可能会发生无法预知的情况:
var b;
function doSomething(a){
b = a + doSomethingElse(a * 2);
console.log(b * 3);
}
function doSomethingElse(a){
return a - 1;
}
doSomething(2); //15
通过函数作用域修改后,是不是更安心了:
function doSomething(a){
var b;
function doSomethingElse(a){
return a - 1;
}
b = a + doSomethingElse(a * 2);
console.log(b * 3);
}
doSomething(2); //15
- 另外,通过作用域隐藏变量还有一个好处:可以避免因为同名造成的变量冲突。
function foo(){
function bar(a){
i = 3;
console.log(a + i);
}
for(var i=0;i<10;i++){
bar(i * 2);
}
};
foo(); //因为调用的i是通一个,造成无限循环
- 针对同名变量冲突,大概有两种处理方式:
(1) 全局命名空间
- 将本来暴露在全局作用域的诸多变量,统一放到全局的某个对象中,这个对象看做一个命名空间。
- jQuery采用的就是此类做法,将所有和jQuery关联的第三方插件,都放到jQuery对象下。
(2) 模块管理
- 如同
seaJS
、requireJS
等现代化的模块机制一样 - 首先将JS第三方库统一注册到模块管理器中,当调用的方法需要某个库时,从模块管理器中将该库挑选出来,显式的注入到函数作用域里。
- 这样不仅避免了全局作用域的污染,更实现了按需加载。
3.3 函数作用域
- 上文说到,通过函数作用域来隐藏变量能避免一系列的问题,但这并不是最理想的:
- 为了隐藏几个变量,我还得声明一个具名函数,那这个函数本身,不也在污染全局作用域吗?
- 其次,声明了函数还得去调用才能运行起来。
- 是否有可以 不用指定函数名,并且还能自动运行的函数 呢?答案是肯定的:
var a = 2;
(function foo(){
var a = 3;
console.log(a); //3
})();
console.log(a); //2 函数作用域对内部的a做了隐藏,不影响全局的作用域
- 当以
()
中括号包含函数时,则是函数表达式,而不是标准的函数声明。 - 不要对函数表达式感到陌生,其实早在定时函数
setTimeout()
我们就有用到过:
setTimeout(function(){
console.log('I waited 1 second!');
},1000);
- 定时函数中用到的叫
匿名函数表达式
,因为没有指定函数名。 - 注意,函数表达式是可以匿名的,但函数声明则不允许匿名。
- 使用函数表达式需要注意的几点:
- 匿名函数不便于调试;
- 当函数需要引用自身的时候比较困难,只能引用已经过期的
arguments.callee
; - 由于匿名函数缺乏函数名,代码的可读性较差;
- 用
()
包含一个函数可以构造一个函数表达式,而 在末尾紧跟着一个()
可以立即执行这个函数 。 - JS社区将它命名为
IIFE(Immediately Invoked Function Expresion)
,代表立即执行的函数表达式。 - IIFE还有另外一种写法:
(function(){
console.log('test');
}())
- IIFE可以传递参数:
var a = 2;
(function(global){
var a = 3;
console.log(a); //3
console.log(global.a); //2
})(window);
console.log(a); //2
- IIFE 还有一种 可以倒置代码的运行顺序 的玩法,这种模式在UMD(Universal Module Definition)项目中运用广泛:
var a = 2;
(function iife(def){
def(window);
})(function(global){
var a = 3;
console.log(a); //3
console.log(global.a); //2
});
3.3 块作用域
- 虽然函数作用域是最常见的作用域,但JavaScript也有其他的作用域单元,甚至在很多场景下,甚至用这些非函数的作用域单元实现功能,代码会更简洁、更优雅。
//虽然i声明于for循环的花括号范围内
//但注意i是用var来声明的,实际上它是全局作用域下的变量
for (var i=0;i<10;i++){
consooe.log(i);
}
-
for
循环中的i
经常会由于忽略,会被绑定到全局作用域,而JavaScript的块级作用域就可以将变量限制在花括号的范围内。
with
- 前文提到的
with
就可以构造块级作用域,但不提倡使用。
try/catch
-
catch
分句中,也会创建一个块级作用域。在其中声明的变量仅在catch
内部有效。
try{
makeError();
}catch(err){
console.log(err);
}
console.log(err); //ReferenceError: err not found
let
- ES6引入了
let
关键字,可以将变量绑定到所在代码的作用域中 (通常是花括号{}
内)。换句话说,let
为其声明的变量隐式的劫持了所在的块作用域。
var foo = true;
if(foo){
{
let bar = foo * 2;
bar = something(bar);
console.log(bar);
}
}
console.log(bar); //ReferenceError
- 注意:但
let
关键字不会进行变量声明的提升 ,请确保代码在运行时已进行声明。
{
console.log(bar); //ReferenceError!
let bar = 2;
}
const
- ES6中的
const
同样也可以创建块级作用域变量,但变量值是固定的(常量)。
var foo = true;
if(foo){
var a = 2;
const b = 3; //在if块级作用域下的常量
a = 3; //赋值正常
b = 4; //赋值失败!常量不能改变
}
console.log(a); //3
console.log(b); //ReferenceError!
3.5 小结
- 函数是JavaScript中最常见的作用域单位,声明在函数内的变量会被函数作用域“隐藏”起来,这是软件设计的最小暴露原则。
- 函数不是唯一的作用域单位,块级作用域是指变量属于某个代码段(通常拥有花括号
{}
)下。 -
try/catch
的catch
分句拥有块作用域。 - ES6的
let
和const
关键字能在任意代码段中创建块作用域变量。