javascript忍者秘籍-第五章 闭包和做用域

5.1 理解闭包

闭包容许函数访问并操做函数外部的变量,只要变量或函数存在于声明函数时的做用域内,闭包就能够访问这些变量和函数javascript

//全局闭包 不明显
var outerValue = "ninja";
function outerFunction(){
    outerValue === "ninja";  //true
}
outerFunction();
复制代码
//闭包例子
var outerValue = "samurai";
var later;

function outerFunction(){
    var innerValue = "ninja";
    
    function innerFunction(){
        outerValue === 'samurai';  //true
        innerValue === 'ninja';    //true
    }
    later = innerFunction();
}
outerFunction();
later();
复制代码

当在外部函数中声明内部函数时,不只定义了函数的声明,还建立了一个闭包java

该闭包不只包含了函数的声明,还包含了 函数声明时该做用域中的全部变量数组

闭包建立了被定义时的做用域内的变量和函数的安全气泡安全

闭包图片

每个经过闭包访问变量的函数 都具备一个做用域链,做用域链包含闭包的所有信息。闭包

:star: 闭包全部的信息都存储在内存中,直到 JavaScript 引擎确保这些信息再也不使用或页面卸载时,才会清理。函数

5.2 使用闭包

封装私有变量

私有变量 是对外部隐藏的对象属性post

function Ninja(){
    var feints = 0;
    this.getFeints = function(){
        return feints;
    }
    this.feint = function(){
        feints++;
    }
}

var ninja1 = new Ninja();
ninja1.feint();

ninja1.feints  //没法直接从外部访问属性
ninja1.getFeints() == 1  //true 能够经过方法来访问

var ninja2 = new Ninja();  //建立一个新的对象实例,新对象实例做为上下文 this指向新的对象实例
ninja2.getFeints() == 0  //true 新实例有本身的私有变量
复制代码

:zap: 在构造器中隐藏变量,使其在外部做用域中不可访问,可是可在闭包内部进行访问动画

  • 经过变量 ninja , 对象实例是可见的
  • 由于 feint 方法在闭包内部,所以能够访问变量 feints
  • 在闭包外部,没法访问变量 feints

闭包

回调函数

//在 interval 的回调函数中使用闭包
//回调函数中 this 指向变了
function animateIt(elementId){
    var elem = document.getElementById(elementId);
    var tick = 0;
    var timer = setInterval(function(){
        if(tick < 100){
            elem.style.left = elem.style.top = tick + 'px';
            tick++;
        }else{
            clearInterval(timer);
            tick === 100;  //true
        }
    },10);
}
animateIt("box1");

//经过在函数内部定义变量,并基于闭包,使得在计时器的回调函数中能够访问这些变量,每一个动画都可以得到属于本身的"气泡"中的私有变量
复制代码

闭包内的函数 不只能够在闭包建立时能够访问这些变量,并且能够在闭包函数执行时,更改这些变量的值。ui

闭包不是在建立的那一时刻的快照,而是一个真实的状态封装,只要闭包存在,就能够对变量进行修改。this

模拟三个闭包

5.3 执行上下文跟踪代码

JavaScript 有两种类型的代码:一种是全局代码,一种是函数代码

上下文分为两种:全局执行上下文和函数执行上下文

全局执行上下文只有一个,当 JavaScript 程序开始执行时就已经建立了全局上下文;而函数执行上下文在每次 调用 函数时,就会建立一个新的。

JavaScript 是单线程的:一旦发生函数调用,当前的执行上下文必须中止执行,并建立新的函数执行上下文来执行函数。当函数执行完成后,将函数执行上下文销毁,并从新回到发生调用时的执行上下文中。

💉的活塞

//建立执行上下文
function skulk(ninja){
    report(ninja + " skulking");
}
function report(message){
    console.log(message);
}
skulk("Kuma");
skulk("Yoshi");
复制代码

image-20181101114022478

能够经过 JavaScript 调试器中查看,在 JavaScript 调试器中能够看到对应的调用栈(call stack)。

调用栈

5.4 使用词法环境跟踪变量的做用域

词法环境 是 JavaScript 引擎内部用来跟踪标识符与特定变量之间的映射关系

词法环境 是 JavaScript 做用域的内部实现机制,称为 做用域 (scopes)

代码嵌套

词法环境主要基于代码嵌套,经过代码嵌套能够实现代码结构包含另外一代码结构

在做用域范围内,每次执行代码时,代码结构都得到与之关联的词法环境。

内部代码结构能够访问外部代码结构中定义的变量。

代码嵌套与词法环境

每一个执行上下文都有一个与之关联的词法环境,词法环境中包含了在上下文中定义的标识符映射表。=> 做用域链

在特定的执行上下文中,咱们的程序不只直接访问词法环境中定义的局部变量,并且还会访问外部环境中定义的变量。

不管什么时候建立函数,都会建立一个与之相关联的词法环境,并存储在名为 [[Environment]] 的内部属性上。两个中括号表示是内部属性。

函数都有词法环境

var ninja = "Muneyoshi";

function skulk(){
    var action = "Skulking";
    
    function report(){
        var intro = "Aha!";
        assert(intro === "Aha!","Local");
        assert(action === "Skulking","Outer");
        assert(ninja === "Muneyoshi","Global");
    }
    report();
}
skulk();
复制代码

词法环境

:question: 为何不直接跟踪整个执行上下文搜索标识符,而是经过词法环境来跟踪呢?

JavaScript 函数能够做为任意对象进行传递,定义函数时的环境与调用函数的环境每每是不一样的(闭包)

:zap: 不管什么时候调用函数,都会建立一个新的执行环境,被推入执行上下文栈。此外,还会建立一个与之关联的词法环境。外部环境与新建的词法环境,JavaScript 引擎将调用函数的内置[[Environment]]属性与建立时的环境进行关联。

5.5 理解 JavaScript 的变量类型

3个关键字定义变量:var let const

不一样之处:可变性、词法环境

vs 不可变

const 不可变,let var 可变

声明时须要初始化,一旦声明完成以后,其值不可更改。 => 指向不可更改

const firstConst = "samurai";
firstConst = "ninja";  //报错

const secondConst = {};
secondConst.weapon = "wakizashi";  //不报错

const thirdConst = [];
thirdConst.push("Yoshi");  //不报错
复制代码

若是 const 的值是 静态变量,则不容许从新赋值;若是 const 的值是对象或者是数组类型,则能够对其增长新元素,可是不能重写。其实不可变的是引用,而不是值。

vs 词法环境

var 一组,let 和 const 一组

关键字 var :变量是在距离最近的 函数内部 或是在 全局词法环境 中定义的。忽略块级做用域

var 声明的变量实际上老是在 距离最近的函数内 或 全局词法环境中 注册的,不关注块级做用域

let 与 const 直接在最近的词法环境中定义变量(能够是块级做用域内循环内函数内或全局环境内)。咱们能够用 let 和 const 定义 块级别、函数级别、全局级别的变量。

var三种词法环境

let const词法环境

词法环境注册标识符

词法做用域又叫静态做用域,由于js的做用域在词法解析阶段就肯定了

动态做用域:区别于静态做用域,即在函数调用时才决定做用域

JavaScript 代码的执行 分两个阶段:

一旦建立了新的词法环境,就会执行第一阶段。在第一阶段,没有执行代码,而是JavaScript引擎会访问并注册在当前词法环境中所声明的变量和函数。变量和函数声明提高

第二阶段的执行取决于变量的类型(let var const 函数声明)以及环境类型(全局环境、函数环境或块级做用域)

1.若是是建立一个函数环境,那么建立形参及函数参数的默认值。若是是非函数环境,将跳过此步骤。

2.若是是建立全局或函数环境,就扫描当前代码进行函数声明(不会扫描其余函数的函数体),可是不会扫描函数表达式或箭头函数。对于所找到的函数声明,将建立函数,并绑定到当前环境与函数名相同的标识符上。若该标识符已经存在,那么该标识符的值将被重写。若是是块级做用域,将跳过此步骤。

3.扫描当前代码进行变量声明。在函数或全局环境中,找到全部当前函数以及其余函数以外经过 var 声明的变量,并找到全部在其余函数或代码块以外经过 let 或 const 定义的变量。在块级环境中,仅查找当前块中经过 let 或 const 定义的变量。对于所查找到的变量,若该标识符不存在,进行注册并将其初始化为 undefined。若该标识符已经存在,将保留其值。

注册标识符的过程

若函数是函数声明进行定义的,则能够在函数声明以前访问函数。

若函数是函数表达式或箭头函数进行定义的,则不会在以前访问到函数


变量的声明会提高至函数顶部,函数的声明会提高至全局代码顶部。

其实,变量和函数的声明并无实际发生移动,只是在代码执行以前,先在词法环境中进行注册。

5.6 闭包的工做原理

闭包能够访问建立函数时所在做用域内的所有变量

//经过函数访问私有变量,而不经过对象访问
function Ninja(){
    var feints = 0;
    this.getFeints = function(){
        return feints;
    }
    this.feint = function(){
        feints++;
    };
}
var ninja1 = new Ninja();
ninja1.feint();

var imposter = {};
imposter.getFeints = ninja1.getFeints;
imposter.getFeints() == 1   //true
复制代码

JavaScript 中没有真正的私有对象属性,可是能够经过闭包实现一种可接受的“私有”变量的方案