也说说闭包

一直觉得闭包这个词, 玄而又玄, 英文名叫 closure , 翻译过来叫闭包. 并不能一眼从字面上看出一点玄机.

在用 chrome 浏览器调试的时候, 发现了函数对象上的一个 [[scope]] 属性. 数组类型.
凡自建函数 , [[scope]] 至少是有一个元素, 那就是全局作用域 也就是 整个 window 对象, 用 debugger 研究了一下, 对闭包与作用域有千丝万缕的关系深信不疑, 甚至闭包只是对一种作用域的描述方式, 于是很自信地噼里啪啦地说起来…

常规答案是:
一个函数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 的解释, 也就是 testColsure , 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]] 恰恰又可以理解为外层 testAO + [[scopes]] ;

那么AO 是什么?

 function a(p1, p2) {
    debugger
    var local = 'local'

    function b() {
      console.log(p1, p2, local)
    }
    return b;
  }

  a(10, 20)();

这里写图片描述

图中所指 Localfunction 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 总是指向函数直接拥有者. 箭头函数除外.
相关链接
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值