JS学习笔记1

针对var a = 1;的流程分析

在执行前,编译器会做以下工作:

  • 分词:把字符串分解成多个有意义的词法单元。
    • 对var a = 1;来说,分词阶段后,这条语句会被分成 var/a/=/1/;这些词法单元。
  • 解析:用多个词法单元生成一个代表程序语法结构的树。
    • 对var a=1;来说,解析结束后,会生成一个树,这个树以VariableDeclaration为根节点,这个根节点有两个子节点,其中一个是值为a的Identifier结点,另外一个是AssignmentExpression的子节点,这个子节点下还有一个值为2的NumericLiteral的结点。
  • 代码生成
    • 编译器首先会询问作用域,是否已经有一个名称为a的变量存在于同一个作用域集合中
      • 如果已经存在了,编译器会忽略这条声明;
      • 如果不存在,编译器会要求作用域在当前作用域集合中声明一个新的变量a
    • 编译器为引擎生成a=2这条代码

引擎开始执行后,执行到a=2这行代码时,会先询问作用域中是否包含名称为a的变量(LHS查询),如果没找到会依次往上层的作用域找,直到找到或者报错。找到后,会把2赋值给这个变量。

总结下,整个编译执行流程中 编译器 作用域 引擎这三方会相互配合,编译器负责让作用域声明变量,引擎通过作用域查找变量

LHS查询和RHS查询

LHS查询指的是,引擎想作用域查找容器,然后为容器赋值。

RHS查询指的是,引擎想要获取容器中的值。

举个例子:

function foo(a){
    console.log(a);
}
foo(2);

针对上面这段代码,使用到的查询如下:

  1. foo:引擎需要向作用域进行RHS查询

  2. 作用域把foo的值(是一个函数)还给引擎

  3. a:引擎执行这个函数,引擎需要向作用域进行LHS查询(参数a赋值)

  4. 作用域返回给引擎a的容器,引擎把2赋值到a容器中

  5. console:引擎需要向作用域进行RHS查询

  6. 作用域把console的值(是一个对象)还给引擎

  7. 引擎在console的对象中查找是否有log方法

  8. a:引擎需要向作用域进行RHS查询

  9. 作用域把a的值还给引擎

    ......

相关异常

ReferenceError

如果对一个变量做RHS查询时,这个变量还没有声明过,会抛出此异常。

如果对一个变量做LHS查询时,严格模式下,也会抛出此异常;非严格模式下,作用域会自动创建一个全局变量

TypeError

拿到变量后,如果做出了一些不合理的操作,那么会抛此异常。比如:

  • 对一个非函数的值进行函数调用
  • 引用null或者undefined类型值中的属性

作用域规则-词法决定

词法作用域的意思是,作用域由书写时函数声明的位置决定。

编译的词法分析阶段其实就已经确定了标识符的位置以及如何声明的。

简单理解,上面的三大模块关注的是整体的交互过程,词法作用域模块关注的是 作用域的生成规则

在JS中,有两种方式可以在运行时修改或者影响已经决定好的作用域

  • eval:在运行时执行一段代码,如果eval包含声明语句并且是非严格模式,那么eval所在位置的作用域就会被改变。

    function foo(str,a){
        eval(str);
        console.log(a,b);
    }
    foo("var b=3;",1); //1,3
    

    eval所处的foo作用域中,在运行时被增加了b的声明,所以可以打印出1和3的结果。

    在严格模式下,eval有自己独立的作用域,不会影响所在的作用域。

  • with:重复引用同一个对象的多个属性的快捷方式,with把传入的obj当作一个作用域,如果不存在某个属性,就会向上查找,在非严格模式下,会创建全局变量。

    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 这里创建了全局变量
    

这两种方式都不推荐使用,因为它们会被严格模式限制,并且会有性能问题。

作用域规则-函数作用域和块作用域

在上一个模块词法规则中介绍了作用域是由声明的位置决定。

具体来说,声明一个函数就会创建一个新的作用域,这种作用域叫做函数作用域。

函数作用域

在函数作用域中声明的变量只能在函数内部或者嵌套内部函数中被访问到,外部无法直接访问到(特例:闭包可以做到)。

为什么需要把变量和函数隐藏在函数作用域内部?

  • 最小特权原则:在软件设计中应最小限度的暴露必要内容。
  • 规避变量冲突

但是将部分变量和函数封装为一个新的函数,其实还是会在全局作用域中引入一个新的标识符,并且需要通过这个标识符去调用这部分被封装好的函数,在具体实践中,我们可以使用IIFE立即执行表达式的方式来避免引入新的标识符,并且简化调用。

IIFE

var a=2;

(function foo(){
    var a = 3;
    console.log(a);
})();//IIFE

console.log(a);

除了函数作用域以外,还可以声明代码块的作用域,叫做块作用域。

块作用域

在块级作用域中声明的变量只能在块内部以及嵌套块或者函数中被访问到,块外部无法访问。

块作用域的实现方式有下面几种:

  • try/catch:catch部分会创建一个块级作用域,其中声明的变量仅在块中有效。
  • let:用let关键字来声明变量,它会去找最近的一个块,并把这个块当作作用域;或者显示的创建一个块来决定作用域(推荐这种做法)
    • 注意:let块作用域中的声明的let变量不会被自动提升!
    • for循环中定义的变量应该使用let;大对象应该用let声明,以便促使垃圾回收。
  • const:const声明的是变量在赋值后不允许再修改。

提升

函数声明和变量声明都会被提升到作用域的顶部,注意,只是声明被提升了,但是表达式没有被提升

函数声明的优先级更高,所以函数声明在前,var变量在后,那么var会被忽略掉。

代码例子如下:

foo();

var foo = function(){
    console.log("2");
};

function foo(){
    console.log("1");
}

这个代码等同于

function foo(){
    console.log("1");
}

foo();

foo = function(){
    console.log("2");
};

多个相同标识符的函数声明,后面的会覆盖前面的。

代码例子如下:

foo();

function foo(){
     console.log("1");
}
function foo(){
     console.log("2");
}

代码等同于

function foo(){
     console.log("2");
}

foo();

闭包

闭包是什么?

闭包是什么?闭包是一个引用,某个作用域中的函数通过某种方式溢出到外部,当其在外部被直接调用时,通过这个引用就可以获取作用域的变量。

实现闭包

如何实现闭包?核心是通过把 作用域中的内部函数通过值传递到外部 来实现。

通过返回值传递

看下面代码示例:

function foo(){
    var a = 2;
    
    function bar(){
        console.log(a);
    }
    return bar;
}

var baz = foo();
baz();//打印2

通过参数值传递

看下面代码示例:

function foo(){
    var a = 2;
    
    function bar(){
        console.log(a); 
    }
    
    baz(bar);
}

function baz(fn){
    fn();
}

foo();//通过调用foo,把bar传递到了baz中,baz实际上是在全局作用域中

通过变量传递

看下面代码示例:

var fn;

function foo(){
    var a = 2;
    
    function bar(){
        console.log(a); 
    }
    
    fn = bar;
}

foo();

fn();//闭包

闭包的应用

闭包在代码中无处不在。只要使用了回调函数,就会产生闭包。

闭包也可以用来实现模块,闭包中可以返回模块公共的API,借此访问内部作用域中的数据。

小问题

需求:希望分别输出数字1到5,每秒一次,每次一个。

下面的代码是否可以正确实现需求?

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

答案:不可以。结果实际上会每秒一次的频率输出5个6。

原因是,JS是单线程的,执行完循环之后才会开始执行timer任务,执行完循环后i的值为6,每一个timer看到的作用域中i都为6,所以输出的都是6。

那么如何修改呢?

第一种方式,使用IIFE。IIFE会通过声明并立即执行一个函数来创建作用域。

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

第二种方式,使用let块作用域。

for(var i = 1;i<=5;i++){
    let j = i;
     setTimeout(function timer(){
     	console.log(j);
     },j*1000);
}
for(let j = 1;j<=5;j++){
     setTimeout(function timer(){
     	console.log(j);
     },j*1000);
}
posted @ 2020-12-21 16:54  Ging  阅读(103)  评论(0编辑  收藏  举报