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 出现在第一个次就是函数声明,否者就是函数表达式。
具名和匿名
匿名函数
优势就是书写快捷,但是缺点很多:
- 错误栈中无法获取函数名称,难以调试
- 没有函数名的情况下,只能通过过期的 arguments.callee 进行自身引用
- 缺少了函数名称,对于函数的可读性下降
立即执行函数
使用()来包裹函数,让它成为一个函数表达式,紧接着在后面直接调用。
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 中不能定义 “行内” 模块,必须在独立文件中定义。