作用域-预编译-作用域链
本文已参与「新人创作礼」活动,一起开启掘金创作之路。
一、 作用域的概念
负责收集并维护由所有声明的标识符(变量)组成的一系列查询并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。简而言之作用域是可访问变量的集合。
二、作用域的分类:
全局作用域:变量在函数外定义,即为全局变量。全局变量有 全局作用域: 网页中所有脚本和函数均可使用。
var a = " codingdream";
console.log(a)// 此处可调用 a 变量
function myFunction() {
console.log(a) //函数内部也能调用
}
局部作用域:变量在函数内声明,变量为局部变量,具有局部作用域。局部变量:只能在函数内部访问。
console.log(a)// 此处可不调用 a 变量
function myFunction() {
var a = " codingdream";
console.log(a) //只有函数内部才能调用变量 a
}
块级作用域
ECMAScript 6(简称ES6)中新增了块级作用域。块作用域由 { } 包括,if语句、for语句while语句和Switch语句里面的{ }也属于块作用域。(通常判断是否属于块级作用域是看是否是有let或者const定义,如果是用var定义的变量不属于块级作用域),块级作用域可以通过let和const声明,所声明的变量在制定块的作用域外无法被访问。
1、在一个函数内部
for(let i = 0;i < 10; i++){
console.log(i)//此时i由let定义属于块级作用域,此处可以调用
}
console.log(i)//块级作用域之外的区域不能调用变量i
2、在一个代码块(由一对花括号包裹)内部
三、词法作用域
代码在执行之前需要被编译,编译过程的第一步称为词法化,而词法化首先要创建词法作用域。词法作用域就是定义在词法阶段的作用域,换句话说就是你在写代码的时候将变量和块级作用域写在哪里决定的。
- 词法化 (创建词法作用域)
- 父级作用域是不能取子集作用域的字符的,而子集作用域是可以调用父级作用域的字符;
- 执行阶段查找作用域是由内而外 (找到第一个之后就停止查找 --- 遮蔽效应);
- 全局变量会变成window对象上的属性;
- 欺骗词法作用域(欺骗词法)
1、eval() 让原本不属于这里的代码,好像原本就写在了这里一样
function foo(str,a){
eval(str) //'var b = 3'就像原本就写在这里
console.log(a,b);
}
var b = 2
foo('var b = 3', 1)
//输出结果( 1 , 3 )
但是在严格模式下,eval也会创建属于它词法作用域,所以不能修改原本的词法作用域;
function foo(str,a){
'use strict'
//eval(str)
function eval1(){
var b = 3
}
console.log(a,b);
}
var b = 2
foo('var b = 3', 1)
//输出结果( 1 , 2 )
2、with关键字,通常被当做重复引用同一个对象中多个属性的快捷方式,可以不需要重复引用对象本身;
var obj = {
a: 1,
b: 2,
c: 3
};
//单调乏味的引用"obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
//简单的快捷方式
with(obj){
a = 3;
b = 4;
c = 5;
}
- 但实际上当使用with修改一个对象中不存在的属性时,那么这个属性会被泄漏到全局;
function foo(obj){
with(obj){
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
a: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( 02 );
console.log( o2.a ); // undefined
console.log( a ); // 2——此时a被泄漏到全局作用域上了!
预编译
在词法作用域中我们曾提到代码在执行之前都需要被编译,那么我们来聊聊关于代码执行之前的预编译,首先是:
预处理到底有什么作用呢?我们先来看个例子
console.log(b);
var b = 123
我们遵从自上而下的代码运行,此处按照常理我们会认为,先打印的b,但是后定义的b,所以此处应该是报错,但事实上输出的是undefined,那为什么会出现这样呢?
原来是预编译时变量存在声明提升,将 var b ;提升到顶部的全局,但是并没有赋值,所以此时输出结果为undefined。
我们再看个例子
test()
function test(){ //函数声明整体提升
var a = 111
console.log('hello ', a)
}
//输出结果为(hello,111)
发生在函数执行之前 四部曲
-
- 创建一个函数的活跃对象——AO对象(Activation Object);
-
- 找形参和变量声明,将变量声明和形参作为AO的属性名,值为undefined;
-
- 将实参和形参统一;
-
- 在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体。
<script>
function fn(a) {
console.log(a);
var a = 123
console.log(a);
function a() {}
console.log(a);
var b = function(){}
console.log(b);
function d(){}
var d = a
console.log(a);
}
fn(1)
</script>
第一步、创建函数的AO对象:
AO{ }
第二步、找形参和变量声明,将变量声明和形参作为AO的属性名,值为undefined;
AO{
a : undefined ;(函数中不存在重复的key值,所以第一个变量声明 var a = 123 重复,会覆盖,无需重复作为AO对象的属性名)
b : undefined ;(var b = function(){}是函数表达式,表达式中b为变量声明,function b(){}才是函数声明)
d : undefined
}
第三步、将实参和形参统一;
a = 1
第四步、在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体。
此时找到第一个函数体声明 function a() {} ,所以 a = f a(){},覆盖掉第三步的a = 1,
第二个函数体声明 var b = function(){},b = f (){}
此时函数的AO对象为:
AO{
a = f a(){};
b = f (){};
d = undefined
}
预编译结束之后开始打印(console.log)自上而下开始执行
- 第一个console.log打印出 f a(){};
再将123 赋值给 a ,所以a = 123
- 第二个console.log打印出 123;
- 第三个同理打印出 123;
- 第四个打印出 f (){};
- 第五个打印出 123;
发生在全局 三部曲(具体方法同上此处忽略)
-
- 创建 GO 对象(Global Object);
-
- 找全局变量声明,将将变量声明作为 GO 的属性名,值为undefined;
-
- 在全局找函数声明,将函数名作为 GO 对象的属性名,值赋予函数体。
四、作用域链
在理解作用域链之前我们先来了解几个相关的概念:1、执行期上下文,2、查找变量,3、[[scope]]--函数的作用域;
执行期上下文:
当函数执行时,会创建一个称为执行期上下文的内部对象(AO对象)。 一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行期上下文都是独一无二的, 所以多次调用一个函数会导致创建多个执行期上下文,当函数执行完毕,它所产生的执行期上下文会被销毁。
- 查找变量
从作用域的顶端依次往下查找
- - [[scope]]:
函数的作用域,是不可访问的,其中存储了运行期上下文的集合
函数本身存在许多属性例如name,prototype等
test.name
test.prototype
test.[[scope]] //存在但拿不到‘[]’,函数的作用域属性属于隐式属性
console.log(test.prototype);//原型
它的打印结果是{};
了解了这些概念之后我们再来给作用域链下个定义
- 作用域链:
[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式连接,我们把这种链式连接叫做作用域链。
function a(){
function b(){
var b =222
}
var a = 111
b()
console.log(a);
}
var b = glob = 100
a()
//a 定义 a.[[scope]] --a的作用域 --->0: GO{}
//a 执行 a.[[scope]] --a的作用域 --->0: AO{} 1: GO{}
//b 定义 b.[[scope]] --b的作用域 --->0: bAO{} 1: aAO{} 2:GO{}
//外层函数不能访问内层函数的原因可能是,内层函数已经执行完了之后,外层函数还没执行完,内层函数执行完了之后,内层函数的AO对象被销毁了(垃圾回收机制),外层函数无法访问到内层函数
这个例题的图解思路如下:
五、结语
本篇文章是经过学习JS之后,粗略的做的一个从作用域-预编译-作用域链的一个小结,希望能帮助到一些刚学习前端的朋友做个大概的了解,如有纰漏,欢迎批评指正,感谢!