关于 JS 闭包看这一篇就够了

关于 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新增的letconst特性

例如:

var name = '夏安'; // 全局作用域

function func() {
    
     //
  var name = '..夏安..'; // 函数作用域
  console.log(name);
}
if (true) {
    
    
  let name = '夏安...'; // 块级作用域
  console.log(name);
}

2.2 作用域链

但几个作用域进行了嵌套,这边现成了作用域链。

LHSRHS查询都会在当前执行作用域中开始,如果它们没有找到所对应的标识符,就会沿作用域向外层作用域查找,直到抵达全局作用域再停止。

不成功的RHs引用会导致抛出ReferenceError。不成功的LHS引用会导致自动隐式地创建一个全局变量(非严格模式下),或者抛出ReferenceError异常(严格模式下)。

例如:

function func(b) {
    
    
  console.log(a + b); // 3
  console.log(c); // ReferenceError: c is not defined
}

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(); // 2

在函数func作用域李没有找到变量a,向外层全局作用域找,而不会在函数func2作用域里找。

词法作用域查找只会查找一级标识符,比如ab等,如果代码中引用了obj.name,词法作用域查找只会试图查找obj标识符,找到这个变量后,对象属性访问规则会接管对name属性的访问。

2.4 欺骗词法作用域

Javascript中有两种机制可以欺骗词法作用域,,分别是evalwith,但欺骗词法作用域会导致性能下降,所以不建议使用。

下面我们以eval为例简单介绍一下:

function func(str) {
    
    
  eval(str);
  console.log(a);
}

var a = 1;
func('var a = 2;'); // 2

eval的参数var a = 2;被当作本来就在那里的代码执行,在函数func作用域里创建了一个变量a,从而遮蔽了外层全局作用域里的变量a

2.5 块级作用域

什么是块级作用域呢?简单来说,花括号内 {...} 的区域就是块级作用域区域。

很多语言本身都是支持块级作用域的。Javascript 中大部分情况下,只有两种作用域类型:全局作用域函数作用域

if (true) {
    
    
  var a = 1;
}

console.log(a); // 1

运行后会发现,结果还是 1,花括号内定义并赋值的 a 变量跑到全局了。这足以说明,Javascript 不是原生支持块级作用域的。

但是 ES6 标准提出了使用 letconst 代替 var 关键字,来“创建块级作用域”。也就是说,上述代码改成如下方式,块级作用域是有效的:

if (true) {
    
    
  let a = 1;
}

console.log(a); // ReferenceError

2.6 模块化

作用域的一个常见运用场景之一,就是 模块化。由于原生Javascript不支持模块化,在正式的模块化方案出来之前,开发者为了解决这类问题想到了使用函数作用域来创建模块的方法。

// module1.js
(function () {
    
    
  var a = 1;
  console.log(a);
})();

// module2.js
(function () {
    
    
  var a = 2;
  console.log(a);
})();

上面的代码中,构建了 module1module2 两个代表模块的不同文件,立即调用函数表达式(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(); // 2

函数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(); // 2

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)  // 15
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](); // 3
result[1](); // 6
result[2](); // 9

参考

おすすめ

転載: blog.csdn.net/p1967914901/article/details/122516076