萌新出行,闲人让道,以免误伤。
我是一个js的初学者,在es6出来之后才开始学习js,所以接触到的东西大多都已经es6化了,比如函数已经习惯于箭头函数、习惯于使用const和let等等。
据说在es6之前,js是没有块级作用域的,因为当时的所有变量定义都是var,会有变量提升的问题。
什么是块级作用域呢,我所理解的就是“{}”中间的就是块级作用域(object对象除外)。
函数的执行体内部就是一个块级作用域,for循环的每一轮都是一个单独的块级作用域,if{...}else{...}中间都是一个块级作用域。
作用域的作用是干嘛的呢?我浅显的理解就是:隔离变量,不同作用域下同名变量不会有冲突。
在学习作用域之前我先去学习了一下js遗留的问题:变量提升。什么是变量提升呢?就是js执行时会先预加载一遍变量,将使用var、function定义的变量提取出来,先赋予一个初始值。
例如:
var a = 1;var b = 2;function c(){ console.log(111) }
上面这段代码预加载的情况下是这样的:
先将所有的使用var和function定义的变量取出来赋予一个初始值undefined:
var a,b,c;
然后再执行代码,将变量的值一一赋予:
a = 1;b = 2;c = function c(){ console.log(111)}
这就是预加载。预加载有一个遗留的问题,就是可以在变量的定义之前就可以读取到变量:
console.log(a)var a = 1console.log(a)
这里打印出来的分别是 undefined 和 1。因为预加载的时候先把a提取出来赋予了undefined,然后再执行代码,所以第一个打印出来的是undefined,第二个打印在a赋值之后,所以打印出来是1。
预加载会产生一些初学者很懵逼的问题(我当时懵逼很久,死活不懂),比如:
for(var i=0;i<3;i++){ setTimeout(function(){ console.log(i) },1000) } console.log(i)
最下面打印的这个i居然有值!!不可思议!!后来我才知道,这里的i使用var定义,在循环结束后会成为全局变量,因为这里没有块级作用域,不会隔离变量,三次循环使用的i是同一个i。
在这里会打印出来三次3,为什么呢?因为setTimeout是异步加载的,在三次循环执行结束之后,i已经变成3了,并且,这时候三次方法从异步队列中出来继续执行,在自己的作用域中找不到变量i,只能找到全局的变量i,此时的i为3。
这个问题要怎么解决呢?很简单,以前的方法是在每一次循环体里面重新定义一个方法,将定时器封装,把每一次的i作为参数传进去:
for(var i=0;i<3;i++){ function a(i){ setTimeout(function(){ console.log(i) },1000) } a(i)}
这样的话就解决了这个问题。
当然更简单的就是把定义i的方式从 var 变成 let,这样就产生了独自的作用域,有独自的变量i,不会产生变量混淆了。
变量提升的问题说到这,开始学习作用域了。
首先了解一个概念:执行上下文环境。
函数每调用一次,会产生一个全新的执行上下文环境。
有点抽象,我来捋一捋。
使用一个例子:
var a = 1,fn, bar = function(x){ var b = 5 fn(x+b) }fn = function (y){ var c = 5 console.log(c+y)}bar(a)
代码执行过程:
首先预加载:
var a,fn,bar
然后执行代码,赋值:
a = 1;bar = function(x){...}fn = function(y){...}
接着创建执行上下文环境:
执行代码最初始先创建一个全局上下文环境。在执行bar(a)的时候,创建一个bar上下文环境。在bar(a)的时候会执行fn(x+b),此时创建一个fn上下文环境。fn执行结束之后,销毁fn上下文环境。bar执行结束之后,销毁bar上下文环境。所以这段代码执行过程中,执行上下文环境的变化为:全局上下文环境 => 全局上下文环境 + bar上下文环境 => 全局上下文环境 + bar上下文环境 + fn上下文环境 => 全局上下文环境 + bar上下文环境 =>全局上下文环境
在上面过程中,创建新的执行上下文环境称为上下文环境压栈,销毁执行上下文环境称为上下文环境出栈。
看完上面不知道观众懂没懂,我也写的很乱,如果不懂的话继续往下看,后面还要用到执行上下文环境,还会介绍。
再了解一个东西:
函数在定义的时候(不是调用的时候)就已经确定了函数体内部的变量的作用域。
再说一个概念:静态作用域。
创建函数时,函数所处的作用域为该函数的静态作用域。
这两个作用域都是依赖于函数的,但是一个是函数体内部所有变量所处的作用域,一个是函数所处的作用域。
什么意思呢?看代码
var a = 1function fn1(){ console.log(a)}function fn2(){ var a = 2 fn1()}fn2()
答案是多少呢?是1。
为什么呢?因为在fn1定义的时候就已经确定了其所在的作用域,其内部需要用到一个变量a,但是其作用域内部没有变量a,只能在其父级作用域中找,最后找到了全局的a,是1。
这就是函数在定义的时候就已经确定了其内部变量的作用域,谨记。
关于作用域记住以下三点:
首先预加载:var a,fn1,fn2然后创建全局上下文环境。接着赋值:a = 1;fn1 = function fn1(){ console.log(a)}fn2 = function fn2(){ var a = 2; fn1()}然后执行方法fn2。执行fn2方法时预加载:var a创建fn2上下文环境然后赋值: a = 2执行方法 fn1fn1执行预加载: 没有变量。创建fn1上下文环境。执行 console.log(a)在fn1上下文环境中找不到变量a,去函数所处的作用域继续找,找到全局的变量a=1。fn1执行结束,销毁fn1上下文环境。fn2执行结束,销毁fn2上下文环境。
现在知道为什么会打印出来1了吧。在上面这段代码剖析中也解释了第三点,在fn1作用域内部找a,需要找到fn1上下文环境,然后在该上下文环境中找变量a。
再看一个栗子:
var a = 10function A(){ var a = 1000 return function B(){ console.log(a) } }var b = A()b()
答案是多少呢?是1000。因为B方法通过执行A赋值给b,执行方法b也就是执行方法B,其执行上下文环境中没有变量a,只能在其静态作用域中继续查找,找到了变量a=1000。
这里也扯出来一个新的概念:作用域链。
var aa = 1function a(){ function b(){ function c(){ function d(){ console.log(aa) } d() } c() } b()}a()
在这里打印出来的自然是1,新手都看得出来,因为整段代码只有1个全局的变量 aa = 1。但是为什么能在方法d中获取到全局的变量呢?这就是作用域链的作用。
a() --> b() --> c() --> d()在执行方法d的时候需要获取变量aa -->但是在 d上下文环境中没有变量 aa,那么就在 d 的静态作用域中继续找 -->d的静态作用域就是c的内部作用域,c上下文环境中也没有变量 aa -->那么就去 c 的静态作用域中找,也没有,继续向上到 方法b中 -->再到方法a中, 方法a的内部作用域也没有,找到了a的静态作用域,也就是全局作用域,在全局上下文环境中找到了 变量 aa = 1
function f(x){ return function (){ console.log(x) }}var f1 = f(10)var f2 = f(15)f1() f2()
上面这个例子打印出来结果是什么呢?