作用域,可以理解为一个 “变量” 与其 “被赋予的值” 所’成立’的范围。
在 JS 中,当变量在执行环境中被声明的那一刻起,它就开始存在了。 但它与它所绑定的值可以影响的区域到哪里呢? 关于这个问题,就必须讨论到作用域。
作用域分类
- 全局级作用域(Global Level Scope)
- 本地级作用域(Local Scope)
- 块级作用域(ES6)
- 静态作用域(Static Scope)
全局级作用域
JavaScript 代码在编译阶段的最初,会产生一个全局执行环境 (Global Execution Context)。 而在全局的执行环境中,会存在一个全局的变量对象(Global Variable Object)。 只要是在全局环境中声明的变量 (或者函数),就会被存放在这个 Global Variable Object 内,并且可在整个程序的任何地方被访问。 这种变量,我们可以说它的作用域是全局 Global Level Scope,也就是所谓的全局变量 (Global Variable)。
因此,全局变量不管是在函数内,还是函数外被使用,都会起作用。
const number = 100 // 全局变量
const func = () => {
console.log(number) // 100
}
console.log(number) // 100
像这个变量 number,在函数内外都可以被使用。
局部级作用域
一个变量如果是在函数内声明的。 它的影响就只限于这个函数最外层的 {} 中。 这种变量出了 {} 就完全不起作用了。 想要使用它会出现 ReferenceError。 而我们会把这种变量称之为局部变量。
const func = (() => {
const number = 100 // 局部变量
console.log(number) // 100
})()
console.log(number) // ReferenceError: a is not defined
另外,在函数外如果有着一个和函数内相同名称的全局变量,会优先使用函数内的区域变量。 如下:
const number = 999 // 全局变量
const func = (() => {
const number = 100 // 局部变量
console.log(number) // 100
})()
console.log(number) // 999
块级作用域
块级作用域 (Block Level Scope) 是一种更小的作用域。 只存在于 {} 中。 最常出现在 Function Scope 中的 {} 中(像是 if、for 等语法)。 聪明的你也许会想,是不是也可以将 Function Scope 看成是 Block Scope 的一种? 是的,虽然有点不同,但要这么想也不是不行。 更何况,在 ES6 之前,是没有块级作用域这种概念的。
在 ES6 之前,我们声明变量只能使用 var 来声明,而使用 var 声明变量会有不少缺点,也无法形成 Block Scope (只有 Function Level Scope 和 Global Level Scope)。 以至于 ES6 推出了另外两个声明变量的方式:const 和 let。
在函数中用 const 或 let 声明变量,都会立即让这个变量在函数中拥有 Block Level Scope。
let func = (() => {
let number = 999; {
console.log(number); // 999
}
console.log(number); // 999
})()
console.log(number); // ReferenceError: number is not defined
上面这段代码,使用了 let 声明变量 number; 所以 number 在 {} 内拥有块级作用域。 number 只成立于声明它的的 {} 内,以及它的子 {} 内。
let func = (() => {
let number = 999;
let number = 9999;
console.log(number) // SyntaxError: Identifier 'number' has already been declared
})()
用 let 或 const 在同一个执行环境中不能重复声明变量
用 var 关键词声明变量的缺点这边稍做简述,就是因为这些缺点,才使得ES6之后,声明变量还是建议只使用 const 或者 let。
- 允许重复声明
- 不支持块级作用域( Block Scope)
- 不支持常数 (Constant) 特性
静态作用域
废话不多说,先看下面的代码:
var thisIs = 'global';
let func1 = () => {
console.log(thisIs)
}
let func2 = () => {
var thisIs = 'local';
func1();
}
func1() // 'global'
func2() // 'global'
惊不惊喜? 意不意外? func2() 打印出 global 很合理,但 func1 竟然也印出 global。这是为什么呢?
某程序语言在 func1 的地方会印出 global ,这种程式语言采用的就是静态作用域 (Static Scope),例如:c / java / javascript ; 反之,如果在 func1 的地方打印出 local,这种程序语言采用的就是动态作用域 (Dynamic Scope),例如:perl。
静态作用域跟动态作用域最大的区别就是,静态作用域函数内的变量是在这个函数声明时就已经设定好的,也就是early binding。 所以不管这个函数在哪被呼叫,它内部的变量取值早就决定了,并不会因为被调用而发生改变。
而动态作用域,取决于函数被执行时代码的状态,进而决定函内的变量。 也就是所谓的late binding。
总之对于 Javascript,只要了解它采用的是静态作用域 (Static Scope) 就可以了。
补充:作用域链
还是先看代码:
let number = 1000000;
let outerFunc = () => {
console.log(number)
let innerFunc = () => {
console.log(number)
}
innerFunc()
}
outerFunc()
虽然在函数内的执行环境中找不到 number 这个变量。 但内部的函数会一层层地往外部的执行环境中找去。 直到找到全局的执行环境为止。 如果还是找不到,就会抛出错误。 在 innerFunc 的执行环境内找不到 number 这个变量,就往 outerFunc 的执行环境找去; 一样找不到? 再往全局的执行环境找… 这样的行为 — 由内到外的这条找寻链,我们就称呼它为作用域链。