Javascript的作用域,作用域链,闭包

1,作用域和作用域链概念

作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在JavaScript中,变量的作用域有全局作用域和局部作用域两种。

1.1 全局作用域,在代码中任何一个地方都能访问的对象拥有全局作用域,一般来说以下几种情形拥有全局作用域:

最外层函数和在最外层函数外面定义的变量拥有全局作用域;

所有未定义直接赋值的变量自动声明为拥有的全局作用域;

浏览器环境中,所有windows对象的属性拥有全局作用域;

1.2 局部作用域,局部作用域一般只是固定的代码片段内可访问到,在JS中一般是函数作用域;

1.3 作用域链,当代码在一个环境中执行是,会常见变量对象的一个作用域链,作用域链的作用是保证队执行环境有权访问的所有变量和函数的有序访问。全局执行环境的变量对象始终都是作用域链中最后一个对象。

2,作用域规则

作用域有一套根据名称查找变量的规则。

实际情况中,通常需要同事顾及几个作用域,当一个块或函数嵌套在另一块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中举行查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止,但不能通过向下搜索作用域链而进入另一个执行环境。

var a = 1;

function f() {

    var a =2;

var b =3;

console.log(a);

  }

  f(); //2

a; //1

b; // 报错

3,作用域工作模型

JS作用域的工作模型是词法作用域,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。

function f1() {
alert(a)
}
f1();
function f2() {
var a = 2;
f1();
}
var a =1;
f1(); //1
f2(); //1,因为f1定义时的作用域的a为全局的a=1,说明函数不管在哪里调用都是基于它定义时的作用域;
上面代码中,当函数执行时,会先查找所需的变量,作用域查找会在找到第一个匹配的标识符时停止,在多层的嵌套作用域中可以定义同名的标识符,这叫做"遮蔽效应"(内部的标识符"遮蔽"了外部的标识符)。抛开遮蔽效应,作用域查找始终从运行是所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。词法作用域规则会使函数在查找变量时从函数内部到函数使用时的作用域。所以无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。

4,函数作用域

JS中没有块级作用域,而是基于函数的作用域。函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)

function d() {
if (true){
var a = 1;
}
console.log(a);
}
d(); //if不会生成新的作用域,所以a会泄露到当前函数作用域
利用函数作用域的特点,我们可以在任意代码片段外部添加包装函数,可以将内部的变量和函数定义"隐藏"起来,外部作用域无法访问包装函数内部的任何内容。
var a = 1;
(function f() {
var a = 2;
console.log(a); //2,利用函数作用域包装代码,使函数内部变量不会被外部访问
})();
console.log(a)
var a =1;
function f() {
console.log(a); //underfined,不是1
var a = 2;
console.log(a); //2
}
f();
//上面代码解析为
function f() {
var a;
console.log(a);
a = 2;
console.log(a);
}
var a;
a = 1;
f();
这个过程就是声明提升。
声明提升是因为JS在执行之前,会有一个预编译过程,预编译阶段生成变量声明和函数声明,没有初始化行为(赋值),匿名函数不参与预编译只有在解释执行阶段才会进行变量初始化。
声明提升原理
函数和变量的预解析特点:
1.函数声明会置顶
2.变量声明也会置顶
3.函数声明比变量声明更置顶:(函数在变量上面)
4.变量和赋值语句一起书写,在js引擎解析时,会将其拆成声明和赋值2部分,声明置顶,赋值保留在原来位置
5.声明过的变量不会重复声明
(function () {
a =1;
alert(window.a);
var a = 2;
alert(a);
})();
//上面代码解析
(function () {
var a ; //a被提升到顶端后,a=1就不是隐式声明全局变量了,而是给a赋值操作,所以window.a未声明为undefined;
a =1;
alert(window.a); //undefined;
a = 2;
alert(a); //2
})();

5,欺骗函数作用域

执行环境类型中有两种,全局和局部函数,但可以用其他方法欺骗函数作用域。

eval(),Javascript中的eval(...)函数可以接受一个字符串为参数,并将其中的内容视为好像在书写是就存在于程序中这个位置的代码并在运行到此位置执行参数代码。因为eval(...)可以在运行期修改书写期的词法作用域,存在安全问题,不建议使用。

 

var a = 1;
function f() {
eval(\'var a =2;\');//eval中的a遮蔽了全局中的a,让作用域弹出的a改变了
console.log(a);
}
f();

with,with语句接收一个对象参数,表示with语句的对象作用域,with语句中的变量都会在这个指定的对象中查找。with会影响性能,不建议使用

 

window.onload = function () {
var a = 0;
var obj = {
a:1,
b:2
};
with(obj){
a = 2; //会在obj查找a属性
}
console.log(obj.a); //2
console.log(a); //0
with(obj){
c = 3; //没有在指定对象中找到c,非严格模式下,with会在全局隐式创建一个全局变量
}
console.log(obj.c); //undefined
console.log(c); //3
};

6,闭包

  闭包closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

   闭包的特性:

1,函数嵌套函数

    2,函数内部可以引用外部的参数和变量

  3,参数和变量不会被垃圾回收机制回收

闭包的定义及其优缺点

闭包 是指有权访问另一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量,闭包的缺点就是常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。闭包是javascript语言的一大特点,主要应用闭包场合主要是为了:设计私有的方法和变量。一般函数执行完毕后,局部活动对象就被销毁,内存中仅仅保存全局作用域。但闭包的情况不同!

   嵌套函数的闭包

  window.onload = function () {
function aaa() {
var a = 1;
return function(){
alert(a++)
};
}
var fun = aaa();
fun();// 1 执行后 a++,,然后a还在~
fun();// 2
fun = null;//a被回收!!
};
//闭包会使变量始终保存在内存中,如果不当使用会增大内存消耗。
javascript的垃圾回收原理
(1)在JavaScript中,如果一个对象不再引用,那么这个对象就会被GC回收;
(2)如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收;
使用闭包的好处:
1,希望一个变量长期驻扎在内存中
2,避免全局变量的污染
3,私有成员的存在
一,全局变量的累加
window.onload = function () {
    var a = 1;
     function f() {


   a++;


alert(a);


   }


f(); //2


   f(); //3


}



二,局部变量


   window.onload = function () {
function f() {
var a = 1;
a++;
alert(a);
   }
f();
f();
//那么怎么才能做到变量a既是局部变量又可以累加呢?
 三,局部变量的累加


  window.onload = function () {
  function outer(){
  var x=10;
  return function(){ //函数嵌套函数
  x++;
  alert(x);
  }
}
var y = outer(); //外部函数赋给变量y;
y(); //y函数调用一次,结果为11,相当于outer()();
y();
}

   函数声明于函数表达式

在js中我们可以通过关键字function来声明一个表达式:

  <script>
window.onload = function () {
function abc(){
alert(123);
}
abc();
}
</script>
//我们也可以通过一个"()"来将这个声明变成一个表达式:
  window.onload = function () {
  (function abc(){
alert(123);
})()
}

四,模块化代码,减少全局变量的污染

  window.onload = function () {
   var abc = (function () { //abc为外部匿名函数的返回值
   var a = 1;
return function () {
a++;
alert(a);
   }
})();
   abc(); //2 ;调用一次abc函数,其实是调用里面内部函数的返回值
abc(); //3
}

5,私有成员的存在

  window.onload = function () {
  var aaa = (function(){
   var a = 1;
function bbb(){
a++;
alert(a);
   }
   function ccc(){
   a++;
   alert(a);
}
   return {
   b:bbb, //json结构
   c:ccc
}
})();
aaa.b(); //2
aaa.c() //3
}

六,使用匿名函数实现累加

window.onload = function () {
function box(){
var age = 100;
return function(){ //匿名函数
age++;
return age;
};
}
var b = box();
alert(b());
alert(b()); //即alert(box()());
alert(b());
alert(b); // function () {
// age++;
// return age;
// }
b = null; //解除引用,等待垃圾回收
}

七,在循环中直接找到对应元素的索引

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<title></title>
<script>
window.onload = function(){
var aLi = document.getElementsByTagName(\'li\');
for (var i=0;i<aLi.length;i++){
aLi[i].onclick = function(){ //当点击时for循环已经结束
alert(i);
};
}
}
</script>
</head>
<body>
<ul>
<li>123</li>
<li>456</li>
<li>789</li>
<li>010</li>
</ul>
</body>
</html>

八,使用闭包改写上面的代码

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
<title></title>
<script>
window.onload = function(){
var aLi = document.getElementsByTagName(\'li\');
for (var i=0;i<aLi.length;i++){
(function(i){
aLi[i].onclick = function(){
alert(i);
};
})(i);
}
};
</script>
</head>
<body>
<ul>
<li>123</li>
<li>456</li>
<li>789</li>
</ul>
</body>
</html>