JavaScript函数式编程——闭包和作用域

闭包和作用域

闭包是纯函数式编程语言的特征之一,能够大大简化复杂操作。

理解闭包

闭包允许函数访问并操作函数外部的变量,只要变量或函数存在于声明函数时的作用域内,闭包即可使函数能够访问这些变量或函数。所声明的函数可以在声明之后的任何时间被调用,甚至当该函数声明的作用域消失之后仍然可以调用。

一个简单的闭包

function assert(condition, text) {
    if(condition) {
        console.log(text);
    }
}
var outerValue = "ninja";

function outerFunction() {
    assert(outerValue == "ninja", "I can see the ninja.");
}

outerFunction();

该函数可以看到并访问变量outerValue。外部变量和外部函数都在全局作用域中声明的,该作用域从未消失。

另一个闭包的例子

function assert(condition, text) {
    if(condition) {
        console.log(text);
    }
}
var outerValue ="samurai";
//声明一个空变量
var later;

function outerFunction() {
    var innerValue = "ninja";

    function innerFunction() {
        assert(outerValue == "samurai", "I can see the samurai.");
        assert(innerValue == "ninja", "I can see the ninja");
    }

    later = innerFunction;
}

outerFunction();
later();

I can see the samurai.
I can see the ninja

为什么内部变量的作用域消失之后,内部的变量依然存在呢?
在这里插入图片描述
闭包创建了被定义时的作用域内的变量和函数安全的气泡。每一个通过闭包访问变量的函数都具有一个作用域链,作用域链包含闭包的全部信息。使用闭包时,所有的信息都会存储在内存中,直到JavaScript引擎确保这些信息不再使用或者页面卸载时,才会清理这些信息。

使用闭包

封装私有变量

很多的编程语言都有私有变量。这些私有变量对外部隐藏的对象数据,非常有用的一种特性。

使用闭包模拟私有变量

function assert(condition, text) {
    if(condition) {
        console.log(text);
    }
}
function Ninja() {
    //因为变量的声明时在构造函数内部的,所以时一个私有变量
    var feints = 0;
    this.getFeints = function() {
        return feints;
    };
    this.feint = function() {
        feints++;
    }
}
var ninja1 = new Ninja();
ninja1.feint();
assert(ninja1.feints === undefined,"And thee private data is inaccessible to us.");
assert(ninja1.getFeints() ===1, "We're able to access the internal feint count.");
var ninja2 = new Ninja();
assert(ninja2.getFeints() === 0,"The second ninja object gets its own feints variable.");
And thee private data is inaccessible to us.
We're able to access the internal feint count.
The second ninja object gets its own feints variable.

在构造函数内部,定义了一个变量feints用于保存状态,由于JavaScript的作用域规则的限制,因此只能在构造内部访问该变量。通过闭包的方法获取私有变量的值,不能直接访问私有变量,有效地阻止私有变量不可控的修改。

回调函数

处理回调函数时常见的一种使用闭包的场景,回调函数指的是需要在将来某个时刻异步调用的函数。

function assert(condition, text) {
    if(condition) {
        console.log(text);
    }
}

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);
            assert(tick === 100, "Tick accessed via a clossure.");
            assert(elem, "Element also access via a closure.");
            assert(timer, "Timer reference also obtained via closure.");
        }
    },10);
}
animateIt("box1");

每个动画需要分别设置3个变量。通过在函数内部定义变量,并给予闭包,是的在计时器的回调函数中可以访问这些变量,每个动画都能获得属于自己的气泡中的私有变量。
在这里插入图片描述

闭包内的函数不仅可以在创建的时刻访问这些变量,而且当闭包内部的函数执行时,还可以更新这些变量的值。闭包不是在创建的那一时刻的状态的快照,而是一个真实的状态封装。

通过执行上下文来跟踪代码

JavaScript代码执行的基础单元是函数,代码有两种类型:一种是全局代码,在所有函数外部定义。一种是函数代码,位于函数内部。那么就有两种执行上下文:全局执行上下文和函数执行上下文

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

最简单的跟踪方法是使用执行上下文栈(调用栈)。

function skulk(ninja){
    report(ninja + " skulking");
}
function report(message) {
    console.log(message);
}
skulk("Kuma");
skulk("Yoshi");
Kuma skulking
Yoshi skulking

在这里插入图片描述

执行上下文
  1. 每个JavaScript程序只能创建一个全局的执行上下文。
  2. 定义了两个函数:skulk和report,然后调用。创建新的函数执行上下文,并置入执行上下文栈的顶部。
  3. skulk函数进而调用report函数。所以暂停skulk执行上下文,然后创建report函数的上下文。
  4. 当report执行完成以后,回到了skulk函数。report执行上下文从执行上下文栈顶部弹出。继续执行skulk函数。
  5. 当skulk函数执行完以后,全局执行上下文并恢复执行。JvaScript全局代码恢复执行。

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

词法环境(lexical environment)是JavaScript引擎内部用来跟踪标识符与特定变量之间的映射关系。词法环境是JavaScript作用域是内部实现机制,人们通常称之为作用域

代码嵌套

在这里插入图片描述
内部代码结构可以访问外部代码结构中定义的变量,例如for循环可以访问report函数、skulk函数以及全局代码中的变量,report函数可以访问skulk函数及全局代码中的变量,skluk函数可以访问的额外变量但仅是全局代码中的变量。

代码嵌套与词法环境

如果当前环境中无法找到某一标识符,就会对外部环境进行查找,一旦查找到匹配的变量,或者是全局环境中仍然无法查找到对应的标识符就会停止查找。
在这里插入图片描述

无论何时调用函数,都会创建一个新的执行环境,被推入执行上下文栈中。

理解JavaScript的变量类型

变量可变性

const变量

通过const声明的变量与普通的变量类型,但是在声明时需要写初始值,一旦声明完成之后,其值就无法更改

两个目的:

  • 不需要重新赋值的特殊变量
  • 指向一个固定的值

const变量并不复杂,,只需要记住const变量只能在声明时被初始化一次,之后再也不允许将全新的值赋值给const变量,但是我们依然可以膝盖const变量已经存在的值,但是不能重写const变量。

定义变量的关键词与词法环境

关键词var

当使用关键词var,该变量是在距离最近的函数内部或者是全局词法环境中定义的,但是不关注块级作用域。

在这里插入图片描述

let和const定义

let和const直接在最近的词法环境中定义变量(可以是在块级作用域内,循环内,函数内或全局环境内)

在这里插入图片描述

在词法环境中注册标识符

function assert(condition, text) {
    if(condition) {
        console.log(text);
    }
}
const firstRonin = "Kiyokawa";
check(firstRonin);
function check(ronin){
    assert(ronin === "Kiyokawa","The ronin was checked.");
}

可以看到我们在check函数定义之前进行了调用。这是什么原因呢?

注册标识符的过程

一旦创建了新的词法环境,就会执行第一阶段,然后执行第二阶段。

  1. 如果是创建一个函数环境,那么创建形参及函数参数的默认值。如果不是函数环境,将会跳过。
  2. 如果时创建全局或函数环境,就扫描当前代码进行函数声明,但是不会扫描函数表达式或箭头函数。
  3. 扫描当前代码进行变量声明,在函数或全局环境中,找到所有当前函数以及其他函数以外通过var声明的变量,并找到所有在其他函数或代码以外通过let或const定义的变量。
    在这里插入图片描述

小结

  • 通过闭包可以访问创建闭包时所处环境中的全局变量。闭包为函数创建时所处的作用域中的函数和变量,创建安全气泡。通过这种方式,即使创建的函数时所处的作用域已经消失了,但是函数仍然能够获得执行所需要的全部内容。
  • 闭包的高级功能
    • 构造函数内的变量以及构造方法来模拟对象的私有属性。
    • 处理回调函数,简化代码
  • JavaScript引擎通过执行上下文栈跟踪函数的执行。
  • JavaScript引擎通过词法环境跟踪标识符
  • 可以使用关键词var、let和const定义变量。
  • 闭包是JavaScript作用域规则的副作用,当函数创建时所在的作用域消失后,仍然可以调用函数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值