为什么要了解作用域呢?
要想深刻地掌握一门编程语言,我们不仅要会用,还要了解一些底层基础和规则,作用域便是其中的一点。
什么是作用域呢?
- 我们又称 js 是浏览器的脚本语言,我们需要知道,我们用 js 写完一行代码之后,浏览器读取到这行代码,会不会第一时间去执行这行代码呢?
js
var a = 1
a = 'hello'
以上代码合理吗? 显然合理,但是为什么我们定义的 a 既能是数字类型,又能是字符型呢?首先,我们要有这些概念
- 弱类型语言,其定义一个变量不需要声明类型
- 强类型语言,其定义一个变量需要声明类型
强类型语言在书写时会给你一些更严谨的逻辑提示,而弱类型的语言则很自由,这样就会出现类型给错的等一些问题,这就需要编译器先来梳理一下这些代码,再交给执行引擎去执行。而 JavaScript 是弱类型的动态语言,所以回到最上面那个问题,浏览器并不会第一时间去执行这行代码。所以对于 JavaScript 这门语言来说,执行代码之前是需要先编译的。
- 那么编译器是如何编译的呢?
js
var a = 1
function foo() {
console.log(a);
}
foo()
还是这个代码,浏览器在执行之前会叫编译器先编译,编译器从上往下、从左往右梳理代码,发现代码中有变量(有效标识符) a 和 foo(),那么这个有效标识符 a 和 foo()定义在哪呢?这就引出了一个概念------作用域。如图有两个作用域:
作用域:作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性,这个可能有点难以理解,通俗来讲,作用域一个隔离带,将里面的有效标识符和外面的有效标识符分开,防止产生矛盾。
作用域的类型及作用
作用
首先,我们需要知道代码的执行有一个规则,变量的查找会先从内到外的作用域中查找,不能从外到内,如下
js
var a = 1
function foo() {
var a = 2
console.log(a);
}
foo()
在上示代码中,全局下有一个变量 a 和函数 foo 。存在如下两个域,那么这里输出的结果是什么呢?是 1 ?还是 2 呢?
在执行代码前是需要先编译的,编译之后我们发现全局下有一个变量 a 和 foo() ,往下有一个函数foo()的调用,同理,在执行调用之前,我们会先编译函数体foo()里面的内容,发现里面也有一个变量 a ,于是console.log(a)就在当前所处域中找到变量 a 的值,然后输出,所以输出的结果是 1。作用域的使用提高了程序逻辑的局部性,增强了程序的可靠性,减少了名字冲突。
声明提升(补充)
在讲作用域的类型之前,我们先补充一个小知识。
- 1、var声明
js
console.log(a);
var a = 1
你们认为这两行代码输出的结果是什么呢?可能很多人都不约而同地认为是 1 。事实上,输出的结果是 undefined,为什么呢?按照正常的逻辑,我们先编译,发现全局里有一个变量 a ,然后执行 console.log(a) 语句,发现找不到 a 的值,因为代码中,赋值是在 console.log(a)后执行的。实际上,也就相当于执行了这样一段代码:
js
var a
console.log(a);
a = 1
即var 声明的变量 存在声明提升,提升到当前作用域的顶端。
- 2、函数声明
js
foo()
function foo() {
console.log(123)
}
这里输出的结果是什么呢,能输出吗?是 123 吗?没错,这里输出的结果就是 123。按照我们正常的思维,怎么能没有声明就直接调用函数呢,真的难以理解。事实上,这里也存在声明提升,即相当于执行了这样一段代码:
js
function foo() {
console.log(123)
}
foo()
}
函数的声明提升和var的声明提升有一点不同,var只是提升变量 ,而函数提升的是整体 ,即函数声明会整体 提升,提升到当前作用域的顶端。
- 3、let声明
js
console.log(a);
let a = 1
这里能输出 1 吗?还是说也是undefined,事实上都不是,这里会报错,这个才符合我们正常的思维,这里没有先声明变量 a 就直接访问了变量 a ,显然是错误的,所以这里会报错。即let不会声明提升
js
let a = 1
let a = 2
console.log(a);
那么这里会输出 2 吗?事实上也不会,这里也会报错,声明已经存在不能重复声明。即let不能重复声明同一变量
- 4、const声明
js
console.log(a);
const a = 1
同let,这里也会报错。即const不会声明提升
js
const a = 1
const a = 2
console.log(a);
同let,报错。即const不能重复声明同一变量
js
const a = 1
a = 'hello'
console.log(a);
这里也会报错,用const 声明的变量不允许修改值。
全局作用域
变量在函数或者代码块{}外定义的即为全局作用域。
js
var a = 1
console.log(a);
在上面的代码中,函数体外有一个变量 a ,它是全局变量,在全局发挥作用。
函数作用域
顾名思义,在函数体内部定义的变量即为函数作用域。
js
var a = 1
function foo() {
var a = 2
console.log(a);
}
foo()
- 在上示代码中,全局下有一个变量 a 和函数 foo 。而函数 foo() 有一个也有变量 a ,它只能在函数体内发挥作用,即函数作用域。
块级作用域
let/const + {} 会形成块级作用域。
js
if(1) {
var a = 1
}
console.log(a);
这里if语句形成了作用域吗?假设形成了作用域,根据代码执行的规则,我们执行console.log()语句时,便会找不到 a 的值,而事实上,输出的结果是 1 。所以if语句并没有形成作用域。那么将i语句中的var改成了let/const呢?
js
if(1) {
let a = 1
}
console.log(a);
输出结果:ReferenceError: a is not defined,报错了,说明这里形成了作用域。换成for语句:
js
for(var i = 0 ; i < 5 ; i++) {
let a = 1
}
console.log(a);
同样,输出结果:ReferenceError: a is not defined,说明这里形成了作用域。这里将let换成const也是一样的。我们把let/const + {} 形成的作用域叫做块级作用域。再看:
js
let a = 1
if(true) {
console.log(a);
let a = 2
}
按照我们上面所说,let不能声明提升,所以console.log(a)访问不到自己所处作用域 a 的值,然后去外层作用域找,所以这里是不是应该输出 1,实际上是不对的,这里会报错,let+{}形成的作用域有一个规则:自己作用域有,但是访问不到,也不允许去访问外面的,这叫做暂时性死区。
欺骗词法作用域
- 1、
js
function foo() {
var a = 1
console.log(a,b);
}
foo()
显然,这里会报错,找不到 b 的值,那么我们这样改:
js
function foo(str) {
eval(str)
var a = 1
console.log(a,b);
}
foo('var b = 2')
输出结果:1 2 为什么呢?这里eval的作用相当于把原本不属于这个作用域的代码搬过来,即相当于如下代码
js
function foo() {
var b = 2
var a = 1
console.log(a,b);
}
foo()
eval把编译器也"骗过了",
- 2、
with函数可以批量修改声明的对象中的某一些属性。
js
function foo(obj) {
with(obj){
a = 2
}
}
var o1 = { b : 4}
foo(o1)
console.log(o1);
这里输出的结果是 2 ,这里是把对象o1中的 a 改成 2,但实际上对象o1中并不存在属性 a,当with修改对象中的属性时,当对象中不存在这个属性,with就会将这个属性泄漏到全局,让其变成全局变量,所以这里输出的结果是 2。对于编译器来说,"一头雾水",因为全局实际不存在这个变量,with将它"骗过了"。
- 3、
当不写关键字声明对象时,不管写在哪都认为是全局变量
第一篇文章,如有错误,请大家指正,感谢!