一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第9天,点击查看活动详情。
作用域(scope)
作用域(scope),它规定了如何去查找变量的规则。通俗点就是说当前执行代码对变量的访问权限
比如我当前定义了这三个值 a b c
var a = 1
var b = function(){}
function c() {}
声明
变量只有在声明之后才能在作用域中查找到它
function f(a){
console.log(a + b) // 报错!抛出 ReferenceError b is not defined
b = a
}
f(2)
function f(a){
console.log(a + b) // 2 + undefined = NaN
var b = a
console.log(a + b) // 4
}
f(2)
上面代码实际等同于
function f(a){
var b
console.log(a + b) // 2 + undefined = NaN
b = a
console.log(a + b) // 4
}
f(2)
如何声明
var:在编译器解析时,会将var a = 1视为var aa = 1两段执行,存在变量提升!let:重复声明报错,属块级作用域,存在变量提升,但会暂时性死区const:重复声明报错,属块级作用域,存在变量提升,但会暂时性死区。内容只读,修改报错,但引用类型保存的是指针function:函数声明,直接提升到最上面,提升优先级最高,且已完成赋值!
静态作用域(词法作用域)
静态作用域(词法作用域),采用词法作用域的变量叫词法变量。词法变量有一个在编译时静态确定的作用域。词法变量的作用域可以是一个函数或一段代码,该变量在这段代码区域内可见;在这段区域以外该变量不可见。如图 scope1 与 scope2 是互相隔离的,作用域链沿定义的位置往外延伸
词法作用域里,取变量的值时,会检查函数定义时的文本环境,捕捉函数定义时对该变量的绑定。
全局作用域
全局作用域:声明在任何函数之外的顶层作用域的变量就是全局变量,变量拥有全局作用域
var a = 0 // 最外层声明,全局变量
function b(){
c = 1 // 不使用 var 声明也会被当作全局变量
}
b()
function d(){
console.log(a, c) // 可以访问全局作用域的变量
}
d()
函数作用域(局部作用域)
全局作用域:声明在函数内的顶变量,拥有函数作用域,外部环境无法访问到函数内部的变量(模块化的原理)
function a(){
var b = 1
console.log(b) // 1
}
a()
console.log(b) // 报错
块作用域(局部作用域)
块作用域:ES6 开始,使用 let 和 const 声明的变量拥有块级作用域,作用域范围在 {} 之间
{
let a = 1
const b = 2
console.log(a, b) // 1 2 属于块级作用域内
}
console.log(a, b) // 报错
{
var a = 1
var b = 2
}
console.log(a, b) // 1 2
为什么需要块级作用域
- 解决声明提前
console.log(a) // undefined var a = 1 console.log(b) // 报错 let b = 2 - 解决
{}中var声明被视为全局变量{ var c = 1 } console.log(c) // 1 { let d = 1 } console.log(c) // 报错
看实际的例子
var val = 'global'
function fn (){
var val = 'fn'
console.log(val)
function _fn(){
var val = '_fn'
console.log(val)
return function inner(){
var _val = 'inner'
console.log(val)
}
}
return _fn()
}
console.log(val)
fn()()
function fn(){
let a = 'a'
console.log(a)
if(a){
let b = 'b'
console.log(b)
}
}
fn()
修改词法作用域的方式 eval 与 with (最好不要用!)
eval
function fn(fnStr){
eval(fnStr)
}
fn('a = "a"')
console.log(a) // a
with
var a = {
b: 'b'
}
with(a){
b = 'change'
c = 'c' // 非严格模式查找键值不存在,会创建一个全局变量!
}
console.log(a.b) // 'change'
console.log(c) // c
动态作用域
动态作用域,采用变量叫动态变量。程序正在执行定义了动态变量的代码段,那么在这段时间内,该变量一直存在;代码段执行结束,该变量便消失。
作用域链沿着调用栈往外延伸,通过逐层检查函数的调用链,并打印第一次遇到的值。
如果是动态作用域
var local = 'in global'
function A(){
var local = 'in A'
function C(){
var local = 'in C'
B()
}
B() // in A!
C() // in C!
B() // in A!
}
function B(){
console.log(local)
}
B() // in global!
A()
实际上执行是这样的
var local = 'in global'
function A(){
var local = 'in A'
function C(){
var local = 'in C'
B()
}
B() // in global!
C() // in global!
B() // in global!
}
function B(){
console.log(local)
}
B() // in global!
A()
无论你在哪个位置调用 B 都只会向上查找到 in global
编译
var a = 1
- 词法分析:将字符打断成为有意义的片段
(token)- 比如上面的声明会被打断成如下
token辅助工具 var a = 1=>vara=1
[ { "type": "Keyword", "value": "var" }, { "type": "Identifier", "value": "a" }, { "type": "Punctuator", "value": "=" }, { "type": "Numeric", "value": "1" } ] - 比如上面的声明会被打断成如下
- 解析:将每个
token数组转换成一个嵌套元素的树,也就是抽象语法树AST(Abstract Syntax Tree){ "type": "Program", "body": [ { "type": "VariableDeclaration", "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "a" }, "init": { "type": "Literal", "value": 1, "raw": "1" } } ], "kind": "var" } ] } - 代码生成:将抽象语法树转换成可执行代码
执行上下文(Execution Context)
当 JavaScript 被解析执行时,需要 执行代码的环境 这个环境被称为 执行上下文
分类
- 全局上下文:全局代码所处的环境,不在函数内的代码均执行与全局上下文中
- 函数上下文:函数调用时创建的环境
- eval上下文:运行
eval函数中代码时创建的环境
生命周期
- 创建阶段:此时还未执行代码,只做了准备工作
- 创建变量对象:
arguments,提升函数声明和变量声明 - 创建作用域链:用于解析变量,从内层开始查找,逐步往外层词法作用域中查找
- 确定
this
- 创建变量对象:
- 执行阶段:开始执行代码,完成变量赋值,函数引用等等
- 回收阶段:函数调用完毕后,函数,对应的执行上下文出栈,等待垃圾回收器回收
var e = 0
function a(d){
var b = 1
function c(){
console.log(e, b, d)
}
c()
}
a(1)
- 全局上下文创建阶段
- 全局上下文执行阶段
- 遇到函数调用
a(1)a函数上下文创建阶段,入栈
a函数上下文执行阶段
c()函数调用,c函数上下文创建阶段,入栈
c执行完毕,出栈
a执行完毕,出栈
特点
- 全局执行上下文在代码开始时创建,有且只有一个,且永远再栈底
- 函数被调用时就会创建函数执行上下文,后入栈。(根据调用创建)
变量对象(Variable Object,VO)
变量对象时上下文相关的数据作用域,存储了上下文定义的变量和函数声明
var e = 0
/* 全局执行上下文的变量对象则是 window
window = {
e: 0
...
}
*/
function a(d){
/* 创建阶段
VO = {
arguments: { 0: 1, length: 1 }
b: undefined
c: fn()
}
*/
var b = 1
/* 开始执行
AO = {
arguments: { 0: 1, length: 1 }
b: 1
c: fn()
}
*/
function c(){
/* 开始执行
AO = {
arguments: { }
}
*/
console.log(e, b, d) // 向外层的作用域查询到变量 e, b,d
}
c()
}
a(1)
作用域链(Scope Chain)
多个变量对象构成的链表则为作用域链(Scope Chain),从离它最近的变量对象(VO)开始查找变量,逐级往上