Skip to content

作用域就是代码区域,JS 中有两种作用域(目前),分别是全局作用域和函数级作用域。

全局作用域就是直接写的代码,没有写在函数内部的代码我们都叫做函数级作用域。

js
var test = 1;//这是全局作用域

function t(){
	var test2 = 2;//这是函数级作用域
}

在前面我们也提到过,全局作用域的变量会被提升到代码块的顶部,并且会成为 window 对象的属性,函数级作用域会被提升到函数的顶部,而非脚本的顶部。

在上例中,test 2 变量为函数 t 的变量,其声明会被提升到函数 t 的顶部,但是并不是全局变量,也就并不会提升为 window 对象的属性。

实际上,全局变量会被挂在 window 对象上,这并不是一个好事,因为这污染了 window 对象。我们可以有很多函数,可以拥有很多函数级作用域,但我们只有一个 window 对象,也只有一个全局对象。比如我这里有两个人合作开发,如果都去污染全局对象,都把自己的变量定义在全局对象上,如果有一个变量我们命名重复了,冲突了,就会导致这个变量发生覆盖。

综上,如果我们每个人都在全局环境上去写代码,都把变量定义在全局对象上,这样在我们协作开发的时候,就很容易导致

全局对象的污染看似是无法解决的问题,因为哪怕我们把功能都封装到函数内部,函数本身也是作为全局变量挂载到 window 对象上的,我们只能通过这种方式来减少污染。

但实际并不是这么一回事,如果函数变成了一个表达式,就不会再污染全局变量,如下所示:

js
(function test(){
	console.log("test");
});

将函数加上小括号,是一个比较典型的将函数变为表达式的方式。当函数变成了一个表达式,就不会再出现提升,也就不会再挂载到 window 对象上去。为什么会导致这个问题?因为表达式结束之后,表达式内部的东西是会全部消失的,例如表达式 1+2,这个就是一个表达式,当执行结束后,会返回 3,而此时1 和 2 都会消失。既然不会挂载到 window 对象上,这个函数也就无法再调用了,那这看似毫无意义,但并不是。

首先,我们看函数执行到底是什么,首先,我们先看以下代码

js
function test(){
	console.log(test);
}
console.log(test);

![Pasted image 20240329224043](/notes-assets/02.JavaScript/4.函数基础/assets/Pasted image 20240329224043.png)

我们可以在控制台看到,我们输出这个函数名的时候,输出的其实是这个函数本身,实际上,在一个函数后面加上括号,括号里传递参数,就可以执行这个函数。

同样,当我给函数加上括号之后,它变成了一个表达式,表达式一定有一个返回值,实际上,这个表达式的返回值就是函数本身,所以当我给它后面添加上一个括号,它也会执行这个函数,如下所示:

js
(function test(){
	var a = 1;
	console.log(a);
})();

在上例中,函数会被立刻执行。这样就非常好,在上例中,函数被当做为了一个表达式,所以并不会作为全局变量,也不会挂载到全局对象上,并且里面的内容还可以立刻执行。所以其实我们就可以把这个函数当做一个全局环境,完全解决了全局变量污染。

像上例这样,将一个函数转换为表达式之后立刻调用,这种函数我们称为立即执行函数(IIFE)。

立即执行函数也可以像如下写法

js
(function test(){
	var a = 1;
	console.log(a);
}());

为什么这样也可以调用呢?因为最外面括号的原因,函数已经被转换为了表达式,此时内层括号就可以调用这个表达式。

可以看到,在上面的例子中,立即执行函数执行之后,函数就被销毁了,无法再通过函数名进行调用,所以函数名在这里是一个毫无意义的东西,所以对于函数表达式来说,函数名是可以直接省略的,如下所示:

js
(function (){
	//函数体
})();

综上,立即执行函数可以完美解决全局变量污染的问题,所以在以后写代码的过程中,都建议将代码写到一个立即执行函数里面去,或者说把一个立即执行函数当做是全局环境。

没有函数名的函数我们也称为匿名函数,匿名函数只存在于函数表达式中,即将函数作为一个表达式的时候,可以省略函数名,使用匿名函数

前面我们介绍过,函数的内部和外部是通过参数和返回值建立起来的联系,参数可以将函数外部的数据传递到函数内部,返回值可以将函数内部的东西返回给函数外部。但实际上,函数的内部和外部并不是一个完全隔绝的环境,我们可以认为,函数内部可以访问函数外部,但是函数外部无法访问函数内部。

js
function test(){
	a = 2;
}
var a = 1;
test();
console.log(a);//此时输出2

显然,test 函数将变量 a 修改为了 2,可见函数调用的时候,函数内部是可以访问并修改函数外部数据的。所以函数的内部和外部可以看做是一个单向阻断的关系。对于函数内部来说,他能看到外面的世界,能访问到外部的变量,但对于函数外部来说,函数的内部就是一个完全未知的东西,我们只有通过返回值的方式拿到函数内部产生的数据。

至于这种单向传递的性质具体是什么原理,将在 JS 进阶部分提到

请读者注意一个细节,现在主流的编程语言,包括 JS 在内,均采用词法作用域(静态作用域),即作用域是在函数创建的时候确定的,和函数的调用位置无关,看下例。

js
function test1() {
    var a = 1;
    test2();
    console.log(a);
}
function test2() {
    a++;
}
test1();

在这个例子中,函数是会直接报错的,可以看到,test 2 在调用的时候,上方是有变量 a 的,但 test 2 函数中的 a++是访问不到这个变量 a 的,因为这个 a 是 test 1 函数的变量。而 test 2 的作用域是在函数定义的时候确定的,可以看到 test 2 的定义是在全局的,所以 test 2 只能访问到自己内部以及全局的变量,尽管是在 test 1 函数内部调用的,也无法访问 test 1 内部的局部变量。

js
function test1() {
	function test2() {
	    a++;
	}
    var a = 1;
    test2();
    console.log(a);
}
test1();

如果我们改成这样,test 2 函数定义在了 test 1 函数的内部,就可以访问到 test 1 函数的变量了。

在实际开发中,我们其实并不常将一个函数声明到另一个函数内部,只有在某个函数比较复杂,需要另外一些函数辅助它完成一些功能,并且这些函数仅仅只会在这一个函数里用到,只有在这种情况下,我们才会考虑将这些函数声明在这个函数内部。

函数内部声明的变量名和外部出现冲突时,优先使用函数内部的变量,如下

js
var a = "外部";
function test(){
	 var a = "内部";
	 console.log(a);//输出内部
}
test();

JS 作用域的原理性内容涉及到非常多的知识,这些知识非常复杂,所以将会在进阶部分介绍。

接下来介绍一下闭包这种现象,这种现象实际上就是内部函数可以使用外部函数的变量和函数。就像我们前面提到的那样,函数内部可以访问函数外部所定义的变量,这就是闭包。闭包其实也是一个值得深入学习的内容,并且难度也不低,所以也放在后面来介绍。

以笔记为镜,可以知得失