深入理解 JS | 青训营笔记

80 阅读5分钟

本文是在学习了JS中作用域、闭包、垃圾回收机制、this指向后作出的笔记总结和相关理解。

作用域

  • 作用域(scope)规定了变量能够被访问的“范围”,离开了这个“范围”变量便不能被访问,

  • 局域作用域(声明的变量外部不能使用)

    • 函数作用域
      • 在函数内部声明的变量,只能在函数内部被访问,外部无法直接访问
      • 函数的参数也是函数内部的局部变量
      • 不同函数内部声明的变量无法互相访问
      • 函数执行完毕后,函数内部的变量实际被清空了
    • 块作用域
      • 在 JavaScript 中使用 { } 包裹的代码称为代码块,代码块内部声明的变量外部将有可能无法被访问(var声明的变量外部可以访问)
      • let、const声明的变量会产生块作用域,var声明的变量不会产生块作用域
      • 不同代码块之间的变量无法互相访问
    • 全局作用域
      • script标签内部和 .js 文件是全局作用域,在此声明的变量在函数内部也可以被访问
      • 函数中未使用任何关键字声明的变量为全局变量,不推荐!
      • 尽可能少的声明全局变量,防止全局变量被污染
  • 作用域链

    • 作用域链本质上是底层的变量查找机制
      • 在函数被执行时,会优先查找当前函数作用域中查找变量
      • 如果当前作用域查找不到则会依次逐级查找父级作用域,直到全局作用域
    • 细节:子作用域能够访问父作用域,父级作用域无法访问子级作用域
    • 小结:嵌套关系的作用域串联起来形成了作用域链,作用域链是按着从小到大的规则查找变量

垃圾回收机制GC

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

  • 内存泄漏:程序中分配的内存由于某种原因程序未释放或无法释放

    • 理解:如果不了解JS的内存管理机制,非常容易出现内存泄漏(内存无法被回收)的情况
  • 内存的生命周期

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

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

闭包Closure

  • 概念:一个函数对周围状态的引用捆绑在一起,内层函数中访问到其外层函数的作用域
  • 理解:闭包 =  内/里层函数 + 外层函数的变量
    • 函数套函数不会产生闭包的,只有当内层函数使用了外层函数的变量(子作用域能够访问父作用域),捆绑在了一起,才会产生闭包
    • 相当于打了个包,捆绑在一起;函数内的变量外面无法使用,是闭合的
  • 作用:封闭数据,提供操作,外部也可以访问函数内部的变量
    • 外部调用内部的函数
//常见的闭包写法 外部可以访问使用函数内部的变量
function outer() {
  let a = 100
  function fn() {
    console.log(a)
  }
  return fn
}
const fun = outer()
fun() // 调用函数

// 简单写法
function outer() {
  let a = 100
  return function () {
    console.log(a)
  }
}
const fun = outer()
fun() // 调用函数
  • 应用:实现数据的私有
// 统计函数调用的次数
// 普通形式
let i = 0
function fn() {
  i++
  console.log(`函数被调用了${i}次`)
}
// 因为i是全局变量,容易被修改

// 闭包形式
function count() {
  let i = 0
  function fn() {
    i++
    console.log(`函数被调用了${i}次`)
  }
  // 如果不返回fn,直接执行count(),i一直赋值为1
  return fn
}
//count函数只调用了一次,后面的fun()是调用fn函数,不会接触到let i = 0(恍然大悟)
//count返回的是fn,调用fun只会执行fn()函数,不会对i重新赋值
const fun = count()
fun()
// 可以说,实际上就是想调用fn函数,count函数起到防止变量被修改
// 标记清除法:全局const fun = count()不会回收(只有关闭会回收)
// fun调用count,count调用fn,fun指向fn,fn里用到i++,i继续被用到,i被找到了,i不会被回收
// 其实就是全局指向局部了,局部的变量不会被回收
// 闭包可能会有内存泄漏的问题:我们return了一个函数,这个函数一直被外部使用,外部是全局作用域,
// 全局指向局部,根据标记清除法,变量能被找到就不销毁,从而导致内存泄漏

this指向

  • ①普通函数
    • 普通函数的调用方式决定了this的值,即“谁调this就指向谁”
    • 类型
      • 普通函数(直接拿来调用的函数)指向window
      • 构造函数(new来调用的)指向实例化对象
      • 对象中的方法指向调用对象
      • 定时器指向window
      • 事件回调函数指向事件源
    • 普通函数没有明确调用者时this指向 window
    • 严格模式下('use strict')没有调用者时this指向undefined
// 普通函数:谁调用我,this就指向谁
console.log(this)   // window
function fn() {
  console.log(this) // window    
}
window.fn()

window.setTimeout(function () {
  console.log(this) // window 
}, 1000)

document.querySelector('button').addEventListener('click', function () {
  console.log(this) // 指向 button
})

const obj = {
  sayHi: function () {
    console.log(this)  // 指向 obj
  }
}
obj.sayHi()
  • ②箭头函数this
    • 事实上箭头函数中并不存在this,箭头函数中的this沿用上一级的this,上一级没有继续往上找,直到找到有this的地方
      • DOM事件回调函数如果里面需要DOM对象的this,则不推荐使用箭头函数
      • 基于原型的面向对象(Star.prototype.walk=()=>{ })或构造函数也不推荐采用箭头函数,因为构造函数和原型对象中的this都指向实例对象,使用箭头函数this就不指向实例对象了