1、做用域
- 做用域共有两种主要的工做模型:第一种是最为广泛的,被大多数编程语言所采用的词法做用域,另一种叫做动态做用域;
- JavaScript所采用的做用域模式是词法做用域。
1.词法做用域
- 词法做用域意味着做用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段基本可以知道所有标识符在哪里以及是如何声明的,从而可以预测在执行过程当中如何对它们进行查找。
-
JavaScript 中有两个机制能够“欺骗”词法做用域:编程
- eval(..):能够对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法做用域(在运行时) ;
- with:经过将一个对象的引用看成做用域来处理,将对象的属性看成做用域中的标识符来处理,从而建立了一个新的词法做用域(一样是在运行时) 。
- 这两个机制的反作用是引擎没法在编译时对做用域查找进行优化,由于引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都将致使代码运行变慢。
2.函数做用域和块级做用域
- 函数做用域: 函数是 JavaScript 中最多见的做用域单元。本质上,声明在一个函数内部的变量或函数会在所处的做用域中“隐藏”起来,即函数内定于的函数和变量为该函数私有;
-
块级做用域:闭包
- 块做用域指的是变量和函数不只能够属于所处的做用域,也能够属于某个代码块(一般指 { .. } 内部)
- ES6前在JavaScript中并不存在块级做用域( 例外:try/catch 结构在 catch 分句中具备块做用域);
- 在 ES6 中引入了 let 关键字( var 关键字的表亲) ,用来在任意代码块中声明变量。 if(..) { let a = 2; } 会声明一个劫持了 if 的 { .. } 块的变量,而且将变量添加到这个块中(另外常量定义const也具备块级做用域)。
3.函数和变量的提高
(1)、提高编程语言
- 函数做用域和块做用域的行为是同样的,即,某个做用域内的变量,都将附属于这个做用域。
- 引擎会在解释 JavaScript 代码以前首先对其进行编译。编译阶段中的一部分工做就是找到全部的声明,并用合适的做用域将它们关联起来;
- 所以包括变量和函数在内的全部声明都会在任何代码被执行前首先被处理;
-
当看到 var a = 2; 时,可能会认为这是一个声明。但 JavaScript 实际上会将其当作两个声明: var a; 和 a = 2; 。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。函数
- 这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动”到了最上面。这个过程就叫做提高。
- 每一个做用域都会进行提高操做;
(2)、函数优先性能
- 函数声明和变量声明都会被提高。可是函数会首先被提高,而后才是变量。
foo(); // 1 var foo; function foo() { console.log( 1 ); } foo = function() { console.log( 2 ); };
- 会输出 1 而不是 2 !这个代码片断会被引擎理解为以下形式:
function foo() { console.log( 1 ); } foo(); // 1 foo = function() { console.log( 2 ); };
- var foo 尽管出如今 function foo()... 的声明以前,但它是重复的声明(所以被忽略了) ,由于函数声明会被提高到普通变量以前。
- 尽管重复的 var 声明会被忽略掉,但出如今后面的函数声明仍是能够覆盖前面的。
2、做用域闭包
(1)、理解闭包
- 当函数能够记住并访问所在的词法做用域时,就产生了闭包,即便函数是在当前词法做用域以外执行。
- 在Javascript语言中,只有函数内部的子函数才能读取局部变量,所以能够把闭包简单理解成"定义在一个函数内部的函数"。
- 在本质上,闭包就是将函数内部和函数外部链接起来的一座桥梁。
(2)、闭包的用途
- 能够读取函数内部的变量;
- 让变量的值始终保持在内存中。
(3)、闭包的产生实例
- 能够读取函数内部的变量:
function foo() { var a = 2; function bar() { console.log( a ); } return bar; } var baz = foo(); baz(); // 2 —— 这就是闭包的效果。
- 在 foo() 执行后,一般会期待 foo() 的整个内部做用域都被销毁,由于咱们知道引擎有垃圾回收器用来释放再也不使用的内存空间;
- 闭包的“神奇”之处正是能够阻止这件事情的发生。事实上内部做用域依然存在,所以没有被回收,由于 bar() 自己在使用;
- 拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部做用域的闭包,使得该做用域可以一直存活,以供 bar() 在以后任什么时候间进行引用。
- bar() 依然持有对该做用域的引用,而这个引用就叫做闭包。
- 循环和闭包:
for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); }
-
正常状况下,咱们对这段代码行为的预期是分别输出数字 1~5,每秒一次,每次一个。但实际上,这段代码在运行时会以每秒一次的频率输出五次 6:优化
- 延迟函数的回调会在循环结束时才执行。事实上,当定时器运行时即便每一个迭代中执行的是 setTimeout(.., 0) ,全部的回调函数依然是在循环结束后才会被执行,所以会每次输出一个 6 出来。
- 实际状况是尽管循环中的五个函数是在各个迭代中分别定义的,可是它们都被封闭在一个共享的全局做用域中,所以实际上只有一个 i,即全部函数共享一个 i 的引用 。
- 解决方案:使用 IIFE在每次迭代中将本次迭代的i传入建立的做用域并封闭起来;
for (var i=1; i<=5; i++) { (function(j) { setTimeout( function timer() { console.log( j ); }, j*1000 ); })( i ); }
- 在迭代内使用 IIFE 会为每一个迭代都生成一个新的做用域,使得延迟函数的回调能够将新的做用域封闭在每一个迭代内部,每一个迭代中都会含有一个具备正确值的变量供咱们访问。
(4)、使用闭包的注意点
-
因为闭包会使得函数中的变量都被保存在内存中,内存消耗很大,因此不能滥用闭包,不然会形成网页的性能问题,在IE中可能致使内存泄露。code
- 解决方案:在退出函数以前,将不使用的局部变量所有删除。
- 闭包会在父函数外部,改变父函数内部变量的值。因此,若是把父函数看成对象(object)使用,把闭包看成它的公用方法(Public Method),把内部变量看成它的私有属性(private value),这时必定要当心,不要随便改变父函数内部变量的值。