一篇就够-JS作用域和执行上下文

作用域

  • 作用域是一个变量区域
  • 作用域决定变量的访问权限,也规定了查找变量的方法

根据查找变量的方法,可以分为词法作用域(静态作用域)和动态作用域
js采用的是静态作用域

静态作用域(词法作用域)和动态作用域

  • 静态作用域:函数的作用域在函数定义的时候就决定了
  • 动态作用域:函数的作用于在函数调用的时候决定
var value = 1;

function foo(){
    console.log(value);
}

function bar(){
    var value = 2; 
    foo();

}

bar();

静态作用域:执行foo -> 从foo内部查找是否有value ->没有则根据函数定义的位置,查找上一层作用域->查找到value为1

动态作用域:执行foo -> 从foo内部查找是否有变量value -> 没有则根据当前调用foo的外层作用域,也就是bar -> 查找到bar的作用域value为2

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

-------------------------------------------------

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

由于js是静态作用域,以上两段代码输出都是local scope(根据f函数定义的位置)

静态作用域和闭包

静态作用域,意味着函数对象的内部状态不仅包含函数逻辑的代码,还包含当前作用域链的引用。
严格意义上来说,所有JS函数都是闭包,他们都是对象,都包含关联到他们的作用于,形成一个所谓的闭包,这样外部函数就无法访问内部变量。
在JS中,我们说的闭包指的是让外部函数访问到其内部的变量,按照一般的做法,是使内部函数返回一个函数,然后操作其中的变量。这样做的话,一是可以读取到函数内部的变量,二是可以让这些变脸搞得值始终保存在内存中。

执行上下文

Js引擎线程创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文

当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。

假设ECStack为执行上下文栈,JS开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文。

ECStack = [ globalContext ]

假设有如下代码:

function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕,就会将函数的执行上下文从栈中弹出:

ECStack.push(<fun1> functionContext)

ECStack.push(<fun2> functionContext)

ECStack.push(<fun3> functionContext)

ECStack.pop()
ECStack.pop()
ECStack.pop()

闭包中的执行上下文栈

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();

-------------------------------------------------

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

上面两段代码的输出都是local scope,但他们的差别在于执行上下文栈的变化不一样:

//第一段代码
ECStack.push(<checkscope> functionContext)
ECStack.push(<f> functionContext)
ECStack.pop()
ECStack.pop()

//第二段代码
ECStack.push(<checkScope> functionContext)
//checkScope执行完毕,f未执行
ECStack.pop()
//f被执行
ECStack.push(<f> function Context)
ECStack.pop()

执行上下文的属性

当JS代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)

对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable Object, VO)
  • 作用域链(Scope Chain)
  • this

变量对象(变量存储在哪里)

变量对象与指向上下文相关的数据作用域,存储了在上下文中定义的 变量和函数声明
变量对象可以理解为一个对象,但内部存储的都是变量

分为全局上下文的变量对象和函数上下文的变量对象

全局上下文变量对象

  • 全局上下文中的变量对象是全局对象
  • 全局对象是预定义的对象,可以访问所有预定义的对象、函数和属性
  • 在顶层js代码中,可以用关键字this引用全局对象。因为全局对象是作用域链的头。
可以通过this引用,在浏览器环境下,全局对象就是Window对象
console.log(this)

全局对象是Object的一个实例
this instanceof Object

预定义的对象、函数
Math.random()
this.Math.random()

全局变量的宿主
var a = 1
console.log(this.a)
console.log(window.a)

函数上下文变量对象

函数上下文,用活动对象activation object,AO来表示变量对象

为什么叫活动对象?

  • 不可在JS环境中被访问,只有进入执行上下文中,这个变量对象才会被激活
  • 只有被激活的变量对象,才能够被访问
  • 活动对象是进入函数上下文时被创建的,通过函数的arguments属性初始化,arguments属性值是Arguments对象

执行上下文的代码会被分成两个阶段进行处理:分析和执行

  1. 进入执行上下文,变量对象会包括
  2. 代码执行

进入执行上下文,变量对象会包括:

  1. 函数的所有形参
    • 由名称和对应值组成
    • 如果没有值,属性值被设为undefined
  2. 函数声明
    • 由名称和对应的函数对象组成
    • 如果变量对象已经存在相同的名称属性,则覆盖
  3. 变量声明
    • 由名称和对应值组成(未被执行所以是undefined)
    • 如果变量名称跟已经声明的形式参数或者函数相同,这个变量生命不会干扰已经存在的属性。

即变量对象会通过Arguments构造函数进行初始化,对函数所有形参进行初始化,如果这些形参没有值,则设为Undefined,函数声明的优先级是最高的,会覆盖形参和变量声明,变量声明的优先级最低,不会覆盖函数生命和形参声明

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};

  b = 3;

}

foo(1);

在进入执行上下文时,代码还没有被执行,此时的AO为:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    //函数变量提升
    c: reference to function c(){},
    //函数表达式,没有被提升
    d: undefined
}

代码执行阶段,此时AO为:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    //函数表达式
    d: reference to FunctionExpression "d"
}
  • 全局上下文的变量对象初始化是全局对象
  • 函数上下文的变量对象初始化只包括Arguments对象
  • 进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
  • 在代码执行阶段,会再次修改变量对象的属性值

变量对象思考题


function foo() {
    console.log(a);
    //赋予全局对象
    a = 1;
}

//Uncaught ReferenceError: a is not defined 非严格模式下输出undefined
foo(); // ???

------------------------

function bar() {
    a = 1;
    console.log(a);
}
//输出 a = 1
bar(); // ???

第一段代码由于AO里面没有,从全局对象找也没有,所以报错

AO = {
    arguments:{
        length:0
    }
}

第二段代码AO里面没有,但是全局对象找有,所以返回1

console.log(foo);

function foo(){
    console.log("foo");
}

var foo = 1;


会打印函数,而不是undefined,因为执行console.log这行代码的时候AO:

AO = {
    arguments:{
        length:0
    },
    foo:reference to function foo()
}

在分析阶段,如果没有function foo()的声明,根据var foo =1 会将foo设置为undefined,但有foo的函数声明时,变量声明不能覆盖函数声明,所以执行到console.log()时,foo还未被替代。


console.log(foo);

var foo = 1;

上面的代码会输出undefined,而不是报错,这是由于在代码分析阶段,赋值了变量声明为undefined

console.log(foo);

上面的代码会输出“Uncaught ReferenceError: foo is not defined”,这是由于在代码分析阶段,foo没有变量声明。

作用域链

作用域链是由多个执行上下文的变量对象构成的链表。
当查找变量的时候,会先从当前上下文中的变量对象中查找,如果没有找到,就会从父级(静态作用域层面上的父级,在定义时就决定)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。

作用域链的创建和变化可以从两个时期来讲解:

  • 函数的创建
  • 函数的激活

函数创建

函数的作用域在函数定义的时候就决定了。
这是因为函数有一个内部属性[[scope]],当函数创建的时候,就会保存所有父变量对象到其中,可以理解[[scope]]就是所有父变量对象的层级链。

function foo(){
    function bar(){

    }
}

函数创建时,各自的[[scope]]为:

foo.[[scope]] = [
    globalContext.VO
]

//bar被创建时 foo已经执行,所以有AO
bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
]


函数激活

函数激活时,会将激活时的变量对象添加到作用域的前端。

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();

  1. checkscope函数被创建,保存作用域到内部属性[[scope]]
checkscope.[[scope]] = [globalContext.VO]
  1. 执行checkscope函数,创建checkscope函数执行上下文,checkscope函数执行上下文被压入执行上下文栈
ECStack = [
    checkscopeContext,
    globalContext
]
  1. checkscope函数不立即执行,复制函数[[scope]]属性创建作用域
checkscopeContext = {
    Scope:checkscope.[[scope]]
}
  1. 第二步,用Arguments初始化活动对象AO,加入形参声明、函数声明、变量声明
checkscopeContext = {
    AO:{
        arguments:{
            length:0
        },
        scope2:undefined
    }
}
  1. 第三步:将活动对象压入checkscope作用域链顶端
checkscopeContext = {
    AO:{
        arguments:{
            length:0
        },
        scope2:undefined
    },
    Scope:[AO,[[Scope]]]
}

  1. 执行函数,随着函数执行,修改AO的属性值
checkScopeContext = {
    AO:{
        arguments:{
            length:0
        },
        scope2:'local scope'
    },
    Scope:[AO,[[Scope]]]
}

  1. 函数执行完毕,函数上下文从上下文执行栈中弹出
ECStack = [
    globalContext
]
  • 函数创建时,其父级变量对象就被保存到属性[[Scope]]中,这一点是根据静态作用域决定的
  • 执行上下文是在函数被执行的时候创建的,在创建执行上下文的同时,首先初始化作用域链为当前的[[Scope]]属性
  • 在函数分析阶段,初始化函数的变量对象AO,然后将其推入到作用域链的顶部
  • 在函数被执行的过程中,逐步完善AO

分析另外一段代码:

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
  1. 执行全局代码,推入全局上下文到执行上下文栈
globalContext = {
    VO:[global,scope,checkcope],
    Scope:[globalContext.VO],
    this:globalContext.VO
}
ECStack = [ globalContext ]
  1. checkscope被创建,赋值checkscope.[[scope]]
checkscope.[[scope]] = [...globalContext.Scope]
  1. checkscope被执行,推入checkscope上下文
ECStack.push(checkscopeContext)
  1. 将[[scope]]赋值给Scope作用域链
checkcopeContext = {
    Scope:[...[[scope]]]
}
  1. checkscope函数分析阶段,用arguments构造函数初始化变量对象,并压入作用域链顶部
checkscopeContext = {
    AO:{
        arguments:{
            length:0
        },
        scope:undefined,
        f:reference to f(){}
    },
    Scope:[AO,...[[scope]]],
    this:undefined
}
  1. f函数被创建,初始化f.[[scope]]为父级变量对象
f.[[scope]] = [...checkscopeContext.Scope]
  1. f函数被执行,推入f函数的执行上下文,并初始化f函数的执行上下文
fContext = {
    AO:{
        arguments:{
            length:0
        }
    },
    Scope:[AO,...[[scope]]]
}
ECStack.push(fContext)
  1. f函数执行,随着作用域链寻找scope变量,之后函数相继弹出执行上下文栈
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351

推荐阅读更多精彩内容