首先,语言分为静态作用域和动态作用域两大类,静态作用域底下的实现是词法作用域。然后又分为全局作用域,块级作用域和函数作用域。
JavaScript用的是词法作用域。意思就是函数的外层作用域不是函数执行的地方作为外部作用域,而是函数声明的地方作为外部作用域。就是字面意思,做个不恰当的比喻,词法作用域是如果把代码比作词的话就是代码写在哪,哪就是外部作用域。
然后块级作用域是 es6 新出的,在大括号内会被视为块级作用域,实际上是由 let 和 const 这两个新的声明变量的关键字实现的。块级作用域外的无法访问块级作用域内声明的变量。一定程度上代替了 iife 解决变量污染的问题,例如 for 循环结束了,但 变量 i 依然存在着。
静态作用域是根据函数声明的位置决定外部所在作用域。他们的反面就是动态作用域,是将函数执行的地方作为外部作用域。
清楚静态作用域和动态作用域的概念后,介绍一下作用域链的形成。假设场景是浏览器,是在 window 内,所以全局作用域是window,也是最外层的作用域。JavaScript代码解析的时候所扫描的最外层。假如这时候收集声明的函数 b,这里收集到的函数 b 处在全局作用域下。然后b调用的时候会扫描内部,再生成 b 的函数作用域,收集里面的变量或函数,然后根据作用域收集的东西,创建函数上下文,同时还有一个集合,里面第一个指向的是函数 b 生成的函数作用域,然后下一个指向 b 所在的外部作用域,在这个例子里就是全局作用域。这时候作用域链就产生了。作用域链就是自己生成的作用域和词法作用域下的作用域。
作用域决定了自己能访问哪些变量,作用域链决定了能访问哪些外部变量,所以在作用域生成的时候,闭包也生成了。
再继续,假如 b 里面声明了一个函数 c,然后函数 c return 出去,在全局调用。这时候函数 c 执行,首先扫描函数 c 内部,生成作用域,然后由 c 的词法作用域决定它的外部作用域的 函数 b 的作用域。然后产生一个集合 第一个位置指向 c 自己的作用域,第二个位置指向 b 的作用域,然后由于 b 的作用域链里有全局作用域,所以伪代码就是 c.b.window 顺着作用域链拿到了全局作用域。
顺着作用域链也没找到变量就会报错。
这样来看 JavaScript 并不会是动态作用域,而是使用静态作用域。但是 JavaScript 中有 上下文和上下文栈,有this,谁调用函数, this 就是谁。默认this 是全局作用域。我们可以通过改绑 this 实现访问不是在作用域链上的变量。例如全局声明的函数 a 理论上他只能拿得到全局作用域的变量。如果在函数 b 内执行 a.call({ count: 1}),就实现了 a 读取非作用域链上的变量。
再描述一次动态作用域和静态作用域,静态作用域是 函数声明的地方作为外部作用域。 动态作用域是函数执行的地方作为外部作用域。
我们可以将 b 作用域内的变量,保存在一个对象里,然后将 a 的 this 改绑成这个对象。就实现了 a 可以得到 执行 a 的地方的作用域内的变量。
当然作为参数传进 a 也是可以的。
上下文的生成一部分是依据作用域,一部分是依据上下文栈,在上下文栈中,我们可以根据函数调用顺序,得到一部分前面的上下文传递进来的参数。
上下文和作用域使得JavaScript同时实现了动态作用域和静态作用域。
// 全局作用域
function a(k) {
// a 作用域内
this.name
this.age
}
function b () {
// b作用域内
let name = 'hello'
let age = 12
let k = 0
let _this = { name, age }
function c() {
// c作用域内
}
a.call(_this)
a(k)
return c
}
let c = b()
c()