摘要:最先执行完毕的一定是最里面的函数,执行过后弹出调用栈,接着执行上一层函数,直至所有函数执行完,调用栈清空。到这里你应该就会明白,上面函数调用栈,就是生成了一个函数的执行上下文。
前言 为什么会有这篇文章?
在书籍或博客上,我们经常会看到「作用域链」、「闭包」、「变量提升」等概念,说明一个问题 —— 它们很重要。
但很多时候,对于这些概念,看的时候觉得自己已经明白了,可过不了多久,再让你说一说,可能就说不清楚了,之所以会这样,是因为我们对于 JavaScript 这门语言的运行机制不清楚。
我相信搞明白了今天所讲的内容,会对你理解那些知识大有裨益!
函数调用栈(call stack) 1. 什么是栈?类似 js 中的数组,栈也是用来存储数据的一种数据结构。他的特点是后进先出(LIFO)。
与之相对的一种数据结构称为队列,队列的特点是先进先出(FIFO)。
可以想象这样一种场景:小明和同学们放学回家,老师让他在排在队伍的最前面,他们每天回家路上都要经过一个胡同,小明每次都是第一个进入胡同,肯定也是第一个出来,这就是所谓「先进先出」。
可是有一天,小明他们走到胡同里发现胡同口停了一辆车,把胡同给堵死了,没办法,他们只能队头变队尾往回撤,这时候,小明虽然最先进入胡同,却只能最后出去,最先出去的是排在队尾的小华,也就是「后进先出」。
2. 什么叫函数调用栈?在 js 中函数的调用也遵照这样以一个原则:最先调用的函数先放到调用栈中,假如这个函数内部又调用了别的函数,那么这个内部函数就接着被放入调用栈中,直至不再有函数调用。最先执行完毕的一定是最里面的函数,执行过后弹出调用栈,接着执行上一层函数,直至所有函数执行完,调用栈清空。
这样说可能会不太明白,举个例子:
// 其他语句 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 执行完,函数栈清空。
执行上下文(Execution Context) 1. 什么是执行上下文?js 代码在执行时,会进入一个执行环境,它会形成一个作用域。这个执行环境,便是执行上下文。
JavaScript 主要有三种执行环境:
全局执行环境: 代码开始执行时首先进入的环境。
函数环境:函数调用时,会开始执行函数中的代码。
eval:不建议使用,可忽略。
2. 执行上下文的生命周期上面讲到 js 代码执行时会生成一个执行上下文。而这个执行上下文的周期,分为两个阶段:
创建阶段。这个阶段会生成变量对象(VO),建立作用域链以及确定 this 的值。
执行阶段。这个阶段进行变量赋值,函数引用及执行代码。
到这里你应该就会明白,上面函数调用栈,就是生成了一个函数的执行上下文。
执行上下文也同样遵循函数调用栈的规则,无非就是多加了一层 —— 全局执行上下文,函数执行完后会跳出执行栈,而全局执行上下文,会在关闭浏览器后跳出执行栈。
还是上面的例子,我们看一下执行栈。
变量对象 1. 什么叫变量对象?从上面其实可以得到答案,变量对象是 js 代码在进入执行上下文时,js 引擎在内存中建立的一个对象,用来存放当前执行环境中的变量。
2. 变量对象(VO)的创建过程变量对象的创建,是在执行上下文创建阶段,依次经过以下三个过程:
创建 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。
根据以上三个步骤,对于变量提升也就知道是怎么回事了。
3. 变量对象变为活动对象执行上下文的第二个阶段,称为执行阶段,在此时,会进行变量赋值,函数引用并执行其他代码,此时,变量对象变为活动对象。
我们还是举上面的例子:
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 的引用地址 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();
原文地址:阿木木的博客
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/99823.html
摘要:举个例子在上面的例子可以看到,我们声明是一个数字,但是我们在之后将的值又改成了字符串和布尔值后面会讲这些类型。基本类型字符串表示一个字符串,如。因此,我们可以写一个函数,用来精确检测类型。 showImg(https://segmentfault.com/img/remote/1460000017309509?w=850&h=572); 定义 1. 什么是数据类型? 数据类型,就是将...
摘要:当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。作用域与作用域链到来有全局作用域函数作用域和块级作用域新增。 引言 Javascript是前端面试的重点,本文重点梳理下 Javascript 中的常考知识点,然后就一些容易出现的题目进行解析。限于文章的篇幅,无法将知识点讲解的面面俱到,本文只罗列了一些重难点,如果想要了解更多内容欢迎点击我的博客。 一、变量类型 1.JS ...
摘要:当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。作用域与作用域链到来有全局作用域函数作用域和块级作用域新增。 引言 Javascript是前端面试的重点,本文重点梳理下 Javascript 中的常考知识点,然后就一些容易出现的题目进行解析。限于文章的篇幅,无法将知识点讲解的面面俱到,本文只罗列了一些重难点,如果想要了解更多内容欢迎点击我的博客。 一、变量类型 1.JS ...
摘要:关键字计算为当前执行上下文的属性的值。毫无疑问它将指向了这个前置的对象。构造函数也是同理。严格模式无论调用位置,只取显式给定的上下文绑定的,通过方法传入的第一参数,否则是。其实并不属于特殊规则,是由于各种事件监听定义方式本身造成的。 this 是 JavaScript 中非常重要且使用最广的一个关键字,它的值指向了一个对象的引用。这个引用的结果非常容易引起开发者的误判,所以必须对这个关...
摘要:技术上来说这个机制被称为动态分配或代理。定义类一个类是一个正式的抽象集,它规定了对象的初始状态和行为。技术上来说一个类表示构造函数原型的组合。因此构造函数创建对象并自动设置新创建实例的原型。第二次调用时,相同的上下文再次被压入栈并恢复。 原文:JavaScript. The Core: 2nd Edition作者:Dmitry Soshnikov 文章其他语言版本:俄语 这篇文章是 ...
阅读 1418·2021-09-03 10:29
阅读 3414·2019-08-29 16:24
阅读 1967·2019-08-29 11:03
阅读 1388·2019-08-26 13:52
阅读 2899·2019-08-26 11:36
阅读 2772·2019-08-23 17:19
阅读 545·2019-08-23 17:14
阅读 764·2019-08-23 13:59