今天小编就给咱们初学js的小白来分享一些干货知识,js作用域的详细介绍。
首先,什么是作用域呢? 作用域是在程序中定义变量的可访问性和生命周期范围。简单来说,作用域规定了在代码中某个特定位置可以访问哪些变量。像js的作用域就可以分为全局作用域、函数作用域、块级作用域三大作用域。
全局作用域
全局作用域很好理解,就是针对全局的范围。其中定义的变量在整个程序中都可以被访问。在浏览器中,全局作用域是指 window
对象。
函数作用域
就是在函数体内的范围。在函数内部定义的变量只能在该函数内部访问。
块级作用域
ES6 引入了 let
和 const
关键字,使得可以在 {}
内部创建块级作用域。简单来说就说带有花括号{}的范围,常见为if和for后面的花括号,但是里面需要let声明。
在聊js的作用域前,咱们先来看看下面这段简单的代码:
scss
var a = 1
function foo(){
console.log(a)
}
foo()
运行结果:1
js在运行代码时首先会先进行编译,也就是先获取有效标识符(包括变量和函数的声明),然后再来执行各种操作,编译的时候就是从上到下获取各个变量。像上面这段代码,到第一行代码时,知道定义了一个变量a,并且值为1,到第三行代码时,发现是一个函数体,这里我们先不用管它,因为没有执行的命令,所以先跳过3~5行的代码,到了第七行时,需要执行foo函数,这时候我们再来看这个foo()函数体,对它进行编译,里面没有任何变量的声明,于是执行console.log(a),这时就要找a这个变量,这里查找变量有个很重要的规则,就是从内部作用域往外部作用域进行查找,不能从外部往内部作用域进行查找 ,例如这里会先在foo函数体内查找a这个变量,发现并没有a,于是到全局作用域进行查找,找到a,并且值为1,于是打印a的值。
作用域有一个重要的点:内层作用域是可以访问外层作用域的,反之则不行。
接下来和大家聊一下声明提升,先看下面这段代码
ini
console.log(a);
var a=1;
运行结果:undefined
原因:变量a的声明提升到代码的最前面,但是没有初始化,所以输出undefined 这个现象在js中是很常见的,因为js是动态语言,变量的声明和初始化是分开的,所以变量的声明提升到代码的最前面,但是没有初始化,所以输出undefined 没有值.你可以理解为这样子:相当于 var a; console.log(a); a=1;
还有的小伙伴在运行程序时可能会出现not defined ,以及NaN各类问题,是啥原因呢?根据刚刚我们谈到的变量查找规则我们引入下面几个案例帮助大家理解
javascript
function foo(){
var b = 3
}
console.log(b)
运行结果:b is not defined
其实这个结果就是报错,console.log(b)这个语句在全局作用域中,因此找b需要在全局作用域来找,但是b只在函数作用域中出现了,刚刚说了不能从外往内找,因此找不到b的值就报错了。
我们来看一个比较特殊的情况:
css
var b = 1
function foo(a , b){
console.log(a + b)
}
foo(2)
运行结果:NaN
给大家解释一下什么是NaN,在js中,NaN是一个特殊的数值,表示非数字(not a number),他是一个全局属性,通常作为一个无效或未定义的数值出现。这里的函数里面用了形参,第3行里面的a,b都是形参,第7行的2是实参,但是只传给了a这个形参,形参的传值只能通过实参,然而实参中并没有传递b,即使全局中有b这个值也不行,因此a + b等于2 + undefined = NaN。这里因为形参就相当于给b声明了,但是b没有值,所以b是undefined不是is not defined.
再来看一个案例:
css
var b
function foo(a){
console.log(a + b)
}
foo(2)
b = 1
运行结果也是NaN
说到这里相信大家应该也可以理解为什么是NaN了,代码是从上往下运行的,编译到第5行的时候,已经知道了全局变量有b这个变量,但是没有赋值,因此这个时候执行函数的时候,b是没有值的,还没执行到第6行就需要知道b的值,显然是有错的。因此结果就是a + b = 2 + undefined = NaN
接下来想问大家一个问题,大家看看这段代码结果是什么?
ini
var a=1;
function foo(){
var a=2;
console.log(a);
}
foo()
打印出1?还是2? 显然结果是2,因为内部函数体内作用域里就有a和它的值,虽然它也能访问外部作用域的a,但时这里它肯定会先用自己的a嘛,你可以这么理解,你自己有钱你肯定不会去用你爸的钱嘛,肯定是先花自己的钱嘛。
其实本质上的原理是:当V8执行到这份代码时,会维护出来一个调用栈,有点类似数据结构里栈的概念,栈就相当于一口井,一端开口,先进后出,当它从上往下运行时,会先把全局里的a先放入栈底部,局部作用域里的a放入顶部,就会被先访问。
块级作用域:
接下来就和大家来说说块级作用域,块级作用域其实是let引出的作用域,let跟var是同样类型的东西,也是用来声明变量的,如果{}里面有let,那么这个{}就形成了一个块级作用域。因此块级作用域与关系很大。
let
let的作用域变量查找的规则和var还是一样的,依旧只能从内到外找,不能从外到内找。其实let的出现就是为了弥补var的缺陷,但是var的优点还是保留在let身上的。
先来看个var的案例:
scss
function foo(){
console.log(a)
var a = 1
}
foo()
我们肯定不会这样写代码,不合常理嘛,这里全局只有一个函数的声明,没有任何变量,因此执行到第5行的时候编译器开始对foo()进行编译,第2行执行语句先不管,先看变量的声明,找到了a这个变量,接下来再执行语句,编译其实就是在当前的作用域里先找到声明,再来执行语句,这里的console.log和=赋值都是执行语句,由于赋值在后,因此这里的a是没有赋值进去的。let就解决了这一问题,如果这里用let声明,这个代码就会报错。并且我们管这个叫做暂时性死区
其实上面这个代码就等同于:
scss
function foo(){
var a
console.log(a)
a = 1
}
foo()
这个其实就是var的声明提升,就是在当前作用域中,var声明一个变量是会自动放到当前作用域最前面。
举个例子:
ini
let a=1
if (1) {
console.log(a);
let a=2;
}
这里想问大家一下打印出来的值是多少?大家的思维是不是这样,首先if{}与let就形成了一个块级作用域,当程序运行到第3行时,会先在自己的函数作用域内找a的值,但是找不到,就会在全局作用域里找,于是找到一个a=1,于是结果就是1.但是结果是这样吗,不是,结果报错了。为什么呢?其实这个就是let独有的语法,就叫暂时性死区,此时if这个作用域形成的语句变成了一个死区了,它不让你访问全局作用域里的,但是你自己家里的又没有先声明访问不到,就叫暂时性死区,总结出来就是这个意思,你寄存在于块级作用域里面,而又因为没有声明提升导致你访问不了这个变量,这种情况下你也没有能力访问外面的变量.
let vs var
1.var 存在声明提升,let不存在
2.let 会和{}形成块级作用域,
3.var可以重复声明变量,let 不可以
欺骗词法作用域
eval案例:
css
function foo(str,a){
eval(str)
console.log(a,b)
}
var b=2;
foo('var b = 3',1)
这个eval(str)函数对大家肯定非常陌生,按正常结果打印出来应该是a为1,b为2.但其实不是,打印出的是a为1,b为3,其实这行代码就等同于var b = 3 ,也就是说将下面的代码搬到了这里,在这里定义var b = 1。起到一个欺骗的作用,将全局作用域搬到函数作用域里面。
1.eval()函数可以将字符串当作代码执行
2.将原本不属于这里的代码变成就像天生就定义了在这里一样
还有一个例子就是with(obj){}这个函数,这个函数的作用是批量修改对象的键值对
ini
var obj = {
a: 1,
b: 2,
c: 3
}
with(obj){
a = 2,
b = 3,
c = 4
}
console.log(obj)
运行结果:{ a: 2, b: 3, c: 4 }
with案例:
css
function foo(obj){
with (obj){
a=2
}
}
var o1={
a:3
}
var o2={
b:3
}
foo(o1)
console.log(a)
运行结果:2
这里全局并没有a这个参数,但是结果却是有的,因为在执行foo(o1)的时候,with(o2)发现o2这个对象没有a这个属性,于是with会将a这个属性泄露到全局作用域中。
with(){} 用于修改一个对象中的属性值,但如果修改的属性在原对象中不存在,那么该属性就会被泄露到全局中。
今天关于js作用域的分享就到这里啦,感谢大家的观看!如果有大佬发现我理解不对的地方欢迎批评指正。最后如果觉得本文对你有帮助的话可以给小编点个赞赞嘛。感谢感谢!