一、相关知识:
静态作用域(闭包的必要条件)
静态作用域(词法作用域)是指声明的作用域是根据程序正文在编译时就确定的。动态作用域,程序中某个变量所引用的对象是在程序运行时刻根据程序的控制流信息来确定的。(简单来说就是函数作用域是在定义时确定的,而不是在调用时)
静态作用域查找的是距离当前作用域最近的外层作用域中同名标识符的声明
采用静态作用域的语言中,基本都是最内嵌套作用域规则:由一个声明引进的标识符在这个声明所在的作用域里可见,而且在其内部嵌套的每个作用域里也可见,除非它被嵌套于内部的对同名标识符的另一个声明所掩盖。为了找到某个给定的标识符所引用的对象,应该在当前最内层作用域里查找。如果找到了一个声明,也就可以找到该标识符所引用的对象。否则我们就到直接的外层作用域里去查找,并继续向外顺序地检查外层作用域,直到到达程序的最外嵌套层次,也就是全局对象声明所在的作用域。如果在所有层次上都没有找到有关声明,那么这个程序就有错误。
链式作用域:
Foo内部的所有局部变量,对匿名函数都是可见的。但是反过来就不行,匿名函数内部的局部变量,对Foo是不可见的。这种关系即是”链式作用域”(chain scope)结构。
作用域链是一个对象列表:JavaScript 调用函数时,都会为之创建一个新的对象用来保存局部变量,把这个对象添加到作用域链中
二、闭包
定义:
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。
示例:
下面的匿名函数即是有权访问另一个函数作用域中的变量的函数
function Foo() {
var num = 3;
return function () {
console.log(num)
}
}
var num = 6;
var execute = Foo();
execute(); // 3
特点:
- 在全局环境中,能够得到到Foo里的变量3
- 由于闭包的存在,在执行完
var execute = Foo();
时,并不会销毁Foo()的执行环境。因为Foo()返回一个匿名函数,赋值给execute。而匿名函数内引用了Foo()执行环境中的变量num。
为了在即使父函数上下文结束的情况下也能访问其中的变量,内部函数在被创建的时候会在它的[[Scopes]]属性中保存父函数的作用域链。当内部函数被调用的时候,它上下文的作用域链会被格式化成活动对象与[[Scope]]属性的和
Scope chain = Activation object + [[Scopes]]
深入:
上面我们只是返回了一个函数,还有一种返回多个函数时。他们拥有相同的父作用域,这种情况下,每个函数[[Scopes]]属性中存储的变量是在拥有相同父作用域链的所有函数之间共享的.
其中一个闭包对共享的父作用域链里的变量修改,会体现在另一个闭包对这些变量的读取上,用这个特性可以封装私有变量
function obj() {
var x = 1;
return {
foo: function foo() { return x+=1; },
bar: function bar() { return x+=1; }
};
}
var closure = obj();
console.dir(closure.foo);
console.log(closure.foo()); // 2
console.log(closure.bar()); // 3
// 计数,counter是私有变量,避免恶意修改
var count = (function () {
var counter = 0;
return function () {
return counter++;
};
})();
私有变量设置,execute 和anther两个是相互独立的。
function counter() {
var n = 0;
return {
get: function () {return n++;},
set: function () {n = 0;}
};
}
var execute = counter(),
anther = counter();
console.log(execute.get()); // 0
console.log(anther.get()); // 0
execute.set();
console.log(execute.get()); // 0
console.log(anther.get()); // 1
三、另一种形式
上面闭包函数是作为返回值,还有一种是作为参数。(高阶函数)
var x = 1;
function foo() {
console.log(x);
}
(function (fn) {
var x = 2;
fn();
})(foo); // 1
这里结果是1而不是2。这正是静态作用域的威力。也是为什么说闭包的必要条件是静态作用域(避免)
函数作用域是在定义时确定的,而不是在调用时
四、额外的
function constNum(v) {
return function () {
return v;
};
}
var funcs = [];
for (var i = 0; i < 10; i++) {
funcs[i] = constNum(i);
}
console.log(funcs[5]()) // 5
// function constNum() {
// var funcs = [];
// for (var i = 0; i < 10; i++) {
// funcs[i] = function () {
// return i;
// };
// }
// return funcs;
// }
// var funcs = constNum();
// console.log(funcs[5]());
第二种当constNum()返回时,变量i是10。而在内所有闭包共享同一个i。
补充:
在做函数防抖,节流的时候遇到个不错的问题。看下下面的代码。
function Throttle (func, wait, mustRun) {
var timeout,
startTime = new Date();
console.log(startTime);
return function() {
// this 指向window,arguments是ansyno
var context = this,
args = arguments,
curTime = new Date();
clearTimeout(timeout);
// console.log(curTime);
if (curTime - startTime >= mustRun) {
// apply的作用是将func方法放到当前的上下文上执行,(执行函数或方法)
func.apply(context,args);
startTime = curTime;
} else {
timeout = setTimeout(func, wait);
}
};
};
function realFunc () {
console.log("ddd");
}
// 这里Throttle(realFunc, 500, 1000)只调用了一次,scroll触发的是闭包,每次滚动都执行闭包。
window.addEventListener('scroll',Throttle(realFunc, 500, 1000));
上面的scroll事件,每次触发的是Throttle函数?还是闭包?
答案是闭包,Throttle(realFunc, 500, 1000)调用了一次返回了一个anonym(也就是闭包),而这个anonym就是每次滚动事件要触发的函数,而不是Throttle(),可以打印一下自己查看究竟是不是。