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. Git 的是使用入门

    Coding 代码管理快速入门 当项目创建好了之后,我们该如何上传代码到 coding 上呢? Coding 网站使用“ Git 仓库”(类似 github )来管理代码. 其操作原理在于:利用 gi ...

  2. QT第二天学习

    回顾: qmake: qmake -project //生成*.pro文件 qmake //makefile make 注:if(qmake -v  >=5) then QT += widget ...

  3. oracle 查询哪些表分区

    如果查询当前用户下得分区表:select * from user_tables where partitioned='YES'如果要查询整个数据库中的分区表:select * from dba_tab ...

  4. Spring util-namespace下标签相关操作

    java代码 package com.stono.sprtest; import java.util.List; import java.util.Map; import java.util.Set; ...

  5. 小兔JS教程(五) 简单易懂的JSON入门

    上一节的参考答案: http://xiaotublog.com/demo.html?path=homework/04/index2 本节重点来介绍一下JSON,JSON(JavaScript Obje ...

  6. 005.数组、for、foreach

    1.方法的传输传递 值参数:传递的是副本 引用参数:自身 保留自定义的方法中对值的改变 形参影响实参ref:对应的形参和实参都用ref修饰 输出参数:实参不用赋值,但是自定义方法内必须对此参数赋值!! ...

  7. 在C++中反射调用.NET(二)

    反射调用返回复杂对象的.NET方法 定义数据接口 上一篇在C++中反射调用.NET(一)中,我们简单的介绍了如何使用C++/CLI并且初步使用了反射调用.NET程序集的简单方法,今天我们看看如何在C+ ...

  8. nRF51800 蓝牙学习 进程记录 1:感想

    一直想开一个高大上点的博客,觉得博客园不错,便申请了.一直没时间看,都快忘了,无意间登上提示申请到了.便写个东西看看. 正在学习nRF51822的蓝牙开发板,为了做毕设准备.备考中,一直没时间学,但今 ...

  9. 苹果手机上开发微信公众号(wap)遇到的问题解决方法

    1.样式 苹果 手机会自行强制css样式,比如input会变得面目全非,你可以加上input{-webkit-appearance: none;}恢复原貌! 2.jquery的live错误,苹果手机上 ...

  10. OVS VxLAN Flow 分析 - 每天5分钟玩转 OpenStack(149)

    OVS 的数据流向都是由 Flow 规则控制的,今天我们就来分析 VxLAN 的 Flow 规则.提个醒:这可能是本教程最烧脑的一节,let's rock it ! 下面分析控制节点上的 flow r ...