上下文的原意是 context
, 做用域的原意是scope
, 这两个不是一个东西。javascript
每个函数的调用(function invocation) 都有对应的scope
和context
.java
scope
指的是 函数被调用的时候, 各个变量的做用区域context
指的是 current scope and its enclosing scope. 就是当前scope 和包裹它外面的scope. 若是一个变量在当前scope没找到,那么它会自底向上继续找enclosing scope 直到找到为止。很像javascript 的prototype那样的找法。常常在javascript中,函数被调用的时候, 查看this
指向哪一个object
, 那么那个object
就是当前的 "上下文"。浏览器
小明告诉小红:“你放心吧,他答应你的条件了。”函数
在读者的眼中,“他”是谁根本无从知晓,由于这句话缺乏“上下文”;this
从小强家里出来后,小明告诉小红:“你放心吧,他答应你的条件了。”spa
谁都知道,“他”指的是“小强”,由于有“上下文”。prototype
Javascript中代码的运行环境分为如下三种:线程
在网上能够找到不少阐述做用域的资源,为了使该文便于你们理解,咱们能够将“执行上下文”看作当前代码的运行环境或者做用域。下面咱们来看一个示例,其中包括了全局以及函数级别的执行上下文:3d
上图中,一共用4个执行上下文。紫色的表明全局的上下文;绿色表明person函数内的上下文;蓝色以及橙色表明person函数内的另外两个函数的上下文。注意,无论什么状况下,只存在一个全局的上下文,该上下文能被任何其它的上下文所访问到。也就是说,咱们能够在person的上下文中访问到全局上下文中的sayHello变量,固然在函数firstName或者lastName中一样能够访问到该变量。code
至于函数上下文的个数是没有任何限制的,每到调用执行一个函数时,引擎就会自动新建出一个函数上下文,换句话说,就是新建一个局部做用域,能够在该局部做用域中声明私有变量等,在外部的上下文中是没法直接访问到该局部做用域内的元素的。在上述例子的,内部的函数能够访问到外部上下文中的声明的变量,反之则行不通。那么,这究竟是什么缘由呢?引擎内部是如何处理的呢?
在浏览器中,javascript引擎的工做方式是单线程的。也就是说,某一时刻只有惟一的一个事件是被激活处理的,其它的事件被放入队列中,等待被处理。下面的示例图描述了这样的一个堆栈:
咱们已经知道,当javascript代码文件被浏览器载入后,默认最早进入的是一个全局的执行上下文。当在全局上下文中调用执行一个函数时,程序流就进入该被调用函数内,此时引擎就会为该函数建立一个新的执行上下文,而且将其压入到执行上下文堆栈的顶部。浏览器老是执行当前在堆栈顶部的上下文,一旦执行完毕,该上下文就会从堆栈顶部被弹出,而后,进入其下的上下文执行代码。这样,堆栈中的上下文就会被依次执行而且弹出堆栈,直到回到全局的上下文。请看下面一个例子:
(function foo(i) { if (i === 3) { return; } else { foo(++i); } }(0));
上述foo被声明后,经过()运算符强制直接运行了。函数代码就是调用了其自身3次,每次是局部变量i增长1。每次foo函数被自身调用时,就会有一个新的执行上下文被建立。每当一个上下文执行完毕,该上上下文就被弹出堆栈,回到上一个上下文,直到再次回到全局上下文。真个过程抽象以下图:
因而可知 ,对于执行上下文这个抽象的概念,能够概括为如下几点:
咱们如今已经知道,每当调用一个函数时,一个新的执行上下文就会被建立出来。然而,在javascript引擎内部,这个上下文的建立过程具体分为两个阶段:
实际上,能够把执行上下文看作一个对象,其下包含了以上3个属性:
(executionContextObj = { variableObject: { /* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */ }, scopeChain: { /* variableObject 以及全部父执行上下文中的variableObject */ }, this: {} }
确切地说,执行上下文对象(上述的executionContextObj)是在函数被调用时,可是在函数体被真正执行之前所建立的。函数被调用时,就是我上述所描述的两个阶段中的第一个阶段 – 创建阶段。这个时刻,引擎会检查函数中的参数,声明的变量以及内部函数,而后基于这些信息创建执行上下文对象(executionContextObj)。在这个阶段,variableObject对象,做用域链,以及this所指向的对象都会被肯定。
上述第一个阶段的具体过程以下:
进入第一个阶段-创建阶段:
每找到一个函数声明,就在variableObject下面用函数名创建一个属性,属性值就是指向该函数在内存中的地址的一个引用
若是上述函数名已经存在于variableObject下,那么对应的属性值会被新的引用所覆盖。
执行函数体中的代码,一行一行地运行代码,给variableObject中的变量属性赋值。
下面来看个具体的代码示例:
function foo(i) { var a = 'hello'; var b = function privateB() { }; function c() { } } foo(22);
在调用foo(22)的时候,创建阶段以下:
fooExecutionContext = { variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: undefined, b: undefined }, scopeChain: { ... }, this: { ... } }
因而可知,在创建阶段,除了arguments,函数的声明,以及参数被赋予了具体的属性值,其它的变量属性默认的都是undefined。一旦上述创建阶段结束,引擎就会进入代码执行阶段,这个阶段完成后,上述执行上下文对象以下:
fooExecutionContext = { variableObject: { arguments: { 0: 22, length: 1 }, i: 22, c: pointer to function c() a: 'hello', b: pointer to function privateB() }, scopeChain: { ... }, this: { ... } }
咱们看到,只有在代码执行阶段,变量属性才会被赋予具体的值。
在网上一直看到这样的总结: 在函数中声明的变量以及函数,其做用域提高到函数顶部,换句话说,就是一进入函数体,就能够访问到其中声明的变量以及函数。这是对的,可是知道其中的原因吗?相信你经过上述的解释应该也有所明白了。不过在这边再分析一下。看下面一段代码:
(function() { console.log(typeof foo); // function pointer console.log(typeof bar); // undefined var foo = 'hello', bar = function() { return 'world'; }; function foo() { return 'hello'; } }());
上述代码定义了一个匿名函数,而且经过()运算符强制理解执行。那么咱们知道这个时候就会有个执行上下文被建立,咱们看到例子中立刻能够访问foo以及bar变量,而且经过typeof输出foo为一个函数引用,bar为undefined。
为何咱们能够在声明foo变量之前就能够访问到foo呢?
由于在上下文的创建阶段,先是处理arguments, 参数,接着是函数的声明,最后是变量的声明。那么,发现foo函数的声明后,就会在variableObject下面创建一个foo属性,其值是一个指向函数的引用。当处理变量声明的时候,发现有var foo的声明,可是variableObject已经具备了foo属性,因此直接跳过。当进入代码执行阶段的时候,就能够经过访问到foo属性了,由于它已经就存在,而且是一个函数引用。
为何bar是undefined呢?
由于bar是变量的声明,在创建阶段的时候,被赋予的默认的值为undefined。因为它只要在代码执行阶段才会被赋予具体的值,因此,当调用typeof(bar)的时候输出的值为undefined。
这样差很少对执行上下文有所理解了吧,大佬们但愿能给出更好地建议