目录
正文内容
执行上下文和执行上下文栈
先上两个例子
var A = function () {
console.log('A');
}
A(); // A
var A = function () {
console.log('B');
}
A(); // B
嗯这看起来很正常,but接下来这段代码
function A() {
console.log('A');
}
A(); //B
function A() {
console.log('B');
}
A(); //B
凌乱了…居然是两个B…,上面两段好像长得差不多诶。通过查阅资料我们得知了 JavaScript 引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。那这个段应该怎么划分呢。
执行上下文
JavaScript 可执行代码分为 3 类:全局代码、函数代码、Eval 代码。也就是说js在执行时会一这些东西为一个整体去执行。(就像人是一个整体,不会是头脚分离去不同地方),而在执行每一个整体时,就会进行”准备工作“,叫做执行上下文。
可执行代码
举几个例子,一目了然
全局代码
let str = 'hello world';
function foo() {
// 函数体中的代码
// 不算全局代码
}
console.log(str); // hello world
函数代码
function foo() {
console.log('hello world'); // 函数代码
}
eval代码(函数可计算某个字符串,并执行其中的的 JavaScript 代码。)
eval('console.log("hello world");'); // eval 的字符串参数为 eval 代码
那我们继续深究,这些作为一个个整体的一段代码究竟是按照什么规则去执行呢。
执行上下文栈
为了方便管理众多上下文,JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)。我们现在通过一个数组模拟这个栈的执行顺序。
ECS_Stack = []; //初始状态
按照顺序,首先执行的肯定是全局变量,那么就会压入一个全局变量的上下文。
ECS_Stack = [
global_variable //全局变量
];
当遇到一些函数方法,就遇到一个压入一个,执行完弹栈,例如来了个方法 f1( ),并且在 f1里调用f2( ),执行上下文栈就应该长这样子,然后f2执行完弹出栈,f1执行完弹出栈。
ECS_Stack = [
f2();
f1();
global_variable //全局变量
];
看到这里,我们好像是明白了一点其中的原理,但是拿出个程序我们还是分析的似懂非懂,只能继续深挖。先从这个执行上下文入手,每个执行上下文都有三个重要属性:变量对象,作用域链,this
执行上下文的重要属性之一------变量对象
这个东西可以理解为一段数据存储区,用来放上下文中定义的变量和函数声明。因此根据前面的东西我们可以推测变量对象有对于全局变量的(全局上下文的变量对象),也有针对函数的(函数上下文的变量对象)。
全局上下文
我们先了解一个概念,叫全局对象。在 W3School 中也有介绍:
全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。
在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。
例如,当JavaScript 代码引用 parseInt() 函数时,它引用的是全局对象的 parseInt 属性。全局对象是作用域链的头,还意味着在顶层 JavaScript 代码中声明的所有变量都将成为全局对象的属性。
emm看完的感觉还是似懂非懂,稍微总结下全局对象,加深下理解。
①可以通过 this 引用,在JavaScript 中,全局对象就是 Window 对象。
console.log(this); //控制台打印出 windows
②并且全局对象是Object类的实例(instanceof 判断左边的是不是右边的实例)
console.log(this instanceof Object); //控制台打印 true
③全局对象预定义了一大堆属性和函数
console.log(Math.random()); //有效
console.log(this.Math.random()); //有效
④作为全局变量的宿主
var a = 1;
console.log(this.a); // 1
⑤所有this.xxx都可以用windows.xxx指向
var a = 1;
window.b = 2;
console.log(this)
所以看到这里,我们终于得出了结论!!全局上下文的变量对象就是全局变量!
函数上下文
在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。
引用一下大佬的解释:
活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object 呐,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问,活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 的对象。
我们来看看Arguments 到底是个什么东西
function A() {
console.log(A.arguments)
}
A();
引用一下大佬的通俗易懂的解释
arguments的东西也是个对象,而且是一个特殊的对象,它的属性名是按照传入参数的序列来的,第1个参数的属性名是’0’,第2个参数的属性名是’1’,以此类推,并且它还有length属性,存储的是当前传入函数参数的个数,很多时候我们把这种对象叫做类数组对象。类数组对象和数组都是对象这个妈生的,但是数组是大哥比类数组对象多了很多其他的方法,类数组对象只是长得很像数组的弟弟而已。
所以这也就是说我们在以后的函数传参或者调用可以变得更加简单!!
function add() {
if( arguments.length === 2 ){
return arguments[0] + arguments[1];
}else{
return '传参非法';
}
}
console.log( add(1,2) );
console.log( add(1,2,3) );
有了这个对象我们以后写函数的时候,就不用给所有的形参指定参数名,然后通过参数名的方式获取参数了,我们可以直接使用arguments对象来获取实参,并且也可以有效规避:调用函数时,会判断当前传入的参数是否与函数定义的参数个数相等,不相等就会报错的问题。
arguments还有一个属性callee,我们也打印下看看
function A() {
var a = "函数A"
console.log(arguments.callee)
}
A();
其实就是我们这个函数的内容。
看到这我们似乎一下”豁然开朗“,那么我们现在再来想想代码的执行顺序究竟是怎样的。
执行代码顺序
执行上下文的代码会分成两个阶段进行处理:分析和执行,我们也可以叫做:
1.进入执行上下文
2.代码执行
进入执行上下文
此时还没有执行代码,变量对象会包括:
1.函数的所有形参 (如果是函数上下文)
由名称和对应值组成的一个变量对象的属性被创建,并且没有实参,属性值设为 undefined
2.函数声明
由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建
如果变量对象已经存在相同名称的属性,则完全替换这个属性
3.变量声明
由名称和对应值(undefined)组成一个变量对象的属性被创建;
如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性
代码执行
然后按照顺序进行执行
好的看着这些概念,似懂非懂,我们还是举例子加深理解:
先看看全局变量的
正常!!
var a = 52;
console.log(a); //52
报错!!
console.log(a); //Uncaught ReferenceError: a is not defined
undefined!!
console.log(a); //undefined
var a = 52;
报错是因为我们没有定义很好理解,但是第三个按理说也应该是报错未定义才对,但是undefined的意思是指找到了变量但是没有值,细品之后发现了猫腻!也就是说,代码的执行顺序应该是这样!!
var a;
console.log(a);
而且我们也看出变量的赋值就是在真正执行顺序里的声明阶段,就是说真正的声明在打印之前,赋值在打印之后,那打印自然是变量存在但是没有值!!这就是上下文的执行原理。
再看看函数的
正常!!
function a(){
this.user = "小王";
}
console.log(a);
正常!!
console.log(a);
function a(){
this.user = "小王";
}
为什么这里又都可以了????
a();
var a = function (){
console.log("调用")
}
这里又不行了。
小结一下,函数分为:函数声明和函数表达式。
函数声明:
function a(){
this.user = "小王";
}
函数表达式:
var a = function (){
console.log("调用")
}
看似两段相同的语句,它们的执行顺序却截然不同,函数声明时的赋值行为是在函数创建的时候进行的,而函数表达式的赋值行为是在执行这句变量时进行的(因为它已经赋值给了变量所以我这里把它称为变量)。
举个例子:
var a = 1;
function b(){
console.log(a); //undefined
var a = 5;
}
b();
他真正的执行顺序:
var a = 1;
function b(){
var a
console.log(a); //undefined
a = 5;
}
b();
总结
这部分主要学习了js代码究竟是如何一段一段执行,根据前一篇学习的原型原型链,可以加以分析js程序的真正运行流程。