关于 JS 闭包看这一篇就够了
今天看完了《你不知道的Javascript 上卷》的闭包,来总结一下。
1. LHS 和 RHS 查询
LHS (Left-hand Side) 和 RHS (Right-hand Side) ,是在代码执行阶段 JS 引擎操作变量的两种方式,字面理解就是当变量出现在赋值操作左侧时进行LHS 查询,出现在右侧时进行RHS 查询。更准确的来说,LHS 是为了找到变量的容器本身从而可以进行赋值,而RHS 则是获取某个变量的值。
例如:
console.log(a);
其中对a 的引用就是一个RHS 引用,因为这里没有给a 赋任何值,而是获取它的值从而将它传递给console.log 。
a = 2;
显然这里对a 的引用是LHS 引用,因为这里并不需要获取值,只是为了将2 赋值给a 这个变量。
现在我们已经知道在代码执行阶段 JS 引擎操作变量这两种方式,那么这两种方式会如何去找到变量呢?
2. 作用域
简单来说,作用域 指程序中定义变量的区域,它决定了当前执行代码对变量的访问权限。
2.1 作用域分类
作用域包括:
- 全局作用域:程序的最外层作用域
- 函数作用域:函数定义时会被创建
- 块级作用域:
ES6 新增的let 、const 特性
例如:
var name = '夏安';
function func() {
var name = '..夏安..';
console.log(name);
}
if (true) {
let name = '夏安...';
console.log(name);
}
2.2 作用域链
但几个作用域进行了嵌套,这边现成了作用域链。
LHS 和RHS 查询都会在当前执行作用域中开始,如果它们没有找到所对应的标识符,就会沿作用域向外层作用域查找,直到抵达全局作用域再停止。
不成功的RHs 引用会导致抛出ReferenceError 。不成功的LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),或者抛出ReferenceError 异常(严格模式下)。
例如:
function func(b) {
console.log(a + b);
console.log(c);
}
var a = 1;
func(2);
上述栗子中,对b 进行RHS 引用,在func 函数内部作用域中无法找到,但可以在上级作用域(全局作用域)中找到,而c 在整个作用域链中都没有找到,所以抛出了ReferenceError 异常。
2.3 词法作用域
作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域,也可以被叫做 静态作用域,另一种则称为动态作用域(如Bash 脚本)。
无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。
我们来看下面这个栗子:
function func() {
console.log(a);
}
function func2() {
var a = 1;
}
var a = 2;
func();
在函数func 作用域李没有找到变量a ,向外层全局作用域找,而不会在函数func2 作用域里找。
词法作用域查找只会查找一级标识符,比如a ,b 等,如果代码中引用了obj.name ,词法作用域查找只会试图查找obj 标识符,找到这个变量后,对象属性访问规则会接管对name 属性的访问。
2.4 欺骗词法作用域
Javascript 中有两种机制可以欺骗词法作用域,,分别是eval 和with ,但欺骗词法作用域会导致性能下降,所以不建议使用。
下面我们以eval 为例简单介绍一下:
function func(str) {
eval(str);
console.log(a);
}
var a = 1;
func('var a = 2;');
eval 的参数var a = 2; 被当作本来就在那里的代码执行,在函数func 作用域里创建了一个变量a ,从而遮蔽了外层全局作用域里的变量a
2.5 块级作用域
什么是块级作用域呢?简单来说,花括号内 {...} 的区域就是块级作用域区域。
很多语言本身都是支持块级作用域的。Javascript 中大部分情况下,只有两种作用域类型:全局作用域 和 函数作用域。
if (true) {
var a = 1;
}
console.log(a);
运行后会发现,结果还是 1 ,花括号内定义并赋值的 a 变量跑到全局了。这足以说明,Javascript 不是原生支持块级作用域的。
但是 ES6 标准提出了使用 let 和 const 代替 var 关键字,来“创建块级作用域”。也就是说,上述代码改成如下方式,块级作用域是有效的:
if (true) {
let a = 1;
}
console.log(a);
2.6 模块化
作用域的一个常见运用场景之一,就是 模块化。由于原生Javascript 不支持模块化,在正式的模块化方案出来之前,开发者为了解决这类问题想到了使用函数作用域来创建模块的方法。
(function () {
var a = 1;
console.log(a);
})();
(function () {
var a = 2;
console.log(a);
})();
上面的代码中,构建了 module1 和 module2 两个代表模块的不同文件,立即调用函数表达式(Immediately Invoked Function Expression 简写 IIFE ),两个函数内分别定义了一个同名变量 a ,由于函数作用域的隔离性质,这两个变量被保存在不同的作用域中(不嵌套),JS 引擎在执行这两个函数时会去不同的作用域中读取,并且外部作用域无法访问到函数内部的 a 变量。这样一来就巧妙地解决了 全局作用域污染 和 变量名冲突 的问题。并且,由于函数的包裹写法,这种方式看起来封装性好多了。
3. 闭包
3.1 什么是闭包
关于什么是闭包,说法很多:
在 JS 忍者秘籍(P90)中对闭包的定义:闭包允许函数访问并操作函数外部的变量。
红宝书上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数。
MDN 对闭包的定义为:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz();
函数bar() 的词法作用域能够访问foo() 的内部作用域。然后我们将bar() 函数本身当作一个值类型进行传递。在这个例子中,我们将 bar 所引用的函数对象本身当作返回值。
在foo() 执行后,其返回值(也就是内部的 bar() 函数)赋值给变量baz 并调用 baz() ,实际上只是通过不同的标识符引用调用了内部的函数bar() 。
bar() 显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。 在 foo() 执行后,通常会期待foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很自然地会考虑对其进行回收。 而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是bar() 本身在使用。
拜bar() 所声明的位置所赐,它拥有涵盖foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。
bar() 依然持有对该作用域的引用,而这个引用就叫作闭包。
3.2 闭包的作用
- 保护函数的私有变量不受外部的干扰。形成不销毁的栈内存。
- 保存,把一些函数内的值保存下来。闭包可以实现方法和属性的私有化
3.3 闭包经典使用场景
下面举例一些典型的闭包场景:
3.3.1 return 回一个函数
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz();
3.3.2 IIFE(自执行函数)
(function (a) {
console.log(a);
})(1)
3.3.3 循环赋值
for(var i = 0; i<10; i++){
(function(j){
setTimeout(function(){
console.log(j)
}, 1000)
})(i)
}
因为存在闭包的原因上面能依次输出1~10,闭包形成了10个互不干扰的私有作用域。将外层的自执行函数去掉后就不存在外部作用域的引用了,输出的结果就是连续的 10。为什么会连续输出10,因为 JS 是单线程的遇到异步的代码不会先执行(会入栈),等到同步的代码执行完 i++ 到 10时,异步代码才开始执行此时的 i=10 输出的都是 10。
3.3.4 回调函数
setTimeout(function(){
console.log(j)
}, 1000)
3.3.5 节流防抖
function throttle(fn, timeout) {
let timer = null
return function (...arg) {
if(timer) return
timer = setTimeout(() => {
fn.apply(this, arg)
timer = null
}, timeout)
}
}
function debounce(fn, timeout){
let timer = null
return function(...arg){
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arg)
}, timeout)
}
}
3.3.6 柯里化实现
function curry(fn, len = fn.length) {
return _curry(fn, len)
}
function _curry(fn, len, ...arg) {
return function (...params) {
let _arg = [...arg, ...params]
if (_arg.length >= len) {
return fn.apply(this, _arg)
} else {
return _curry.call(this, fn, len, ..._arg)
}
}
}
let fn = curry(function (a, b, c, d, e) {
console.log(a + b + c + d + e)
})
fn(1, 2, 3, 4, 5)
fn(1, 2)(3, 4, 5)
fn(1, 2)(3)(4)(5)
fn(1)(2)(3)(4)(5)
最后,看下面这道题检验一下自己吧:
var result = [];
var a = 3;
var total = 0;
function foo(a) {
for (var i = 0; i < 3; i++) {
result[i] = function () {
total += i * a;
console.log(total);
}
}
}
foo(1);
result[0]();
result[1]();
result[2]();
参考
|