深入理解JavaScript作用域与作用域链:从基础到进阶实践

164 阅读5分钟

JavaScript作用域与作用域链 是重要的一个组成部分, 是提供了一种机制,用于解析变量访问时如何在多个嵌套的作用域之间查找变量的过程。具体来说,作用域链更多关注的是变量访问的机制和如何在不同作用域层次间维护变量的可访问性,而不是变量在单一作用域内的直接效果。

作用域

概念:  作用域定义了变量、函数以及对象的可访问性范围。在JavaScript中,主要分为两种类型的作用域:

1. 全局作用域:在代码的任何地方都能被访问到的变量或函数拥有全局作用域。通常,直接在脚本文件或最外层函数外部定义的变量属于全局作用域。

2. 局部作用域 (函数作用域) :在函数内部定义的变量拥有局部作用域,只能在该函数内部访问。

3. 块级作用域(ES6引入let和const) :let和const关键字引入了块级作用域,这意味着在if语句、for循环等由{}内部代码块中声明的变量只在该块内有效,这与var声明的变量行为不同。

作用域问题还产生了变量提升的问题:变量和函数声明在作用域中的提升行为,特别是变量提升与var和let/const声明的区别,以及函数声明与函数表达式的差异。不清楚的可以看一下

作用域链

概念:  当代码在一个作用域中查找某个变量时,如果在当前作用域未找到,就会向上一级作用域继续查找,这个链条就是作用域链。它保证了函数可以访问其自身作用域以及包含它的所有上级作用域中的变量。

有作用域链产生了一些特定的情况,这里举两个简单了例子:

闭包:闭包是函数与其词法环境(作用域链)的组合。当一个函数可以访问并修改其外部作用域中的变量时,就形成了闭包。闭包常用于设计私有变量、实现异步操作的回调等,但不当使用可能导致内存泄漏。

function outer() {
 let outerVar = "I am outside!";
  function inner() {
      console.log(outerVar); *// 访问外部变量*
  }
  return inner;
}

let closureFunc = outer(); *// outer函数执行后返回inner函数*

closureFunc(); *// 输出: I am outside!*

在这个例子中,inner函数形成了一个闭包,因为它访问了outer函数的变量outerVar,即使在outer函数执行完毕后,inner函数仍然能够访问到outerVar。而outer函数并没有定义修改方法outerVar的方法因此,这样做对于内部数据的保护非常优越。

而闭包的实现也正是依赖于作用域链在本作用域内未找到该数据时会向上一级作用域继续查找的特点。因为在outer函数的内部只有inner函数可以调用到outerVar,而外部又无法访问到outer函数内部,所以outerVar便成为了inner的私密数据。

 

this指向问题:在涉及多个作用域和函数嵌套时,this关键字的指向可能会变得复杂。它通常取决于函数调用方式(直接调用、作为对象方法、构造函数等),而非定义位置,这需要特别注意。

this关键字在JavaScript中表示函数执行的上下文,其值取决于函数的调用方式,而不是函数定义的位置。this的指向在不同情况下会有不同的表现:

1. 全局作用域或非严格模式下普通函数调用:this指向全局对象(浏览器中是window,Node.js中是global)。

    function showThis() {
    
      console.log("In showThis, this === global is", this === global);
      
    //In showThis, this === global is true
    
      console.log("Value of 'this' here is:", this);

    }
    *// 直接调用showThis函数*

    showThis();

2. 对象方法中调用:this指向调用该方法的对象。

    let Person = {

      firstName"李",

      lastName"明",

      fullNamefunction() {

          console.log(this.firstName + " " + this.lastName);

      }

    };
    Person.fullName(); *// 输出: 李 明*

 

3. 构造函数中使用 new 关键字调用:this指向新创建的实例对象。

    function Person(name) {
    
      this.name=name
      
    };

    //将函数挂载到原型链上让实例直接访问

    Person.prototype.fullName=function() {

      console.log(this.name); // 输出: li ming*

      console.log(this);//person { name: '李明' }

    }



    let liming=new Person('李明')

    liming.fullName()

这里的this指向的便是指向的是实例对象本身。

 

4. 箭头函数:箭头函数没有自己的this,它会捕获其所在上下文的this值作为自己的this值。

    const person = {
      name"李明",
      regularFunctionfunction() {

        setTimeout(function() {
          console.log("Regular Function:", this.name); *// 输出: undefined 或者 Window.name 如果在浏览器环境下*
        }, 1000);
      },
      arrowFunctionfunction() {

        setTimeout(() => {

          console.log("Arrow Function:", this.name); *// 输出:* *李明*

        }, 1000);
      }
    };

    person.regularFunction(); *// 调用常规函数*
    person.arrowFunction(); *// 调用箭头函数*

这样的情况可以解释为:箭头函数没有自己的this,它会捕获其所在上下文的this值而arrowFunction中的this指向person所以箭头函数把this捕获返回this.name。而setTimeout的回调函数被调用时,它失去了与person的直接联系,因为它是被浏JavaScript运行环境(如Node.js)的事件循环机制所调用,而非直接由person对象调用。

简单来说就是

const person = {
  name"李华",

  regularFunctionfunction() {

  console.log(this.name)//输出:李华

    setTimeout(function() {

      console.log("Regular Function:", this.name); *// 输出: undefined*

    }, 1000);
  },

而通过回调函数调用的函数一般会指向全局,除非指定函数this的指向。

5. 使用 .call() , .apply() , 或 .bind() 方法:这些方法可以手动设置函数执行时的this值。

    function regularFunction(){
    
      this.name = '李明';
      
      function aaa() {

        console.log("Regular Function:", this.name);

      }

      *// 使用bind确保aaa函数内的this指向regularFunction的this*

      setTimeout(aaa.bind(this), 1000);

    }

    regularFunction(); *// 输出: Regular Function: 李明*

简单来说aaa.bind(this)确保了aaa函数的执行上下文(即其内部的this)与调用bind方法时的上下文一致。

这里函数中this的使用是非常多样的情况的,以后我们再详细叙述。,这里就先这样。文章有问题的欢迎提出指教。