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作为参数传入