在一次面试的时候, 被问到谈谈闭包.
一直觉得闭包这个词, 玄而又玄, 英文名叫 closure , 翻译过来叫闭包. 并不能一眼从字面上看出一点玄机.
在用 chrome 浏览器调试的时候, 发现了函数对象上的一个 [[scope]]
属性. 数组类型.
凡自建函数 , [[scope]]
至少是有一个元素, 那就是全局作用域 也就是 整个 window
对象, 用 debugger
研究了一下, 对闭包与作用域有千丝万缕的关系深信不疑, 甚至闭包只是对一种作用域的描述方式, 于是很自信地噼里啪啦地说起来……
说的兴起时, 面试官打断了我, 你说的是作用域吧, 我问的是闭包.!
难道是我错了? 闭包已成仙, 独自能顶起一片天?
后来我想他要的答案很简单, 是我装X过头了?
为人所称道的闭包解答通常是这样子的:
一个函数A里定义并返回了一个函数B , 函数B在函数A外调用, 函数B能访问到函数A里面定义的局部变量.
看起来像是变量跨作用域访问. 而且不是自底向上, 由内到外, 可能是在全局范围内访问一个局部作用域的变量.
像这样:
var local = 'golobal variavle';
function a(){
var local = 'local variable';
return function b(){
console.log(local);
}
}
var c = a();
/**
* c 在全局作用域执行, 按一般逻辑, 它的作用域访问,也就是变量查找范围是在全局,
* 而全局的 local 是 'golobal variable', 打印的却是 'local variable',
* 说明优先访问了 a 函数的局部作用域的 local, 这就是闭包.
*/
c(); // local variable
也有一种变式, 不一定是要返回一个函数, 只要能跨作用域传递执行, 那就能达成以上效果.
var local = 'golobal variable'
function a(){
var local = 'local variable';
function b(){
console.log(local);
}
// 全局定义 c 变量引用函数
window.c = b;
// 回调函数引用函数
setTimeout(b,2000);
}
window.c();
但是不管怎么说, 说闭包, 还是绕不开的就是变量的查找的规则. a 函数成功将 local 造起来看似局部私有变量了, 也终究还是围绕变量.
那么问题来了, 在 js 中, 变量的查找规则是什么? js 的变量查找有它的一套体系, 那就是作用域链, 这不又回来了, 说闭包怎能不从作用域说起. 哎.
在 js 中 , 函数作为一等公民, 也就是函数是高度自由, 可以作为参数传递, 也可以作为返回值返回. 那么函数在各个作用域各个场景中乱窜的时候, 怎么保证尽量简单的复杂度, 又能保证函数正常运行, 函数赖以生存的环境, 也就是它所处的作用域, 该如何查找变量. 如果函数跳出了当前环境, 在另一个环境, 只有营造出该函数所有所需要的变量, 该函数才不会蹦出一个错误, 说变量没找到. 可以想象那样的复杂度.
在 js 中, 采用了一种简单的方式, 静态作用域, 即在 js 引擎解析的时候 , 声明一个函数时, 函数的作用域就已经确定了, 该怎么去查找一个变量. 而且这个作用域并不随着这个函数所处的位置改变而改变, 它永远表现得像在它定义的地方执行一样. 用debugger 模式看 , 最终在外面执行 c
函数的时候, 不还是跳到 a
里面的 b
执行. 所以就是函数作用域是封闭的.
比如:
function test(){
var a = 10 ;
function testInner(){
console.log(a)
}
// testInner(); // 10
return testInner;
}
function test2(){
var a = 20;
// 函数结构与 test 中 testInner 一模一样
function testInner(){
console.log(a)
}
testInner(); // 20
// 拿到 test 中的 testInner 函数对象
var testFn = test();
testFn(); // 10
}
test2();
可以看出,
test2
中拿到了test
中的testInner
方法, 在test2
中执行该方法时的a
却并不是取的离函数执行时最近的a
, 而是跨到了外面, 取了test
中的a
,
所以推断出, 函数的作用域是在函数声明创建时就已经确定了, 作用域是一个属性, 挂载在函数的一个属性上. 该属性定义了一部分
函数查找的作用域对象.且作用域各个元素的引用不会改变.
从 chrome 上看到的是这样的.
function test(){
var a = 10 ;
function testInner(){
console.log(a)
}
window.aInner = testInner;
}
test();
按 chrome 的解释, 也就是 test
是 Colsure
, Global
是最大闭包, 也就是最大的封闭的作用域. 所以可以说, 所以自建函数 , 都是闭包, 都有 Global
这一层封闭作用域且不会再发生改变.
所以总结是 闭包(closure)就是在函数定义的时候, 作用域便已生成 , 而且闭合(closing)不可更改 。
好 , 接下来解决上面遗留的一个坑, 那就是函数定义创建时, 作用域便已生成, 定义了一部分
函数查找的作用域对象. 这里说的一部分是指什么?
var a = 1
function test(){
var a = 2
return function inner(a){
console.log(a)
}
}
test()(3); // 3
最后打印的不是 2 , 也不是1 , 所以函数 inner
的变量查找范围并不只是 [[scopes]]
上, 最上层最优先查找的是 , inner
本身的局部作用域范围, 这个作用域也称 AO
, 活动对象, 在每次函数执行时激活. 而这个 AO
和 [[scopes]]
共同组成了函数的作用域链.
而换一个角度, inner
的[[scopes]]
恰恰又可以理解为外层 test
的 AO
+ [[scopes]]
;
那么AO
是什么?
function a(p1, p2) {
debugger
var local = 'local'
function b() {
console.log(p1, p2, local)
}
return b;
}
a(10, 20)();
图中所指 Local
即function a
函数的执行上下文
, 执行上下文主要包括以下几个部分:
- 参数表 , p1 , p2
- 局部变量, local
- 局部函数, b
- this
而粗略地分有的也分成两块:
- VO/AO => 参数 + 局部变量局部函数
- this
也有把
执行上下文
分成三块, 也就是在以上的基础上再叫一个[[scope chain]]
作用域链, 而作用域链实质是当前函数的AO + 函数的[[Scopes]]
, 该作用域链将是该函数内部函数的[[Scopes]]
.
function a(){
var aLocal = 'a local';
function b(){
var bLocal = 'b local' ;
console.log(aLocal);
}
}
// a 的[[Scopes]] => [Global]
// a 的[[AO]] => {aLocal:'a local'}
// a 的 [[scope chain]] => [{aLocal:'a local'},Global]
// b 的[[Scopes]] = a 的 [[scope chain]] = [{aLocal:'a local'},Global]
// b 的[[AO]] => {bLocal:'b local'}
// b 的[[scope chain]] = [[AO]](b)+[[Scopes]](b)= [{bLocal:'b local'},{aLocal:'a local'},Global]
所以, [[Scopes]] 部分只定义了
一部分
变量访问范围, 与当前函数环境的AO
配合才是完整的变量查找范围, 也就是作用域链,, 而[[Scopes]]只与外部函数有关, 不由当前函数所控制, 所以是外层函数的封闭区域, 子函数不能更改这个区域范围, 但可以操作区域里面的值.这个封闭的区域就是闭包.
不知不觉, 前面又留下了一个坑, 那就是一笔带过的 执行上下文
中的 this
;
执行上下文
在每次函数执行的时候初始化生成, 那么 this
在每次执行的时候, 都将重新确认指向.
那么 this 到底指向谁?
this 指向函数执行时的直接拥有者. 也就是函数
.
前面的对象是谁.
function test(){
console.log(this);
}
// 此时 test 前面并没有 . , 但全局范围内默认为 window
// 类似这样 with(window){ test(); }
test(); // window 对象
加入 ‘use strict’ 后, 默认的window
指向会失效; 这样看起来更合理一些
function test(){
'use strict' ;
console.log(this);
}
test(); // undefined
window.test(); // window 对象
再验证
var name = 'zhangsan'
function test(){
'use strict' ;
console.log(this.name);
}
window.test(); // 'zhangsan'
var obj = {
name:'lisi'
}
obj.test = test
obj.test(); // 'lisi'
test(); // Uncaught TypeError: Cannot read property 'name' of undefined
说到this
那就不得不说 call
,apply
,bind
这几个能改变this
指向的方法了.
- call , apply 将直接执行, 两者区别在于参数的形式不同, apply 的第二个参数为数组
- bind 将返回一个绑定后的函数
function test(){
'use strict' ;
console.log(this.name);
}
// 1.最普通改变 this 指向为 obj 的方法
var obj = {
name:'lisi'
}
obj.test = test
obj.test(); // 'lisi'
// 2. call 方式实现
test.call(obj); // 'lisi'
// 3. apply
test.apply(obj); // 'lisi'
// 4.bind
test.bind(obj)(); // 'lisi'
// 那么简单手写 call
function call(obj,fn){
obj._fn = fn;
var argsStr = ''
for(var i=2,len=arguments.length;i<len;i++){
argsStr += ',arguments['+i+']'
}
argsStr = argsStr.substring(1)
var result = eval('obj._fn('+argsStr+')')
delete obj._fn;
return result;
}
再来看 ES6
中箭头函数的 this
this
是动态的, 也就是每次函数执行的时候, 它都有可能表现不同, 这根据函数的执行方式不同而不同, 也就是函数的拥有者是可以变化着的. 那么很困扰的是, 我就想要this
的值能够固定, 可以预期. 那么怎么办?
最常见的是在延时器回调中的表现
var name = 'zhangsan';
function Test(name){
this.name = name ;
this.sayName = function(){
setTimeout(function(){
console.log(this.name)
},0)
}
}
var test = new Test('lisi');
test.sayName(); // 'zhangsan'
// setTimeout 的回调执行的时候的 this 已经指向的不是 test 实例, 而是 window
// 那么想正确的打印 test 实例的 name , 通常用一个变量保存 this 的引用, 然后回调函数就生成一个闭包 , 持有"正确"的 this 引用; 或者用bind , call ,apply 的方式
function Test2(name){
this.name = name ;
this.sayName = function(){
var self = this;
setTimeout(function(){
console.log(self.name)
},0)
}
}
var test2 = new Test2('lisi');
test2.sayName(); // 'lisi'
// 箭头函数闪亮登场 => 图二 ,this 指向了外部的 this , 还生成了一个闭包
// 那么 this 内在的实现原理是什么呢? 是否利用了 bind? 还是闭包临时变量的方式
// 尝试将 Function.prototype 全部置空, 也不影响箭头函数运行, 内在逻辑不得而知.
function Test3(name){
this.name = name ;
this.sayName = function(){
setTimeout(()=>{
console.log(this.name)
},0)
}
}
var test3 = new Test3('lisi');
test3.sayName(); // 'lisi'
图一:
图二:
总结: 箭头函数执行, 生成的执行上下文,
this
不遵守拥有者原则, 箭头函数内部不改变this
指向, 而仅仅沿袭外层的this
指向.
跑题了, 收!
一个闭包的概念, 就可以牵扯一系列概念和知识点. 不是闭包难理解, 而是环环相扣 , 涉及范围广, 一个地方的疏漏都不能很好地理解概念.
- 预编译 , 函数创建, 函数的[[Scopes]] 生成确定.
- 作用域 , 函数所创建的作用域, VO/AO
- 作用域链, 函数的 AO + [[Scopes]]
- 执行上下文 VO/AO this [[Scopes]]
闭包应用
1.闭包中保有正确索引值
for(var i=0;i<5;i++){
setTimeout(function(){
console.log(i);
},i*1000)
}
// 5,5,5,5,5
// 匿名函数的闭包是 window 的 AO
// 匿名函数的 [[scope chain]] = [AO(匿名) + AO(window)]
function timeoutlog(i){
return function log(){
console.log(i)
}
}
for(var i=0;i<5;i++){
setInterval(timeoutlog(i),1000)
}
// 0,1,2,3,4 这里执行是 timeoutlog 中的 log 函数,
// log 中的 i 引用 timeoutlog 的 AO 中的参数 i
// 闭包是 timeoutlog 的 AO
// log 的 [[scope chain]] = [AO(log)+AO(timeoutlog)+AO(window)]
// 简写
for(var i=0;i<5;i++){
setTimeout((function(a){
return function(){
console.log(a)
}
})(i),i*1000)
}
// 0,1,2,3,4
// dom 元素绑定事件
var aLis = document.getElementsByTagName('li')
for(var i=0;i<aLis.length;i++){
aLis[i].onClick = (function(a){
return function(){
console.log(a)
}
})(i)
}
通用模式就是 , 把一个延期或者异步执行的效果 , 像同步执行一样, 那就要保留同步执行现场. 而异步通常绑定的是一个回调函数B, 函数B不直接定义, 而是通过一个函数A执行, 并且返回一个函数体B, B 不管在何时何地执行, 它始终是可以访问到 A 函数的参数和局部变量的. B 的作用域就类似是这样 [AO(B),AO(A),…AO(window)] , 所以在访问 AO(window) 时, 需要先访问 AO(A), 如果闭包 AO(A) 不保有所需变量, 那也就毫无意义.
2.私有变量, 变量缓存.
(function test1(){
var a = 10;
window.test1 = {
log:function(){
console.log(a)
},
add:function(){
a ++;
}
}
})();
(function test2(){
var a = 20;
window.test2 = {
log:function(){
console.log(a)
},
add:function(){
a ++;
}
}
})()
test1.log(); // 10
test1.add();
test1.log(); // 11
test2.log(); // 20
// test1 和 test2 中的 a 互不影响, test1.add() 可以对 a 进行累加.
总结
- 闭包 => 在创建函数的地方生成的作用域链, 不管函数在何时何地执行都依然有效.
- 执行上下文 => 在函数执行的时候生成, 包括 变量对象 VO/AO , this
- VO/AO => 函数的参数和局部变量
- 作用域 => 表示变量访问的有效范围 , 一个函数就是一个作用域
- 作用域链 => 当前函数的 VO/AO + 当前函数的外层函数的作用域链 , 最外层是全局作用域.
- this 指向 => 函数执行时, this 总是指向函数直接拥有者. 箭头函数除外.