参考博客:
前端基础进阶(三):变量对象详解
js闭包其实不难,你需要的只是了解何时使用它
执行上下文
代码在被调用时,会创建一个执行上下文,执行上下文可以理解为当前代码的执行环境,它会形成一个作用域,我们一般讨论全局作用域和函数作用域两种情况。
执行上下文有生命周期,分别为上下文创建阶段,在该阶段会确定变量对象、作用域链和this指向,创建结束后会进入执行阶段,完成相关赋值与调用。
变量对象/活动变量
执行上下文在创建阶段吧会产生变量对象,变量对象存储了当前作用域中的变量参数和函数,当创建完成后,代码开始执行,变量被赋予相应的值,变量对象就被激活为活动变量。 变量对象的创建,依次经历了以下几个过程。
- 建立arguments对象。检查当前上下文中的参数,建立该对象下的属性与属性值。
- 检查当前上下文的函数声明,也就是使用function关键字声明的函数。在变量对象中以函数名建立一个属性,属性值为指向该函数所在内存地址的引用。如果函数名的属性已经存在,那么该属性将会被新的引用所覆盖。
- 检查当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性,属性值为undefined。如果该变量名的属性已经存在,为了防止同名的函数被修改为undefined,则会直接跳过,原属性值不会被修改。
第3条的含义是,首先,函数声明的优先级要比变量高,会声明提前到变量前面,对于函数与变量同名的情况,同名的变量就不会被赋值undefined。当然这条只适用于创建阶段阶段,当代码开始执行时,赋值就按照先后顺序来。
// 不同名情况
console.log(a) // f a(){}
console.log(b) // undefined
function a(){}
var b=10
console.log(b) // 10
// 上述代码的编译执行顺序为:
(1) 函数声明提前
function a(){}
(2) 变量声明提前并赋值undefined
var b = undefined
(3) 编译完成执行代码
function a(){}
var b=undefined
console.log(a) // f a(){}
console.log(b) // undefined
b=10 // 执行时被赋值10
console.log(b) // 10
// 同名情况
console.log(a) // f a(){}
function a(){
}
var a=10
console.log(a) // 10
// 这段代码编译执行的顺序为:
(1) 函数声明提前
function a(){}
(2) 变量因为同名,所以跳过赋值undefined
(3) 编译完成,执行代码
function a(){}
console.log(a) // f a(){}
a=10 // 执行时被赋值10
console.log(a) // 10
复制代码
作用域
首先要明确的是,作用域在代码定义时就产生了,无论该段代码在哪里调用,作用域都不会改变。那代码何时定义?我理解的是当所处的环境被调用时,如一个函数嵌套一个函数,只有当外部函数被调用时,内部函数才被定义。
因为函数存在嵌套关系,会形成一个作用域链,作用域链是由当前环境与上层环境的一系列变量对象组成,它保证了当前执行环境对符合访问权限的变量和函数的有序访问。
var a = 10;
var fn = null;
function outer() { // 作用域链为 自己->全局
console.log(a) // 10
console.log(b) // undefined
var b = 20;
console.log(b) // 20
function inner() {
console.log(b) // 20
console.log(c) // 报错
}
fn = inner;
}
function other() {
var c = 30;
fn();
}
outer(); // 10 undefined 20
other(); // 20,报错,找不到c
复制代码
首先,是全局代码执行,进入函数调用栈,生成全局执行上下文,分为两个阶段,首先是创建阶段,函数声明提升,变量提升并赋值undefined,因为outer和other被定义了,所以创建了各自的作用域链,分别为各自的作用域到全局作用域;创建完成后开始执行,变量被赋值,开始调用函数outer。
调用outer时,outer进入函数调用栈,开始生成outer的执行上下文,也分为两个阶段,创建阶段,变量提升,函数定义,产生变量对象放到自己的作用域中,此时inner被定义,创建了自己的作用域链,为 inner的作用域到outer的作用域再到全局作用域;outer的执行上下文创建完成后开始执行相应代码,依次输出10 unndefined 20;然后执行结束,弹出函数调用栈。
接着调用other,也是同样的步骤,注意执行到fn时,因为fn被赋值inner,作用域跟inner一样,而inner的作用域链在定义的时候就确定了,无论在哪里执行都不会改变,所以此时inner的作用域链也只有自己、outer和全局的,并不能访问到other中的变量,所以找不到c。
闭包
其实上述代码就是闭包的场景,outer虽然调用结束了,但是因为作用域链的原因,并不会被摧毁回收,inner仍然可以访问到作用域链上的变量。
简单来说,闭包就是外部函数可以访问某个函数内部变量的机制,所以形成闭包的必要条件就是,函数嵌套函数,同时将内部函数返回出来。
为什么要有闭包
- 延长变量的生命周期同时不会污染全局变量
- 可以缓存数据
(1)点赞功能,每个点赞按钮点击加1,同时互不影响
function add(){
var count=0;
return function(){
return count++
}
}
// 给按钮绑定不同的点击事件
for(i in n){
var fn=add()
btn[i].onclick=fn;
}
复制代码
(2)数据缓存.假如有一个计算乘积的函数,mult函数接收一些number类型的参数,并返回乘积结果。为了提高函数性能,我们增加缓存机制,将之前计算过的结果缓存起来,下次遇到同样的参数,就可以直接返回结果,而不需要参与运算。这里,存放缓存结果的变量不需要暴露给外界,并且需要在函数运行结束后,仍然保存,所以可以采用闭包。
var mult = (function(){
var cache = {};
var calculate = function() {
var a = 1;
for(var i = 0, len = arguments.length; i < len; i++) {
a = a * arguments[i];
}
return a;
}
return function() {
var args = Array.prototype.join.call(arguments, ',');
if(args in cache) {
return cache[args];
}
return cache[args] = calculate.apply(null, arguments);
}
}())
复制代码
感觉闭包还是需要多写案例才能真正弄得清楚,为什么这么设计,会持续更新。