你不知道的JavaScript笔记

163 阅读6分钟

作用域是什么

传统编译语言过程:

  1. 分词/词法分析 将字符生成字符串分解为有意义的代码块

  2. 解析/语法分析

    将词法单元数组转换成由元素逐级嵌套的树结构数据-----抽象语法树(AST)

  3. 代码生成

    将AST语法树转换成机器指令

但是JavaScript引擎要更为复杂,因为在语法分析和代码生成阶段还要对代码进行优化,冗余元素进行优化。而且给予JavaScript引擎优化的时间很短,毕竟是在每次执行前进行编译。

编译器

引擎在执行编译器的代码时,会进行对变量的查找 分为以下两种查找方式

  • LHS(左查询)
  • RHS(右查询)

左查询代表着对于变量的重新赋值

右查询代表着对于值的引用

为什么需要区分左右查询?

在非严格模式下

当在一个作用域中进行右查询 如果没有查到就会报出“ReferenceError”的异常

如果使用左查询时没有找到该变量,就会向上级作用域中查找,如果一直到全局作用域中还是没有该变量 就会声明一个该名称的变量并返还给当前调用的作用域。

词法作用域

作用域主要有两种工作模型

  • 词法作用域 (绝大多数编程语言所采用 普遍)
  • 动态作用域 (较小众编程语言仍在使用)

JavaScript属于词法作用域 ,在你写代码时将变量和作用域写在哪里来决定的。

欺骗词法

因为词法作用域是写代码期间声明位置所定义的,当在运行时修改作用域。即为欺骗词法作用域

平时不建议使用

主要有以下两种方法

  • eval()

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

    而且使用修改作用域的操作会导致JavaScript编译优化失效,因为他不知道你会修改成什么样子。

  • with()

    通常当做重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身

    var obj={
        a:1,
        b:2,
        c:3,
    }
    // 单调乏味的重复obj
    obj.a=2;
    obj.b=3;
    obj.c=4;
    // 简单快捷的方法
    with(obj){
        a:3;
        b:4
        c:5;
    }
    

    例如下面:

    function foo(obj){
        with(obj){
            a:1
        }
    }
    var o1={
        a:2
    }
    var o2={
        b:3
    }
    foo(o1)
    console.log(o1.a) // 1
    foo(o2)
    console.log(o2.a) // undefined
    console.log(a) // 1
    // 泄漏到全局变量中了 因为上面使用了 左查询
    

    两者的区别: eval函数接收的参数是更改其所处的作用域,with函数 是为传递的参数声明一个全新的作用域。

性能

使用eval和with函数会严重影响性能,因为代码在编译时会进行底层优化,如果你的代码中更改了作用域会导致编译器无法知道变量的作用域,就会导致运行时间长。

函数作用域和块作用域

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

隐藏内部实现

就是为一个代码块增加函数声明,这样可以从原来作用域中重新圈起来。也叫最小授权或最小暴露原则。

主要作用就是为了不暴露变量,只有全局用到的才写在外面。还可以规避冲突,相同名字的变量。

函数作用域

使用函数声明定义作用域的好处是减少变量污染,但是声明函数时,就已经在全局作用域中声明了一个变量。也能严格的说这个函数名已经污染了作用域了。可以使用立即执行函数来解决。

函数式声明和表达式声明的区别就是看function是不是在开头。像立即执行函数就是表达式声明的。

匿名和具名

匿名函数虽然写起来很方便 但是具有以下几点缺点

  1. 匿名函数在这追踪栈中并不会显示具有意义的名称,使得调试变得困难。
  2. 如果没有函数名想要使用自身函数进行再次调用就只能使用arguments.callee 例如在递归中的使用。
  3. 匿名函数缺少可读性。

所以建议尽量为函数指定名称。

立即执行函数表达式(IIFE)

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

立即执行函数就是一个匿名函数被执行了。第一个括号将函数变成表达式,第二个括号执行了这个表达式。

另一种普遍的进阶用法是把他们当做函数调用并传递参数进去:

var a=2
(function IIFE(global){
    var a=3
    console.log(a) // 3
    console.log(global.a) // 2
})(window)
console.log(a) // 2

还有另一种方法是倒置代码的运行顺序 看个人喜好

var a =3
(function(foo){
    foo(window)
})(function foo(global){
    console.log(global.a)
})

提升

只有声明本身会被提升,而赋值或其他运算逻辑会留在原地

每个作用域中都有提升这个操作。

函数声明会被提升,但是函数表达式并不会被提升。

函数优先

当同一个作用域中同时出现函数和变量,会优先提升函数,然后才是变量。(避免同个作用域中多个相同命名的变量和函数名)

作用域闭包

function foo(){
    var a = 2;
    function bar(){
        console.log(a)
    }
    return bar
}
var bar =foo()
bar() // 2 这就是闭包的效果

foo函数执行后,在正常情况下,JavaScript的垃圾回收会自动进行回收,但是在后面将其内部的函数赋值给一个变量,然后进行调用。组织了垃圾回收,使得foo函数作用域一直存活。

闭包可以一直访问定义时的词法作用域。

循环和闭包

for(var i=0;i<=5;i++){
    setTimeout(function(){
        console.log(i)
    },1000)
}
// 结果为6次6

为什么会出现6次6呢?

因为计时器会在 循环结束后才触发,而你的i变量是一个全局变量

相当于六个计时器共享一个变量i

而当i结束时的条件是6 所以循环六次打印6 最终结果就是6个6

那么如何才能改变这一现状呢?

在for循环中增加作用域空间,隔离每次循环的i变量。这里就想到了闭包

for(var i=0;i<=5;i++){
    // 使用立即执行函数创建了个闭包环境
    (function(j){
        setTimeout(function(){
            console.log(j)
        })
    })(i)
}
// 0
// 1
// 2
// 3
// 4
// 5

以上使用es6进行改写就更简单了

只要使用let定义变量就可以省去了闭包这个环节。

模块

模块就是闭包的完美实现方式。

模块模式具备以下两个必要条件

  1. 必须有外部封闭函数,,该函数必须至少被调用一次(每次调用都会生成新的模块实例)
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,且可以访问或者修改私有的状态。

附录

this词法

箭头函数

解除了当前函数中的this,继承至上一层的词法作用域。