您不知道的javascript系列学习(作用域)
一、javascript作用域的理解
- 1、什么是作用域
(1)、作用域是根据名称查找变量的一套规则。如果查找的目的是对变量进行赋值,那么会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。
(2)、在当前作用域中无法找到某个变量时,会在外层嵌套的作用域中继续查找,直到找到或者到最外层的作用域(全局作用域)为止。
如:
function foo(a){
console.log(a+b);//b在函数作用域内无法找到,可以在上一级作用域找到b=2;
}
var b=2;//外层作用域
foo(2);//4
(3)、不成功的RHS引用会导致抛出ReferenceError异常;不成功的LHS引用会导致自动隐式的创建一个全局变量(非严格模式下),该变量使用LHS引用的目标作为标识符,或者抛出ReferenceError异常(严格模式下)
- 2、词法作用域
作用域分为词法作用域和动态作用域
(1)、词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里决定的。
如:
function foo(a){
var b=a*2;
function bar(){
console.log(a,b,c);
}
bar(b*3);
}
foo(2);//最终输出2,4,12
说明:
最外层包含整个全局作用域,其中只有一个标识符:foo。
foo层包含所创建的作用域,有三个标识符 a,bar,b。
bar层包含所创建的作用域,只有一个标识符 c。
(2)、欺骗词法 在运行时“修改”词法的作用域,有两种机制来实现整个目的,一、用eval()函数,二、用with语句。但不推荐使用这两种方法,欺骗词法作用域会导致性能下降。
如:
function(str,a){
eval(str);//欺骗,将屏蔽了外层var b=2;将b定义为3
console.log(a,b);//1,3
}
var b=2;
foo("var b=3;",1);
with 本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当做作用域的标识符来处理,从而创建了 一个新的词法作用域(在运行时)。
这两个机制的副作用是引擎无法再编译时对作用域查找进行优化,将导致代码运行变慢。不要使用它们。
- 3、函数和块作用域
(1)、函数作用域是指属于这个函数的全部变量都可以在整个函数的范围内使用及复用。
任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。
如:
var a=2;
function foo(){
var a=3;
console.log(a);//3 访问的是foo函数内部变量a.如果foo函数内部没有定义a,则访问外部var a=2.
}
foo();
console.log(a);//2 访问的是全局变量a,不能访问foo函数里面的变量 var a=3
(2)、函数表达式
(function foo(){
var a=3;
})();
作为函数表达式,意味着foo只能在所代表的位置中访问,外部作用域则不行。函数被包含在一对()括号内部,因此成为一个表示式,通过末尾加上另外一个()可以立即执行这个函数。
(3)、块作用域
块作用域是只变量在一个代码块中有效。
最熟悉的块作用域:
for(var i=0;i<10;i++){
console.log(i);//0,1,2,3,4,5,6,7,8,9
}
console.log(i);//10
可以看出i并不是在for块中有效,在外层作用域依然可以访问到。像if块里面的定义的变量也是一样,那么怎么样才是块作用域?
a、从with对象中创建的作用域
b、try/catch 的catch分句会创建一个块作用域,其中声明的变量仅在catch内部有效。
如:
try{
undefined();//执行一个非法操作来强制抛出异常
}catch(err){
console.log(err);//正常打印出错信息
}
console.log(err);//ReferenceError:err not found
c、let 关键字
let关键字可以将变量绑定到所在的任意作用域中。通常{}内部。
重新看回for循环的代码块:将var 变为let定义i
for(let i=0;i<10;i++){
console.log(i);//正常输出
}
console.log( i);//
ReferenceError: i is not defined 无法再外层引用,i只在for 代码块有效。
d、const 同样可以用来创建块作用域变量,但其值是固定的(常量)
- 4、作用域闭包
(1)、什么是闭包?当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。(能够读取其他函数内部变量的函数)
如:
function foo(){
var a=2;
function baz(){
console.log(a);//2
}
bar(baz);
}
function bar(fn){
fn();//妈妈快看啊!这就是闭包
};
只要使用回调函数,实际上就是在使用闭包。
(2)、闭包与循环
for循环是最常见的例子:
for(var i=1;i<=5;i++){
setTimeout(function(){
console.log(i);// 输出5次6
},i*1000);
}
预想情况,是分别输出1~5,每秒一次,每次一个。
实际情况,每秒一次输出6.
因为,试图假设循环中每个迭代在运行时都给自己“捕获”一个i的副本,但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,
但是他们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。
要想按预期输出需要有自己的变量,用来在每个迭代中存储i的值:
for(var i=1;i<=5;i++){
(function(){
var j =i;
setTimeout(function(){
console.log(j);
},j*1000);
})();
}
或者 可以用let 块作用域
for(let i=1;i<=5;i++){
setTimeout(function(){
console.log(i);
},i*1000);
}
(3)、模块
模块模式需要具备两个必要条件:
1、必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例);
2、封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
如:
var foo = (function CoolModule(id){
function change(){
publicAPI.identify = identify2;
}
function identify1(){
console.log(id);
}
function identify2(){
console.log(id.toUpperCase());
}
var publicAPI ={
change:change,
identify:identify1
}
return publicAPI;
})("foo module");
foo.identify();//foo module
foo.change();
foo.identify();//FOO MODULE