javascript做用域和闭包之我见

javascript做用域和闭包之我见

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

编译过程

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

  1. 引擎node

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

    引擎的好朋友之一,负责语法分析及代码生成等脏活累活(详见前一节的内容)。
  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(); // 妈妈快看呀,这就是闭包!
}
相关文章
相关标签/搜索