精华内容
下载资源
问答
  • javascript是一门编译型语言。 传统编译语言流程分为:分词/词法分析;解析语法分析;代码生成。 分词/词法分析:将字符串代码分解为有意义的代码块。这些代码块被称为词法单元。 解析/语法分析:将词法单元流...

        javaScript是一门编译型语言。


        传统编译语言流程分为:分词/词法分析;解析语法分析;代码生成。

            分词/词法分析:将字符串代码分解为有意义的代码块。这些代码块被称为词法单元。

            解析/语法分析:将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树(抽象语法树)。

            代码生成:将抽象语法树转换为可执行的代码的过程称为代码生成。

        

        javaScript引擎:(负责整个javaScript的编译及执行)

            编译不在构建之前,而在代码执行前的几微秒(甚至更短)的时间里,javaScript使用这种方式来保证性能最佳。

        编译器:

            负责词法分析及代码生成。

        作用域:

           收集并维护所有声明的标识符(变量),确定当前执行的代码对这些标识符的访问权限。

    例:var num  = 2

        1.编译器询问作用域是否已存在变量num在当前作用域内,如果有则忽略声明,继续编译,如果没有,则作用域在当前作用域集合中声明一个变量num;

        2.编译器为引擎生成运行所需代码。引擎询问当前作用域是否存在变量num,如果有则赋值,如果没有则继续寻找。如果最总找到了则会赋值,如若最终没有生成,则抛出异常。

    总结:变量的赋值操作会执行两个操作,首先会在当前作用域中声明变量(如果之前没有声明过的话)。然后引擎在当前作用域中查找该变量,并赋值。

    左查询/右查询:

        左查询:赋值操作的目标是谁(赋值)。      右查询:谁是赋值操作的源头(取值)。

    展开全文
  • 原文: JS学习系列 01 - 编译原理作用域   在学习 javascript 的过程中,我们第一步最应该了解和掌握的就是作用域,与之相关还有程序是怎么编译的,变量是怎么查找的,js 引擎是什么,引擎和作用域的关系又是...

    原文: JS学习系列 01 - 编译原理和作用域

     

    在学习 javascript 的过程中,我们第一步最应该了解和掌握的就是作用域,与之相关还有程序是怎么编译的,变量是怎么查找的,js 引擎是什么,引擎和作用域的关系又是什么,这些是 javascript 这门语言最基础的地基,至于对象、函数、闭包、原型链、作用域链以及设计模式等等都是地基以上的建筑,只有地基打牢了,建筑才会稳。同样只有先把最基础的部分掌握了,之后的扩展学习才会更容易。

    这一节我要说的,就是作用域和编译原理,从这里开始,我会一点点的把深入学习 javascript 的过程中总结的知识点以及遇到的问题,一篇一篇的梳理出来,如果有志同道合的朋友,可以关注我这个系列,我们一起玩转 javascript。

    1. 编译原理

    大家通常把 javascript 归类为一种“动态”或“解释执行”的语言,但事实上,它是一门编译语言,但和传统的编译语言不同,它不是提前编译的,编译结果也不能进行移植。

    在传统编译语言中,程序在执行之前会经历三个步骤,统称为“编译”:

    • 分词/词法分析
      这个过程会把字符串分解成有意义的代码块,这些代码块被称为词法单元
      例如 var a = 5; 这段程序通常会被分解成下面这些词法单元: var、a、=、5、; 。空格是否会被当成词法单元取决于空格在这门语言中是否有意义。
    • 解析/语法分析
      这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。
      var a = 5; 的抽象语法树中可能如下图所示:

      抽象语法树

    • 代码生成
      将 AST 转换为可执行代码的过程被称为代码生成。这个过程与语言、目标平台等息息相关。简单来说,就是通过某种方法可以将 var a = 5; 的 AST 转化为一组机器指令,用来创建一个叫做 a 的变量(包括分配内存等),并将一个值 5 存储在 a 中。

    比起那些编译过程只有三个步骤的语言的编译器来说,javascript 引擎要复杂的多
    例如,在词法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。

    首先我们要清楚,javaScript 引擎不会有太多的时间来进行优化(相对于其它语言的编译器来说),因为与其它语言不同,javascript 的编译过程不是发生在构建之前的

    对于 javascript 来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短)的时间内。在我们将要讨论的作用域背后,javascript 引擎用尽了各种办法(比如 JIT,可以延迟编译甚至重新编译)来保证性能最佳。

    总结来说,任何 javascript 代码片段在执行前都要进行编译(预编译)。因此,javascript 编译器首先会对 var a = 5; 这段程序进行编译,然后做好执行它的准备,并且通常马上就会执行它。

    2. 三位好友

    要真正理解作用域,我们首先要知道 javascript 中有三位好朋友:

    • 引擎
      从头到尾负责整个 javascript 程序的编译及执行过程。
    • 编译器
      负责语法分析及代码生成。
    • 作用域
      负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

    当遇见 var a = 5; 这一段代码时,其实执行了两个步骤:

    (1)var a; 编译器会询问作用域是否已经有一个该名称的变量存在于同一作用域的集合中。如果是,编译器会忽略该声明,继续进行编译,否则它会要求在当前作用域的集合中声明一个新的变量,并命名为 a 。
    (2)a = 5; 编译器会为引擎生成运行时所需的代码,这些代码用来处理 a = 5; 这个赋值操作。引擎运行时会首先询问作用域,在当前作用域的集合中是否存在一个叫作 a 的变量,如果是,引擎就会使用这个变量。如果否,引擎会继续向父级作用域中查找,直到找到全局作用域,如果在全局作用域中仍没有找到 a ,那么在非严格模式下,引擎会为全局对象新建一个属性 a ,并将其赋值为5,在严格模式下,引擎会报错误 ReferenceError: a is not defined

    总结来说,变量的赋值会执行两个操作,首先编译器会在当前作用域声明一个变量(如果之前没有声明过),然后在运行时引擎会在当前作用域中查找该变量(找不到就向上一级作用域查找),如果能够找到就会对它赋值。

    3. LHS 和 RHS

    前面说到引擎在为变量赋值的时候会在作用域中查找变量,但是执行怎样的查找,用什么方式,会对最终的查找结果造成影响。

    var a = 5; 这个例子中,引擎会对 a 进行 LHS 查询,当然,另外一个查找类型叫作 RHS。

    对变量进行赋值所执行的查询叫 LHS(Left-hand Side)
    找到并使用变量值所执行的查询叫 RHS(Right-hand Side)

    举个例子:

    function foo(a) {
       // 这里隐式包含了 a = 2 这个赋值,所以对 a 进行了 LHS 查询
       var b = a;
       // 这里对 a 进行了 RHS 查询,找到 a 的值,然后对 b 进行 LHS 查询,把 2 赋值给 b
       return a + b; 
       // 这里包含了对 a 和 b 进行的 RHS 查询
    }
    
    var c = foo(2);
    // 这里首先对 foo 进行 RHS 查询,找到它是一个函数,然后对 c 进行 LHS 查询把 foo 赋值给 c 
    

    所以上面的例子共包含 3 个 LHS 查询和 4 个 RHS 查询,你们都找对了吗?

    4. 作用域嵌套

    当一个块或函数嵌套在另一个块或函数中时,就发生了作用域嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。

    举个例子:

    function foo(a) {
       console.log(a + b);
    }
    
    var b = 2;
    
    foo(2);    // 4
    

    这里对 b 进行的 RHS 查询在 foo 作用域中无法找到,但可以在上一级作用域(这个例子中就是全局作用域)中找到。

    总结来说,遍历嵌套作用域链的规则很简单:引擎从当前执行的作用域中开始查找变量,如果都找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。

     

     

    展开全文
  • 虽然作用域相关知识是 JavaScript 的基础, 但要彻底理解必须要从原理入手. 从面试角度来讲, 词法/动态作用域作用域(链)、变量/函数提升、闭包、垃圾回收 实属一类题目, 打通这几个概念并熟练掌握, 面试基本就不用...

    虽然作用域相关知识是 JavaScript 的基础, 但要彻底理解必须要从原理入手. 从面试角度来讲, 词法/动态作用域作用域(链)变量/函数提升闭包垃圾回收 实属一类题目, 打通这几个概念并熟练掌握, 面试基本就不用担心这一块了. 这篇文章是对《JavaScript 高级程序设计 (第三版)》第四章, 同样也是 《你不知道的 JavaScript (上卷)》第一部分的学习和总结.

    编译原理

    对于大部分编程语言, 编译大致有三个步骤.

    • 分词/词法分析 (Tokenizing/Lexing)

      此过程将源代码分解成 词法单元 (token), 如代码 const firstName = 'Yancey' 会被分解成 const, firstName, =, 'Yancey', 空格是否会被当成词法单元, 取决于空格对这门语言的意义. 这里推荐一个网站 Parser 可以用来解析 JavaScript 的源代码. 对于这个例子, 分词结构如下.

      [
        {
          type: 'Keyword',
          value: 'const',
        },
        {
          type: 'Identifier',
          value: 'firstName',
        },
        {
          type: 'Punctuator',
          value: '=',
        },
        {
          type: 'String',
          value: "'Yancey'",
        },
      ];
      复制代码
    • 解析/语法分析 (Parsing)

      这个过程将词法单元流转换成一棵 抽象语法树 (Abstract Syntax Tree, AST). 语法分析会根据 ECMAScript 的标准来解析成 AST, 比如你写了 const new = 'Yancey', 就会报错 Uncaught SyntaxError: Unexpected token new.

      对于上面那个例子, 生成的 AST 如下图所示, 其中 Identifier 代表着变量名, Literal 代表着变量的值.

    • 代码生成

      这个阶段就是将 AST 转换为可执行代码, 像 V8 引擎会将 JavaScript 字符串编译成二进制代码(创建变量、分配内存、将一个值存储到变量里...)

    除上面三个阶段之外, JavaScript 引擎还对 语法分析代码生成编译过程 进行一些优化, 这一块估计得看 v8 源码了, 先留个坑. 有个库叫做 Acorn, 用来解析 JavaScript 代码, 像 webpack、eslint 都有用到, 有时间可以玩一玩.

    词法作用域和动态作用域

    作用域有两种模型, 一种是 词法作用域(Lexical Scope), 另一种是 动态作用域 (Dynamic Scope).

    词法作用域是定义在词法阶段的作用域, 换句话说就是你写代码时将变量和块作用域写在哪里决定的. JavaScript 可以通过 evalwith 来改变词法作用域, 但这两种会导致引擎无法在编译时对作用域查找进行优化, 因此不要使用它们.

    而动态作用域是在运行时定义的, 最典型的就是 this 了.

    作用域

    不管是编译阶段还是运行时, 都离不开 引擎, 编译器, 作用域.

    • 引擎用来负责 JavaScript 程序的编译和执行.

    • 编译器负责语法分析、代码生成等工作.

    • 作用域用来收集并维护所有变量访问规则.

    以代码 const firstName = 'Yancey' 为例, 首先编译器遇到 const firstName, 会询问 作用域 是否已经有一个同名变量在当前作用域集合, 如果有编译器则忽略该声明, 否则它会在当前作用域的集合中声明一个新的变量并命名为 firstName.

    接着编译器会为引擎生成运行时所需的代码, 用于处理 firstName = 'Yancey' 这个赋值操作. 引擎会先询问作用域, 在当前作用域集合中是否有个变量叫 firstName. 如果有, 引擎就会使用这个变量, 否则继续往上查找.

    引擎在作用域中查找元素时有两种方式:LHSRHS. 一般来讲, LHS 是赋值阶段的查找, 而 RHS 就是纯粹查找某个变量.

    看下面这个例子.

    function foo(a) {
      var b = a;
      return a + b;
    }
    
    var c = foo(2);
    复制代码
    1. var c = foo(2); 引擎会在作用域里找是否有 foo 这个函数, 这是一次 RHS 查找, 找到之后将其赋值给变量 c, 这是一次 LHS 查找.

    2. function foo(a) { 这里将实参 2 赋值给形参 a, 所以这是一次 LHS 查找.

    3. var b = a; 这里要先找到变量 a, 所以这是一次 RHS 查找. 接着将变量 a 赋值给 b, 这是一次 LHS 查找.

    4. return a + b; 查找 ab, 所以是两次 RHS 查找.

    全局作用域

    以浏览器环境为例:

    • 最外层函数在最外层函数外面定义的变量拥有全局作用域

    • 所有末定义直接赋值的变量自动声明为拥有全局作用域

    • 所有 window 对象的属性拥有全局作用域

    const a = 1; // 全局变量
    
    // 全局函数
    function foo() {
      b = 2; // 未定义却赋初值被认为是全局变量
    
      const name = 'yancey'; // 局部变量
    
      // 局部函数
      function bar() {
        console.log(name);
      }
    }
    
    window.navigator; // window 对象的属性拥有全局作用域
    复制代码

    全局作用域的缺点很明显, 就是会污染全局命名空间, 因此很多库的源码都会使用 (function(){....})(). 此外, 模块化 (ES6、commonjs 等等) 的广泛使用也为防止污染全局命名空间提供了更好的解决方案.

    函数作用域

    函数作用域指属于这个函数的全部变量都可以在整个函数范围内使用及复用.

    function foo() {
      const name = 'Yancey';
      function sayName() {
        console.log(`Hello, ${name}`);
      }
      sayName();
    }
    
    foo(); // 'Hello, Yancey'
    
    console.log(name); // 外部无法访问到内部变量
    sayName(); // 外部无法访问到内部函数
    复制代码

    值得注意的是, if、switch、while、for 这些条件语句或者循环语句不会创建新的作用域, 虽然它也有一对 {} 包裹. 能不能访问的到内部变量取决于声明方式(var 还是 let/const)

    if (true) {
      var name = 'yancey';
      const age = 18;
    }
    
    console.log(name); // 'yancey'
    
    console.log(age); // 报错
    复制代码

    块级作用域

    我们知道 let 和 const 的出现改变了 JavaScript 没有块级作用域的情况(具体可以看高程三的第 76 页, 那个时候还没有块级作用域的概念). 关于 let 和 const 不去细说, 这两个再不懂的话... 不过后面会介绍到临时死区的概念.

    此外, try/catchcatch 分句也会创建一个块级作用域, 看下面一个例子:

    try {
      noThisFunction(); // 创造一个异常
    } catch (e) {
      console.log(e); // 可以捕获到异常
    }
    
    console.log(e); // 报错, 外部无法拿到 e
    复制代码

    提升

    在 ES6 之前的"蛮荒时代", 变量提升在面试中经常被问到, 而 let 和 const 的出现解决了变量提升问题. 但函数提升一直是存在的, 这里我们从原理入手来分析一下提升.

    变量提升

    我们回忆一下关于编译器的内容, 引擎会在解释 JavaScript 代码之前首先对其进行编译, 编译阶段的一部分工作就是找到所有的声明, 并且使用合适的作用域将它们串联起来. 换句话说, 变量和函数在内的所有声明都会在代码执行前被处理.

    因此, 对于代码 var i = 2; 而言, JavaScript 实际上会将这句代码看作 var i;i = 2, 其中第一个是在编译阶段, 第二个赋值操作会原地等待执行阶段. 换句话说, 这个过程将会把变量和函数声明放到其作用域的顶部, 这个过程就叫做提升.

    可能你会有疑问, 为什么 let 和 const 不存在变量提升呢?这是因为在编译阶段, 当遇到变量声明时, 编译器要么将它提升至作用域顶部(var 声明), 要么将它放到 临时死区(temporal dead zone, TDZ), 也就是用 let 或 const 声明的变量. 访问 TDZ 中的变量会触发运行时的错误, 只有执行过变量声明语句后, 变量才会从 TDZ 中移出, 这时才可访问.

    下面这个例子你能不能全部答对.

    typeof null; // 'object'
    
    typeof []; // 'object'
    
    typeof someStr; // 'undefined'
    
    typeof str; // Uncaught ReferenceError: str is not defined
    const str = 'Yancey';
    复制代码

    第一个, 因为 null 根本上是一个指针, 所以会返回 'object'. 深层次一点, 不同的对象在底层都表示为二进制, 在 Javascript 中二进制前三位都为 0 的会被判断为 Object 类型, null 的二进制全为 0, 自然前三位也是 0, 所以执行 typeof 时会返回 'object'.

    第二个想强调的是, typeof 判断一个引用类型的变量, 拿到的都是 'object', 因此该操作符无法正确辨别具体的类型, 如 Array 还是 RegExp.

    第三个, 当 typeof 一个 未声明 的变量, 不会报错, 而是返回 'undefined'

    第四个, str 先是存在于 TDZ, 上面说到访问 TDZ 中的变量会触发运行时的错误, 所以这段代码直接报错.

    函数提升

    函数声明和变量声明都会被提升, 但值得注意的是, 函数首先被提升, 然后才是变量.

    test();
    
    function test() {
      foo();
      bar();
      var foo = function() {
        console.log("this won't run!");
      };
      function bar() {
        console.log('this will run!');
      }
    }
    复制代码

    上面的代码会变成下面的形式: 内部的 bar 函数会被提升到顶部, 所以可以被执行到;接下来变量 foo 会被提升到顶部, 但变量无法执行, 因此执行 foo() 会报错.

    function test() {
      var foo;
      function bar() {
        console.log('this will run!');
      }
      foo();
      bar();
      foo = function() {
        console.log("this won't run!");
      };
    }
    test();
    复制代码

    闭包

    闭包是指那些能够访问独立(自由)变量的函数(变量在本地使用, 但定义在一个封闭的作用域中). 换句话说, 这些函数可以「记忆」它被创建时候的环境. -- MDN

    闭包是有权访问另一个函数作用域的函数. -- 《JavaScript 高级程序设计(第 3 版)》

    函数对象可以通过作用域链相互关联起来, 函数体内部的变量都可以保存在函数作用域内, 这种特性在计算机科学文献中称为闭包. -- 《JavaScript 权威指南(第 6 版)》

    当函数可以记住并访问所在的词法作用域时, 就产生了闭包, 即使函数是在当前词法作用域之外执行. -- 《你不知道的 JavaScript(上卷)》

    似乎最后一个解释更容易理解, 所以我们从"记住并访问"来学习闭包.

    何为"记住"

    在 JavaScript 中, 如果函数被调用过了, 并且以后不会被用到, 那么垃圾回收机制(后面会说到)就会销毁由函数创建的作用域. 我们知道, 引用类型的变量只是一个指针, 并不会把真正的值拷贝给变量, 而是把对象所在的位置传递给变量. 因此, 当函数被传递到一个还未销毁的作用域的某个变量时, 由于变量存在, 所以函数会存在, 又因为函数的存在依赖于函数所在的词法作用域, 所以函数所在的词法作用域也会存在, 这样一来, 就"记住"了该词法作用域.

    看下面这个例子. 在执行 apple 函数时, 将 output 的引用作为参数传递给了 fruit 函数的 arg, 因此在 fruit 函数执行期间, arg 是存在的, 所以 output 也是存在的, 而 output 依赖的 apple 函数产生的局部作用域也是存在. 这也就是 output 函数"记住"了 apple 函数作用域的原因.

    function apple() {
      var count = 0;
      function output() {
        console.log(count);
      }
      fruit(output);
    }
    function fruit(arg) {
      console.log('fruit');
    }
    apple(); // fruit
    复制代码

    "记住" 并 "访问"

    但上面的例子并不是完整的"闭包", 因为只是"记住"了作用域, 但没有去"访问"这个作用域. 我们稍微改造一下上面这个例子, 在 fruit 函数中执行 arg 函数, 实际就是执行 output, 并且还访问了 apple 函数中的 count 变量.

    function apple() {
      var count = 0;
      function output() {
        console.log(count);
      }
      fruit(output);
    }
    
    function fruit(arg) {
      arg(); // 这就是闭包!
    }
    
    apple(); // 0
    复制代码

    循环和闭包

    下面是一道经典的面试题. 我们希望代码输出 0 ~ 4, 每秒一次, 每次一个. 但实际上, 这段代码在运行时会以每秒一次的频率输出五次 5.

    for (var i = 0; i < 5; i++) {
      setTimeout(function timer() {
        console.log(i);
      }, i * 1000);
    }
    复制代码

    因为 setTimeout 是异步执行的, 1000 毫秒后向任务队列里添加一个任务, 只有主线程上的任务全部执行完毕才会执行任务队列里的任务, 所以当主线程 for 循环执行完之后 i 的值为 5, 而用这个时候再去任务队列中执行任务, 因此 i 全部为 5. 又因为在 for 循环中使用 var 声明的 i 是在全局作用域中, 因此 timer 函数中打印出来的 i 自然是都是 5.

    我们可以通过在迭代内使用 IIFE 来给每个迭代都生成一个新的作用域, 使得延迟函数的回调可以将新的作用域封闭在每个迭代内部, 每个迭代中都会含有一个具有正确值的变量供我们访问. 代码如下所示.

    for (var i = 0; i < 5; i++) {
      (function(j) {
        setTimeout(function timer() {
          console.log(j);
        }, j * 1000);
      })(i);
    }
    复制代码

    如果你 API 看得仔细的话,还可以写成下面的形式:

    for (var i = 0; i < 5; i++) {
        setTimeout(function(j) {
            console.log(j);
        }, i * 1000, i);
    }
    复制代码

    当然最好的方式是使用 let 声明 i, 这时候变量 i 就能作用于这个循环块, 每个迭代都会使用上一个迭代结束的值来初始化这个变量.

    for (let i = 0; i < 5; i++) {
      setTimeout(function timer() {
        console.log(i);
      }, i * 1000);
    }
    复制代码

    垃圾回收

    上面提到, 函数被调用过了, 并且以后不会被用到, 那么垃圾回收机制就会销毁由函数创建的作用域. JavaScript 有两种垃圾回收机制, 即 标记清除引用计数, 对于现代浏览器, 绝大多数都会采用 标记清除.

    标记清除

    垃圾收集器在运行的时候会给存储在内存中的所有变量加上标记, 然后它会去掉环境中变量以及被环境中的变量引用的变量的标记. 而在此之后再被加上标记的变量将被视为准备删除的变量, 原因是环境中的变量已经无法访问到这些变量了. 最后, 垃圾收集器完成内存清除工作, 销毁那些带标记的值并且回收它们所占用的内存空间.

    引用计数

    引用计数是跟踪记录每个值被引用的次数. 当声明了一个变量并将一个引用类型值赋给该变量时, 这个值得引用次数就是 1;相反, 如果包含对这个值引用的变量又取得了另外一个值, 则这个值得引用次数减 1;下次运行垃圾回收器时就可以释放那些引用次数为 0 的值所占用的内存. 缺点:循环引用会导致引用次数永远不为 0.

    总结

    Q: 什么是作用域?

    A: 作用域是根据名称查找变量的一套规则.

    Q: 什么是作用域链?

    A: 当一个块或函数嵌套在另一个块或另一个函数中时, 就发生了作用域嵌套. 因此, 在当前作用域下找不到某个变量时, 会往外层嵌套的作用域继续查找, 直到找到该变量或抵达全局作用域, 如果在全局作用域中还没找到就会报错. 这种逐级向上查找的模式就是作用域链.

    Q: 什么是闭包?

    A: 当函数可以记住并访问所在的词法作用域时, 就产生了闭包, 即使函数是在当前词法作用域之外执行.

    最后

    导致这篇文章写这么长的根本原因就是 面试 该死的 var 关键字! 它就是一个设计错误!不要去用它!

    以一道笔试题收尾:写一个函数, 第一次调用返回 0, 之后每次调用返回比之前大 1. 这道题不难, 主要是在考察闭包和立即执行函数. 我写的答案如下, 如果你有更好的方案请在评论区分享.

    const add = (() => {
      let num = 0;
      return () => num++;
    })();
    复制代码

    参考

    《JavaScript 高级程序设计 (第三版)》 —— Nicholas C. Zakas

    《深入理解 ES6》 —— Nicholas C. Zakas

    《你不知道的 JavaScript (上卷)》—— Kyle Simpson

    javascript 的词法作用域

    《JavaScript 闯关记》之作用域和闭包

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

    JavaScript 编译原理、编译器、引擎及作用域

    作用域闭包, 你真的懂了吗?


    欢迎关注我的公众号:进击的前端

    展开全文
  • 1.由闭包引出作用域 2.理解作用域的实现机制 3.作用域作用域,就有了作用域链 1.由闭包引出作用域 闭包:一个绑定了执行环境的函数...2-2 结合 JS 编译原理理解作用域实现机制 JavaScript 属于解析型语言,其执行分为

    1.由闭包引出作用域

    2.理解作用域的实现机制

    3.作用域套作用域,就有了作用域链

    1.由闭包引出作用域

    闭包:一个绑定了执行环境的函数。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。(引出作用域、作用域链等js核心知识点)

    2.理解作用域的实现机制

    2-1 什么是作用域?

    作用域是一套约定了如何存储变量、访问变量的规则。更多时候指的是在这个规则约束下,一个变量、函数、标识符可以被访问的区域。

    2-2 结合 JS 编译原理理解作用域实现机制

    JavaScript 属于解析型语言,其执行分为两个阶段:

    解释阶段:

    • 词法分析
    • 语法分析
    • 作用域规则确定

    执行阶段:

    • 编译代码得到执行上下文和可执行代码
    • 执行函数代码
    • 垃圾回收
    var name = 'wayliu'
    
    • var name (编译代码时处理) ,使用undefined对其初始化
    • name = ‘waylilu’ (执行阶段时处理)

    编译代码:先找遍当前作用域,看看是不是有name,有就忽略var name 的声明,没有则在作用域里新增一个name。
    执行代码:找遍当前作用域,看看是不是有name,有就给name赋值,如果没有则找上级的作用域,如果最终没有找到则抛出异常。

    3.作用域套作用域,就有了作用域链

    3-1 三种作用域
    • 全局作用域
    • 函数作用域
    • 块级作用域
    {
    	let a =10;
    	var b = 1;
    }
    a   // ReferenceError: a is not defined
    b   //  1
    

    关于块级作用域,需要在块内用let、const声明的变量才只在代码块内有效。

    作用域链

    当一个块或者一个函数嵌套在另一个块或者函数中时,就发生了作用域嵌套,先找遍当前作用域,如果没有就往上级作用域查找,在这个过程中,层层递进的作用域就形成了一条作用域链。

    展开全文
  • //Uncaught ReferenceError: name is not defined 可以看到块级作用域中的变量,出了那个代码块,就找不到了 其实上面的三种情况,结合JS编译原理作用域链向外不向内查找,思考一下,也不难理解 作用域链 回到...
  • 执行全局上下文1 创建 GO对象2 寻找变量声明3 寻找函数声明4 执行练习题1练习题2二、函数作用域1. 对象2. [[scope]] 域三、函数作用域链四、闭包五、立即执行函数1.函数语法两种写法传参、返回值2.立即执行函数原理...
  • 什么是作用域及js编译原理

    千次阅读 2016-12-31 22:41:15
    显然,我们必须定义一套规则来操作这些变量简单的进行读取和存储,而这套规则应该就是作用域了。来看一个简单的赋值语句var a=2; 大多数人会认为这是一个声明变量并赋值而已,事实也确实如此,而要强调的是电脑中的...
  • 首先,JavaScript确实是一门编译型语言,与C等典型编译型语言相比,区别在于JavaScript的编译过程(通常)是在实际执行前进行的,而且并不会产生可移植的编译结果。
  • 编译原理 词法作用域 this 对象拷贝 不变性 存在性 遍历for of 类(混合对象) 混入 原型 反射 对象关联 行为委托 对与对象 1、作用域形成的原因:变量存储在哪?如何找到? 2、编译原理:编译: 分词-解析:AST抽象...
  • 编译原理》期末试题(三)1、描述由正规式b*(abb*)*(a| ε)定义的语言,并画出接受该语言的最简DFA。2、证明文法E → E + id | id是SLR(1)文法。3、下面是表达式和赋值语句的文法,其中and的类型是bool ? bool → ...
  • 并且JavaScript是有预编译过程的,在执行每一段脚本代码之前, 都会首先处理var关键字和function定义式(函数定义式和函数表达式)。   一、变量执行之前,会被赋为undefined p id="scope2" style="color:...
  • 编译原理》期末试题(一)、是非题(请在括号内,正确的划V,错误的划X)(每个2分,共20分)1编译程序是对高级语言程序的解释执行。(X2.—个有限状态自动机中,有且仅有一个唯一的终态。(X3.—个算符优先文法可能不存在...
  • 并且JavaScript是有预编译过程的,在执行每一段脚本代码之前, 都会首先处理var关键字和function定义式(函数定义式和函数表达式)。 一、变量执行之前,会被赋为undefined <p id="scope2" style="color:red">...
  • JS函数作用域原理

    2019-06-06 21:16:17
    本篇文章在于详细解读JavaScript的作用域,从底层原理来解释一些常见的问题,例如变量提升、隐式创建变量等问题,在和大家一起交流进步的同时,也算对自己知识掌握的记录,方便以后复习  首先,直接捡干的来,JS...
  • 首先让让我们来看看Javasript(简称JS, 不完全代表JScript)的作用域原理: JS权威指南中有一句很精辟的描述: ”JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里.”  为了接下来的知识...
  • 在 http://www.cnblogs.com/strick/p/3994209.html 有介绍说明,修正于2014.9.29
  • 编译原理笔记

    2020-05-11 22:37:31
    文章目录编译原理笔记第一章 引论1.1 语言处理器1.2 一个编译器的结构第二章 一个简单的语法制导翻译器2.7 符号表2.7.1 为每个作用域设置一个符号表2.7.2 符号表的使用第三章 词法分析3.1 词法分析器的作用3.1.1 ...
  • js作用域深层原理

    2020-04-22 16:15:09
    编译原理 编译 在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编 译”。 分词/词法分析(Tokenizing/Lexing) 这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码...
  • JavaScript 预编译原理 今天用了大量时间复习了作用域、预编译等等知识 看了很多博文,翻开了以前看过的书(好像好多书都不会讲预编译) 发现当初觉得自己学的很明白,其实还是存在一些思维误区 (很多博文具有...
  • https://blog.csdn.net/q1056843325/article/details/52951114 ...fps=1 最近急于学习JS,为了找工作。所以只能先转载了,之后闲了(找到工作)会补上。嘿嘿 转载于:https://ww...
  • JSP内置对象、基本动作指令、常见编译指令及四大作用域 (一) JSP工作原理 当一个jsp页面第一次被访问的时候,jsp将执行以下三个步骤,之后的访问速度会因为class文件已经生成而大大提高,当jsp引擎发送一个客户端...
  • 作用域貌似简单,实则复杂,由于作用域与this机制非常容易混淆,使得理解作用域原理更为重要。本文是深入理解javascript作用域系列的第一篇——内部原理  内部原理分成编译、执行、查询、嵌套和异常五个部分进行...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 554
精华内容 221
关键字:

编译原理作用域