搞定JS作用域和执行上下文
1.作用域
举个简单的函数例子
function getNum(num){
num =100;
console.log(num);
}
getNum();
console.log(num);
结果是:100和Uncaught ReferenceError: num is not defined
虽然在函数中声明了一个隐式全局变量,可是函数的形参跟隐式全局变量的名字是相同的,而函数形参是个局部变量,而且是在隐式全局变量之前声明的,相当于
function getNum( ){
var num;
num =100;
console.log(num);
}
所以在外面不能获取到内部的值。
光知道“javascript没有块级作用域”是完全不够的,你需要知道的是——javascript除了全局作用域之外,只有函数可以创建的作用域。作用域在函数定义时就已经确定。
所以,我们在声明变量时,全局代码要在代码前端声明,函数中要在函数体一开始就声明好。除了这两个地方,其他地方都不要出现变量声明。而且建议用“单var”形式。
下面继续说作用域。作用域是一个很抽象的概念,类似于一个“地盘”
如上图,全局代码和fn、bar两个函数都会形成一个作用域。而且,作用域有上下级的关系,上下级关系的确定就看函数是在哪个作用域下创建的。例如,fn作用域下创建了bar函数,那么“fn作用域”就是“bar作用域”的上级。
作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。例如以上代码中,三个作用域下都声明了“a”这个变量,但是他们不会有冲突。各自的作用域下,用各自的“a”。
2.上下文context
你在JS中找不到context。是因为上下文这个东西不是一个具体的东西,上下文在不同的地方表示不同的含义,要感性理解。
context其实说白了,和文章的上下文是一个意思,在通俗一点,我觉得叫环境更好。
在JS中,当浏览器打开时,首先会开辟形成一个顶层的栈内存,就是全局作用域,而对象是存储在堆中。
例如:
var obj = { foo: 5 };
上面的代码将一个对象赋值给变量obj。JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj。也就是说,变量obj是一个地址(reference)。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。
对象的属性在全局作用域中需要地址索引间接读取,就是这个间接性导致对象和全局上下文相对独立,形成“对象上下文”。
JavaScript 语言之中,一切皆对象,运行环境也是对象,所以函数都是在某个对象之中运行,
由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。
var f = function () {};
var obj = { f: f };
// 单独执行
f()
// obj 环境执行
obj.f()
JavaScript 允许在函数体内部,引用当前环境的其他变量。
var f = function () {
console.log(x);
};
上面代码中,函数体里面使用了变量x。该变量由运行环境提供。
现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。
var f = function () {
console.log(this.x);
}
上面代码中,函数体里面的this.x就是指当前运行环境的x。
3.执行上下文Execution Context
执行上下文其实就是“动态意义的”上下文。
3.1全局执行上下文
在执行全局代码前将window确定为全局执行上下文
在一段js代码拿过来真正一句一句运行之前,浏览器已经做了一些“准备工作”,其中就包括对变量的声明以及变量提升(此处重点不是变量提升,感兴趣的童鞋可以自行搜索学习),而不是赋值。变量赋值是在赋值语句执行的时候进行的。
在“准备工作”中,需要注意 “函数表达式”和“函数声明”。虽然两者都很常用,但是这两者在“准备工作”时,却是两种待遇。
对待函数表达式就像对待“ var a = 10 ”这样的变量一样,只是声明。而对待函数声明时,却把函数整个赋值了,所以可以可直按调用。
在“准备工作”中完成了哪些工作:
- 变量、函数表达式——变量声明,默认赋值为undefined;
- this——赋值(指向window);
- 函数声明——赋值;
这三种数据的准备情况我们称之为“执行上下文”或者“执行上下文环境”。
变量对象(variable object),
作用域链(scope chain),
this指针(this value),
它们影响着变量的解析,变量作用域
3.2作用域链
前文第2节说到作用域在函数定义时就已经确定。由于在全局上下文中声明了许多函数,这些函数之间就产生了作用域链式关系。
在说作用域链之前, 先解释一下什么是“*变量”。
在A作用域中使用的变量x,却没有在A作用域中声明(即在其他作用域中声明的),对于A作用域来说,x就是一个*变量。如下图
函数有个倔强的特点:在函数被调用时要到创建这个函数的那个作用域中取变量值。
如果找了,还没找到呢?
接着向上一层作用域找!
一直找到全局作用域为止。要是在全局作用域中都没有找到,那就是真的没有了。
这个一步一步“找”的路线,我们称之为——作用域链。
3.3函数执行上下文
在调用函数,准备执行函数体之前,也会创建对应的函数执行上下文对象
函数执行上下文的“准备工作”保存着函数执行所需的重要信息,其中有三个属性:
变量对象(variable object),
this指针(this value),
作用域链(scope chain)(函数内部嵌套函数)
它们影响着变量的解析,变量作用域和函数this的指向。
首先明确最重要的一点:当函数被调用的时候,调用函数的那个对象会被传递到执行上下文中,成为this的值。
4.作用域和执行上下文关系
全局作用域和函数作用域是在全局执行上下文在做“准备工作”时产生的,是静态的不变的。
全局执行上下文只产生一次,函数执行上下文是在函数调用时候才会产生是动态的,多次调用产生多次,同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。
第一步,在加载程序时,已经确定了全局上下文环境,并随着程序的执行而对变量就行赋值。
第二步,程序执行到第27行,调用fn(10),此时生成此次调用fn函数时的上下文环境,压栈,并将此上下文环境设置为活动状态。
第三步,执行到第23行时,调用bar(100),生成此次调用的上下文环境,压栈,并设置为活动状态。
第四步,执行完第23行,bar(100)调用完成。则bar(100)上下文环境被销毁。接着执行第24行,调用bar(200),则又生成bar(200)的上下文环境,压栈,设置为活动状态。
第五步,执行完第24行,则bar(200)调用结束,其上下文环境被销毁。此时会回到fn(10)上下文环境,变为活动状态。
第六步,执行完第27行代码,fn(10)执行完成之后,fn(10)上下文环境被销毁,全局上下文环境又回到活动状态。
结束
由上述过程可以注意到,在bar作用域中出现了两个函数执行上下文环境,即同一个函数作用域下,不同的调用会产生不同的执行上下文环境。
5.执行上下文栈
由上述过程其实已经可以发现,执行上下文有类似栈数据结构的特点。没错!执行上下文确实有的概念。
执行上下文栈的创建过程如下
1.在全局代码执行前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象
2.在全局执行上下文(window)确定后,将其添加到栈中(压栈)
3.在函数执行上下文创建后,将其添加到栈中(压栈)
4.在当前函数执行完后将栈顶的对象移除(出栈)
5.当所有的代码执行完后,中只剩下window
以上内容为本人参考以下文章,结合自己思考做的整理和修改,有错误之处欢迎在评论区指出!
深入理解javascript原型和闭包(完结) - 王福朋 - 博客园
https://www.cnblogs.com/wangfupeng1988/p/3977924.html
彻底理解js的执行上下文,以及变量对象 - 简书
https://www.jianshu.com/p/f8e628b5c312