你不知道的JavaScript --- 做用域相关

本篇是《你不知道的JavaScript》的读书笔记性能优化

什么是做用域?

程序离不变量,那么变量存储在哪里?程序须要时如何找到他们?闭包

这些问题说明须要一套设计良好的规则来存储变量, 而且以后能够方便地找到这些变量。这套规则被称为做用域函数

做用域负责收集并维护由全部声明的标识符(变量) 组成的一系列查询, 并实施一套很是严格的规则, 肯定当前执行的代码对这些标识符的访问权限。性能

做用域嵌套

当一个块或函数嵌套在另外一个块或函数中时, 就发生了做用域的嵌套。 所以, 在当前做用域中没法找到某个变量时, 引擎就会在外层嵌套的做用域中继续查找, 直到找到该变量,或抵达最外层的做用域(也就是全局做用域) 为止。优化

function foo(a) { 
        console.log( a + b ); // foo的做用域中没有变量b,去外层找
    }
    var b = 2;
    foo( 2 ); // 4

词法做用域

刚学的时候就知道JavaScript是词法做用域,那么到底是什么意思?设计

JavaScript的源代码在执行以前会在编译器中经历词法分析、语法分析、代码生成等环节。code

词法化的过程会对源代码中的字符进行检查,若是是有状态的解析过程,还会赋予单词语义。词法做用域是由你在写代码时将变量和块做用域写在哪里决定的,所以当词法分析器处理代码时会保持做用域不变。对象

词法做用域意味着做用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段基本可以知道所有标识符在哪里以及是如何声明的,从而可以预测在执行过程当中如何对它们进行查找。blog

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

欺骗词法

正常状况下,词法做用域彻底由写代码期间函数所声明的位置来定义。可是JavaScript也有两种机制能够在运行的时候来“修改”(也能够说欺骗)词法做用域。eval()with

JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于可以根据代码的词法进行静态分析,并预先肯定全部变量和函数的定义位置,才能在执行过程当中快速找到标识符。但若是引擎在代码中发现了 eval(..) 或 with,它只能简单地假设关于标识符位置的判断都是无效的,由于没法在词法分析阶段明确知道 eval(..) 会接收到什么代码,这些代码会如何对做用域进行修改,也没法知道传递给 with 用来建立新词法做用域的对象的内容究竟是什么。那么全部的优化可能都是无心义的,所以最简单的作法就是彻底不作任何优化

若是代码中大量使用 eval(..) 或 with ,那么运行起来必定会变得很是慢。不管引擎多聪明,试图将这些悲观状况的反作用限制在最小范围内,也没法避免若是没有这些优化,代码会运行得更慢这个事实。

提高

先看个小栗子

console.log(a)
    var a = 2;

直觉上认为,JavaScript是从上而下一行一行执行的,应该会报错ReferenceError. 但实际上这里会输出undefined.

引擎会在解释 JavaScript 代码以前首先对其进行编译。编译阶段中的一部分工做就是找到全部的声明,并用合适的做用域将它们关联起来。包括变量和函数在内的全部声明都会在任何代码被执行前首先被处理

因此上述栗子能够理解为

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

定义在编译阶段进行,赋值留在原地等待执行阶段,这个过程就叫作提高

须要注意的是:

  1. 函数声明会被提高,可是函数表达式不会被提高
foo1(); // 'foo1'
    foo2(); // TypeError : foo2 is not a function  此处的foo2未被赋值,为undefined

    function foo1(){
        console.log('foo1');
    }
    var foo2 = function (){
        console.log('foo2');
    }
  1. 函数会首先被提高,而后才是变量
foo(); //foo1  而不是TypeError 说明函数声明先被提高,而后才是变量提高,可是同名,因此变量的声明被忽略了

    var foo = function (){
        console.log('foo2');
    }

    function foo(){
        console.log('foo1');
    }

    foo(); //foo2 执行赋值以后,foo函数输出foo2

闭包

闭包是基于词法做用域写代码时所产生的天然结果,闭包的建立和使用在代码中随处可见,咱们须要的是根据本身的意愿来识别,拥抱和影响闭包的思惟环境。

当函数能够记住并访问所在的词法做用域时,就产生了闭包,即便函数是在当前做用域以外执行。

function foo() {
    var a = 2;
    
    function bar() {
        console.log(a);
    }

    return bar;
}

var baz = foo();

baz() // 2  --- 闭包效果

函数baz(实际上就是bar的引用)能够访问到foo内部做用域,虽然是在foo做用域外部执行的。而正是因为bar的存在,因此foo函数执行后,内部做用域没有被销毁,bar会使用这个内部做用域。

bar依然持有对该做用域的引用,这个引用就叫作闭包。闭包使得函数能够继续访问定义时词法做用域。不管使用何种方式对函数类型的值进行传递,当函数在别处别调用时均可以观察到闭包

闭包的一个经典问题

for(var i = 1; i <= 5 ; i++) {
        setTimeout(function timer() {
            console.log(i);
        },i * 1000);
    }

这里会每间隔一秒,打印一个6。每次循环都会建立一个timer函数传递个setTimeout。timer中使用的变量i都是上层做用域中定义的变量i(闭包),当循环执行完以后,i的值为6,因此会连续打印5个6.

若是想依次打印1到5。有如下处理方式。

  1. 在定时器外建立一层做用域,使每次循环产生的timer使用的i都不同。
for(var i = 1; i <= 5 ; i++) {
        (function(j){
                    setTimeout(function timer() {
            console.log(j);
        },j * 1000);
        })(i)
    }
  1. 使用块级做用域 - let
for(let i = 1; i <= 5 ; i++) {
        setTimeout(function timer() {
            console.log(i);
        },i * 1000);
    }

块级做用域会使每次建立定时器的做用域都不同。并且语言特性会使循环时记住上一次i的值。

相关文章
相关标签/搜索