JavaScript作用域与作用域链优化方式

目录
  • 前言
  • 内容
    • 作用域
      • 作用域的嵌套
      • 作用域的一些实现细节
    • 作用域链
    • 相关优化
  • 总结

前言

作用域和作用域链是所有JavaScript开发人员每天都要接触和应用的内容。不管是面试中的作用域链的面试考察,还是日常代码研发中变量与作用域链的构建,它的身影几乎无处不在。它就像一顶优秀厨师的厨师帽,只要我们走进厨房,我们就要将它整理好,套在头上。没有它整洁干净的戴在头上,你就不是一名好的JavaScript工程师。

其实,作为一名前端工程师,我也曾经疑惑过:基本上所有的计算机语言都具有作用域的概念,但是为何JavaScript开发人员总是对作用域这个概念执着不已?直到,我多次在编写代码过程中遇到涉及到作用域的问题后,我才渐渐了解这个问题并去仔细研究。

而这篇文章,就是想要和大家聊聊有关JavaScript作用域以及作用域链的那些事情,以及针对它们的一些我们在代码中优化小技巧。

内容

对于几乎所有的编程语言来说,最基本的功能之一,就是储存变量当中的值并且能在之后对这个值进行访问和修改。这种能力的引入,是程序的状态存在的基础。但是,能力的引入需要我们解决几个问题,例如:变量存储在哪里?以何种形式存储?需要读取和修改变量的时候,以什么方式获取到这个变量?

很明显,为了解决这些问题,我们需要一套设计良好的规则来存储变量,并且之后可以方便的找到这些变量。与此同时,整套完整规则的设计就会衍生出额外规则概念。而作用域,就是这套规则下衍生出来的概念。

作用域

我们可以把作用域理解为上面讲到的这套规则下的限定范围。作用域的职责是,在这段限定范围中根据这套设计好的规则存储所声明的变量,并且提供修改该变量的支持。在变量的访问权限安全上,作用域还承担着保护当前作用域内的变量不被外部作用域访问的权限保护作用。

通过类比,我们可以把作用域想象成一个气泡。在这个气泡里所声明的变量成员被包含在其中。每个气泡都配备有一位有原则的管家,将所有的成员管理起来,并针对他们声明的位置和要求对它们提供保护。当气泡中代码语句想要访问和修改变量成员时,管家会结合变量成员的要求关联对应访问和修改操作。

随着ECMAScript标准的不断发展和完善,JavaScript目前存在着四种作用域类型:

  • 全局作用域(Global Scope): JavaScript语言环境的最顶级作用域,在语言环境初始化时创建。
  • 模块作用域(Module Scope): 由ECMAScript模块标准(ES Module)引入,在解析ECMAScript模块时创建。
  • 函数作用域(Function Scope): 在函数声明function() {}或者() => {}时创建。
  • 块级作用域(Block Scope): 由ECMAScript2015的变量声明标识符letconst引入,在使用这两者进行变量声明时,根据最近的一对花括号{}创建。
/* 全局作用域 start,JavaScript语言环境初始化时就被创建 */
/* 模块作用域 start,作为ES Module解析和执行时被创建 */
let name = 'Wu';
{
  /* 块级作用域 start,const进行变量声明在最近的花括号{}内创建 */
  const prefix = Hardy;
  name = prefix + name;
  /* 块级作用域 end */
}
export function sayMyName(myName) {
  /* 函数作用域 start,函数声明时自动创建,初始化默认包含函数的形参变量 */
  if (!myName) {
    /* 块级作用域 start */
    const noNameAnswer = 'Sorry!';
    console.log(noNameAnswer);
    return;
    /* 块级作用域 end */
  }
  const wordPrifix = 'Hi! My Name is ';
  const answer = wordPrifix + myName + '.';
  console.log(answer);
  /* 函数作用域 end */
}
/* 模块作用域 end */
/* 全局作用域 end */

作用域的嵌套

作用域在使用上具有嵌套特征。一个作用域能够在自身内部创建一个新作用域从而形成内部和外部作用域的嵌套关系。

全局作用域作为JavaScript的初始作用域,是所有其他作用域最外层的作用域。另外,每一个ES Module都具有模块自己的顶级作用域(top-level scope),模块中的顶级作用域变量和函数都包含在这个模块顶级作用域中,而模块作用域的外部作用域是全局作用域。而函数作用域和块级作用域则相对比较灵活,可以相互嵌套。

作用域的一些实现细节

在JavaScript中,每一个函数、代码块{...}以及script脚本被运行前,都会有一个相对应的称为词法环境(Lexical Environment) 的内部关联对象被创建。

词法环境由两部分组成:

  • 环境记录(Environment Record):一个存储所有局部变量作为其属性(包括一些执行上下文信息,例如this的值)的对象。
  • 外部词法环境引用(Outer):对外部词法环境的引用,以此关联外部词法环境。

代码执行的过程中,每一个局部变量和局部函数的声明,都会作为一个属性字段被添加到环境记录中,后续对变量和函数的读取则通过对应标识符在环境记录中进行查找。

根据上面的概念,我们可以通过下面的对象结构理解词法环境:

  lexicalEnvironment = {
    environmentRecord: {
      <identifier>: <value>,
      <identifier>: <value>,
    },
    outer: <Reference to the parent lexical environment>,
  }

再来通过下面的代码例子来理解词法环境:

/*
  当前模块运行时,模块的词法环境被创建,
  moduleLexicalEnvironment = {
    environmentRecord: {
      name: <uninitialized>,
      sayName: <reference to function object>,
    },
    outer: <globalLexicalEnvironment>,
  }
*/
let name = 'Hardy';
/*
  变量声明和赋值,修改环境记录的字段属性值,
  moduleLexicalEnvironment = {
    environmentRecord: {
      name: 'Hardy',
      sayName: <reference to function object>,
    },
    outer: <globalLexicalEnvironment>,
  }
*/
function sayName(myName) {
  /*
    执行函数时,函数的词法环境被创建,
    functionLexicalEnvironment = {
      environmentRecord = {
        myName: 'Hardy',
      },
      outer: <moduleLexicalEnvironment>,
    }
  */
  /* 通过读取环境记录的对应标识符字段属性值获取myName的变量值 */
  console.log(myName);
}
sayName(); // Hardy

我们来分析下上面的代码例子:

根据声明提前的特性,变量name和函数sayName都会在模块的词法环境创建时被添加在环境记录中。但是,由于let的暂时性死区特性,变量name在自身声明和初始化赋值之前处于不可引用和未初始化状态。函数的声明则不同,除了声明提前外还会初始化函数的引用。这就是我们可以在函数执行声明语句前调用函数的原因。另外,函数的词法环境在被创建时,对应函数的参数会被初始化在环境记录中,并且会被赋值上调用函数时的所传值或者函数参数的默认值。

outer引用方面,模块词法环境moduleLexicalEnvironmentouter引用指向JavaScript最外部的全局词法环境globalLexicalEnvironment,而函数词法环境functionLexicalEnvironmentouter引用指向外部的模块词法环境moduleLexicalEnvironment

我们可以看出,词法环境是JavaScript对作用域概念的内部技术实现。它是JavaScript引擎创建一个执行上下文时,创建用来存储变量和函数声明的环境。代码执行过程中,通过它访问到存储在其内部的变量和函数。在代码执行完毕后,执行上下文会从堆栈中被销毁回收,而词法环境也会根据情况的被销毁(如果词法环境被其他外部的词法环境所引用,则不会被销毁回收,例如闭包)。

作用域链

作用域可以嵌套,嵌套在内部的作用域可以访问外部的作用域所声明的变量和函数。通过上面词法环境的介绍,我们大概清楚,作用域的这种嵌套关系是通过词法环境的外部词法环境引用outer来关联实现的。这种词法环境的外部引用的关联关系,构建了一条单向的词法环境的链条。这就是我们常说的作用域链。

本质上,作用域链是JavaScript引擎给所执行代码维护的一条词法环境链条。代码执行中对外部作用域的变量的引用,通过这一条链条进行变量的查找、读取、修改。

代码执行中对某个变量的访问大致如下:

  • 当代码要访问一个变量时,首先会搜索当前内部词法环境。如果搜索成功,就返回对一个变量值或变量引用,结束搜索。如果搜索不到,则通过outer引用继续搜索外部词法环境,以此类推,直到全局词法环境。
  • 如果在任何地方都找不到这个变量,那么在严格模式下就会报错。

根据上面的概念,我们来看看下面的例子:

let phrase = 'Hello';

function sayHello(name) {
  /*
    函数的作用域链,
    functionLexicalEnvironment{ name: 'Hardy' } ==outer==>
    moduleLexicalEnvironment{ phrase: 'Hello' } ==outer==>
    globalLexicalEnvironment

    变量name从当前functionLexicalEnvironment中查找到并获取,
    变量phrase沿作用域链查找,从moduleLexicalEnvironment中查找到并获取
  */
  console.log(`${phrase}, ${name}!`);
}
sayHello('Hardy'); // Hello, Hardy!

上面例子中,函数sayHello在内部引用了namephrase两个变量,函数被调用的执行时会创建functionLexicalEnvironment > moduleLexicalEnvironment > globalLexicalEnvironment的作用域链。

其中,变量name作为函数参数属于当前函数作用域的局部变量,变量可以直接从当前函数的词法环境functionLexicalEnvironment中查找到并返回相关信息。而变量phrase属于外部作用域中声明的变量,存储在外部的模块词法环境moduleLexicalEnvironment中。函数sayHello引用变量phrase,会首先从在自身函数词法环境functionLexicalEnvironment中进行查找,查找不到后,会沿外部词法环境引用outer找到模块词法环境moduleLexicalEnvironment,并从中继续进行变量的查找,查找到了并返回变量的相关信息。

值得注意的是console.log()是全局内置对象console上的方法,对该方法的调用需要引用console。这个变量的引用会沿作用域链一直查找到全局词法环境globalLexicalEnvironment中,从中查找到并返回相关变量信息。

变量标识符解析和引用的过程就是沿作用域链迭代查找变量是否在作用域链节点中并返回变量相关信息的过程。

相关优化

综合上面的标识符的解析过程和作用域以及作用域链的关系,我们可以了解到,变量标识符解析的性能是和变量标识符所处在作用域链中的位置是息息相关的。变量标识符所出的作用域节点越靠近整个作用域链的前端,则需要沿作用域链迭代查找的次数就越少,变量标识符解析的速度就会越快,性能就越好。

这种标识符解析性能的规律,让我们可以得出以下使用变量的优化点:

  • 对于频繁引用的外部作用域的变量,可以根据情况在当前作用域内声明赋值为局部变量后使用。
  • 减少作用域增强with语句的使用。

外部作用域变量标识符的多次引用,会造成执行过程中的标识符解析沿作用域链查找的频繁执行,这种查找在第一次解析引用时是必须的,但是后续解析引用却是重复的。将外部作用域变量通过在当前作用域内声明赋值为局部变量,可以优化后续查找的需要经过的作用域链节点个数,得到一定的性能提升。

with语句可以在当前作用域链前端临时添加一个词法环境,从而在位置构建和使用新的作用域链。但是这方式问题也很显而易见:作用域链被加长了,除了被添加到前端的词法环境中的存储的变量外,其他变量的标识符解析性能都会变差。因此,我们应该减少with语句的使用。

总结

随着JavaScript语言的发展,语言中的作用域的种类也变得丰富起来,不再局限于函数作用域作为最小变量声明范围来使用,而是可以基于更小范围的跨级作用域来管理我们的变量引用范围。变量的管理变得更加的灵活、安全。

作用域链是作用域链嵌套的结构产物,所有变量标识符的解析和引用会沿着作用域链进行查找。而词法环境,是JavaScript对于作用域的内部技术实现。深入了解词法环境后,也让我们更清楚代码在解析变量标识符时的内部执行过程。也根据这个过程,我们大概总结出了两点关于作用域和变量使用的性能优化点。

作用域的使用作为每一位JavaScript开发人员的必修课,了解得深入才能在使用它的时候不再迷茫。它就像空气,存在于JavaScript的许多地方,值得我们去好好了解。

到此这篇关于JavaScript作用域与作用域链优化方式的文章就介绍到这了,更多相关JS 作用域链优化内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • JavaScript中的执行环境和作用域链

    前言 JS 中的执行环境和作用域链是非常重要的概念,它们是 JS 引擎在处理 JS 代码的时候对变量和函数的处理方式,这两个概念的正确理解能够帮助我们更好地理解和预测代码的行为. 执行环境 执行环境定义了变量或者函数有权访问的数据集合,每一个执行环境都有一个与之关联的变量对象,该执行环境中定义的所有变量和函数都保存在这个对象中.我们无法直接访问这个对象,这个对象只是在解析器处理数据的时候使用. 我们平时说的全局变量就是在最外围的一个执行环境中定义的变量,全局执行环境根据 ECMAScript 的

  • 中高级前端必须了解的JS中的内存管理(推荐)

    前言 像C语言这样的底层语言一般都有底层的内存管理接口,比如 malloc()和free()用于分配内存和释放内存. 而对于JavaScript来说,会在创建变量(对象,字符串等)时分配内存,并且在不再使用它们时"自动"释放内存,这个自动释放内存的过程称为垃圾回收. 因为自动垃圾回收机制的存在,让大多Javascript开发者感觉他们可以不关心内存管理,所以会在一些情况下导致内存泄漏. 内存生命周期 JS 环境中分配的内存有如下声明周期: 1.内存分配:当我们申明变量.函数.对象的时候

  • JavaScript函数执行、作用域链以及内存管理详解

    目录 前言 函数执行 全局执行上下文 函数执行上下文 作用域链 内存管理 引用计数 标记清除 前言 在我们平常编写JavaScript代码的时候,难免会用到函数,函数里面会有各种变量,这些变量的作用的范围,以及在使用内存存储这些变量时,内存管理的问题,在平时编程亦或者面试时,多多少少都会遇到,所以这篇文章针对这三个问题,进行了深入的探讨. 函数执行 首先说一下JavaScript执行代码的顺序,JavaScript在执行一段可执行代码的时候,会创建一个执行上下文栈(Execution Conte

  • javascript内存管理详细解析

    介绍 低层次的语言,如C,具有低级别的内存管理命令,如:malloc()和free(),需要开发者手工释放内存.然而像javascript这样的高级语言情况则不同,对象(objects, strings 等)创建的时候分配内存,当他们不在使用的时候内存会被自动回收,这个自动回收的过程被称为垃圾回收.因为垃圾回收的存在,让javascript等高级语言开发者产生了一个错误的认识,以为可以不用关心内存管理. 内存生命周期 不管什么样的编程语言,内存的生命周期基本上是一致的. 1.分配你需要的内存 2

  • JavaScript内存管理与闭包实例详解

    目录 1. 内存管理的理解 1.1 认识内存管理 1.2 JavaScript的内存管理 2. 垃圾回收(GC) 2.1 认识垃圾回收 2.2 GC算法 – 引用计数 2.3 GC算法 – 标记清除 2.4 其他算法优化补充 3. 闭包的概念理解 3.1 JavaScript的函数式编程 3.2 定义 4. 闭包的内存流程 5. 闭包的内存泄漏 5.1 认识内存泄露 5.2 内存泄露的测试 5.3 浏览器的优化 总结 1. 内存管理的理解 1.1 认识内存管理 不管什么样的编程语言,在代码的执行

  • JavaScript内存管理介绍

    简介 低级语言,比如C,有低级的内存管理基元,想malloc(),free().另一方面,JavaScript的内存基元在变量(对象,字符串等等)创建时分配,然后在他们不再被使用时"自动"释放.后者被称为垃圾回收.这个"自动"是混淆并给JavaScript(和其他高级语言)开发者一个错觉:他们可以不用考虑内存管理. 内存生命周期 不管什么程序语言,内存生命周期基本一致: 1.分配你所需要的内存 2.使用它(读.写) 3.当它不被使用时释放   ps:和"把

  • javascript作用域和作用域链详解

    目录 一.javascript的作用域 1.全局作用域 2.局部作用域 二.javascript的作用域链 三.作用域链和优化 四.改变作用域链 1.with语法改变作用域链 2.catch语法 总结 一.javascript的作用域 1.全局作用域 1.最外层函数和最外层函数定义的变量 var age = 20 function func1() { var sex = "男" function func2() { console.log("hello func2"

  • JavaScript中的作用域链和闭包

    作用域 全局作用域 局部作用域 作用域链 执行上下文 活动对象 闭包 闭包优化 JavaScript中出现了一个以前没学过的概念--闭包.何为闭包?从表面理解即封闭的包,与作用域有关.所以,说闭包以前先说说作用域. 作用域(scope) 通常来说一段程序代码中使用的变量和函数并不总是可用的,限定其可用性的范围即作用域,作用域的使用提高了程序逻辑的局部性,增强程序的可靠性,减少名字冲突. 全局作用域(Global Scope) 在代码中任何地方都能访问到的对象拥有全局作用域,以下几种情形拥有全局作

  • 深入理解Javascript中的作用域链和闭包

    首先我们回顾下之前一篇关于介绍数组遍历的文章: 请先看上一篇中提到的for循环代码: var array = []; array.length = 10000000;//(一千万) for(var i=0,length=array.length;i<length;i++){ array[i] = 'hi'; } var t1 = +new Date(); for(var i=0,length=array.length;i<length;i++){ } var t2 = +new Date();

  • 跟我学习javascript的作用域与作用域链

    作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理.今天这篇文章对JavaScript作用域和作用域链作简单的介绍,希望能帮助大家更好的学习JavaScript. 一.JavaScript作用域 任何程序设计语言都有作用域的概念,简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期.在JavaScript中,变量的作用域有全局作用域和局部作用域两种. 1. 全局作用域(Global

  • 深入理解JavaScript作用域和作用域链

    作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理.今天这篇文章对JavaScript作用域和作用域链作简单的介绍,希望能帮助大家更好的学习JavaScript. JavaScript作用域 任何程序设计语言都有作用域的概念,简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期.在JavaScript中,变量的作用域有全局作用域和局部作用域两种. 1. 全局作用域(Global Sc

  • JavaScript作用域与作用域链深入解析

    作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理.今天这篇文章对JavaScript作用域和作用域链作简单的介绍,希望能帮助大家更好的学习JavaScript. JavaScript作用域 任何程序设计语言都有作用域的概念,简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期.在JavaScript中,变量的作用域有全局作用域和局部作用域两种. 1. 全局作用域(Global Sc

  • 结合代码图文讲解JavaScript中的作用域与作用域链

    先上三段说明作用域的代码 //==========例1========== var scope='global'; function fn(){ alert(scope); var scope='local'; alert(scope); } fn(); //输出结果? alert(scope);//输出结果? //===========例2========== var scope='global'; function fn(){ alert(scope); scope='local'; ale

  • 聊一聊JavaScript作用域和作用域链

    每种编程语言,其变量都有一定的有效范围,超过这个范围之后,变量就失效了,这就是变量的作用域.从数学的角度来看,就是自变量的域. 作用域是变量的可访问范围,即作用域控制着变量与函数的可见性和生命周期.在 JavaScript 中, 对象和函数同样也是变量,变量在声明他们的函数体以及这个函数体嵌套的任意函数体内部都是有定义的. 一.静态作用域和动态作用域 静态作用域 是指声明的作用域是根据程序正文在编译时就确定的,也称为词法作用域.大多数现代程序设计语言都是采用静态作用域规则,JavaScript就

  • 图解Javascript——作用域、作用域链、闭包

    什么是作用域? 作用域是一种规则,在代码编译阶段就确定了,规定了变量与函数的可被访问的范围.全局变量拥有全局作用域,局部变量则拥有局部作用域. js是一种没有块级作用域的语言(包括if.for等语句的花括号代码块或者单独的花括号代码块都不能形成一个局部作用域),所以js的局部作用域的形成有且只有函数的花括号内定义的代码块形成的,既函数作用域. 什么是作用域链? 作用域链是作用域规则的实现,通过作用域链的实现,变量在它的作用域内可被访问,函数在它的作用域内可被调用. 作用域链是一个只能单向访问的链

  • JavaScript闭包与作用域链实例分析

    本文实例讲述了JavaScript闭包与作用域链.分享给大家供大家参考,具体如下: 闭包定义 闭包指的是有权访问另一个函数作用域中的变量的函数.创建闭包的常见方式,就是在一个函数A内部创建另一个函数B,那么函数B就是一个闭包,可以访问函数A作用域中的所有变量. JavaScript的闭包与作用域链密不可分,因此本文可以和JavaScript的作用域链相对照分析,一定可以对JavaScript的闭包和作用域链有更深的理解. 下面我们仍然以createComparisonFunction为例进行闭包

  • 详解JavaScript作用域和作用域链

    前言 JavaScript 中有一个被称为作用域(Scope)的特性.虽然对于许多新手开发者来说,作用域的概念并不是很容易理解,本文我会尽我所能用最简单的方式来解释作用域和作用域链,希望大家有所收获! 作用域(Scope) 1. 什么是作用域 作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性.换句话说,作用域决定了代码区块中变量和其他资源的可见性.可能这两句话并不好理解,我们先来看个例子: function outFun2() { var inVariable = "内层变量2&

随机推荐