js执行环境和作用域链

1.执行环境

执行环境execution context)是JavaScript中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据。

全局执行环境是最外围的一个执行环境。根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样。在Web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境直到应用程序退出----例如关闭网页或浏览器时才会被销毁)。

每个函数都有自己的执行环境。当执行流进入到一个函数时,函数的执行环境就会被推入一个执行环境栈中并获取执行权。在该函数执行完后,栈将其执行环境弹出,把控制权返还给之前的执行环境。

我们来看一个执行环境栈运作的例子

<script>
      var scope = "global"; 
      function fn1(){
         return scope; 
      }
      function fn2(){
         return scope;
      }
      fn1();
      fn2();
   </script>

如下图示意,当 JavaScript 代码执行时,第一个进入的总是默认的全局执行环境(在 Web 浏览器中也就是 window 对象)。 当执行fn1函数时,该函数的执行环境被推入栈中,执行完毕后被弹出,fn2亦如此。


环境栈操作过程

对于每个执行环境都有三个重要的属性,变量对象VO作用域链this。这三个属性和代码运行的行为有很重要的关系。

2.变量对象(variable object )

每一个执行环境都有一个与之关联的变量对象variable object 一般简写为 VO),执行环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

从上面的定义,我们可以知道一般OV包含以下信息:

  • 变量声明(var,Variable Declaration)
  • 函数声明(FD,Function Declaration)
  • 函数的形参 (formal parameters)

举个例子:

<script type="text/javascript">
var a = "global";
function fn(x1) {
    var x2 = "local";
}
</script>

在这个程序中,全局执行环境中的 VO 就有两个部分:

    1. 通过变量声明的变量 a
    1. 通过函数声明的函数 fn

变量对象是一个抽象的概念,在进入具体的执行上下文时,变量对象在具体实现上也会有相应地差别。在global全局上下文中,变量对象也是全局对象自身,在函数上下文中,变量对象被表示为活动对象AO。

2.1全局上下文中的变量对象

全局对象是一个在进入任何执行上下文前就创建出来的对象;此对象以单例形式存在;它的属性在程序任何地方都可以直接访问,其生命周期随着程序的结束而终止。

全局对象的属性在任何地方都可以被访问到,可以通过 this 或者 DOM 中的 Window 对象来访问。全局对象中的变量对象就是全局对象本身,理解这一点很重要,正是因为这个原因才使得可以通过全局对象的属性来访问在全局上下文中声明的变量。

2.2函数上下文中的变量对象

当函数被调用时,一个特殊的对象——活动对象就随之创建了。它包含普通参数与特殊参数对象(具有索引属性的参数映射表arguments)。活动对象在函数上下文中作为变量对象使用。

在函数执行上下文中,VO是不能直接访问的,此时由活动对象(activation object,缩写为AO)扮演VO的角色

VO(functionContext) === AO;

在上面 VO 例子中,当开始执行到 foo 的时候,就会有一个 foo 的 AO 被创建,这个活动对象由两个部分组成:

  1. 初始化生成的 arguments 对象
  2. 通过变量声明的变量x2

3.作用域链

了解了变量对象之后,我们再来讲讲作用域链。
在JavaScript中,函数也是对象,实际上,JavaScript里一切都是对象。函数对象和其它对象一样,拥有可以通过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是[[Scope]],该内部属性包含了函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链,它决定了哪些数据能被函数访问。

var a='123';
function testFn(b){
  var c='abc';
  function testFn2(){
    var d='efg';
    alert(a);
  }
  testFn2();
}
testFn(10);

estFn2内未声明变量a,为什么testFn2能调用全局变量a?整个过程是怎么发生的呢?请看下图。



当解析器进入全局执行环境时,调用变量和函数时只在Global对象中查找。

当解析器进入testFn函数执行环境时,函数内部属性[[scope]]中首先填入Global对象,然后将testFn活动对象添加到Global对象之前,形成一个作用域链。


当解析器进入testFn2函数执行环境时,函数内部属性[[scope]]首先填入父级的作用域链,然后再将当前的testFn2活动对象添加到作用域链的前端,形成一个新的作用域链。

testFn2调用变量a时,首先在当前的testFn2活动对象中查找,如果没有找到就顺着作用域链向上,在testFn活动对象中查找变量a,如果没有找到再顺着作用域链向上查找,直到在最后Global对象中找到为止,否则报错。所以函数内部可以调用外部环境的变量,外部环境不能调用函数内部的变量,这就是作用域特性的原理。

4.延长作用域链

虽然执行环境的类型就只有两种————全局和局部,但还是有其他办法来延长作用域链。
用with语句和try catch 都可以延长作用域链。
语法形如:

with(object)
statement

这个with语句,将object添加到作用域链的头部,然后执行statement,最后把作用域链恢复到原始状态

function buildUrl(){
  var qs = "?debug=true";
  with(location){
    var url = href + qs;
  }
  return url;
}

当使用with语句时,相当于在现在的作用域顶部上添加了传递给with的对象。这个例子中with语句接受的是location对象,因此其变量对象中就包含了location对象所有的属性和方法,当在with语句中引用变量href时(实际上是引入location.href),可以在当前执行环境中找到。with语句内部,定义了一个名为url的变量,因而url就成了函数执行环境的一部分,所以可以作为函数的值被返回.

再看下面代码

person={name:"yhb",age:22,height:175,wife:{name:"lwy",age:21}};
with(person.wife){
    console.log(name);
}

with语句将person.wife添加到当前作用域链的头部,所以输出的就是:“lwy".
with语句结束后,作用域链恢复正常。

5.提升

提升分为变量提升和函数提升,下面我们依次介绍

5.1变量提升(variable hoisting)

var name="haha";
function changeName(){
  console.log(name);
  var name="xixi";
 }
changeName();
console.log(name);

大家认为第6行和第7行代码输出的结果应该是什么?答案是:输出结果结果分别是 undefined 和 haha。为什么是undefined?
那我们先来分析一下代码 函数changeName() 的作用域链: 自己的变量对象 -----> 全局变量对象。解析器在函数执行环境中发现变量 name,因此不会再向全局环境的变量对象中寻找。但是大家要注意的是,解析器在解析第3句代码时,还不知道变量name的值,也就是说只知道有变量name,但是不知道它具体的值(因为还没有执行第4句代码),因此输出是 undefined。javascript解析器执行原理不懂的童鞋可移步这里

上述代码其实等价于下面的形式:

var name="haha";
function changeName(){
  var name;
  console.log(name);
  name="xixi";
}
changeName();
console.log(name);

这个现象就是变量提升!
变量提升,就是把变量提升到函数的顶部,需要注意的是,变量提升只是提升变量的声明,不会把变量的值也提升上来!

5.2函数提升

在JavaScript中函数的创建方式有三种:函数声明(静态的)、函数表达式(函数字面量)、函数构造法(动态的,匿名的)。
函数表达式的形式如下:

var func1 = function(n1,n2){
   //function body;
};

函数构造法构造函数的形式如下:

var func2 = new Function("para1","para2",...,"function body");    

在这里需要说明的是:只有函数声明形式才能被提升!例如:

//函数声明
function myTest1(){ 
    func(); 
    function func(){ 
        console.log("我可以被提升"); 
    } 
} 
myTest1();

这几个概念真的是绕,自己梳理了一遍可能还没记住,还需要多加记忆,以上若有错误,请指正。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,601评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,367评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,249评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,539评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,967评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,929评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,098评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,825评论 0 207
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,609评论 1 249
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,796评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,282评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,603评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,277评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,159评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,959评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,079评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,874评论 2 277

推荐阅读更多精彩内容