闭包
一、什么是闭包?
引用了自由变量的函数就产生了闭包。
自由变量:在函数中被使用,但它既不是函数参数、也不是函数的局部变量,而是一个不属于当前作用域的变量,此时它相对于当前作用域来说,就是一个自由变量。
二、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
russ