目录:
1.作用域
无论是C/C++/C#,java亦或者js,这些编程语言最基础的功能就是可以存储变量当中的值,例如我们编写的第一个“Hello world!”程序,并且我们可以在对这个变量赋值之后,进行访问或者修改。正是这种访问或者修改的能力,让程序拥有了“状态”。
当我们将状态这个概念带给程序,那么我们就会希望知道:
1、我们定义的变量在哪里。
2、当程序需要时,应该如何找到这个变量。
因此,我们需要一套良好的规则(至少目前来说是良好的),来存储和查找我们定义的这些变量,这套规则就称之为“作用域”。
1.1从编译过程理解作用域
引擎:从头到尾负责整个JS代码的编译与执行过程
编译器:负责语法分析和代码生成
作用域:负责收集并维护由所有声明的变量组成的查询,并按照一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
三个步骤:
1、分词/词法分析:字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元 =>
2、解析/语法分析:将词法单元流(数组)转换成一个由元素逐级嵌套所组成的,代表了程序语法的结构树(抽象语法树 AST)=>
3、代码生成:将AST转换成可执行代码的过程
运行下面代码:
cosnt name = "tang";
在我们写代码时,通常会认为是我们声明了 name=“tang”,但是,引擎却不这么认为。
在引擎眼里,这是两个完全不同的声明,一个由编译器在编译时处理,另一个由引擎在运行时处理。
工作过程:
1、看见const name,编译器会询问作用域,是否有一个叫name的变量,存在于与当前name相同的作用域中,如果有,忽略该声明,继续执行;如果无,在当前作用域声明一个叫name的新变量。
2,编译器为引擎生成运行时所需要的代码,引擎运行时会首先询问作用域,在当前作用域集合中,是否存在一个叫name的变量,如果有,使用该变量;如果没有,继续查找该变量。(查找规则:向父级作用域查找)
变量的赋值操作会有两个动作:编译器在当前作用域声明该变量,运行时引擎会在作用域中查找该变量,如果找到,进行赋值。
因此,引擎与作用域一直在处于一个类似于查询状态。
1.2 作用域嵌套
作用域是根据名称查找的一套规则,因此,当一个块/函数,嵌套在另一个块/函数中,就会发生作用域嵌套,因此,在当前作用域中无法找到某一个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到,当抵达最外层作用域时(全局作用域)为止。
遍历嵌套作用域链的规则为:引擎从当前执行的作用域开始查找变量,如果找不到,就会向上一级继续查找,当抵达最外层作用域时,无论找到还是没找到,查询过都会停止。
2、 词法作用域与动态作用域
作用域共有两种主要的模型:词法作用域 和 动态作用域
2.1 词法作用域
概念:词法作用域是由写代码时,将变量和块作用域写在哪里决定的,因此,大多情况下,词法分析器在处理代码时,会保持作用域不变。
function foo(a) {
const b = a*2;
function bar(c) {
console.log(a,b,c);
}
bar(b*3);
}
foo( 2 )
//输出:2,4,12
1、foo包含着整个全局作用域
2、foo所创建的作用域中,有三个标识符:a,bar,b
3、bar所创建的作用域,有一个标识符c
2.1.1 查找过程
作用域的结构和相互之间的位置关系给引擎提供了足够的位置信息,引擎利用这些信息来查找标识符的位置。
上文bar()的查找过程:首先从最内部开始查找(注意:查找过程是从内到外),在当前bar()的作用域中,引擎没有找到a,b,因此到bar()的父级作用域(foo()形成的作用域)去查找,找到了就引用,对c来说,在当前作用域便找到了它。
2.1.2 遮蔽效应
遮蔽效应:作用域查找会在找到第一个匹配的变量时停止,在多层的嵌套作用域中定义同名变量,内层变量遮蔽了外层变量。
note:可以在运行时来修改(欺骗)词法作用域,但是这并不是一个良好的实践。一般可以通过eval和with,这通常会导致性能下降,还可能会导致一些奇奇怪怪的结果。
2.2 动态作用域
提到动态作用域,就必须提到this机制(在接下来的文章中会详细介绍),需要明确的一点是,JS机制是不具备动态作用域的(就像JS中没有真正的类一样),只有词法作用域,但是this机制在某种程度上很像动态作用域。
动态作用域并不关心函数和作用域是如何声明以及在何处声明,只关心是从何处调用,作用域链是基于调用栈的,而不是代码中的作用域嵌套。
function foo() {
console.log(a);//输出:2
}
function bar() {
let a = 1;
foo();
}
let a = 2;
bar();
如果JS有动态作用域,上述代码将输出1,而不是2。
当foo()无法找到a变量的引用时,会顺着调用栈在调用foo()的地方查找a。而不是在嵌套的词法作用域中查找。
两种作用域的区别:词法作用域是在定义时确定,而动态作用域是在运行时确定(this也是),词法作用域关注的是函数在何处声明,动态作用域关注的是函数在何处调用。
note: this机制,只关心在何处以及被谁调用,关于this的指向问题,将在以后的文章介绍。
3、函数作用域和块作用域
3.1函数作用域
函数的作用域,可以理解为,每声明一个函数,自动为该函数创建一个作用域气泡,虽然这并不准确。
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用。(在嵌套作用域中也可以使用)
3.2 隐藏内部实现
从写好的代码中,挑选出来任意片段,然后用函数声明进行包装,实际上就是把这些代码”隐藏“起来。
实际所造成的结果就是在这个代码片段周围创建了一个作用域气泡,也就是说,这段代码中的任何声明(变量和函数),都将绑定在这个新创建的包装函数的作用域中,而不是先前的作用域中。
可以把变量和函数包装在一个函数作用域中,然后用这个作用域来”隐藏“它们。
内容私有化是一个很好的实践,如果给予外部作用域对函数私有内容的访问权限,是非常糟糕的,可能会造成一些出人意料的结果,在软件设计原则中,最小限度地暴露必要内容(最小授权原则),将这些变量和函数私有化,可以阻止其他作用域对它的访问权限。
虽然这种方法可以解决一些问题,但是它同时也带来了两个问题:
1、必须声明一个具名函数,这个具名函数污染了所在作用域。
2、必须显式地通过函数名进行调用
JS对以上两点的解决方案为:
(function foo () {
xxxxxxxxx
})()
函数会被当作函数表达式处理,而不是一个标准的函数处理。
note:函数声明会被提升,函数表达式不会被提升(后面的文章将陆续介绍,变量提升,this指向)
如何区分函数声明还是函数表达式?
你就看function 的位置,在头头,就是函数声明,前面有东西,就是表达式。
函数声明和表达式的区别在于,它们的标识符会被绑定在何处:
函数声明,函数被绑定到所在的作用域中,函数表达式,函数被绑定到函数表达式自身的函数中,而不是所在作用域。
IIFE立即执行表达式
(function IIFE(){
})()
(function IIFE(){
}())
两种写法都可以。
3.2块作用域
可以理解为,像for/if/try/catch/while 等等,所拥有的{}作用域
下面着重来介绍ES6中,let和const
let关键字
let关键字可以将变量绑定到所在的任意作用域中,(通常是{···}内部),let 为其声明的变量隐式地劫持了所在块作用域
if(){
let a = 1;
console.log(a);
}
其实,用let将变量附加在一个已经存在的作用域上的行为是隐式的。显式如下:
if(){
{//显式的块
let a = 1;
console.log(a);
}
}
note:let进行声明,不会将变量提升(也就是说,声明的代码在运行之前,声明并不存在),const也不会,只有var会
for (let i = 0; i < 10; i++) {
console.log(i)
}
for (vari = 0; i < 10; i++) {
console.log(i)
}
试着运行这两段代码,观察有何不同。
for 循环头部的let,不仅将i绑定到了for循环中,实际上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值进行重新赋值,也就是这样:
{
let j;
for (j = 0; j < 10 ; j++) {
let i = j;
console.log(j);
}
}
const
const也可以用来创建块作用域变量,但是其值是固定的常量,之后任何修改都将报错。
note:若const声明的为引用类型,可能会有惊喜。
1注释:
参考:《你不知道的javascript》,《JS高程》,网络上的一些资料和视频 ↩︎