JavaScript 进阶 - day1

135 阅读12分钟

JavaScript 进阶 - 作用域&闭包&箭头函数&解构

学习作用域、变量提升、闭包等语言特征,加深对 JavaScript 的理解,掌握变量赋值、函数声明的简洁语法,降低代码的冗余度。

  • 理解作用域对程序执行的影响
  • 能够分析程序执行的作用域范围
  • 理解闭包本质,利用闭包创建隔离作用域
  • 了解什么变量提升及函数提升
  • 掌握箭头函数、解析剩余参数等简洁语法

作用域

目标:了解作用域对程序执行的影响及作用域链的查找机制,使用闭包函数创建隔离作用域避免全局变量污染。

作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问,作用域分为全局作用域和局部作用域。

局部作用域

局部作用域分为函数作用域块作用域

函数作用域

在函数内部声明的变量只能在函数内部被访问,外部无法直接访问。

<script>
  function getSum() {
    // 函数内部是函数作用域,属于局部变量
    const num = 10
  }
  console.log(num) //报错,外部不能使用局部作用域变量
</script>

总结

  1. 函数内部声明的变量,在函数外部无法被访问
  2. 函数的参数也是函数内部的局部变量
  3. 不同函数内部声明的变量无法互相访问
  4. 函数执行完毕函数内部的变量实际被清空了

块作用域

在 JavaScript 中使用 {} 包裹的代码称为代码块,代码块内部声明的变量外部将【有可能】无法被访问。

<script> 
  for(let t = 1; t <= 3; t++) {
    // t 只能在该代码块中被访问
    console.log(t); // 1 2 3
  }
  console.log(t); //超出了 t 的作用域,报错
</script>

JavaScript 中除了变量外还有常量,常量与变量本质的区别是常量必须要有值且不允许被重新赋值,常量值为对象时,属性方法允许重新赋值

  <script>
      // 常量值为对象类型
      const user = {
        name: '小明',
        age: 18
      }
      // 不能重新赋值
      user = {};
      // 属性和方法允许被修改
      user.name = '小小明';
      user.gender = '男';
  </script>

总结

  1. let 声明的变量会产生块作用域,var 不会产生块作用域
  2. const 声明的常量也会产生块作用域
  3. 不同代码块之间的变量无法互相访问
  4. 推荐使用 letconst

注:开发中 letconst 经常不加区分的使用,如果担心某个值会不小被修改时,则只能使用 const 声明成常量。

全局作用域

<script> 标签和 .js 文件的【最外层】就是所谓的全局作用域,在此声明的变量在函数内部也可以被访问。

<script>
  // 此处是全局
  
  function sayHi() {
    // 此处为局部
  }

  // 此处为全局
</script>

全局作用域中声明的变量,任何其它作用域都可以被访问,如下代码所示:

<script>
    // 全局变量 name
    const name = '小明'
  
   // 【函数作用域】中访问全局
    function sayHi() {
      // 此处为局部
      console.log('你好' + name)
    }

    // 全局变量 flag 和 x
    const flag = true
    let x = 10
  
   // 【块作用域】中访问全局
    if(flag) {  //flag 是全局变量
      let y = 5
      console.log(x + y) // x 是全局的
    }
</script>

总结:

  1. window 对象动态添加的属性默认也是全局的,不推荐!
  2. 函数中未使用任何关键字声明的变量为全局变量,不推荐!!!
  3. 尽可能的声明全局变量,防止全局变量被污染

JavaScript 中的作用域是程序被执行时的底层机制,了解这一机制有助于规范代码书写习惯,避免因作用域导致的语法错误。

作用域链

在解释什么是作用域链前先来看一段代码:

<script>
  // 全局作用域
  let a = 1
  let b = 2
  // 局部作用域
  function f() {
    let a =1
    // 局部作用域
    function g() {
      a = 2
      console.log(a)
    }
    g()  //2
  }
  f()  //2
</script>

函数内部允许创建新的函数,f 函数内部创建的新函数 g,会产生新的函数作用域,由此可知作用域产生了嵌套的关系。

父子关系的作用域关联在一起形成了链状的结构,作用域链的名字也由此而来。

作用域链本质上是底层变量查找机制,在函数被执行时,会优先查找当前函数作用域中查找变量,如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域

总结:

  1. 嵌套关系的作用域串联起来形成了作用域链
  2. 相同作用域链中按着从小到大的规则查找变量
  3. 子作用域能够访问父作用域,父级作用域无法访问子级作用域

JS垃圾回收机制(Garbage Collection) 简称 GC

垃圾回收机制 : JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收

正因为垃圾回收器的存在,许多人认为JS不用太关心内存管理的问题

但如果不了解JS的内存管理机制,我们同样非常容易成内存泄漏(内存无法被回收 huo 无用内存没有及时释放)的情况

内存的生命周期

  1. 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
  2. 内存使用:即读写内存,也就是使用变量、函数等
  3. 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存

说明

  1. 全局变量一般不会回收(关闭页面回收)
  2. 一般情况下局部变量的值, 不用了, 会被自动回收掉

JS垃圾回收机制-算法说明

堆栈空间分配区别:

  1. 栈(操作系统): 由操作系统自动分配释放函数的参数值、局部变量等,基本数据类型放到里面。
  2. 堆(操作系统): 一般由程序员分配释放,若程序员不释放,由垃圾回收机制回收。复杂数据类型放到里面。

两种常见的浏览器垃圾回收算法: 引用计数法标记清除法

引用计数法:“内存不再使用”,就是看一个对象是否有指向它的引用,没有引用了就回收对象

算法:

  1. 跟踪记录被引用的次数

  2. 如果被引用了一次,那么就记录次数1,多次引用会累加 ++

  3. 如果减少一个引用就减1 --

  4. 如果引用次数是0 ,则释放内存

致命的问题:嵌套引用(循环引用)

如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露

1.png

因为他们的 引用次数 永远不会是0。这样的相互引用如果说很大量的存在就会导致大量的内存泄露

现代的浏览器已经不再使用引用计数算法

标记清除法:现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。 核心:

  1. 标记清除算法将“不再使用的对象”定义为“无法达到的对象”
  2. 就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使用的。
  3. 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。

2.png

闭包

概念:一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域

简单理解:闭包 =  内层函数 + 外层函数的变量

 <script>
  //常见的写法
        function outer() {
        let a = 100
            function fn() {
                console.log(a)
            }
            return fn
        }
  //outer()  === fn === function fn(){}
  //const fun = function fn(){} 
        const fun = outer()
        fun()  //100

  //简写
        function outer(){
            let a = 100
            return function(){
                console.log(a)
            }
        }
        const fun1 = outer()
        fun1() //调用函数


// 闭包的应用 
    // 【普通形式】 统计函数调用的次数
        let i = 0
        function fn() {
            i++
            console.log(`函数被调用了${i}次`)
        } 
       //因为 i 是全局变量,容易被修改

    //【闭包形式】实现了【数据私有】无法直接修改
        function count() {
        let i = 0
            return function fn() {
                i++
                console.log(`函数被调用了${i}次`)
            }
        }
        const fun = count()
        fun()  //函数被调用了1次
        fun()  //函数被调用了2次
 </script>

闭包的作用:

  • 封闭数据,实现数据私有,外部也可以访问函数内部的变量
  • 闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来

闭包优点 : 1.延长局部变量的生命周期,2.函数外部能够访问到内部变量,3.可以避免全局污染

闭包缺点 : 1.闭包会携带包含其他的函数作用域,会更加占内存。2.内存泄漏 (不再用到的内存,没有及时释放)原因:没有及时的断开对变量的引用

变量提升

变量提升是 JavaScript 中比较“奇怪”的现象,它允许在变量声明之前 即被访问,

<script>
  // 访问变量 str
  console.log(str + 'world!');

  // 声明变量 str
  var str = 'hello ';
</script>

总结:

  1. 变量在未声明 即被访问时会报语法错误
  2. 变量在var声明之前即被访问,变量的值为 undefined
  3. let const 声明的变量不存在变量提升,推荐使用
  4. 变量提升出现在相同作用域当中
  5. 实际开发中推荐先声明再访问变量

注:关于变量提升的原理分析会涉及较为复杂的词法分析等知识,而开发中使用 let 可以轻松规避变量的提升,因此在此不做过多的探讨,有兴趣可查阅资料

函数进阶

知道函数参数默认值、动态参数、剩余参数的使用细节,提升函数应用的灵活度,知道箭头函数的语法及与普通函数的差异。

函数提升

函数提升与变量提升比较类似,是指函数在声明之前即可被调用。

<script>
  // 调用函数
  foo()
  // 声明函数
  function foo() {
    console.log('声明之前即被调用...')
  }

  // 函数表达式 不存在提升现象
  bar()  // 错误
  var bar = function () {
    console.log('函数表达式不存在提升现象...')
  }
</script>

总结:

  1. 函数提升能够使函数的声明调用更灵活
  2. 函数表达式不存在提升的现象
  3. 函数提升出现在相同作用域当中

函数参数

函数参数的使用细节,能够提升函数应用的灵活度。

动态参数

问题: 写一个求和函数,不管用户传入几个实参,都要把和求出来

arguments(动态参数): 是函数内部内置的伪数组变量,它包含了调用函数时传入的所有实参。

<script>
     function getSum(){
         let sum = 0
        // arguments 动态参数 只存在于 函数里面
        // 是伪数组 里面存储的是传递过来的实参
         for(let i =0 ; i<arguments.length ; i++){
             sum += arguments[i]
         }
         console.log(sum)
     }
     getSum(2,3,4)  //9
     getSum(1,2,2,23,4,5,6,7,8)  //58
</script>

剩余参数

问题: 写一个求和函数,不管用户传入几个实参,都要把和求出来 (不使用动态参数情况下)

<script>
   //借助 ... 获取的剩余实参,是个真数组,开发中,还是提倡多使用 剩余参数。
      function getSum(a,b, ...arr){
          console.log(arr)
      }
      getSum(2,3)  //[]
      getSum(1,2,3,4,5)  //[3, 4, 5]
</script>

总结:

  1. ... 是语法符号,置于最末函数形参之前,用于获取多余的实参
  2. 借助 ... 获取的剩余实参,是个真数组

展开运算符

展开运算符(…),将一个数组进行展开

   <script>
        const arr = [1,2,3]  // 展开运算符 可以展开数组
          console.log(...arr)  //1 2 3
          console.log(Math.max(1,2,3))  //3

          // 【1】求数组的最大值
          const arr1 = [1,2,3]
          console.log(Math.max(...arr1))  //3
          console.log(Math.min(...arr1))  //1
          // 【2】合并数组
          const arr2 = [3,4,5]
          const arr3 = [...arr1, ...arr2]
          console.log(arr3)  //[1, 2, 3, 3, 4, 5]
  </script>

剩余参数 & 展开运算符 区别

剩余参数:函数参数使用,得到真数组

展开运算符:数组中使用,数组展开

箭头函数

目的:引入箭头函数的目的是更简短的函数写法并且不绑定this,箭头函数的语法比函数表达式更简洁

使用场景:箭头函数更适用于那些本来需要匿名函数的地方

  <script>
// 【实例函数】
        const fn = function(){
            console.log(123)
        }

        //【1】箭头函数 - 基本语法
        const fn1 = () =>{
            console.log(123)
        }
        fn1()  //123

        const fn2 = (x) =>{
            console.log(x)
        }
        fn2(1)  //1

        //【2】只有一个形参,可以省略()
        const fn3 = x => {
            console.log(x)
        }
        fn3(2)  //2

        //【3】只有一行代码的时候,可以省略{}
        const fn4 = x => console.log(x) 
        fn4(3)  //3

        //【4】只有一行代码的时候,可以省略return
        const fn5 = x => x+x
        console.log(fn5(4))  //8

        //【5】箭头函数可以直接返回一个对象
        const fn6 = (uname) => ({uname:uname})
        console.log(fn6('刘德华'))  //{uname: '刘德华'}
  </script>

总结:

  1. 箭头函数属于表达式函数,因此不存在函数提升
  2. 箭头函数只有一个参数时可以省略圆括号 ()
  3. 箭头函数函数体只有一行代码时可以省略花括号 {},并自动做为返回值被返回

箭头函数参数

箭头函数中没有 arguments,只能使用 ... 动态获取实参

<body>
  <script>
    const getSum = (...arr) => {
      let sum = 0
      for (let i = 0; i < arr.length; i++) {
        sum += arr[i]
      }
      return sum
    }
    const result = getSum(234)
    console.log(result) // 9
  </script>

箭头函数 this

箭头函数不会创建自己的this,它只会从自己的作用域链的上一层沿用this。

DOM事件回调函数推荐使用箭头函数吗?

不太推荐,特别是需要用到this的时候,事件回调函数使用箭头函数时,this 为全局的 window

 <script>
   //以前this的指向:  谁调用的这个函数,this 就指向谁
        console.log(this)  // window
        // 普通函数
        function fn() {
            console.log(this)  // window
        }
        window.fn()

        // 对象方法里面的this
        const obj = {
            name: 'andy',
            sayHi: function () {
                console.log(this)  // obj
            }
        }
        obj.sayHi()

    //【箭头函数的this】是上一层作用域的this 指向
        const fn = () => {
           console.log(this)  // window
        }
        fn()

    // 对象方法箭头函数 this
        const obj1 = {
            uname: 'pink老师',
            sayHi: () => {
                console.log(this)  // this 指向谁? window
            }
        }
        obj1.sayHi()

        const obj2 = {
            uname: 'pink老师',
            sayHi: function () {
                console.log(this)  // obj
                let i = 10
                const count = () => {
                    console.log(this)  // obj 
                }
             count()
            }
        }
        obj2.sayHi()

  </script>

解构赋值

知道解构的语法及分类,使用解构简洁语法快速为变量赋值。

解构赋值是一种快速为变量赋值的简洁语法,本质上仍然是为变量赋值,分为数组解构、对象解构两大类型。

数组解构

数组解构是将数组的单元值 快速 批量赋值给一系列变量的简洁语法,如下代码所示:

<script> 
     // const arr = [100,60,80]
     // 数组解构赋值
     // const [max,min,avg] = arr
        const [max,min,avg] = [100,60,80]
        console.log(max)  //100
        console.log(avg)  //80

        //【交换2个变量的值】
        let a = 1
        let b = 2;
        [b,a] = [a,b]
        console.log(a,b)  // 2 1
        
        //【1】变量多, 单元值少 , undefined
        const [a, b, c, d] = [1, 2, 3]
        console.log(a) // 1
        console.log(b) // 2
        console.log(c) // 3
        console.log(d) // undefined

    //【2】变量少, 单元值多
        const [a1, b1] = [1, 2, 3]
        console.log(a1) // 1
        console.log(b1) // 2  
        
    //【3】剩余参数 变量少, 单元值多
        const [a2, b2, ...c2] = [1, 2, 3, 4]
        console.log(a2) // 1
        console.log(b2) // 2
        console.log(c2) // [3, 4]  真数组 
        
    //【4】防止 undefined 传递
        // const [a3 = 0, b3 = 0] = [1, 2]
        const [a3 = 0, b3 = 0] = []
        console.log(a3) // 1
        console.log(b3) // 2 
        
        
    //【5】按需导入赋值
        const [a4, b4, , d4] = [1, 2, 3, 4]
        console.log(a4) // 1
        console.log(b4) // 2
        console.log(d4) // 4

        const arr = [1, 2, [3, 4]]
        console.log(arr[0])  // 1
        console.log(arr[1])  // 2
        console.log(arr[2])  // [3,4]
        console.log(arr[2][0])  // 3
    
    // 多维数组解构
        const arr1 = [1, 2, [3, 4]]
        const [a5, b5, c5] = [1, 2, [3, 4]]
        console.log(a5) // 1
        console.log(b5) // 2
        console.log(c5) // [3,4]

        const [a6, b6, [c6, d6]] = [1, 2, [3, 4]]
        console.log(a6) // 1
        console.log(b6) // 2
        console.log(c6) // 3
        console.log(d6) // 4
</script>

总结:

  1. 赋值运算符 = 左侧的 [] 用于批量声明变量,右侧数组的单元值将被赋值给左侧的变量
  2. 变量的顺序对应数组单元值的位置依次进行赋值操作
  3. 变量的数量大于单元值数量时,多余的变量将被赋值为 undefined
  4. 变量的数量小于单元值数量时,可以通过 ... 获取剩余单元值,但只能置于最末位
  5. 允许初始化变量的默认值,且只有单元值为 undefined 时默认值才会生效

注:支持多维解构赋值,比较复杂后续有应用需求时再进一步分析

对象解构

对象解构是将对象属性和方法快速批量赋值给一系列变量的简洁语法,如下代码所示:

<script>
        const obj = {
            uname: '小明',
            age: 18
        }
        console.log(obj.uname)
        console.log(obj.age )

    //【解构的语法】 
        const { uname, age } = {uname: '小明',age: 18} 
        // 等价于 const uname =  obj.uname
        // 要求属性名和变量名必须一直才可以
        console.log(uname)
        console.log(age)

    //【.给新的变量名赋值】 旧变量名: 新变量名
        const { uname: username, age:ages } = { uname: 小明', age: 18 }
        console.log(username)
        console.log(ages)
    
    //【解构数组对象】
        const pig = [{
            uname1: '佩奇',
            age1: 6
        }]
        const [{ uname1, age1 }] = pig
        /* console.log(uname)
        console.log(age) */
        console.log(uname1,age1) //佩奇  6

    //【例子】
        const goods=[{
            goodsName:'小米',
            price:1999
        }]
        const[{goodsName, price}] = goods
        console.log(goodsName, price)   //小米  1999
</script>

总结:

  1. 赋值运算符 = 左侧的 {} 用于 批量声明变量,侧对象的属性值将被赋值给侧的变量
  2. 对象属性的值 将被赋值给与 属性名相同的变量
  3. 对象中找不到与变量名一致的属性时变量值为 undefined
  4. 允许初始化变量的默认值,属性不存在或单元值为 undefined 时默认值才会生效

注:支持多维解构赋值

<body>
  <script>
    const person = [{
            name: '佩奇',
            family: {
                mother: '猪妈妈',
                father: '猪爸爸',
                sister: '乔治'
            },
            age: 6
        }]
        const [{ name, family: { mother, father, sister } }] = person
        console.log(name)
        console.log(mother)
        console.log(father)
        console.log(sister)
    render(msg)
  </script>

综合案例

forEach遍历数组

forEach() 方法用于调用数组的每个元素,并将元素传递给回调函数

注意:

1.forEach 主要是遍历数组

2.参数当前数组元素(item)是必须要写的, 索引号(index)可选。

<body>
  <script>
   const arr = ['red','green','pink']
        const result = arr.forEach(function(item,index){
            console.log(item) //数组元素 red  green pink
            console.log(index) //索引号  0 1 2
        })
        console.log(result)  //undefined
  </script>
</body>

filter筛选数组

filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素

主要使用场景: 筛选数组符合条件的元素,并返回筛选之后元素的新数组

  <script>
    const arr = [10,20,30]
        /* const newArr = arr.filter(function(item,index){
            // console.log(item)  //10 20 30
            // console.log(index)  // 0 1 2
            return item >= 20
        }) */

        // 简写
        const newArr = arr.filter(item => item >=20)
        console.log(newArr)
  </script>

案例-商品列表价格筛选

需求:

1:渲染数据列表

2:根据选择不同条件显示不同商品

分析:

1:渲染页面 利用forEach 遍历数据里面的 数据,并渲染数据列表

(1) 初始化需要渲染页面,同时,点击不同的需求,还会重新渲染页面,所以渲染做成一个函数 (2) 做法基本跟前面案例雷同,就是封装到了一个函数里面

2:根据 filter 选择不同条件显示不同商品

(1) 点击采取事件委托方式 .filter

(2) 利用过滤函数 filter 筛选出符合条件的数据,因为生成的是一个数组,传递给渲染函数即可

(3) 筛选条件是根据点击的 data-index 来判断

(4) 可以使用对象解构,把 事件对象 解构

(5) 因为 全部区间不需要筛选,直接 把goodList渲染即可