在书籍或博客上,咱们常常会看到「做用域链」、「闭包」、「变量提高」等概念,说明一个问题 —— 它们很重要。数组
但不少时候,对于这些概念,看的时候以为本身已经明白了,可过不了多久,再让你说一说,可能就说不清楚了,之因此会这样,是由于咱们对于 JavaScript 这门语言的运行机制不清楚。浏览器
我相信搞明白了今天所讲的内容,会对你理解那些知识大有裨益!数据结构
相似 js 中的数组,栈也是用来存储数据的一种数据结构。他的特色是后进先出(LIFO)。闭包
与之相对的一种数据结构称为队列,队列的特色是先进先出(FIFO)。函数
能够想象这样一种场景:小明和同窗们放学回家,老师让他在排在队伍的最前面,他们天天回家路上都要通过一个胡同,小明每次都是第一个进入胡同,确定也是第一个出来,这就是所谓「先进先出」。this
但是有一天,小明他们走到胡同里发现胡同口停了一辆车,把胡同给堵死了,没办法,他们只能队头变队尾往回撤,这时候,小明虽然最早进入胡同,却只能最后出去,最早出去的是排在队尾的小华,也就是「后进先出」。spa
在 js 中函数的调用也遵守这样以一个原则:最早调用的函数先放到调用栈中,假如这个函数内部又调用了别的函数,那么这个内部函数就接着被放入调用栈中,直至再也不有函数调用。最早执行完毕的必定是最里面的函数,执行事后弹出调用栈,接着执行上一层函数,直至全部函数执行完,调用栈清空。3d
这样说可能会不太明白,举个例子:code
// 其余语句 function first() { console.log('first') function second() { console.log("second") } second(); third(); // 其余语句 } //其余语句 function third() { console.log("third") } // 调用 first first();
在上述代码中,首先调用的是函数 first
, 此时 first
进入函数栈,接着在 first
中调用函数 second
,second
入栈,当 second
执行完毕后,second
出栈,third
入栈,接着 third
执行完出栈,执行 first
其余代码,直至 first
执行完,函数栈清空。对象
js 代码在执行时,会进入一个执行环境,它会造成一个做用域。这个执行环境,即是执行上下文。
JavaScript
主要有三种执行环境:
eval
:不建议使用,可忽略。上面讲到 js 代码执行时会生成一个执行上下文。而这个执行上下文的周期,分为两个阶段:
到这里你应该就会明白,上面函数调用栈,就是生成了一个函数的执行上下文。
执行上下文也一样遵循函数调用栈的规则,无非就是多加了一层 —— 全局执行上下文,函数执行完后会跳出执行栈,而全局执行上下文,会在关闭浏览器后跳出执行栈。
仍是上面的例子,咱们看一下执行栈。
从上面其实能够获得答案,变量对象是 js 代码在进入执行上下文时,js 引擎在内存中创建的一个对象,用来存放当前执行环境中的变量。
变量对象的建立,是在执行上下文建立阶段,依次通过如下三个过程:
建立 arguments 对象。对于函数执行环境,首先查询是否有传入的实参,若是有,则会将参数名是实参值组成的键值对放入arguments 对象中,不然,将参数名和 undefined,组成的键值对放入 arguments 对象中。
function bar(a, b, c) { console.log(arguments); // [2, 4] console.log(arguments[2]); // undefined } bar(2,4)
检查当前环境中的函数声明。当遇到同名的函数时,后面的会覆盖前面的。
console.log(a); // function a() {console.log('fjdsfs') } function a() { console.log('24'); } function a() { console.log('fjdsfs') }
在上面的例子中,在执行第一行代码以前,函数声明已经建立完成,后面的对以前的声明进行了覆盖。
检查当前环境中的变量声明并赋值为undefined
。当遇到同名的函数声明,为了不函数被赋值为 undefined
,会忽略此声明
console.log(a); // function a() {console.log('fjdsfs') } console.log(b); // undefined function a() { console.log('24'); } function a() { console.log('fjdsfs'); } var b = 'bbbbbbbb'; var a = 46;
在上例咱们能够看到,在代码以前前,a 仍旧是一个函数,而 b 是 undefined。
根据以上三个步骤,对于变量提高也就知道是怎么回事了。
执行上下文的第二个阶段,称为执行阶段,在此时,会进行变量赋值,函数引用并执行其余代码,此时,变量对象变为活动对象。
咱们仍是举上面的例子:
console.log(a); // function a() {console.log('fjdsfs') } console.log(b); // undefined function a() { console.log('24'); } function a() { console.log('fjdsfs'); } var b = 'bbbb'; console.log(b); // 'bbbb' var a = 46; console.log(a); // 46 var b = 'hahahah'; console.log(b); // 'hahah'
在上面的代码中,代码真正开始执行是从第一行 console.log()
开始的,自这以前,执行上下文是这样的:
// 建立过程 EC= { VO: {}; // 建立变量对象 scopeChain: {}; // 做用域链 } VO = { argument: {...}; // 当前为全局上下文,因此这个属性值是空的 a: <a reference> // 函数 a 的引用地址 b: undefiend // 见上文建立变量对象的第三步 }
根据步骤,首先是 arguments 对象的建立;其次,是检查函数的声明,此时,函数 a 声明了两次,后一次将覆盖前一次;最后,是检查变量的声明,先声明了变量 b,将它赋值为 undefined,接着遇到 a 的声明,因为 a 已经声明为了一个函数,因此,此条声明将会被忽略。
到此,变量对象的建立阶段完成,接下来时执行阶段,咱们一步一步来。
console.log(a)
,咱们知道,此时 a 是第二个函数,因此会输出function a() {...}
;console.log(b)
,不出咱们所料,将会输出 undefined
;b = 'bbbb'
;console.log(b)
,此时,b 已经赋值,因此会输出 'bbbb'
; a = 46
;console.log(a)
,此时,a 的值变为 46。b = 'hahahah'
;console.log(b)
, b 已经被从新赋值,输出 hahahah
。由上面咱们能够看到,在执行阶段,变量对象是跟着代码不断变化的,此时,咱们把变量对象成为活动对象。
执行到最后一步时,执行上下文变成了这样。
// 执行阶段 EC = { VO = {}; scopeChain: {}; } // VO ---- AO AO = { argument: {...}; a: 46; b: 'hahahah'; this: window; }
以上,就是变量对象在代码执行前及执行时的变化。
刚开始就说过,这部分概念将会对你理解后面的知识有很大的帮助,因此刚开始接触的话可能会有些晦涩,建议就是认真读两遍,结合后面的知识,常常回过头来看看。
最后留一道题,给你们做为练手,观察观察执行上下文及变量对象的变化。
console.log(a); console.log(b); var a = 4; function a() { console.log('我是a1'); b(3, 5); } var a = function a() { console.log('我是a2'); b(3, 5); } var b = function (m, n) { console.log(arguments); console.log('b') } a();