你不知道的javascript笔记(一)—— 作用域和闭包

1. 作用域闭包

1.1 作用域是什么

1.1.1 编译原理过程

js 的代码编译发生在执行前,过程如下:

  • 词法分析

    将代码字符串分解成有意义的代码块

  • 语法分析

    将上一步的代码块转换成一个由元素逐级嵌套所组成的 AST。

  • 代码生成

    将 AST 转换为可执行的代码(机器指令)的过程。

1.1.2 RHS 和 LHS

RHS 表示查找某个变量的值。LHS 指找到变量容器本身,对其赋值。

RHS 和 LHS 在查找时,如果在当前作用域没有找到目标就会继续向上进行查找,依次类推。

如果 RHS 最后没有找到变量的值,js 引擎就会抛出 ReferenceError 异常。

在非严格模式下,如果 LHS 没有找到容器的定义,js 引擎会在全局作用域下自动创建这个容器。

但是在非严格模式下,引擎会也会抛出 ReferenceError 异常。

如果你对 RHS 的值进行了不适当操作,引擎会抛出 TypeError 异常。

1.2 词法作用域

词法作用域定义在词法阶段。

作用域查找会在找到第一个匹配符时停止。多层嵌套的同名标识符,内部的标识符会“遮蔽”外部。

欺骗词法的方式:

  • eval

动态的生成代码

  • with

快速访问对象内部属性

词法欺骗缺陷:会导致编译阶段进行的静态优化无效,降低性能。

1.3 函数作用域和块作用域

属于这个函数的全部变量都可以在整个函数的范围内使用。

1.3.1隐藏内部实现

这些变量都绑定在新创建的函数的作用域中。

规避命名冲突

  • 全局命名空间

命名一个独特的对象在顶级词法作用域中,对象中封装具体的信息。

  • 模块化管理

模块化管理工具。

1.3.2 函数作用域

函数声明和表达式:function 出现在第一个次就是函数声明,否者就是函数表达式。

具名和匿名

匿名函数

优势就是书写快捷,但是缺点很多:

  1. 错误栈中无法获取函数名称,难以调试
  2. 没有函数名的情况下,只能通过过期的 arguments.callee 进行自身引用
  3. 缺少了函数名称,对于函数的可读性下降
立即执行函数

使用()来包裹函数,让它成为一个函数表达式,紧接着在后面直接调用。

UMD 模块管理中,就是将要运行的代码倒置在参数中,然后在立即执行函数中运行。

1.3.4 块作用域

  • with

在 with 中访问的对象属性仅存在与当前作用中。

  • try catch

catch 中会单独形成一个块作用域

  • let/const

会自动将声明的变量隐式的劫持了所在的块作用域。而且不会存在变量提升问题。

可以通过 let 解决 for 循环中无法获得及时变量的问题,此外还可以通过 let 劫持块作用域来释放已经使用完的数据内存。

函数作用域和块作用域都是一样的,任何声明在作用域中的变量,都将附属于这个作用域。

1.4 提升

  • var

通过 var 定义变量时会先在编译阶段声明,然后执行的时候才会代码原位置进行复制。

每个作用域都会进行提升操作。

  • 函数

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

foo()
var foo = function bar(){}
复制代码

这里变量 foo 会被提升,然后函数 bar 不会被提升,所以报错是 Uncaught TypeError: foo is not a function

  • 函数优先

函数会优先于变量提升。

一个普通块内部的函数声明通常会被提升到作用域顶部。

foo()
if (true) {
    function foo () {
        console.log(1)
    }
} else {
    function foo () {
        console.log(2)
    }
}
复制代码

上面的代码,现在在最新 chrome 中执行已经不会被提升了,最后报错依然为 Uncaught TypeError: foo is not a function。可见在编译阶段,js 引擎判断到 if else 之后并没有对函数声明进行提升。

1.5 作用域闭包

闭包使得函数可以继续访问定义时的词法作用域。它会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

循环问题

  • 可以使用立即执行函数在循环时创造一个实时的作用域,并且作用域内部保存下当时循环中的值, 就是利用闭包访问循环中的即时值
for (var i = 0; i < 10; i++) {
    (function(j) {
        // 新建作用域
        setTimeout(() => {
            // 访问从闭包中获取 j
            console.log(j)
        })
    })(i)
}
复制代码
  • 每次迭代时,创建一个块作用域进行方法
for (let i = 0; i < 10; i++) {
    setTimeout(() => {
        console.log(j)
    })
}
复制代码

模块

模块模式的两个必要条件:

  • 必须要有外部的封闭函数,且至少被调用一次
  • 封闭函数必须至少返回一个内部函数,这样外部才可以访问到内部的作用域,也就形成了闭包。

现代模块模式

就是在外层加上一个有好的包装工具,内部不会发生任何改变。

const myModule = (function Manager() {
    const modules = {}
    function define (name, dep_names, impl) {
        //获取所需依赖
        const deps = dep_names.map((dep_name) => {
            return modules[dep_name]
        })
        // 将依赖交给模块
        modules[name] = impl.apply(impl, deps)
    }
    function get (name) {
        return modules[name]
    }
    return {
      define,
      get
    }
})()
复制代码

ES6 中的模块

ES6 中不能定义 “行内” 模块,必须在独立文件中定义。

转载于:https://juejin.im/post/5cfc638a6fb9a07ed74070f8

你可能感兴趣的:(你不知道的javascript笔记(一)—— 作用域和闭包)