大厂面试题系列-初识作用域
前言:
前端面试必考题-作用域,算是入门级必须掌握的一块知识,JavaScript中有一个特性叫作用域(Scope)
scope: 每个javascript函数都是一个对象,对象中有些属性是我们可以访问的,但是有些不可以,scope 这个属性就是其中之一,它只能被javascript引擎存取
话不多说,先上一段简短的代码体验一下作用域:
例1.1
function foo () {
var a = 1
console.log(a);
}
foo()
console.log(a);
看完上面这段代码,请问打印出的2个a的值是多少呢?都是1吗?相信有基础的同学一眼就能看出答案:第一个a的值为1,第二个a应该会报错( a is not defined)。这个时候不知道有没有同学会觉得,a不是等于1吗?为什么会报错呢?这就是作用域了,下面我们就一起来揭秘一下~
一.初识作用域
注:有基础的同学可以直接跳过这一节
1.什么是作用域
小黄书上对作用域的解释是:作用域是一套设计良好的规则来储存变量,并且之后可以方便的找到这些变量。刚接触js的小白们看不懂也不要紧张,接着往后看,看完还不懂评论区留言我上门一对一辅导哈哈哈~ 话不多说,接着上代码:
例1.2
function foo (a) {
var b = a * 2
function bar (c) {
console.log( a,b,c );
}
bar( b * 3 )
}
foo(2)
在上面的例子中,存在着三个作用域:
- 整个全局的作用域,在此作用域中只存在 foo 一个标识符。
- 函数foo所创建的作用域,在此作用域中存在 a,bar,b 三个标识符。
- 函数bar所创建的作用域,在此作用域中只存在 c 一个标识符。 下面上个经典的图来理解例1.2当中的作用域:
不同颜色的气泡代表着不同的作用域,这样看作用域是不是更加显而易见。包括作用域中所存在的各个标识符。作用域气泡由其对应的作用域代码块写在哪里决定,它们是逐级包含的。后面会讲到不同类型的作用域,现在只要假设每一个函数都会创建一个作用域气泡就好了。
在此处先介绍一下作用域的两种主要的工作模式:第一种是最为普遍的,被大多数编程语言所采用的词法作用域,后面会对词法作用域进行深入的讲解。另外一种叫作动态作用域,后面也会略做讲解。
2.作用域的作用是什么?
作用域是在运行的时候,对某些变量,函数和对象的可访问性。什么意思呢?我们还是来看一段代码:
例1.3
function foo () {
var a = 1
}
foo()
console.log(a); // a is not defined
为什么这里打印a的值会是not defined呢? 正是因为变量a是在函数foo中定义的,所以在全局环境下打印变量a自然会报错。
所以对于作用域的作用是什么我们可以这样理解:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
二.作用域全家桶
在例1.2 和1.3 中我们都提到了全局作用域和函数作用域,那么在这一节中我们将会对‘作用域全家桶’有个深入的了解。
1.全局作用域
什么是全局作用域?简单通俗的来说就是:定义在全局下的变量或函数都处在全局作用域当中。
那么什么时候会产生全局作用域呢?话不多说,接着上代码:
(1)定义在全局的变量和函数,就会产生全局作用域。
例2.1
var a = '我是全局变量' // 定义在全局的变量a
function foo() { // 定义在全局的函数
var b = '我在函数里面' // 定义在函数foo中的变量b
function bar() { // 定义在函数foo中的函数
}
}
变量a和函数foo就处在全局作用域下,而变量b和函数bar在函数foo所产生的的函数作用域当中。
(2)直接赋值的变量
例2.2
function foo() {
a = 3
var b = 2
}
foo() // 执行foo才能执行里面的代码
console.log(a); // 3
console.log(b); // b is not defined
为什么这里的变量a会有值呢?因为变量a未声明而是直接赋值,所以会自动放入全局作用域中,而变量b就是正常情况下,定义在函数foo作用域中的变量在全局下无法访问。
全局作用域的弊端
例2.3
var a = 2
var a = 4
console.log(a);
如上所示,一旦代码量过于复杂,可能就会存在变量名相同的情况,导致命名冲突。
2.函数作用域
什么是函数作用域呢?简单来说:函数作用域就是在声明函数的时候,会产生一个作用域,这个作用域就被称为函数作用域。先上代码:
例2.4
function foo() {
var a = 2
bar()
function bar() {
var b = 1
console.log(b); // 1
console.log(a); // 2
}
console.log(a); // 2
}
console.log(a); // a is not defined
foo()
在这段代码中,第一个打印b为1,是因为变量b是在函数bar的作用域中定义,所以可以正常打印出b的值。第二个打印a为什么值为2,这是因为在函数作用域中,内部的作用域可以访问到外部作用域的变量,而通过第四个打印a的值为not defined 可以看出来,从外部是无法访问到函数内部变量的。
3.块级作用域
ES6 中引入的块级作用域,增加了对变量的可控性,大大的提高了变量使用的灵活性。
那么块级作用域在什么时候产生呢?-块级作用域可通过新增命令let和const声明,所声明的变量在指定块的作用域外无法被访问。一个块级作用域可以理解为一对花括号{}之间的区域。接着上代码:
(1)函数中的块级作用域
例2.5
function foo() {
let a = 1
console.log(a); // 1
}
foo()
console.log(a); // a is not defined
用let声明的变量a只在函数foo中有定义,而在此块级作用域外则没有定义。
(2)for循环中的块级作用域
例2.6
for(let i = 0;i < 3;i++) {
console.log(i); // 0,1,2
}
console.log(i); // i is not defined
变量i只在for循环中有定义,因为用let声明变量i产生了块级作用域,而在for循环外面则无法访问到变量i。
相信很多小伙伴看到例2.6的时候会有这么个疑惑:let声明的变量i到底是在括号内的作用域中还是在for循环内部的{}中呢?让我们来看一段代码:
例2.7
for(let i = 0;i < 3;i++) {
let i = 'a'
console.log(i); // a,a,a
}
没错,循环体打印出了三个a,这说明在for循环当中,在括号内用let声明的变量会产生一个父作用域,而在循环体内会产生一个子作用域,那么为什么例2.6可以打印出1,2,3呢?这是因为内部作用域可以访问到外部作用域,在这里就是说,子作用域可以访问到父作用域的变量i,看到这里相信你一定对块级作用域有个认知了吧。
注意:不可重复声明变量
例2.8
var a = 2
let a = 1 // Identifier 'a' has already been declared
在一个作用域中重复声明变量则会报错。
三.作用域收关
看完了上面两节的内容相信大家已经对作用域有个初步的认识了吧,在第三节中,会聊到一些拓展以及作用域之后相关的内容,在后续也会更新发布本系列。
1.作用域相关知识概要
在本文中只谈到了作用域的概念和一些不同的作用域,其实在作用域中还存放着一个叫做执行上下文的东西,以及作用域的查找规则和变量提升等内容,要学习这些内容必须熟练掌握作用域的知识所以在后续会发布相关文章来详细讲解面试系列。
2.作用域产生的作用域链
在此先简单的提到一下由许多作用域产生的作用域链,本系列中将作用域和其他相关内容分开,可以将每个内容的部分更加详细的描述到。
作用域链:scope 中存储的执行期上下文对象的集合,这个集合成链式连接,我们把这种链式连接叫做作用域链
觉得作者写的还行的可以动动小手点个赞支持一下哦,也可以关注作者,后续会写一整套面试系列内容~ 本文中存在错误也可以在评论区讨论哦!