让“作用域和闭包”说人话

整理自《你不知道的JavaScript(上卷)》,整篇总结围绕着以下两句话:

① “生米煮成熟饭”——赋值可以跑在声明前,反正声明会提升;

② 闭包是一个保留访问作用域的函数,是一种解决问题的方法;

一、作用域是什么

作用域与编译原理息息相关,但是我们暂时还不用管编译原理的复杂过程,它的作用可以简单看下面这张图:
在这里插入图片描述

简单来说就是“我得知道这些变量在哪里,不然我怎么取到它们再进行下一步操作呀!”


JavaScript的编译发生代码执行前的几微妙内,并不是发生在构建之前。

引擎根据作用域进行变量查询,存在两种影响结果的查找方式——LHS/RHS。

RHS(谁是源头【获取变量的值】):需要找到这个变量的值才可以进行下一步操作。如console.log(a),即得到a的值后执行操作。查询操作失败报ReferenceError异常,不合理操作报TypeError异常。

LHS(目标是谁【对变量赋值】):为操作找到一个容器,如var a = 2,即找到一个a,给它赋值为2。查询操作失败在非严格模式下会创建变量。这个特性可以解释变量提升这个东西,也就是“赋值可以跑到声明前”。为了完成赋值这个动作,我可以自己声明一下嘛。现在可以暂时放一下这句话,等到提升时再想。

不推荐使用with/eval

二、函数作用域和块作用域

函数作用域含义:属于这个函数的全部变量都可以在整个函数的范围内使用或复用。

function 开头的就是函数声明,其余则是函数表达式。
正常函数声明被绑定在作用域中,函数表达式则绑定在函数自身中。
这句话不理解可以先埋下一个伏笔,等下面看到闭包的时候再回头看会回味无穷。

// 函数声明
function foo(){
  var a = 2;
  console.log(a);
}
foo()
// 函数表达式
(function foo(){
  var a = 2;
  console.log(a);
})()

var a = 2;
(function IIFE(global){
  var a = 3;
  console.log(a, global.a)// 3,2
})(window)
// 函数表达式
var foo = function(){
  console.log(1)
}

那么怎么搞出一个块作用域呢?块作用域的声明方式有:

  1. with 仅在with声明中有效
  2. try/catch catch内的变量仅在catch内使用
  3. let 劫持所在块的作用域
  4. const 与let类似,变量不可修改

块作用域的优势:1. 有利于垃圾回收(块作用域内代码执行后可以直接回收);2. 解决 var 循环问题。

三、提升

没有赋值只有声明的变量是会报错 undefined,提升也只是提升了个声明而已;再换句话说,赋值可以跑在声明的前面。光说不干假把式~

例一:

a = 2;
var a;
console.log(a); // 正常输出2

-----------------------------------

console.log(a);
var a = 2; // undefined

第一段代码可以顺利执行,因为对于 a 的声明会提升到最顶部,因此在作用域内能够找到a。但是这第二段代码中,var a = 2;会被拆分为两步,第一步是声明 a ,然后就执行了输出语句,还没有进行赋值呢,因此会出现报错。
这个例子恰好说明了“赋值可以跑在声明前面”。


例二:

foo()

function foo(){
  console.log(a); // undefined
  var a;
}

这里也是和例一一样,a会提升声明(作用域在foo内),但是只有声明,没有赋值,所以仍然会报错。


例三:

foo() // 3
function foo(){
  console.log(1)
}
function foo(){
  console.log(2)
}
function foo(){
  console.log(3)
}

还记得前面那个小伏笔吗?**“正常函数声明绑定在作用域中,函数表达式绑定在自身函数中。”**连环声明函数的时候,后面的函数会覆盖之前的同名函数,现在的它们都在同一个作用域呀。


例四:

foo(); // 1
var foo;
function foo(){
  console.log(1) // 这个当作foo1吧
}
foo = function() {
	console.log(2) // 这个就是foo2
}

同样是声明提升,函数的优先级高于变量。这里的例子我们可以把函数表达式当作是一个变量的声明,相当于给foo2 这个变量赋值了一个函数。那么由于函数的优先级更高,这个foo2无法覆盖之前的foo1,所以输出仍然是1。

四、闭包

1. 闭包是什么

首先我们得知道闭包是什么,来看看书里是怎么说的:

函数在 定义时的词法作用域以外的地方 被调用,闭包使得函数可以继续访问定义时的词法作用域。

这句话的形容使闭包看起来像一种_访问作用域的方法_。我们继续看下一句:

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

因此,不应该把比较理解成一种现象或者原理,应该作为一种工具方便我们使用来解决 JavaScript 中的一些问题。这是一个重要的理解点。
用自己的话再来说一下闭包是什么吧:

一个函数(A)引用着另一个函数的变量(B),即使 B 已经顺利执行结束了也不会被回收,因为 A 还引用着呢,所以 B 的各种变量也要被保存。


2. 闭包怎么用

既然说闭包是一种方法,那么闭包怎么用呢?
经典例题六个六(此题会输出6个6,原因还涉及到异步和宏任务微任务那边的,暂且不论)。这里就是因为里面的 setTimeout 函数和外面的 for 循环一样,使用的是全局作用域,大家都用了同一个变量 i ,那么在循环执行结束之后只能继续使用循环完的 i 啦,也就是6。

for(var i=1;i<=5;i++){
  setTimeout(function timer(){
    console.log(i);
  }, i*1000);
}

如何解决这个问题呢,我们可以给这个 setTimeout 塞闭包嘛,通过创建新的作用域来解决。前面说了,闭包可以让保留引用的作用域,也就可以保留我们每次想使用的那个循环过程中的 i 了。
小伏笔在此时又跳出来了:**“正常函数声明绑定在作用域中,函数表达式绑定在自身函数中。”**此处的这个包了括号的函数(IIFE)是一种函数表达式,因此它的作用域就在自身函数里。

for(var i=1;ii<=5;i++){
	(function(j){
    setTimeout(function timer(){
      console.log(j);
    }, j*1000)
  })(i)
}

前面有提到过,let 可以劫持所在块的作用域,因此使用 let 可以妥妥解决这个问题!这也是目前主流推荐使用的方法,有效避免了内存泄漏。

for(let i=1;i<=5;i++){
  setTimeout(function timer(){
    console.log(i);
  }, i*1000)
}

五、箭头函数

箭头函数长这样 =>
在分析作用域和闭包的时候,最容易联想到的相关问题是箭头函数。这玩意儿到底有什么神奇之处呢?来看看书里是怎么说的:

简单来说,箭头函数在涉及this绑定时的行为和普通函数的行为完全不一致。它放弃了所有普通this绑定的规则,取而代之的是用当前的词法作用域覆盖了this本来的值。

注意看,是**“当前的词法作用域”**!不再是执行时候的作用域了,多么伟大的发明!也就是说箭头函数里的 this 将绑定在定义时,而不是运行调用时,这是它最大的作用。关于它,还有几个小tips:

  1. 不可以当作构造函数,不可以使用new命令,会报错;
  2. 不可以直接调用 arguments,可以在外面包一层普通函数调用外面这个普通函数的 arguments,但这种操作有点迷;
  3. 不可以使用 yield 命令,不能用作 Generator 函数;

来道小题目,看看最后会输出啥呢?

window.color = "red";
let color = "green";
let obj = {
  color: "blue"
};
let sayColor = () => {
  return this.color;
};
sayColor.apply(obj);

放一下本人的语雀原文 https://www.yuque.com/zhoulifer/frontend/mcuebs

欢迎交流!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值