js基础之-作用域/this 指针/闭包

92 阅读6分钟

闭包是前端绕不开的话题。实际上我们可能在不经意间使用了很多次闭包,但是印象里只留下了节流和防抖函数。

深入了解闭包之前我们需要了解一个概念 —— 作用域

作用域简单理解可以说是变量的有效范围。

作用域分为:

  • 全局作用域:全局作用域是指在程序中最外层定义的变量或函数所处的作用域,它们在整个代码中都是可访问的,从声明开始直到程序结束都存在。在 JavaScript 中,全局作用域中的变量会挂载在全局对象上(浏览器中是 window,Node.js 中是 global)。在非严格模式下,在函数中直接声明的变量也可以在全局中访问(不用var let 声明)

    // 这是全局变量
    var globalVar = "I am global";
    
    function showGlobal() {
      globalNonVar = "I am global non-var"; // 👈 这是全局变量
      console.log(globalVar); // ✅ 可以访问全局变量
    }
    
    showGlobal();
    
    console.log(globalVar); // ✅ 依然可以访问
    console.log(globalNonVar); // ✅ 可以访问全局变量
    
  • 函数作用域:在一个函数内部声明的变量(使用 varletconst),只能在该函数内部访问,在函数外部是无法访问的。使用 var 定义的变量没有块级作用域,会被函数作用域提升,即在声明之前可以被访问到,只不过值为undefined

    function testFunctionScope() {
      console.log(innerVar) // 可以访问到,只不过值为undefined
      var innerVar = "I am inside the function";
      console.log(innerVar); // ✅ 可以访问函数内部变量
    }
    
    testFunctionScope();
    
    console.log(innerVar); // ❌ 报错:innerVar is not defined
    
  • 块级作用域:块级作用域是编程语言中一个重要的概念,指的是在代码块(通常由花括号 {} 包围的区域)内部声明的变量只能在该代码块内部访问,而在代码块外部无法访问这些变量。在 JavaScript 中,letconst 关键字声明的变量具有块级作用域,而 var 声明的变量则是函数作用域。

    {
      let blockLet = "I am in a block";
      const blockConst = "I am also in a block";
    
      console.log(blockLet);   // ✅ 正常访问
      console.log(blockConst); // ✅ 正常访问
    }
    
    console.log(blockLet);   // ❌ 报错:blockLet is not defined
    console.log(blockConst); // ❌ 报错:blockConst is not defined
    

作用域链

作用域链 是一种查找变量的机制,当代码在某个作用域中访问一个变量时,JavaScript 引擎会:

  1. 先从当前作用域查找该变量;
  2. 如果找不到,就向上层作用域查找;
  3. 一直查找到全局作用域为止;
  4. 如果仍找不到,就报错(ReferenceError)。

这个“层层向上查找”的过程,就是所谓的 作用域链

需要注意的是⚠️

  • 作用域链基于词法结构(定义时)而非调用位置!

  • 也就是说,变量查找的链条是根据“函数在哪里定义”决定的,而不是“在哪里调用”。

闭包的本质,就是函数“记住”了它定义时的作用域链,即使函数在定义所在作用域之外执行,也能访问到原来的变量。

function outer() {
  let a = 10;

  return function inner() {
    console.log(a); // 🔥 访问 outer 的变量 a
  };
}

const fn = outer(); // outer 执行完毕,但 a 没有被销毁
fn(); // 输出:10

函数在执行时,会创建一个函数作用域,其中的变量只在其作用域内生效,在作用域外是无法访问的。在函数执行结束的时候,里面的局部变量应该就会被垃圾回收机制回收掉。但在闭包中,在函数作用域之外保留了对函数作用域内变量的引用,所以这个变量就不会主动被垃圾回收机制释放。

也因此,闭包有可能会造成内存泄漏,因为无法自动被垃圾回收机制回收,但是通过手动把引用置为null,即解除对变量的引用,就可以被回收了

很多人把 作用域链this 混为一谈,但它们是完全不同的机制! 作用域链基于定义时的作用域,而this的指向是调用时决定的。

因为 this 是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候

this 的绑定规则

this指向主要分为以下几种情况

  • 大多数时候,this都指向实际调用它的对象
  • 被call/apply/bind显式调用的话会指向绑定的对象
  • 如果有 new 关键字,this 指向 new 出来的实例对象,但如果构造器显式地返回一个对象,那么this是指向返回的这个对象的,注意这里返回一个普通类型的值是不影响this指向的
    let MyClass=function(){
        this.name='xiaohu'
    }
    let obj=new MyClass()
    console.log(obj.name) // xiaohu
    
     let MyClass=function(){
        this.name='xiaohu'
        return {
        name:'xiaolong'
    }
    let obj=new MyClass()
    console.log(obj.name) // `xiaolong`
    
  • 普通函数的this指向的是window 在es6中 指向undefined 因为有严格模式
  • 箭头函数体内的 this 对象就是定义时所在的对象,而不是使用时所在的对象。
    • 箭头函数的外层如果有普通函数,那么箭头函数的 this 就是外层普通函数的this
    • 箭头函数的外层如果没有普通函数,那么箭头函数的 this 就是全局变量

闭包

定义:当一个函数在其定义的作用域之外被调用时,它仍然保留对定义时作用域中变量的访问能力,这就形成了一个闭包。

创建闭包的方式

function outer() {
  let secret = "secret value";

  return function inner() {
    console.log(secret);
  };
}

const fn = outer(); // outer() 执行完毕,secret 应该被销毁
fn(); // 但 inner() 还能访问 secret,这就是闭包

闭包的使用

// 防抖 在delay时间内触发多次都会重新计时,最终delay时间后执行
const throttle = function (fn, delay) {
  let timer = null
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

// 节流 在delay时间内只会执行一次
const debounce = function (fn, delay) {
  let timer = null
  return function (...args) {
    if (timer) return
    timer = setTimeout(() => {
      fn.apply(this, args)
      timer = null
    }, delay)
  }
}

闭包的小试牛刀

function createCounter() {
  let count = 0
  return function () {
    return ++count
  }
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3


// 输出 0 到 4,每隔 1 秒打印一个数字,但不能用 let

for (var i = 0; i < 5; i++) {
  (function (j) {
    setTimeout(() => {
      console.log(j);
    }, j * 1000)
  })(i);
}
// 在迭代内使用IIFE会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。


// 实现一个 createBankAccount(initialBalance) 函数,返回对象提供以下方法:

// deposit(amount):存钱

// withdraw(amount):取钱

// getBalance():查看余额

function createBankAccount(initialBalance) {
  let balance = initialBalance

  this.deposit = function (amount) {
    balance += amount
  }
  this.withdraw = function (amount) {
    if (amount <= balance) {
      balance -= amount
    } else {
      console.log("余额不足")
    }
  }

  this.getBalance = function () {
    return balance
  }

  return this
}


const account = createBankAccount(100);
account.deposit(50);       // +50
account.withdraw(30);      // -30
console.log(account.getBalance()); // 输出 120