JavaScript 进阶 - 作用域、解构和箭头函数

114 阅读4分钟

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

1. 作用域

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

1.2 局部作用域

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

1.2.1 函数作用域

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

<script>
  // 声明 sum 函数
  function sum(x, y) {
    // 函数内部声明的变量
    const s = x + y
    console.log(s)
  }
  // 使用 sum 函数
  sum(10, 8)
  // 访问变量 s
  console.log(s)// 报错
</script>

总结:

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

1.2.2 块作用域

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

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

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

<script>
  // 常量必须要有值
  const version = '1.0.0';

  // 不能重新赋值
  // version = '1.0.1';

  // 常量值为对象类型时
  const user = {
    name: '小王',
    age: 26
  }

  // 不能重新赋值
  user = {};

  // 但属性和方法允许被修改
  user.name = '小李';
  user.gender = '男';
</script>

总结:

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

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

1.3 全局作用域

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

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

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

总结:

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

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

1.4 作用域链

作用域链本质上是底层的变量查找机制

  • 在函数被执行时,会优先查找当前函数作用域中的变量
  • 如果当前作用域查找不到则会依次逐级查找父级作用域直到全局作用域,如下代码所示:
  // 全局作用域
  let a = 1
  let b = 2

  // 局部作用域
  function f() {
    let c
    let a = 10;
    console.log(a) // 10
    console.log(d) // 报错
    
    // 局部作用域
    function g() {
      let d = 'yo'
      console.log(b) // 2
    }
    
    // 调用 g 函数
    g()
  }

  console.log(c) // 报错
  console.log(d) // 报错
  
  f()

总结:

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

1.5 垃圾回收机制

1.5.1 什么是垃圾回收机制

垃圾回收机制(Garbage Collection) 简称 GC JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。 正因为垃圾回收器的存在,许多人认为JS不用太关心内存管理的问题,但如果不了解JS的内存管理机制,会非常容易成内存泄漏(内存无法被回收)的情况

不再用到的内存,没有及时释放,就叫做内存泄漏

1.5.2 内存的生命周期

JS环境中分配的内存, 一般有如下生命周期:

  1. 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
  2. 内存使用:即读写内存,也就是使用变量、函数等
  3. 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存
  4. 说明
  • 全局变量一般不会回收(关闭页面回收);
  • 一般情况下局部变量的值, 不用了, 会被自动回收掉

1.5.3 垃圾回收机制算法分析

堆栈空间分配区别:

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

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

引用计数法

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

  1. 跟踪记录被引用的次数
  2. 如果被引用了一次,那么就记录次数1,多次引用会累加 ++
  3. 如果减少一个引用就减1 --
  4. 如果引用次数是0 ,则释放内存 问题: 嵌套引用(循环引用) ,如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄露
function fn() {
  let o1 = {}
  let o2 = {}
  o1.a = o2
  o2.a = o1
  return '引用计数无法回收'
}
fn()

标记清除法

现代的浏览器已经不再使用引用计数算法了。 现代浏览器通用的大多是基于标记清除算法的某些改进算法,总体思想都是一致的。 核心:

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

标记清除法的核心思路: 从根部周期扫描对象,能查找到的就是使用的,查找不到的就要回收

1.6 闭包

闭包:一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域 简单理解:闭包 = 内层函数 + 外层函数的变量

闭包是一种比较特殊的函数,使用闭包能够访问函数作用域中的变量。从代码形式上看闭包是一个作为返回值的函数,如下所示:

闭包 : 内层函数 + 外层函数变量

function outer() {
  let i = 0
  function inner() {
    i++
    console.log(i)
  }
  inner()
}
outer()

闭包的应用:数据私有化

    //统计函数的调用次数
    //不使用闭包的写法
    let i = 1
    function fn() {
      i++
      console.log(`函数被调用${i}次`)
    }
    // i 为全局变量,可以被更改,导致数据不准确
    //统计函数的调用次数
    //使用闭包的写法
    function outer() {
      let count = 1
      function inner() {
        count++
        console.log(`函数被调用${count}次`)
      }
      return inner
    }
    const fn = outer()
    //const fn === outer() === function fn() {}
    fn()
    fn()

闭包存在的问题:可能会造成内存泄漏

总结:

  1. 怎么理解闭包?
  • 闭包 = 内层函数 + 外层函数的变量
  1. 闭包的作用?
  • 封闭数据,实现数据私有,外部也可以访问函数内部的变量
  • 闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来
  1. 闭包可能引起的问题?
  • 内存泄漏

1.7 变量提升

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

  // 访问变量 a
  console.log(a + '个')   //undefined个

  // 声明变量 a
  var a = 10

如上,将发生变量提升:

  • 把 var 声明的变量提升到 当前作用域 的最前面
  • 只提升声明,不提升赋值

以上代码相当于:

  //声明不赋值
  var a
  //访问
  console.log(a + '个')  //undefined个
  //赋值
  var a = 10

总结:

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

注:关于变量提升的原理分析会涉及较为复杂的词法分析等知识,而开发中使用 let 可以轻松规避变量的提升

2. 函数

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

2.1 函数提升

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

  // 先调用函数
  fn()
  // 再声明函数
  function fn() {
    console.log('声明之前即被调用...')
  }

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

总结:

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

2.2 函数参数

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

2.2.1 默认值

<script>
  // 设置参数默认值
  function sayHi(name="小明", age=18) {
    document.write(`<p>大家好,我叫${name},我今年${age}岁了。</p>`);
  }
  // 调用函数
  sayHi();
  sayHi('小红');
  sayHi('小刚', 21);
</script>

总结:

  1. 声明函数时为形参赋值即为参数的默认值
  2. 如果参数未自定义默认值时,参数的默认值为 undefined
  3. 调用函数时没有传入对应实参时,参数的默认值被当做实参传入

2.2.2 动态参数

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

// 求和函数
function getSum() {
  let sum = 0
  for(let i = 0; i < arguments.length; i++) {
    sum += arguments[i]
  }
  return sum
}
// 调用求和函数
getSum(5, 10)       // 两个参数 15
getSum(1, 2, 4)     // 三个参数 7

总结:

  1. arguments 是一个伪数组
  2. arguments 的作用是动态获取函数的实参

2.2.3 剩余参数

function getSum(a,b,...arr){
  let sum = 0
  sum = a+b
  for(let i = 0;i<arr.length;i++){
    sum += arr[i]
  }
  return sum
}
getSum(1,2,8,9)  // 其中1和2分别传给a和b,剩余参数传入arr[]数组中

总结:

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

2.2.4 展开运算符

... 即为展开运算符,它可以将一个数组展开,并且不会修改原数组

展开运算的应用场景:求数组最大值(最小值)、合并数组等

 //求数组最大、最小值
let arr1 = [0, 1, 2, 3]
// ...arr1 === 0,1,2,3
let max = Math.max(...arr1)
let min = Math.min(...arr1)
console.log(max, min)  // 3 0

//合并数组
let arr2 = [4, 5, 6, 7]
let arr = [...arr1,...arr2]
console.log(arr)  // [0,1,2,3,4,5,6,7]

2.3 箭头函数(重点)

目的:引入箭头函数的目的是更简短的函数写法并且不绑定this,箭头函数的语法比函数表达式更简洁 使用场景:箭头函数更适用于那些本来需要匿名函数的地方

箭头函数是一种声明函数的简洁语法,它与普通函数并无本质的区别,差异性更多体现在语法格式上。

// 1. 箭头函数 基本语法
const fn = () => {
  console.log(123)
}
fn()
const fn = (x) => {
  console.log(x)
}
fn(1)
//相当于
// const fn = function () {
//   console.log(123)
// }
// 2. 只有一个形参的时候,可以省略小括号
  const fn = x => {
    console.log(x)
  }
  fn(1)
// 3. 只有一行代码的时候,我们可以省略大括号
const fn = x => console.log(x)
fn(1)
// 4. 只有一行代码的时候,可以省略return
const fn = x => x + x
console.log(fn(1))
 // 5. 箭头函数可以直接返回一个对象
const fn = (uname) => ({ uname: uname })
console.log(fn('刘德华'))

总结:

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

2.3.1 箭头函数参数

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

//箭头函数求和
const getSum = (...arr) => {
  let sum = 0
  for (let i = 0; i < arr.length; i++) {
      sum += arr[i]
  }
  return sum
}
console.log(getSum(1,2,3,4))

2.3.2 箭头函数 this

在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的this值。

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

// 以前this的指向:谁调用的这个函数,this 就指向谁
console.log(this)      // window

// 普通函数
function fn() {
  console.log(this)   // window
}
fn()                  // window

// 对象方法里面的this
const obj = {
  name: 'andy',
  sayHi: function () {
    console.log(this) // obj
  }
}
obj.sayHi()           // obj
// 1.箭头函数的 this 是上一层作用域的 this 指向
const fn = () => {
  console.log(this)    // window
}
fn()
//2.对象方法中的箭头函数 this
const obj1 = {
  uname: 'pink老师',
  sayHi: () => {
    console.log(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()

3. 解构赋值

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

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

3.1 数组解构

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

<script>
  // 普通的数组
  let arr = [1, 2, 3]
  // 批量声明变量 a b c 
  // 同时将数组单元值 1 2 3 依次赋值给变量 a b c
  let [a, b, c] = arr
  console.log(a); // 1
  console.log(b); // 2
  console.log(c); // 3
</script>

总结:

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

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

3.2 对象解构

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

<script>
  // 普通对象
  const user = {
    name: '小明',
    age: 18
  };
  // 批量声明变量 name age
  // 同时将数组单元值 小明  18 依次赋值给变量 name  age
  const {name, age} = user

  console.log(name) // 小明
  console.log(age) // 18
</script>

总结:

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

注:支持多维解构赋值

<body>
  <script>
    // 1. 这是后台传递过来的数据
    const msg = {
      "code": 200,
      "msg": "获取新闻列表成功",
      "data": [
        {
          "id": 1,
          "title": "5G商用自己,三大运用商收入下降",
          "count": 58
        },
        {
          "id": 2,
          "title": "国际媒体头条速览",
          "count": 56
        },
        {
          "id": 3,
          "title": "乌克兰和俄罗斯持续冲突",
          "count": 1669
        },

      ]
    }

    // 需求1: 请将以上msg对象  采用对象解构的方式 只选出  data 方面后面使用渲染页面
    // const { data } = msg
    // console.log(data)
    // 需求2: 上面msg是后台传递过来的数据,我们需要把data选出当做参数传递给 函数
    // const { data } = msg
    // msg 虽然很多属性,但是我们利用解构只要 data值
    function render({ data }) {
      // const { data } = arr
      // 我们只要 data 数据
      // 内部处理
      console.log(data)

    }
    render(msg)

    // 需求3, 为了防止msg里面的data名字混淆,要求渲染函数里面的数据名改为 myData
    function render({ data: myData }) {
      // 要求将 获取过来的 data数据 更名为 myData
      // 内部处理
      console.log(myData)

    }
    render(msg)

  </script>

4. 综合案例

4.1 forEach遍历数组

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

注意:

1.forEach 主要是遍历数组

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

<body>
  <script>
    // forEach 就是遍历  加强版的for循环  适合于遍历数组对象
    const arr = ['red', 'green', 'pink']
    const result = arr.forEach(function (item, index) {
      console.log(item)  // 数组元素 red  green pink
      console.log(index) // 索引号
    })
    // console.log(result)
  </script>
</body>

4.2 filter筛选数组

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

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

<body>
  <script>
    const arr = [10, 20, 30]
    // const newArr = arr.filter(function (item, index) {
    //   // console.log(item)
    //   // console.log(index)
    //   return item >= 20
    // })
    // 返回的符合条件的新数组

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