Skip to content
本页目录

闭包

一、什么是闭包?

引用了自由变量的函数就产生了闭包。

自由变量:在函数中被使用,但它既不是函数参数、也不是函数的局部变量,而是一个不属于当前作用域的变量,此时它相对于当前作用域来说,就是一个自由变量。

二、LHS、RHS—— 到底是什么?

  • LHS、RHS,是引擎在执行代码的时候,查询变量的两种方式。其中的 L、R,分别意味着 Left、Right。这个“左”和“右”,是相对于赋值操作来说的。当变量出现在赋值操作的左侧时,执行的就是 LHS 操作,右侧则执行 RHS 操作:
js
name = 'xiuyan';

在这个例子里,name 变量出现在赋值操作的左侧,它就属于 LHS。LHS 意味着 变量赋值或写入内存,

它强调的是一个写入的动作,所以 LHS 查询查的是这个变量的“家”(对应的内存空间)在哪。

js
var myName = name
console.log(name)

在这个例子里,第一行有赋值操作,但是 name 在操作的右侧,所以是 RHS;第二行没有赋值操作,name 就可以理解为没有出现在赋值操作的左侧,这种情况下我们也认为 name 的查询是 RHS。RHS 意味着 变量查找或从内存中读取,它强调的是读这个动作,查询的是变量的内容。

三、词法作用域和动态作用域

我们都知道 JS的作用域就是基于词法作用域的,那动态作用域是什么呢?

其是站在语言的层面来看,作用域其实有两种主要的工作模型:

  • 词法作用域:也称为静态作用域。这是最普遍的一种作用域模型,也是我们学习的重点。
  • 动态作用域:相对“冷门”,但确实有一些语言采纳的是动态作用域,如:Bash 脚本、Perl 等。

词法作用域和动态作用域的区别其实在于划分作用域的时机:

  • 词法作用域: 在代码书写的时候完成划分,作用域链沿着它定义的位置往外延伸。
  • 动态作用域: 在代码运行时完成划分,作用域链沿着它的调用栈往外延伸。

四、修改词法作用域

我们上面已经知道在 JS 中的作用域链在代码书写的时候已经划分好了,作用域链沿着它定义的位置往外延伸。 那我们有什么方法能改变 JS 中的作用域链吗?那就要请出 eval 和 with 两位了。

  • eval
js
function jing(str) {
  eval(str)
  console.log(name)
}
var str = '"var name = haohao"'
jing(str)

五、闭包的身影

一、函数作为返回值

js
function create() {
	const a = 100
	return function () {
		console.log(a)
	}
}

const fn = create()
const a = 200
fn() // 100

二、函数作为参数被传递

js
function print(fn) {
	const a = 200
	fn()
}
const a = 100
function fn() {
	console.log(a)
}
print(fn) // 100

提示

所有的自由变量的查找,是在函数定义的地方,向上级作用域查找 而不是在执行的地方!!!

三、经典⾯试题 var 定义函数的作用域问题

js
function closure() {
  for (var i = 1; i < 10; i++) {
    setTimeout(() => {
      console.log(i)
    }, i * 1000)
  }
}

closure()

原本期望以上代码执行效果为每隔一秒从 1 到 9 输出一个数字

真正的结果

每隔一秒输出一个 10,一共输出 9 个 10。

为什么会出现这样的结果,涉及到 js 的运行机制,这里不深入下去,会另起一篇文章详解。

这里记录自己学习到的解决方法如下:

方法一 利用闭包

js
function closure() {
  for (var i = 0; i < 10; i++) {
    (function (j) {
      setTimeout(() => {
        console.log(j)
      }, i * 1000)
    })(i)
  }
}

closure()

方法二 利用setTimeout函数的第三个参数

js
function closure() {
  for (var i = 1; i < 10; i++) {
    setTimeout((j) => {
      console.log(j)
    }, i * 1000, i)
  }
}

closure()

方法三 ES6 let (最简单)

js
function closure() {
  for (let i = 1; i < 10; i++) {
    setTimeout(() => {
      console.log(i)
    }, i * 1000)
  }
}

closure()

2020/5/1

六、闭包的应用

1、模拟类中的私有变量

  • JS 是面向对象的语言。但 JS 的面向对象,本质上是基于原型,而非像 Java 一样基于类。在JS中,强调的是对象,而不是类的概念。在JS未出ES6之前,我们想要生成对象实例,只有一种办法就是使用构造函数模拟类。如下:
js
// 定义构造函数
function Dog(name) {
  this.name = name
}

// 手动挂载原型方法
Dog.prototype.showName = function() {
  console.log(this.name)
}

// 通过构造函数创建对象实例
var dog = new Dog('哈士奇')

dog.showName()  // 输出 ’哈士奇‘
  • 早期的 JS 程序员,会手动去用构造函数去 Class ,后来ES 标准直接吸纳了这种模拟的思路,把模拟实现的 Class 内置掉,于是我们就有了 ES6 中的 Class:
js
// 构造函数,相当于上一个例子中的 Dog 函数
class Dog {
    constructor(name) {
    // 构造函数的函数体内容
    this.name = name
  }

  showName() {
    console.log(this.name)
  }
}

// 仍然是使用 new 关键字来创建实例
let dog = new Dog('哈士奇')
// 输出 '哈士奇'
dog.showName()
  • 说是模拟实现类,就意味着不是“真的”类。ES6 中的这个 class,本质上仍然是构造函数的语法糖,所以上面两段代码其实本质上是一样的,只是写法不同。这模拟出来的类,仍然无法实现传统面向对象语言中的一些能力 —— 比如私有变量的定义和使用。私有变量到底是干嘛的?为啥没它不行?大家看这样一个 User 类
js
class User {
  constructor(username, password) {
    // 用户名
    this.username = username
    // 密码
    this.password = password
  }

  login() {
    // 使用 fetch 进行登录请求
    fetch(登录请求的目标url地址, {
      method: 'POST', // 指定请求方法为post
      body: JSON.stringify({
        username,
        password
      }), // 请求参数带上用户名和密码
            ...  // 这里省略其它 fetch 参数
    }).then(res => res.json())
  }
}

const user = new User('jingjing','haohao');
console.log(user.password);  // haohao
  • 我们惊恐地发现,登录密码这么关键且敏感的信息,竟然通过一个简单的属性就可以拿到!这就意味着,后面的人只要能拿到 user 这个对象,就可以非常轻松地得知、甚至改写他的密码。像 password 这样的变量,我们希望它仅在对象内部生效,无法从外部触及,这样的变量,就是私有变量。那么在 JS 中,既然无法通过 private 这样的关键字直接在类里声明变量的私有性,我们就只能另寻它法。这时候就轮到闭包登场了,我们的思路就是把私有变量用函数作用域来保护起来,形成一个闭包!
js
const User = function(){
  let _password;
  class User {
    constructor(username,password) {
      this.username = username;
      _password= password;
    }

    login() {
      console.log(this.username, _password);
      return 'baobei';
    }
  }
  return User;
}();

const user = new User('jingjing','haohao');

console.log(user.username);
console.log(user.password);
console.log(user._password);
console.log(user.login());

2020/11/01

MIT Licensed