理解闭包需要先弄明白两个概念,作用域链和变量生存周期
你不知道的 js 中有个形象的比喻
把程序中的嵌套作用域链比作一座大楼。如果一楼代表当前执行作用域。顶层代表全局作用域。
当访问变量时,先在当前楼层进行查找,如果没有找到,就会坐电梯前往上一层楼,如果还是没有找到就继续向上,以此类推。一旦抵达顶层(全局作用域)
JavaScript 具有基于函数的作用域,当我们把代码片段外部添加函数,可以将内部的变量和函数定义隐藏起来,外部作用域无法访问函数内部的任何内容
例如下面这段代码:
function foo() {
var a = 2;
console.log(a); // 2 当前作用域
}
foo();
console.log(a); // ReferenceError: a is not defined
var a = 1;
function foo() {
var a = 2;
console.log(a); // 2 当前作用域
}
foo(); // foo执行后出栈,就像这个函数从来不存在一样
console.log(a); // 1 全局作用域
foo 拥有内部变量,这样看似实现了封装的目的,但是这么做有几个问题,后面会有更好的方案
- 要额外创建 foo 函数
- 污染当前作用域(这段代码之前或之后存在其他 foo 变量,可能导致异常)
- 必须显式调用 foo 函数
IIFE 立即执行函数表达式
继续刚才 foo 封装的话题,有没有这样一种函数,没有名称,而且还能自执行呢,这样就解决了以上的几个问题。既然这么问了肯定有。。
例如下面这段代码:
var a = 1;
(function foo() {
var a = 2;
console.log('我来自内部', foo); // 我来自内部 ƒ foo() {...}
console.log(a); // 2 局部变量
})();
console.log(a); // 1 全局变量
console.log(foo); // ReferenceError: foo is not defined foo 去哪了???
what?发生了什么,内部变量访问不了还说得过去,怎么 foo 函数也没了?想搞明白这个,先了解一下函数声明和函数表达式的区别,仔细看下函数表达式的作用域绑定就清楚了。
函数表达式:常见的以(!±~等开头的,都是函数表达式。箭头函数也是。
function 不是声明的第一个词
作用域绑定 绑定在函数表达式自身的函数中
function 是否命名都一样, 这里只是为了输出展示,当然如果没有名称,内部可以通过 arguments.callee 来获取当前执行的函数
比如 (function foo(){ … }) foo 只能在… 所代表的位置中被访问,外部作用域则不行
// 这些都是函数表达式
!(function() {})();
+(function() {})();
-(function() {})();
~(function() {})();
void (function() {})();
new (function() {})();
(function() {}.call());
(function() {}.call());
// 这两种方式,会有不好的情况:如果括号前面有东西
a(function() {}).call();
//等价于
a().call();
函数声明:这个就不用介绍了,平时一直在用
function 是声明中的第一个词 作用域绑定 绑定在函数所在的作用域 比如 function foo() {} foo
被绑定在声明时的作用域 例子中是全局 也就是 window
除 JavaScript 外的很多编程语言都支持块作用域,那么 JavaScript 没有块作用域吗?
可以理解为 JavaScript 中,只有函数作用域,全局作用域,没有块作用域。特殊情况除外(后面会说到 try cache/let/const)
例如下面这段代码:
// 同步执行 依次输出0-9
console.log('变量提升 ', i); // 变量提升 undefined
for (var i = 0; i < 10; i++) {
console.log(i);
}
console.log('循环执行完毕 全局变量 ', i); // 循环执行完毕 全局变量 10
上面这个比较好理解,因为代码是同步的,执行顺序和书写顺序一致,从上到下依次执行。下面我们增加使用定时器模拟异步操作
// 异步执行 输出10次10
for (var i = 0; i < 10; i++) {
setTimeout(() => {
// console执行时 for循环已经执行完毕 此时i=10
console.log(i);
}, 0);
}
console.log('循环执行完毕 全局变量 ', i); // 循环执行完毕 全局变量 10
异步代码和我们平时的认知不同,执行顺序和书写不一致。我们简单分析下执行过程:
执行 for 循环 遇到异步代码 依次在事件队列尾部添加回调。这里添加 10 次 执行最后一句 console
同步任务执行完毕,就去事件队列中查找需要执行的事件,这里是 setTimeout 的回调,然后同步的执行,输出全局变量
i,此时循环已经执行完毕,i 是 10,所以输出 10 个 10
JavaScript 实现块作用域
try/catch
try {
undefined(); // 执行一个非法操作来强制制造一个异常
} catch (err) {
console.log(err); // 能够正常执行!
}
console.log(err); // ReferenceError: err not found
let/const
es5 引入了 let/const,用于创建块作用域,而且声明的变量不再提升作用域。还是以经典的 for 循环为例:
// 异步执行 依次输出0-9
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 0);
}
// 这个的执行结果是:0,1,2,..9
// 这样也行
for (var i = 0; i < 10; i++) {
let j = i; // 这里创建了块级作用域
setTimeout(() => {
console.log(j);
}, 0);
}
同样是异步执行,为啥这里就不是 10?
循环内部使用 let 创建了块级作用域,变量 i 查找时,使用块级作用域内的值。 因为 setTimeout 内部函数引用了变量
i,循环执行完毕,定时器执行前,变量 i 还没有被回收。
IIFE 立即执行函数(模拟块作用域)
// 异步执行 依次输出0-9 (利用闭包改造)
for (var i = 0; i < 10; i++) {
(function(j) {
setTimeout(() => {
// console执行时 j取的是当前作用域中的值 所以输出0-9
console.log(j);
}, 0);
})(i);
}
// 还可以这样
for (var i = 0; i < 10; i++) {
(function() {
var j = i;
setTimeout(() => {
console.log(j);
}, 0);
})();
}
这里的原因和上面的 let/const 相同,唯一的区别是,这里的变量 i 来自闭包函数创建的作用域
for 循环中 let、var 问题
什么是闭包
过去我一直这样理解闭包:函数中返回一个内部函数,且内部函数使用了函数中的变量,所以判断是不是闭包,首先就找 return,然后看变量的引用情况。
翻了下你不知道的 js,发现这么理解格局有点小了,这个形式只是一种经典案例
// 经典重现
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 ———— 朋友,这就是经典闭包。
这些也是闭包
只要函数在定义时的词法作用域以外的地方被调用,并且可以继续访问定义时的词法作用域。
回想下上一节学习的作用域,概念中的词法作用域怎么理解呢?
function outFoo() {
var a = 2;
function innerFun() {
console.log(a); // 2
}
outBar(innerFun);
}
function outBar(fn) {
// 此时fn就是innerFun 定义是的作用域是outFoo 在outFoo外调用
fn(); // 朋友,这也是闭包!
}
var fn;
function outFoo() {
var a = 2;
function innerFun() {
console.log(a);
}
fn = innerFun; // 将innerFun分配给全局变量
}
function outBar() {
// 此时fn还是innerFun 定义是的作用域是outFoo 在outFoo外调用
fn(); // 朋友,这还是闭包!
}
outFoo();
outBar(); // 2
IIFE 立即执行函数是不是闭包
立即执行函数是闭包,其实这个说法是有争议的,仁者见仁,智者见智吧
我比较认可你不知道的js作者的观点是,“IIFE 本身 并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是最常用来创建可以被封闭起来的闭包的工具。因此 IIFE 的确同闭包息息相关,即使本身并不会真的使用闭包”
var a = 2;
(function IIFE() {
console.log(a);
})();
闭包的用处
封装模块
var foo = (function CoolModule(id) {
function change() {
// 修改公共API
publicAPI.identify = identify2;
}
function identify1() {
console.log(id);
}
function identify2() {
console.log(id.toUpperCase());
}
var publicAPI = {
change: change,
identify: identify1,
};
return publicAPI;
})('foo module');
foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODUL
业务场景
img 上报请求丢失问题,img 是 report 函数中的局部变量,当 report 函数的
调用结束后,img 局部变量随即被销毁,而此时或许还没来得及发出 HTTP 请求,所以此次请求就会丢失掉。
var report = function(src) {
var img = new Image();
img.src = src;
};
report('http://xxx.com/getUserInfo');
// 利用闭包,report保存了innerFun的引用,innerFun又引用了imgs,
所以imgs不会释放,除非指定report = null;
释放innerFun的引用,才会在下一次垃圾回收中清除
// js中垃圾回收使用了引用计数的机制,只有变量没有被引用,才会被回收
var report = (function() {
var imgs = [];
return function innerFun(src) {
var img = new Image();
imgs.push(img);
img.src = src;
};
})();
模块依赖加载器
书中的这个加载器的例子还是挺有意思的,看上去有 SeaJS/RequesJS 的感觉,挺巧妙的。
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i = 0; i < deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply(impl, deps);
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get,
};
})();
MyModules.define('bar', [], function() {
function hello(who) {
return 'Let me introduce: ' + who;
}
return {
hello: hello,
};
});
MyModules.define('foo', ['bar'], function(bar) {
var hungry = 'hippo';
function awesome() {
console.log(bar.hello(hungry).toUpperCase());
}
return {
awesome: awesome,
};
});
var bar = MyModules.get('bar');
var foo = MyModules.get('foo');
console.log(bar.hello('hippo')); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO
mdn 闭包
闭包什么时候销毁
js 闭包的理解、运用和销毁
第 3 章 3.3 高阶函数
所谓高阶函数,是指使用其他函数作为参数、或者返回一个函数作为结果的函数
函数作为参数
常用的数组方法,比如 map/reduce/filter/sort 等。
面向切面编程(AOP) 解决了什么问题
使用 AOP 之前
有这样一个场景,第一天,产品说:现在需要在订单提交前后,增加统计时间的功能,有没有难度?当然没有,几行代码,轻松搞定
let submitOrder = () => {
let nowTime = +new Date();
console.log('提交订单 开始计时', nowTime);
/*提交订单 原逻辑*/
let spendTime = +new Date() - nowTime;
console.log('提交订单 耗时计算', spendTime);
};
第二天,产品说:订单提交前后,再加个埋点吧,有没有问题?当然没有,再加几行。。
随着不确定需求增加,代码越来越长。长倒还好,关键部分逻辑揉在一块,剪不断理还乱~
我们可能都没注意,函数的名字是 submitOrder,但是这也太不专一了。这就是传说中的海王函数吧~
本以为游进了哥哥的鱼塘,没想到哥哥是个海王 ,哥哥真心不挑
let submitOrder = () => {
const nowTime = +new Date();
// 继续增加提交前库存校验
// 继续增加...
console.log('提交订单之前 数据上报');
console.log('提交订单 原逻辑');
// 继续增加失败统计
// 继续增加...
const spendTime = +new Date() - nowTime;
console.log(spendTime);
console.log('提交订单之后 再次数据上报');
};
AOP 登场
还是上面这个需求,我们是用 AOP 的思想实现一下,先请出主角,2 个切面函数
在原型链上增加或修改方法,需要谨慎。因为是全局的,有可能会冲突,而且问题不好排查。
Function.prototype.before = function(beforefn) {
var self = this; // 保存原函数的引用
return function() {
// 返回包含了原函数和新函数的"代理"函数 function拥有before/after方法
beforefn.apply(this, arguments); // 执行新函数,修正 this
return self.apply(this, arguments); // 执行原函数
};
};
Function.prototype.after = function(afterfn) {
var self = this;
return function() {
// 执行原函数
var ret = self.apply(this, arguments);
// 执行after
afterfn.apply(this, arguments);
// 返回原函数的返回值
return ret;
};
};
修改后的效果
let submitOrder = () => {
console.log('提交订单 原逻辑');
};
// 时间统计函数
const timeLog = fn => {
let nowTime;
return fn
.before(() => {
nowTime = +new Date();
console.log('提交订单 开始计时', nowTime);
})
.after(() => {
const spendTime = +new Date() - nowTime;
console.log('提交订单 耗时计算', spendTime);
return this;
});
};
const reportLog = fn => {
return fn
.before(() => {
console.log('提交订单之前 数据上报');
})
.after(() => {
console.log('提交订单之后 再次数据上报');
});
};
const submitOrderAddTimeLog = timeLog(submitOrder);
const submitOrderAddTimeLogAndReportLog = reportLog(submitOrderAddTimeLog);
submitOrderAddTimeLogAndReportLog();
// 提交订单之前 数据上报
// 提交订单 开始计时 1618496989387
// 提交订单 原逻辑
// 提交订单 耗时计算 2
// 提交订单之后 再次数据上报
这样就实现了,不修改原函数逻辑的同时,扩展一些未来的功能。函数名字是取的长了些,但是注释都省得写了~
高阶函数应用
柯里化与反柯里化
柯里化
柯里化就是把接受多个参数的函数,变换成接受单参数的函数,内部再返回调用下一个单参数的函数。书中实现了通用的 currying 函数,这个有点东西,我们来分析一下
/**
* 这里实现了一个通用的包装函数,保存args的引用
* 调用时传递参数 则push进args,同时返回当前函数(也就是说可以连续调用)
* 调用时不传参数 则使用args执行传入的函数fn
*/
var currying = function(fn) {
var args = [];
return function() {
if (arguments.length === 0) {
return fn.apply(this, args);
} else {
[].push.apply(args, arguments);
return arguments.callee;
}
};
};
// 这里使用自执行函数只是为了模仿块级作用域 方便理解完全可以改为普通函数
var cost = (function() {
var money = 0;
return function() {
for (var i = 0, l = arguments.length; i < l; i++) {
money += arguments[i];
}
return money;
};
})();
var cost = currying(cost); // 转化成 currying 函数
cost(100); // 未真正求值
cost(200); // 未真正求值
cost(300); // 未真正求值
// cost(100)(200)(300) 还可以这样调用
console.log(cost()); // 求值并输出:600
反柯里化
反柯里化如果按照字面意思,就是把接受单个参数的函数,变换成接受多参数的函数。
这样理解也没问题,但是总感觉哪里不对,另外一种更通俗的解释是函数的借用,是函数能够接受处理其他对象,通过借用泛化、扩大了函数的使用范围。
Function.prototype.uncurrying = function() {
// self 是调用uncurrying的函数 也即是Array.prototype.push
var self = this;
return function() {
/**
* arguments是push传入的参数 [{"length": 1,"0": 1}, 2] (类数组)
* obj是从arguments截取的第一个参数 {"length": 1,"0": 1}
* 此时arguments为[2](类数组)
*/
var obj = Array.prototype.shift.call(arguments);
// Array.prototype.push.apply(obj, [2])
return self.apply(obj, arguments);
// 分析下书上的另外一种实现 有种解方程的感觉
// return Function.prototype.call.apply(self, arguments);
/**
* Function.prototype.call.apply(self, arguments)
* -> (Function.prototype.call).apply(self, arguments)
* -> (Function.prototype.call).call(self, obj, 2)
* -> self.call(obj, 2)
* -> Array.prototype.push.apply(obj, [2])
*/
};
};
var push = Array.prototype.push.uncurrying();
var obj = {
length: 1,
'0': 1,
};
// push从接收1个参数 变为接收2个参数
push(obj, 2);
console.log(obj); // 输出:{0: 1, 1: 2, length: 2}
// 如果call.apply让你无语的话,下面这个就有点崩溃了
var uncurrying = Function.prototype.uncurrying.uncurrying();
var pushAgain = uncurrying(Array.prototype.push);
pushAgain(obj, 2);
console.log(obj); // 输出:{0: 1, 1: 2, length: 2}
你没有看错,uncurrying 还能借用自己,这也许就是人们说 js 灵活的原因吧。
因为 Function.prototype.uncurrying 是函数,我们在函数的原型链上增加了 uncurrying
方法,试试下面这个你就知道为什么了console.log(Function.prototype.uncurrying.uncurrying.uncurrying)
原型链就像黑洞,没有尽头
函数节流 throttle
原理:使用 setTimeout 延迟执行,如果该次延迟执行还没有完成,则忽略接下来调用该函数的请求
var throttle = function(fn, interval) {
var __self = fn, // 保存需要被延迟执行的函数引用
timer, // 定时器
firstTime = true; // 是否是第一次调用
return function() {
var args = arguments,
__me = this;
if (firstTime) {
// 如果是第一次调用,不需延迟执行
__self.apply(__me, args);
return (firstTime = false);
}
if (timer) {
// 如果定时器还在,说明前一次延迟执行还没有完成
return false;
}
timer = setTimeout(function() {
// 延迟一段时间执行
clearTimeout(timer);
timer = null;
__self.apply(__me, args);
}, interval || 500);
};
};
分时函数
书上的例子是在短时间内往页面中大量添加 DOM 节点,优化为每隔一段时间创建一些节点。其实仔细想下,我们平时的项目也是这样,只不过这个事情是框架做的。
react16 采用了时间片的方式解决卡顿问题,只不过它分的更细,以帧为单位,所以看上去渲染很流畅。
模拟 react 实现分片渲染,这样就不用设置时间间隔,浏览器不忙的时候自动执行
var timeChunk = function(ary, fn, count) {
var obj, t;
var len = ary.length;
var start = function() {
for (var i = 0; i < Math.min(count || 1, ary.length); i++) {
var obj = ary.shift();
fn(obj);
}
};
return function(deadline) {
while (deadline.timeRemaining() > 0 || deadline.didTimeout) {
start();
}
if (ary.length) {
requestIdleCallback(arguments.callee);
}
};
};
var ary = [];
for (var i = 1; i <= 1000; i++) {
ary.push(i);
}
var renderFriendList = timeChunk(
ary,
function(n) {
var div = document.createElement('div');
div.innerHTML = n;
document.body.appendChild(div);
},
8,
);
requestIdleCallback(renderFriendList, { timeout: 2000 });
惰性加载
核心是函数重写,第一次执行后原函数就被替换了
var addEvent = function(elem, type, handler) {
if (window.addEventListener) {
addEvent = function(elem, type, handler) {
elem.addEventListener(type, handler, false);
};
} else if (window.attachEvent) {
addEvent = function(elem, type, handler) {
elem.attachEvent('on' + type, handler);
};
}
// 第一次执行 重写addEvent = newFunction 调用newFunction,后续直接执行newFunction
addEvent(elem, type, handler);
};
JS 中的反柯里化
用 AOP 改善 javascript 代码