Scope and closure(作用域和闭包)
- 作用域和闭包 是所有js都有的,也是写js一直写到的,为什么在这里还要做一次笔记呢?原因有二:
- 作用域和闭包对于新手来说不太好理解,我希望通过我的笔记,能让刚入行的朋友们在面试时不会死在这个小细节上;
- 在es6中多了点新玩法,我想在这里分享给大家,有错误希望可以批评指正;
- 当然了,你在看这篇笔记时,需要你是写过一些js的。。。
作用域概念
- 编程语言最基本的功能之一就是能存储变量中的值,设计良好的规则来存储变量的值并且之后可以方便的找到这些变量的规则被称为作用域。
- 简言之: 作用域就是能放变量的值并且能被调用的到区域(我自己浅显的理解。。。)
- 为了进一步理解这苦涩难懂的东西,我们来看看js的编译原理:
- 第一步: 词法分析
eg: var a = 2; 会被分解为对编程来说有意义的词法单元: var、 a、 =、 2、 ; 五部分形成词法单元流数组[‘var’,’a’,=,2,’;’] - 第二步: 语法分析
eg: (将词法单元流转换为一个逐级嵌套的“抽象语法树”),上面的var a = 2; 最顶层是var节点,再往下是一个变量a和一个“=”,再往下就是一个number节点,值为2,最后遇到“;”表示结束 - 第三步: 代码生成
eg: 根据抽象语法树将var a = 2; 转化为一组机器指令,用来创建一个叫做a的变量,并将一个number类型的值存储在其中。
- 第一步: 词法分析
接下来再看看浏览器引擎执行程序的原理:
var a = 2; 在编译后,会被分两步执行:- 执行var a , 此时如果同一作用域集合中没有变量a就会给a创建一个内存存储,有的话就会忽略,进入下一步
- 执行赋值 a = 2,对已有的变量赋值。
- 对于编译器来说,上面第一步操作叫做查询(LHS),第二步操作叫右查询(RHS)。通俗的说: 找赋值操作的目标–>LHS,找赋值操作的源头–> RHS。
fucntion foo(a){ var b = a; return a + b; } var c = foo(2); //LHS有三处: a=2、 b、 c //RHS有四处: =a、 =foo(2..、 ..b、 a..
作用域嵌套: 引擎查找变量(LHS)会先在内部作用域查找,没找到会向上级作用域找,直到找完全局作用域为止,若仍未找到,会报ReferenceError错
词法作用域
- js的作用域在ES6之前主要是两种: 词法作用域和动态作用域,ES6新增块作用域(oh,no no no! ES3以后就有了一个有着块作用域的写法,只是没人会去用它而已。所以这算不上新增,不然编译器也没法把ES6的块作用域写法编译为浏览器认识的ES5,这里先放着后边说)。
- 大多标准语言编译器的第一个工作阶段都是词法化
词法作用域示例:
function foo(a){ var b = a * 2; function bar(c){ console.log(a,b,c); } bar (b * 3); } foo(2); // 2 4 12
这个例子中有三个逐级嵌套的作用域:
- 包含整个全局的作用域,其中只有foo一个标识符;
- 包含在foo内部的foo创建的作用域,包含: a、 bar 和 b ;
- 包含bar所创建的作用域,包含标识符: c
动态作用域都是由函数被申明时所处的位置决定
var b = 3; function foo(){ var b = 2; bar(b); } function bar(x){ console.log(x); } foo(); // 输出结果为3 ,而不是2
事实上,javascript并不具有动态作用域,上面的表现,也是此法作用域,唯一的区别就是this是在运行期间确定的作用范围,,this机制在某种程度上很像动态作用域。
欺骗词法: eval() 和 with(现已不推荐使用,关键我也不会用^_^…………)
var b = 2; function foo(str,a){ eval(str); console.log(a,b); } foo("var b = 3", 1); // 1, 3
此处foo外申明的b表示受到了欺骗,因为在执行时,eval把字符串“var b = 3;”解释成了js语句,插入到了foo的内部作用域,所以会优先找内部的b并打印内部的b
函数作用域和块作用域
每申明一个function都会创建一个函数作用域:
function foo(a){ var b = 2; function bar(){ var c = 3; console.log(b,c); } } foo(1); // 2 , 3 bar(); // RefenceError
标识符 a、 b 和 bar 都是附属于foo作用域的,c 是附属于 foo 内的 bar 的作用域的,作用域访问原则是内部可以访问外部,未暴露内部作用域,外部不能访问内部,所以上例中找不到 bar 而报错。
变量和函数的外部隐藏和内部实现:
function foo(){ var a = 1; bar(); } function bar(){ console.log(a); } var a;
上面的函数,严格来说,a 和 bar 都应该是 foo 私有的,因为除了foo 没其他地方使用 a 和 bar ,放在foo外面就显得不太安全了,可能获取不到目标值,或调用 bar 时出现冲突等等。下面对写法就相对安全很多了
function foo(){ var a = 1; (function bar(){ console.log(a); })(); }
立即执行函数 :(上面示例foo中的bar就是立即执行函数)
两种写法:(function(){ console.log(a); }()) (function(){ console.log(a); })();
立即执行函数在编译完就会自己调用自己,执行一次
块作用域
- 什么是块作用域?
for(var i = 0; i < 10; i ++){ console.log(i); } console.log(i); //10
本应该只在for循环内部有效的变量i,为什么被污染到外面了?
如果在for循环内部生成一个块,限制变量泄漏出去?这样的作用域是不是更安全呢?
表面上,ES6之前貌似不能实现呢。。。- ES6以前的块作用域 try{}catch(err){}
try{ undefined(); }catch(err){ for(var i = 0; i < 10; i ++){ console.log(i); } } console.log(i); // ReferenceError
catch(){}中的变量i居然没泄漏出来!!!
- ES6的块作用域: let 和 const 申明(详见上一篇笔记)
- 来点有趣的: babel等转码工具怎么将let申明转换为现在浏览器兼容的代码?
{ let a = 2; console.log(a); // 2 } console,log(a); //ReferenceError // babel 等转码黑魔法 ----->>> try{ throw 2; }catch(a){ console.log(a); // 2 } console.log(a); //ReferenceError
目的达到了,转码成功!!!
- 论块作用域的重要性:
for(var i = 0; i < 10; i++){ setTimeout(function(){ console.log(i); // 每秒打印1次10,打印10次 },i*1000); }
好像不符合写这段代码的初衷呢,我只想每秒递增打印数字而已!!!
改写:for(var i = 0; i < 10; i ++){ (function(){ setTimeout(function(){ console.log(i) },i * 1000) }()) }
完全一样,没发生变化啊,再改写:
for(var i = 0; i < 10; i ++){ (function(){ var j = i; setTimeout(function(){ console.log(j) },j * 1000) }()) }
终于好了,但是我想说,这么麻烦的写法,真的好么?还能简化点儿。。。
for(var i = 0; i < 10; i ++){ (function(j){ setTimeout(function(){ console.log(j) },j * 1000) }(i)) }
好吧,依然复杂。。。
来看看块作用域在这里的优秀表现吧:for(let i = 0; i < 10; i ++){ setTimeout(function(){ console.log(i); }, i * 1000) }
我就想问下,还有谁???
每循环一次,let都会创建一个块作用域,这个块儿作用域里的setTimeout()不会受到其他块作用域里的i的值影响,所以会正确执行
块作用域更多神奇表现希望可以在各位大神的代码中体现
作用域闭包
- 闭包是面试中很多人都会问到的,希望看完这一节,你会说:函数闭包我天天见,但我就是不知道这是闭包,也不知道为什么把它叫做闭包。。。Orz
- 讨论了那么多词法作用域,你可能还不知道,闭包是基于词法作用域书写代码时所产生的自然结果,你甚至不需要为了利用它而有意识的创建闭包,闭包在你的代码中随处可见,你缺少的知识识别、影响它的思维环境。。。
定义: 当函数可以记住并访问所在的词法作用域时,就产生理闭包,即使是在当前此法作用于外执行该函数(联想下上面所说的动态作用域)。
function foo(){ var a = 2; function bar(){ console.log(a); } bar(); } foo(); // 2 这里的bar就是一个闭包函数
特点: 闭包函数可以访问所处的词法作用域的变量,但当前词法作用域的父级函数不能直接访问闭包函数内的变量
function foo(){ var a = 2; function bar(){ var b = 3; console.log(a,b); // 没错的 } console.log(a,b) // b is not defined }
闭包的效果
function foo(){ var a = 2; function bar(){ console.log(a); } return bar; } var brz = foo(); brz(); // 2
闭包的强大威力表现 —— 模块
// 再看上面的代码 function foo(){ var a = 2; function bar(){ console.log(a); } return{ bar: bar }; } var brz = foo(); brz.bar(); // 2
好神奇诶!!!,居然成功的调用出了bar!!!
来剖析下原理:- foo()是一个函数,里面有一个闭包函数 bar
- bar 被当作返回值return暴露出来了
- foo()被调用了一次,并且将调用结果赋值给了brz(用模块化的话来说就是foo()是一个函数,必须通过调用它来创建一个模块实例)
- brz里面就有了bar()这个方法,bar是在foo()内部申明的一个闭包函数,调用bar时会获取到foo的作用域,得到 a = 2 ;
这里就是模块的雏形 … …
模块模式必须的两个条件:
1. 必须有外部封装函数,并且至少被调用一次(每次调用都会创建一个新的模块实例)
1. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问并且修改私有的状态
```
var brz = (function foo(){
var a = 2;
function bar(){
console.log(a);
}
return{
bar: bar
};
})();
brz.bar(); // 2
```
这里把foo变成IIFE(立即执行函数)并赋值给brz,然后在下面直接调用brz.bar(),简化写法
ES6的模块机制:
ES6为模块增加了一级语法支持,但是通过模块加载时ES6会将文件当作独立的模块来处理,所以ES6的模块没有“行内式”,必须被定义在独立的文件中:// bar.js function hello(who){ return "let me introduce :" + who; } export hello; // foo.js // 仅从 bar 导入模块 hello import hello from "./bar" bar hungry = "hippo"; function awesome(){ console.log(hello(hungry).toUpperCase()); } export awesome; // brz.js // 导入完整的 foo 和 bar 模块 module foo from "./foo"; module bar from "./bar"; console.log( bar.hello("fhino"); // let me introduce : hippo ) foo.awesome(); // let me introduce : hippo
更多的模块机制后续学习中在详述… …
敬请期待下期*_*