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

简介: 关于 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

参考

  • JS 闭包经典使用场景和含闭包必刷题 - 掘金 (juejin.cn)
  • 闭包 - JavaScript | MDN (mozilla.org)
相关文章
|
2月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理与实战
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理与实战
|
1月前
|
JavaScript 前端开发
js 闭包的优点和缺点
【10月更文挑战第27天】JavaScript闭包是一把双刃剑,在合理使用的情况下,它可以带来很多好处,如实现数据封装、记忆功能和模块化等;但如果不注意其缺点,如内存泄漏、变量共享和性能开销等问题,可能会导致代码出现难以调试的错误和性能问题。因此,在使用闭包时,需要谨慎权衡其优缺点,根据具体的应用场景合理地运用闭包。
114 58
|
1月前
|
缓存 JavaScript 前端开发
js 闭包
【10月更文挑战第27天】JavaScript闭包是一种强大的特性,它可以用于实现数据隐藏、记忆和缓存等功能,但在使用时也需要注意内存泄漏和变量共享等问题,以确保代码的质量和性能。
38 7
|
1月前
|
自然语言处理 JavaScript 前端开发
JavaScript闭包:解锁编程潜能,释放你的创造力
【10月更文挑战第25天】本文深入探讨了JavaScript中的闭包,包括其基本概念、创建方法和实践应用。闭包允许函数访问其定义时的作用域链,常用于数据封装、函数柯里化和模块化编程。文章还提供了闭包的最佳实践,帮助读者更好地理解和使用这一强大特性。
22 2
|
1月前
|
存储 缓存 自然语言处理
掌握JavaScript闭包,提升代码质量与性能
掌握JavaScript闭包,提升代码质量与性能
|
1月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包(Closures)
深入理解JavaScript中的闭包(Closures)
|
1月前
|
存储 自然语言处理 JavaScript
深入理解JavaScript的闭包(Closures)
深入理解JavaScript的闭包(Closures)
26 0
|
2月前
|
设计模式 JavaScript 前端开发
探索JavaScript中的闭包:从基础概念到实际应用
在本文中,我们将深入探讨JavaScript中的一个重要概念——闭包。闭包是一种强大的编程工具,它允许函数记住并访问其所在作用域的变量,即使该函数在其作用域之外被调用。通过详细解析闭包的定义、创建方法以及实际应用场景,本文旨在帮助读者不仅理解闭包的理论概念,还能在实际开发中灵活运用这一技巧。
|
2月前
|
缓存 JavaScript 前端开发
深入了解JavaScript的闭包:概念与应用
【10月更文挑战第8天】深入了解JavaScript的闭包:概念与应用
|
2月前
|
自然语言处理 JavaScript 前端开发
Javascript中的闭包encloure
【10月更文挑战第1天】闭包是 JavaScript 中一种重要的概念,指函数能够访问其定义时的作用域内的变量,即使该函数在其词法作用域之外执行。闭包由函数及其词法环境组成。作用域链和词法作用域是闭包的核心原理。闭包常用于数据隐藏和封装,如模块模式;在异步操作中也广泛应用,如定时器和事件处理。然而,闭包也可能导致内存泄漏和变量共享问题,需谨慎使用。
下一篇
DataWorks