总结从js编译原理到作用域及闭包

267 阅读5分钟

js编译原理

编译大致有三个步骤

  • 分词/词法分析
  • 解析/语法分析
  • 代码生成 js引擎在语法分析和代码生成阶段进行了性能优化

作用域

作用域有词法作用域(也叫静态作用域)和动态作用域
引擎在作用域中查找元素时有两种方式:LHS 和 RHS. 一般来讲, LHS 是赋值阶段的查找, 而 RHS 就是纯粹查找某个变量.

    function foo(a) 
    { 
        var b = a; 
        return a + b; 
    } 
    var c = foo(2);

词法作用域

词法作用域就是定义在词法阶段的作用域
就是你写代码的时候将变量和块作用域决定的所以词法分析器就是处理代码时会保持作用域不变(大部分情况下)
javascript可以通过一些手段欺骗作用域eval跟with来改变词法作用域

// eval进行欺骗
function foo(str,a) {
eval(str) //欺骗
  console.log(a,b) // 1 ,3
}
var b = 2
foo('var b = 3', 1)


//with

function foo(obj) {
  with (obj) {
      a = 2
  }
}
var o1 = {
  a:3
}
var o2 = {
  b:3
}

foo(o1)
console.log(o1.a) // 输出2

foo(o2)
console.log(o2.a) // 输出undefined
console.log(a) // 输出2 lhs查询泄露到全局变量上面去了


动态作用域

就是你在哪里运行时定义,典型的例子是this

不管在哪里都离不开作用域

  1. 对于引擎而言js程序的编译和执行
  2. 编辑器辅助语法分析代码生成等工作
  3. 作用域收集并维护所以变量的访问规则

以var a = 1; 为例首先好执行 var a;在作用域里面先去查找在作用域里面有没有同名变量如果有同名变量这行代码将会忽略,没有的话就好在内存里面创建一个a的变量,然后在给a赋值为1

a = 1;时会在当前作用域下面查找有没有变量a 如果没有就会一直查找的全局变量上面去,如果全局变量上面还没有变量a 则会在全局变量上面创建变量a(容易变量污染)只限制于LHS查询,RHS查询好报错

全局作用域

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

  • 所有未声明的变量自动声明拥有全局变量(RHS查询除外)
  • 所有window对象拥有全局变量

const a = 1; // 全局变量

// 全局函数
function foo() {
  b = 2; // 未定义却赋初值被认为是全局变量

  const name = 'txp'; // 局部变量

  // 局部函数
  function bar() {
    console.log(name);
  }
}

window.navigator; // window 对象的属性拥有全局作用域

全局变量的缺点: 污染全局变量的命名规则,封装代码一般使用(function(){....})()函数

函数作用域(作用域气泡)

函数作用域也叫作用域气泡

函数作用域就是这个作用域或气泡中的使用变量都可以在函数中复用,外部无法访问到内部函数的变量

if switch while for这些不会创建新的作用域

function foo() { const name = 'Yancey'; function TXP() { console.log(Hello, ${name}); } sayName(); }

foo(); // 'Hello, Yancey'

console.log(name); // 外部无法访问到内部变量 TXP(); // 外部无法访问到内部函数

块级作用域

let和const的出现改变了javascript中没有块级作用域的情况,在es3的时候一般使用try/catch创建块级作用域但是性能不好


if(true){
  let a = 2
  console.log(a) //输出2
}
console.log(a)  //Uncaught ReferenceError: a is not defined 报错

提升

变量提升

引擎会在解释javascript之前进行预编译,其中的一部分工作就是找到所以变量的声明,变量和函数都会提升

对于代码 var a =1;时会先声明var a;在赋值 a=1 但是let 跟const不会进行变量提升


console.log(a)  //undefined
var a = 10



console.log(a) // 报错ReferenceError: Cannot access 'a' before initialization
let a = 2

函数提升

函数和变量都会进行提升,但是函数优先

函数会进行变量提升,但是函数表达式不会


foo()
function foo() {
var a;
  console.log(a) //undefined
  a=2;
}

// 会被提升为这样

function foo() {
var a;
  console.log(a) //undefined
  a=2;
}
foo()

// 函数表达式不会被提升

foo()
var foo = function bar(){}
// ....

闭包

闭包就是从一个作用域中调用另外一个作用域的变量通俗理解就是函数下面返回一个函数,被返回的函数就是闭包

闭包和循环

典型的例子就是for循环的时候调用定时器

//希望代码输出 0 ~ 4, 每秒一次, 每次一个. 但实际上, 这段代码在运行时会以每秒一次的频率输出五次 5

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

上面的例子可以把var 换成let就可以实现效果

垃圾回收机制

函数被调用了并且以后不会调用了,垃圾回收机制就会销毁函数创建的作用域

js中有2种垃圾回收机制标记清除和引数清除 目前使用的浏览器大部分都是使用的标记清除

标记清除

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

引用计数

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