闭包

1 理解闭包

1.1 闭包必备知识回顾

1.1.1 函数基础知识回顾

函数语法、函数参数
  • 1 函数是什么时候执行的 - 调用的时候才执行
  • 2 函数返回值可以包含什么类型
    • a 基本类型(number/string/boolean)
    • b 对象类型(object)
  • 3 怎么理解函数的返回值
函数内用来返回数据,相当于没有函数的时候直接使用该数据,即:
function foo() {
    var o = {age: 12};
    return o;
}

var o1 = foo();

// 相当于:
var o2 = {age: 18};

1.1.2 作用域的结论

1 函数才会形成作用域
2 JavaScript的作用域是词法作用域
3 词法作用域:变量的作用范围 在代码写出来的就已经决定, 与运行时无关
4 函数内部可以访问函数外部的变量(函数外部不能访问函数内部的变量)
5 变量搜索原则:从当前链开始查找直到0级链,从高到低查找
6 函数外部无法访问函数内部的变量

当定义了一个函数,当前的作用域链就保存起来,并且成为函数的内部状态的一部分。

1.2 闭包的概念

闭包从字面上看就是封闭和包裹, 在函数中定义的变量在函数外部无法访问, 因此这个函数就构成闭包。

> 闭包是一个受保护的变量空间。
  • 计算机科学中对闭包的解释
闭包是由 函数 以及 函数所处的环境 构成的综合体。
或者
闭包是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,
即使已经离开了创造它的环境也不例外。


函数会形成一个闭包

包括:函数 以及 创建该函数的环境(作用域链)
  • 闭包代码抢先看
function foo() {
    var num = 0;

    return function() {
        return num++;
    };
}

var getNum = foo();
console.log(getNum());

1.3 要解决闭包的什么问题(目标)

  • 想办法(在外部)访问到函数内部的数据

1.4 获取函数内部数据

函数内部的数据,在函数外面访问不到。
函数对其内部的数据有一个保护的作用。

1.4.1 利用函数返回值

function foo() {
    var num = 123;
    return num;
}

var num1 = foo();
  • 疑问:获取两次数据是同一个数据吗? 不是同一个数据
var num1 = foo();
var num2 = foo();
console.log(num1 === num2); // true
  • 返回对象的情况
function foo() {
    var obj = {num: 123};
    return obj;
}

var o1 = foo();
var o2 = foo();
console.log(o1 === o2); // false

1.4.2 普通的函数返回值说明

两次调用函数,返回的数据并不是同一个数据。
出现这个原因是:函数在每次调用的时候,函数内部的数据会被新创建一次

要解决这个问题, 只需要保证, 函数 foo 只调用一次即可
  • 问题:函数只调用一次,但想获取多次数据怎么办?

2 闭包模型

function foo() {
    var str = "BOSS";

    return function() {
        return str;
    };
}

// 调用
var f = foo();
var str1 = f();
var str2 = f();
console.log(str1 === str2);
  • 闭包说明:
在函数(outer)内部定义的函数(inner),执行的时候可以访问到上一级作用域中的变量。
因此,在函数(outer)外部,就可以间接访问到函数(outer)中的数据了。

2.1 函数内嵌函数

// 嵌套的函数
function bar() {
    var num = 123;
    function f() {
        console.log(num);
    }
    f();
}
bar(); // 123
  • 练习
// 问题:没有办法多次获得同一个数组
function foo() {
    var arr = [1, 3, 5, 7];
    return arr;
}

// 在函数外面将arr中的所有数据遍历出来
// 要求:遍历两次结果相同
function func() {
    var arr = ["a", "b", "c", "d"];

    return function() {
        return arr;
    };
}
  • 案例
// 利用闭包返回两个数的值
function func() {
    var n = Math.random();
    var m = Math.random();

    // ...
}

// 方式一:返回数组
// 方式二:返回对象
  • 练习:
// 给 foo 提供两个方法,分别实现对 num 设置值和读取值
function foo() {
    var num;

    return {
        // 1
        // 2
    };
}

2.2 闭包概念小结

闭包对内部的变量起到了保护的作用,
除了返回的函数之外,无法通过任何其他手段访问到函数内部的数据

3 闭包的实际应用

  • 实现数据缓存
缓存:暂存数据方便后续计算中使用。

缓存中存储的数据可以简单的认为是 键值对。

工作中,缓存是经常被使用的手段。

计算机中的缓存

3.1 递归存在的问题

  • 存在大量的重复计算,使得执行效率很低。

  • 递归代码

// 计数
var count = 0;
// 递归
function fib(n) {
    count++;
    if(n === 0 || n === 1) {
        return 1;
    }
    return fib(n - 1) + fib(n - 2);
}

fib(20);
console.log(count);

3.2 闭包实现缓存

  • 解决方式:缓存计算结果
即在计算的时候,
1 首先查看缓存中有没有该数据,
2 如果有,直接从缓存中取出来;
3 如果没有,即递归,并将计算结果放到对应的缓存位置上。

3.2.1 抛开闭包谈问题

// 直接把 斐波那契数列 的前10项 放在数组中
var fibsArr = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55];

// 如何求第5项的值?第10项呢?
  • 没有闭包实现存在的问题:
1 数组放在全局环境会造成全局污染
2 全局环境中谁都可以对数组修改
3 缓存中完全信任数组(缓存),就会造成数据不准确的问题
  • 实现过程:
var fib = (function() {
    // 缓存容器,放在闭包中,对数据起到保护作用
    var arr = [];
    return function(n) {
        // 1 判断缓存容器中有没有
        if(arr[n] !== undefined) {
            // 1.1 取出结果
            return arr[n];
        } else {
            // 缓存中没有
            var res;
            // 2 如果是0或者1直接 = 1
            if (n === 0 || n === 1) {
                // 保存计算结果
                res = 1;
            } else {
                // 否则,递归调用,并保存计算结果
                res = arguments.callee(n - 1) + arguments.callee(n - 2);
            }

            // 3 将递归调用的结果放入缓存中以便下次使用
            arr[n] = res;

            // 将结果返回
            return res;
        }
    };
})();

4 分析jQuery缓存实现

4.1 最基本的缓存结构

var cache = {};
var cache = [];
  • 案例:实现缓存的设置和获取
// 设置
var fn = function(k, v) {
    cache[k] = v;
};

// 获取
// 1 var cacheValue = cache[k];
// 2 
var getV = function(k) {
    return cache[k];
};

4.1.1 缓存注意事项

  • 1 缓存数量要在一定范围内,例如:100条
  • 2 缓存数据要可控,增删改查
  • 3 缓存需要被保护

4.2 实现数据缓存

var createCache = function() {
    var internalCache = {};
    var arr = [];

    return function (k, v) {
        if(v) {
            if(!internalCache[k]) {
                if(arr.length >= 50) {
                    var deleteKey = arr.shift();
                    delete internalCache[deleteKey];
                }
                arr.push(k);
            }
            internalCache[k] = v;
        } else {
            return internalCache[k];
        }
    };
};
  • jQuery源码中缓存的实现
/**
 * Create key-value caches of limited size
 * 创建带有长度限制的键值对缓存
 */
function createCache() {
    var keys = [];

    function cache( key, value ) {
        // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
        // 使用(key + " ") 是为了避免和原生(本地)的原型中的属性冲突
        if ( keys.push( key + " " ) > Expr.cacheLength ) {
            // Only keep the most recent entries
            // 只保留最新存入的数据
            delete cache[ keys.shift() ];
        }
        return (cache[ key + " " ] = value);
    }
    return cache;
}

// 就是用来设置缓存
var typeCache = createCache();
typeCache("cls", "stra");

// 读取缓存
typeCache["name"]
typeCache["cls"];

5 沙箱模式

沙箱模式、沙盒模式、隔离模式

沙箱(sandbox)介绍:
用于为一些来源不可信、具备破坏力或无法判定程序意图的程序提供试验环境。然而,沙盒中的所有改动对操作系统不会造成任何损失。

5.1 JavaScript的沙箱模式

5.1.1 沙箱模式的作用

作用:隔离

JavaScript中的作用域是:词法作用域。
不存在 块级作用域,但是可以使用沙箱模式来模拟块级作用域:
(function() {
    var num = 123;
})();

var f = function() {
    var num = 22;
}

f();

// alert(num); // 函数外部无法访问num,这样就模拟了一个块级作用域
  • 在 JS 中讨论隔离,要隔离什么?变量
变量?代码逻辑?
  • 在JS中如何实现隔离?
考虑,JavaScritp中的作用域。只有函数能限定作用域,所以,只有函数才能实现隔离。

5.1.2 沙箱模式模型

// 沙箱模式 模型
(function() {

    // 代码

    // 通过给 window 添加成员暴露沙箱提供的变量
    // window.$ = window.jQuery = jQuery;
})();

// $()
// jQuery()


var num = (function() {
    // return xxx;    
})();
  • 问题:
1 为什么要是自调用函数?
    要执行,不污染,隔离
2 隔离的效果是什么?
    沙箱内外 代码互不影响

5.1.3 沙箱模式应用

  • 练习:利用沙箱模式打印1-100的和
var count = 0;
(function() {
    // 应为js中没有块级作用域,所以在for循环中声明的变量 i
    // 是一个全局变量
    for(var i = 0; i <= 100; i++) {
        count += i;
    }
})();
alert(count);
  • 最佳实践:
在函数内定义变量的时候,将 变量定义 提到最前面。
  • 实际应用
(function(w) {
    // 独立的环境
    function it() {}
    it.prototype.say = function() {};

    // 其他操作代码
    // ...

    // 暴露到全局环境中
    w.i$ = it;
})(window);

5.1.4 沙箱模式的优势

将代码放到一个 立即执行的函数表达式(IIFE) 中,这样就能实现代码的隔离
1 使用 立即执行的函数表达式 的好处就是:减少一个函数名称的污染,将全局变量污染降到最低
2 代码在函数内部执行,形成了一个独立且外部无法访问的空间,这样就使得函数外部代码不会影响到函数内部的代码执行
3 如果外部需要,则可以根据需求返回适当的数据。可以把window作为参数传入