JavaScript的三座大山--(2)--作用域和闭包

文章可能有点长,但 🌰 栗子🌰 都很经典,简单易懂,一起学习吧!

壹 ❀ 作用域

1、作用域就是一个变量或者函数的有效作用范围,在JS中作用域一共有三种,分别是全局作用域、局部作用域(函数作用域)、块级作用域;

2、变量声明的三种方式:varletconst 

1. 全局作用域

1、全局作用域:声明在函数外部的变量(声明在script标签中的变量和函数),在代码中任何地方都能访问到的对象拥有全局作用域;var和let变量在全局作用域中都是全局变量;

注意:所有没有var直接赋值的变量都属于全局变量;

2、全局作用域中声明的变量和函数会作为window对象的属性和方法保存,可以通过 window.变量名 去调用它;

3、全局作用域在页面打开时被创建,页面关闭时被销毁;

(1)最外层函数和在最外层函数外面定义的变量拥有全局作用域,例如:
var authorName="波妞";
function doSomething(){
    var blogName="中介";
    function innerSay(){
        console.log(blogName);
    }
    innerSay();
}
console.log(authorName); //波妞
doSomething(); //中介
console.log(blogName); //错误
innerSay() //错误
(2)所有末定义直接赋值的变量自动声明为拥有全局作用域,例如:
function doSomething(){
    var authorName="波妞";
    blogName="中介";
    console.log(authorName);
}
doSomething(); // 波妞
console.log(blogName); //中介
console.log(authorName); //错误

2. 局部作用域(又称 函数作用域)

 在函数中用 var 、let 、const 声明的所有变量,都是函数的局部变量,作用范围为局部作用域,即:只能在函数内部使用,函数外部使用是不行的。无论是通过var还是let定义在局部作用域的变量都是局部变量;

1、调用函数时,函数作用域被创建;函数执行完毕,函数作用域被销毁;

2、每调用一次函数就会创建一个新的函数作用域,他们之间是相互独立的;

3、在函数作用域中可以访问到全局作用域的变量,在函数外无法访问到函数作用域内的变量;

4、在函数作用域内访问变量/函数时,会现在自身作用域中查找,若没有找到,则会到函数的上一级作用域中寻找,一直到全局作用域;

注意:所有没有var直接赋值的变量都属于全局变量 

3. 块级作用域

 

 任何一对花括号{ }中的语句集都属于一个,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域

在ES6中只要 { } 没有函数结合在一起,那么就是“块级作用域”。ES6之前没有块级作用域。使用 let 关键字或者 const 关键字来实现块级作用域。

let 和 const声明的变量只在 let 或 const命令所在的代码块 { } 内有效,在 { } 之外不能访问。

 注意:

  • 块级作用域中,var定义的变量是全局变量,let定义的变量是局部变量;
  • 块作用域由 { } 包括,if语句和for语句里面的{ }也属于块作用域。但函数funtion(){ }里面的{ }不属于块级作用域,而是局部作用域(函数作用域);

4.块级作用域和局部(函数)作用域区别 

        4.1、在块级作用域中通过var定义的变量是全局变量

{
    //块级作用域
    var num = 123;//全局变量
}
console.log(num);  // 123

        4.2、在局部作用域中通过var定义的变量是局部变量

function test() {
     var value = 666;//局部变量
     console.log(value);
}
test(); // 666
console.log(value);//value is not defined

5、无论是在块级作用域还是局部作用域,省略变量前面的let或者var就会变成一个全局变量。

         5.1、验证在块级作用域中

{
     var num1 = 678;//全局变量
     num2 = 678;//全局变量
     let num3 = 678;//局部变量
}
console.log(num1);//678
console.log(num2);//678
console.log(num3);//num3 is not defined

        5.2、验证在局部作用域中

function f() {
      num1 = 456;//全局变量
      var num2 = 678;//局部变量
      let num3 = 123;//局部变量
}
f();
console.log(num1);//456
console.log(num2);//num2 is not defined
console.log(num3);//num3 is not defined

❀加餐❀ 

1. 通过这个🌰 栗子🌰加深一下理解

        var num1 = 123;//**全局变量**
        function f() {
            var num2 = 456//局部变量
        }
        function test() {
            num3 = 666;//局部作用域,没有var或者let修饰。**全局变量**
        }
        {
            var num4 = 789;//块级作用域、**全局变量**
        }
        {
            let num5 = 789;//块级作用域、let定义变量。局部变量
        }
        {
            let value7 = 123;
            {
                //注意点:在不同的作用域范围中,可以有同名变量
                let value7 = 456;//不会报错。
            }
        }
     

 2. 为什么需要块级作用域

 ES5只有全局作用域和函数作用域,没有块级作用域,会带来以下问题:

1) 变量提升导致内层变量可能会覆盖外层变量

var i = 5;  
function func() {  
    console.log(i);  
    if (true) {  
        // var i = 6;  
    }  
}  
func(); // 5



var i = 5;  
function func() {  
    console.log(i);  
    if (true) {  
        let i = 6;  
    }  
}  
func(); // 5
var i = 5;  
function func() {  
    console.log(i);  
    if (true) {  
        var i = 6;  
    }  
}  
func(); // undefined

2) 用来计数的循环变量泄露为全局变量

for (var i = 0; i < 10; i++) {    
        console.log(i);    
}    
console.log(i);  // 10 
for (let i = 0; i < 10; i++) {    
        console.log(i);    
}    
console.log(i);  // i is not defined

贰 ❀ 变量提升 

首先我们要知道,js的执行顺序是由上到下的,但这个顺序,并不完全取决于你,因为js中存在变量的声明提升。

🌰 栗子1

console.log(a)  //undefined
var a = 100

fn('zhangsan')
function fn(name){
    age = 20
    console.log(name, age)  //zhangsan 20
}

结果:

打印a的时候,a并没有声明,为什么不报错,而是打印undefined。

执行fn的时候fn并没有声明,为什么fn的语句会执行?

 这就是变量的声明提升,代码虽然写成这样,但其实执行顺序是这样的。

var a

function fn(name){
    age = 20
    console.log(name, age)
}

console.log(a) 
a = 100

fn('zhangsan')

 js会把所有的声明提到前面,然后再顺序执行赋值等其它操作,因为在打印a之前已经存在a这个变量了,只是没有赋值,所以会打印出undefined,为不是报错,fn同理。

变量提升:JS在解析代码时,会将所有的声明提前到所在作用域的最前面

 🌰 栗子2

console.log(name);      //undefined
var name = '波妞';
console.log(name);      //波妞
function fun(){
    console.log(name)   //undefined
    console.log(like)   //undefined
    var name = '大西瓜';
    var like = '宗介'
}
fun();

相当于

var name;
console.log(name);      //undefined
name = '波妞';
console.log(name);      //波妞
function fun(){
    var name;
    var like;
    console.log(name)   //undefined
    console.log(like)   //undefined
    name = '大西瓜';
    like = '宗介'
    
    // 此时再打印
    console.log(name)   //大西瓜
    console.log(like)   //宗介
}
fun();

注意:是提前到当前作用域的最前面

这里也要注意函数声明和函数表达式的区别。上例中的fn是函数声明。接下来通过代码区分一下。

 🌰 栗子3

// 函数声明
fn1('abc')
function fn1(str){
    console.log(str)
}

// 函数表达式
fn2('def')
var fn2 = function(str){
    console.log(str)
}

结果:

可以看到fn1被提升了,而fn2的函数体并没有被提升。

效果等同于:

var fn2
fn2('def')
fn2 = function(str){
    console.log(str)
}

这下大家应该就明白报错原因了吧! 

好了,上面我们已经了解了作用域与变量提升的概念,下面我们来看一下作用域链;


叁 ❀ 作用域链

 很简单,直接看🌰 栗子

console.log(name);      //undefined   (1)
var name = '波妞';
var like = '宗介'
console.log(name);      //波妞         (2)
function fun(){
    console.log(name);  //波妞         (3)
    console.log(eat)    //ReferenceError: eat is not defined    (4)
    (function(){
        console.log(like)   //宗介      (5)
        var eat = '肉'
    })()
}
fun();
  1. name定义在全局,在全局可以访问到,所以 (2) 打印能够正确打印;
  2. 在函数fun中,如果没有定义name属性,那么会到它的父作用域去找,所以 (3) 也能正确打印。
  3. 定义:内部环境可以访问所有外部环境,但外部环境不能访问内部环境的任何变量和函数。类似单向透明,这就是作用域链,所以 (4) 不行而 (5) 可以。

前后呼应:为什么第一个打印是"undefined",而不是"ReferenceError: name is not defined"。原理就是JS的变量提升


肆 ❀ 闭包

JavaScript高级程序设计中对闭包的定义:闭包是指有权访问另外一个函数作用域中变量的函数。

从概念上,闭包有两个特点

1、函数嵌套;(有外部函数, 有内部函数)

2、内部函数可以引用外部函数的变量/函数;

以下是我在学习中做的笔记:

1. 🌰引入----代码有点长,但很经典,可以自己运行理解一下哦

<body>
<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
<!--
需求: 点击某个按钮, 提示"点击的是第n个按钮"
-->
<script type="text/javascript">
  var btns = document.getElementsByTagName('button')
  //遍历加监听
  /*
   (1) 为什么用 length=btns.length?
      答:因为btns是一个伪数组,如果直接写  i < btns.length ,每次循环时都要重新统计btns的长度,所以拆分为
          length=btns.length; i < length;
    (2) 用 var 的效果?
      无论我点击哪个按钮,都只是返回 "第4个" 。
      将 var 关键字改为 let 关键字就好了;
   */

  // 总返回 第4个
  /* for (var i = 0,length=btns.length; i < length; i++) {
    let btn = btns[i]
    btn.onclick = function () {
      alert('第'+(i+1)+'个')
    }
  } */

  // 解决方式一:
  /* for (var i = 0,length=btns.length; i < length; i++) {
    var btn = btns[i]
    //将btn所对应的下标保存在btn上
    btn.index = i
    btn.onclick = function () {
      alert('第'+(this.index+1)+'个')
    }
  } */

  // 解决方式二:块级作用域
  /* for (let i = 0,length=btns.length; i < length; i++) {
    let btn = btns[i]
    btn.onclick = function () {
      alert('第'+(i+1)+'个')
    }
  }
 */

  // 解决方式三:
  //利用闭包
  for (var i = 0,length=btns.length; i < length; i++) {
    // 自调用函数
    (function (j) {
      var btn = btns[j]
      btn.onclick = function () {
        alert('第'+(j+1)+'个')
      }
    })(i)
  }
</script>
</body>

 2. 🌰理解闭包

1. 如何产生闭包?
  * 当一个被嵌套的内部(子) 函数引用了嵌套其的外部(父) 函数的变量(函数)时, 就产生了闭包
2. 闭包到底是什么?
  * 使用chrome调试查看
  * 理解一: 闭包是嵌套的内部函数(绝大部分人)
  * 理解二: 包含被引用变量(函数)的对象(极少数人)
  * 注意: 闭包存在于被嵌套的内部函数中
3. 产生闭包的条件?
  * 函数嵌套   (有外部函数,有内部函数)
  * 内部函数引用了外部函数的变量/函数
  * 执行外部函数;(必须执行外部函数才会产生闭包)  ---产生几个闭包要看执行多少次外部函数

🌰栗子

由于种种原因,我们有时候需要得到函数内的局部变量。但是,在正常情况下,这是办不到的,那么怎么才能得到函数内部的局部变量呢?

那就是闭包,在函数的内部,再定义一个函数,然后把这个函数返回。

function F1(){
    var a = 100
    //返回一个函数 (函数作为返回值)
    return function (){
        console.log(a)
    }
}

//f1得到一个函数
var f1 = F1()
var a = 200
f1()  // 100

在本🌰中就实现了闭包,简单的说,闭包就是能够读取其他函数内部变量的函数。 

下面解释一下为什么打印的是100,

看这句 var f1 = F1(); F1这个函数执行的结果是返回一个函数,所以就相当于把F1内的函数付给了f1变量,类似于这样:

var f1 = function(){
  console.log(a)    //这里的a是一个自由变量
}

 这里的a是一个自由变量,所以根据作用域链的原理,就应该去上一级作用域去找。之前说过,作用域链在定义时确定,和执行无关,那就去想上找,这个函数定义定义在F1中,所以会在F1中找a这个变量,所以这里会打印的100。

通过这种方式,我们在全局下就读取到了F1函数内部定义的变量,这就是闭包。谈到这里那就说说闭包的作用吧!

3. 🌰闭包的作用

<body>
<!--
作用:
  1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中 --- (延长了局部变量的生命周期)
     其实这是闭包作用,同时也是闭包的缺点,占用内存,所以说要及时释放闭包(方式: = null)
  2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数)

问题:
  1. 函数执行完后, 函数内部声明的局部变量是否还存在?  
    一般情况下是不存在(调用完就清除),;存在闭包时,变量才存在(延长了局部变量的生命周期);
  2. 在函数外部能直接访问函数内部的局部变量吗? 
    不能, 但我们可以通过闭包让外部操作它(栗子在上面)
-->
<script type="text/javascript">
  function fn1() {
    var a = 2;  // 局部变量
    function fn3() {
      a--;
      console.log(a);
    }
    return fn3
  }
  var f = fn1()
  f() // 1
  f() // 0
</script>
</body>

 4. 🌰闭包的两种应用场景

<body>
<!--
1. 将函数作为另一个函数的返回值;----函数作为返回值
2. 将函数作为参数传递给另一个函数调用;----函数作为参数
-->
<script type="text/javascript">
  // 1. 将函数作为另一个函数的返回值
  function fn1() {
    var a = 2
    function fn2() {
      a++
      console.log(a)
    }
    return fn2
  }
  var f = fn1()
  f() // 3
  f() // 4

  
  // 2. 将函数作为实参传递给另一个函数调用
  function F1(){
    var a = 100
    return function (){
        console.log(a)
    }
  }
  var f1 = F1()

  function F2(fn){
    var a = 200
    fn()
  }
  F2(f1)  // 100
</script>
</body>

 5.  🌰闭包的生命周期

<body>
<!--
1. 产生: 在嵌套内部函数定义执行完时就产生了(不是在调用)
2. 死亡: 在嵌套的内部函数成为垃圾对象时
-->
<script type="text/javascript">
  function fn1() {
    //此时闭包就已经产生了(由于函数提升, 内部函数对象已经创建了)
    var a = 2
    function fn2 () {
      a++
      console.log(a)
    }
    return fn2
  }
  var f = fn1()

  f() // 3
  f() // 4
  f = null //闭包死亡(包含闭包的函数对象成为垃圾对象)
</script>
</body>

好了,就到这里,我这只是冰山一角,一点皮毛而已,如果哪里有问题,希望各位大佬指出DayDayUp!!!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值