javascript作用域和闭包之我见

看了《你不知道的JavaScript(上卷)》的第一部分——作用域和闭包,感受颇深,遂写一篇读书笔记加深印象。路过的大牛欢迎指点,对这方面不懂的同学请绕道看书,以免误人子弟... 看过这本书的可以一起交流交流。

编译过程

理解js作用域首先要了解js的编译过程(或者说解析过程)。

  1. 引擎

    从头到尾负责整个 JavaScript 程序的编译及执行过程。
  2. 编译器

    引擎的好朋友之一,负责语法分析及代码生成等脏活累活(详见前一节的内容)。
  3. 作用域

    引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

都说node是基于chrome的V8引擎开发 的。那么V8是引擎,node是编译器吗?这个理解是错误的!我之前就是这么错误理解的,听说node是用C++实现的,之前我一直以为V8是负责把javascript语言转换成底层的C++,然后node很高级node负责编译,做js的语法检察,ES6的新特性全都是node的开发人员,一点点的开发支持起来的。然而现实是,V8包办了所有js编译的过程,而node只是一个环境。如nodejs.cn首页所说Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。 ,是运行环境!node只是在V8的基础上,做了终端命令行的支持、文件处理的支持、http服务的支持等等,相当于一个给V8提供了各种功能的壳子。

上面说的三点是包含关系,不是并行关系!引擎包含编译器,对js进行编译,然后根据作用域和语句执行不同的代码逻辑。

编译器的查询

我们将 var a = 2; 分解,看看引擎和它的朋友们是如何协同工作的。

编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树结构。但是当编 译器开始进行代码生成时,它对这段程序的处理方式会和预期的有所不同。

可以合理地假设编译器所产生的代码能够用下面的伪代码进行概括:“为一个变量分配内 存,将其命名为 a,然后将值 2 保存进这个变量。”然而,这并不完全正确。

事实上编译器会进行如下处理。

1. 遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的 集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作 用域的集合中声明一个新的变量,并命名为 a。

2. 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理a = 2这个赋值 操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的 变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。

如果引擎最终找到了 a 变量,就会将 2 赋值给它。否则引擎就会举手示意并抛出一个异常!

在我们的例子中,引擎会为变量 a 进行 LHS 查询。另外一个查找的类型叫作 RHS。

RHS 查询与简单地查找某个变量的值别无二致,而 LHS 查询则是试图 找到变量的容器本身,从而可以对其赋值。从这个角度说,RHS 并不是真正意义上的“赋 值操作的右侧”,更准确地说是“非左侧”。

你可以将RHS理解成retrieve his source value(取到它的源值),这意味着“得到某某的 值”。

怎么理解呢,我的理解是LHS 查询是查询变量的命名空间,然后进行赋值。RHS 查询是在作用域链中,一级级的往上查找该变量的引用

所以:

function foo(a) {
var b=a;
return a + b;
}
var c=foo(2);
  1. 找到其中所有的LHS查询。(这里有3处!)
  2. 找到其中所有的RHS查询。(这里有4处!)

LHS:var c=的赋值、foo(2)传参给foo(a)时的赋值、var b=的赋值

RHS:foo(2)函数调用时查找foo()方法、var b=a中a查找自己的值、a+b中a和b两个参数查找自己的值。

作用域和作用域链

作用域的概念,应该两张图几句话就能解释吧。

这个建筑代表程序中的嵌套作用域链。第一层楼代表当前的执行作用域,也就是你所处的 位置。建筑的顶层代表全局作用域。

LHS 和 RHS 引用都会在当前楼层进行查找,如果没有找到,就会坐电梯前往上一层楼, 如果还是没有找到就继续向上,以此类推。一旦抵达顶层(全局作用域),可能找到了你 所需的变量,也可能没找到,但无论如何查找过程都将停止。

① 包含着整个全局作用域,其中只有一个标识符:foo。

② 包含着 foo 所创建的作用域,其中有三个标识符:a、bar 和 b。

③ 包含着 bar 所创建的作用域,其中只有一个标识符:c。

作用域气泡由其对应的作用域块代码写在哪里决定,它们是逐级包含的。

我觉得,说一个变量属于哪个作用域,可以顾名思义用该变量生效的区域来解释,所以上图中的b变量,可以说属于bar()的函数作用域内,也可以说是foo()的函数作用域内,也可以说是全局作用域内。

一层嵌一层的作用域形成了作用域链,变量b在作用域链中的foo()函数内得到了自己的定义。

改变作用域

eval(..) 和 with 会在运行时修改或创建新的作用域,以此来欺骗其他在书写时定义的词法作用域。

JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的 词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到 标识符。

但如果引擎在代码中发现了 eval(..) 或 with,它只能简单地假设关于标识符位置的判断 都是无效的,因为无法在词法分析阶段明确知道 eval(..) 会接收到什么代码,这些代码会 如何对作用域进行修改,也无法知道传递给 with 用来创建新词法作用域的对象的内容到底 是什么。

这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认 为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。

call()bind()之类的是改变作用域吗?他们只是改变了this的指向并不算改变作用域,是可以在编译阶段进行静态分析,所以不会导致上面说的无法优化的情况。

形成作用域

我们知道函数可以形成作用域,还有哪些方式形成作用域呢?

with

可以指定变量的作用域(选择一个对象),在它的块作用域内,变量就相当于这个对象的属性。

var obj={
a: 1,
b: 2,
c:3
};
// 单调乏味的重复 "obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式
with (obj) {
a=3;
b=4;
c=5;
}

不被推荐,因为它会影响性能,且不易阅读(代码块内的代码特别多的情况,根本不知道这个是普通的变量还是某个对象的属性,还是某个对象的属性的属性的属性)。

try/catch

try {
undefined(); // 执行一个非法操作来强制制造一个异常
}
catch (err) {
console.log( err ); // 能够正常执行!
}
console.log( err ); // ReferenceError: err not found

做错误状态传参的err变量是当前块的局部变量。

但是如果在catch(err){…}内部var其它变量,并没有效果,见下面代码。

try {
var abc='测试try块中的变量'
}
catch (err) {
var b=2; // 没有错误,不会被执行到的。
}
console.log( abc ); // 测试try块中的变量
try {
throw '55'; // 制造一个异常
}
catch (err) {
var abc='测试catch块中的变量';
}
console.log(abc); // 测试catch块中的变量

这是只属于err参数用的伪块作用域。

let、const

ES6新特性,大神器。在{}中形成块作用域,且不会遇到提升 的问题出现。

为变量显式声明块作用域,有助于回收内存垃圾

function process(data) {
// 在这里做点有趣的事情
} // 在这个块中定义的内容可以销毁了! (这里指的是下面let定义的`someReallyBigData`)
{
let someReallyBigData = { .. };
process( someReallyBigData );
} var btn = document.getElementById( "my_button" ); btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );

let有一个很有意思的地方,就是在for循环中。

for (let i=0; i<10; i++) {
console.log( i );
}
console.log( i ); // ReferenceError

for 循环头部的 let 不仅将 i 绑定到了 for 循环的块中,事实上它将其重新绑定到了循环 的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。

下面通过另一种方式来说明每次迭代时进行重新绑定的行为:

{
let j;
for (j=0; j<10; j++) {
let i = j; // 每个迭代重新绑定!
console.log( i );
}
}

提升

编译器在解析作用域时,会对作用域中var声明的变量、函数进行提升

a=2;
var a;
console.log( a ); // 2

相当于

var a;
a=2;
console.log( a ); // 2

console.log( a ); 	// undefined
var a=2;

相当于

var a;
console.log( a ); // undefined
a=2;

函数声明和变量声明都会被提升。但是一个值得注意的细节(这个细节可以出现在有多个“重复”声明的代码中)是函数会首先被提升,然后才是变量。

foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};

相当于

function foo() {
console.log( 1 );
}
foo(); // 1
foo = function() {
console.log( 2 );
};

闭包

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。

function foo() {
var a=2;
function baz() {
console.log( a ); // 2
}
bar( baz );
}
function bar(fn) {
fn(); // 妈妈快看呀,这就是闭包!
}

javascript作用域和闭包之我见的更多相关文章

  1. JavaScript 作用域和闭包——另一个角度:扩展你对作用域和闭包的认识【翻译+整理】

    原文地址 --这篇文章有点意思,可以扩展你对作用域和闭包的认识. 本文内容 背景 作用域 闭包 臭名昭著的循环问题 自调用函数(匿名函数) 其他 我认为,尝试向别人解释 JavaScript 作用域和 ...

  2. JavaScript作用域和闭包

    在JavaScript中,作用域是执行代码的上下文.作用域有3种类型: 1.全局作用域 2.局部作用域---(又叫函数作用域) 3.eval作用域 var foo =0;//全局作用域console. ...

  3. 举例详细说明javascript作用域、闭包原理以及性能问题(转)

    转自:http://www.cnblogs.com/mrsunny/archive/2011/11/03/2233978.html 这可能是每一个jser都曾经为之头疼的却又非常经典的问题,关系到内存 ...

  4. JavaScript 作用域和闭包

    作用域的嵌套将形成作用域链,函数的嵌套将形成闭包.闭包与作用域链是 JavaScript 区别于其它语言的重要特性之一. 作用域 JavaScript 中有两种作用域:函数作用域和全局作用域. 在一个 ...

  5. javascript作用域、闭包、对象与原型链

    原文作者总结得特别好,自己收藏一下.^-^ 1.作用域1.1函数作用域JS的在函数中定义的局部变量只对这个函数内部可见,称之谓函数作用域.它没有块级作用域(因此if.for等语句中的花括号不是独立作用 ...

  6. javascript作用域与闭包

    Javasript作用域概要 在javascript中,作用域是执行代码的上下文,作用域有三种类型: 1)  全局作用域 2)  局部作用域(函数作用域) 3)  eval作用域 var foo = ...

  7. JavaScript作用域与闭包总结

    1.全局作用域 所有浏览器都支持 window 对象,它表示浏览器窗口,JavaScript 全局对象.函数以及变量均自动成为 window 对象的成员.所以,全局变量是 window 对象的属性,全 ...

  8. javascript——作用域与闭包

    http://www.cnblogs.com/lucio-yr/p/4047972.html 一.作用域: 在函数内部:用 var 声明的表示局部变量,未用var的是全局变量. 作用域取决于变量定义时 ...

  9. 《JavaScript 闯关记》之作用域和闭包

    作用域和闭包是 JavaScript 最重要的概念之一,想要进一步学习 JavaScript,就必须理解 JavaScript 作用域和闭包的工作原理. 作用域 任何程序设计语言都有作用域的概念,简单 ...

随机推荐

  1. 1.1.1.持久化存储协调器(Core Data 应用程序实践指南)

    持久化存储协调器(persistent store coordinator)里面包含一份持久化存储区,而存储区里又含有数据表里的若干行数据. 与原子存储不同,SQLite数据库会在用户提交变更日志时进 ...

  2. 2.8. 创建 NSManagedObject 的子类 (Core Data 应用程序实践指南)

    现在根据模型来创建NSManagedObject的子类.如果模型改变了,那就就重新生成这些文件.所以,不要在生成的文件里自定义方法,因为重新生成之后,这些修改就丢失了.假如确实需要重新生成自定义的方法 ...

  3. 记一次DG搭建过程中备库ORA-00210,ORA-00202,ORA-27086错误

    ORA-00210: cannot open the specified control file ORA-00202: control file: '/u01/app/oracle/oradata/ ...

  4. SQL查询根节点

    /* 标题:查询指定节点及其所有父节点的函数 作者:爱新觉罗.毓华(十八年风雨,守得冰山雪莲花开) 时间:2008-05-12 地点:广东深圳 */ create table tb(id varcha ...

  5. mysql 无法启动的原因Can't start server: can't create PID file: No space left on device

    一大早来到公司,看到了一个噩梦,后台总是登录不上,登录就出错,还以为被黑客入侵了.经过1个小时的排错原因如下: 我的服务器是linux的,mysql的报错日志路径是/var/log/,经过查看日志发现 ...

  6. 走进React

    走进React React是一个构建用户界面的JavaScript库,是Facebook公司在2013年5月在github上开源的.其特点如下: 高效--React通过对DOM的模拟,最大程度地减少和 ...

  7. Wireshark网络抓包(二)——过滤器

    一.捕获过滤器 选中捕获选项后,就会弹出下面这个框,在红色输入框中就可以编写过滤规则. 1)捕获单个IP地址 2)捕获IP地址范围 3)捕获广播或多播地址 4)捕获MAC地址 5)捕获所有端口号 6) ...

  8. vue.js 常用语法总结(一)

    作者:曾萍,目前就职于京东商城. 概述 2016年已经结束了.你是否会思考一下,自己在过去的一年里是否错过一些重要的东西?不用担心,我们正在回顾那些流行的趋势.通过比较过去12个月里Github所增加 ...

  9. iOS 测试三方 KIF 的那些事

    一: KIF 三方库的配置   今天的广州天气还不错,原本想试试UI测试的,前几天也了解到很多公司都在用 KIF 这这三方框架!!今天也就试着做做,可就跪在了这个安装上,我用cocopods 导入了 ...

  10. java多线程四种实现模板

    假设一个项目拥有三块独立代码块,需要执行,什么时候用多线程? 这些代码块某些时候需要同时运行,彼此独立,那么需要用到多线程操作更快... 这里把模板放在这里,需要用的时候寻找合适的来选用. 总体分为两 ...