关于js中的作用域

萌新出行,闲人让道,以免误伤。

我是一个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 = 1
console.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 = 1
function fn1(){
  console.log(a)
}

function  fn2(){
  var a = 2
  fn1()
}

fn2()

  答案是多少呢?是1。

 

  为什么呢?因为在fn1定义的时候就已经确定了其所在的作用域,其内部需要用到一个变量a,但是其作用域内部没有变量a,只能在其父级作用域中找,最后找到了全局的a,是1。

  这就是函数在定义的时候就已经确定了其内部变量的作用域,谨记。

  关于作用域记住以下三点:

    ① 作用域只是一个“地盘”,一个抽象的概念,其中没有变量,但是变量却都有一个所处的作用域,要通过作用域对应的执行上下文环境来获取变量的值。
    ② 函数体中变量的值是在 执行过程中产生的确定的,而作用域却是在函数创建时就确定了。
    ③ 如果要查找一个作用域下某个变量的值,就需要找到这个作用域对应的执行上下文环境,再在其中寻找变量的值。
  上面三点中,第一点比较好理解,因为作用域是一个抽象的东西,并不具体。但是我一直在说 “XX函数的作用域内部的变量” 这种话只是为了便于理解,其实是有问题的,毕竟作用域内部没有变量,但是变量都有一个自己所处的作用域,用来隔离不同变量。
  第二点中这个变量的值执行过程中产生并确定的。联系一下上面说到的执行上下文环境,把上面这段代码剖析一下:
首先预加载:

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
执行方法 fn1

fn1执行预加载: 没有变量。
创建fn1上下文环境。
执行 console.log(a)
在fn1上下文环境中找不到变量a,去函数所处的作用域继续找,找到全局的变量a=1。

fn1执行结束,销毁fn1上下文环境。
fn2执行结束,销毁fn2上下文环境。

  现在知道为什么会打印出来1了吧。在上面这段代码剖析中也解释了第三点,在fn1作用域内部找a,需要找到fn1上下文环境,然后在该上下文环境中找变量a。

  再看一个栗子:  

var a = 10
function 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 = 1
function 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
   这就是作用域链,从你执行的方法的内部作用域开始寻找所需的变量,一直找到全局作用域,如果找到全局作用域都没有找到所需变量的话,就会报错“xxx is not defined”。
 
最后说一下作用域和执行上下文环境的区别:
  1)执行上下文环境是用来存储变量的, 是在方法调用的时候创建,会因为调用环境不同而产生不同的执行上下文环境。
      作用域是用来区分“地盘”的,也就是隔离变量,在两个不同作用域内部定义的相同名称的变量是不同的。 作用域在方法定义的时候就确定了
  2)一个作用域中可以有多个执行上下文环境。
function f(x){
  return function (){     
    console.log(x)
  }
}
var f1 = f(10)
var f2 = f(15)
f1() 
f2() 

  上面这个例子打印出来结果是什么呢?

  在上面代码中,f运行后会返回一个匿名函数,并在这个匿名函数中调用了属于 f 作用域的 变量x,所以 f 执行结束后不会销毁上下文环境
  第一次执行,作用域中会新产生一个上下文环境,其中 x=10
  第二次执行,作用域中会再次产生一个新的上下文环境,其中 x=15
 
以上全部就是我在学习作用域过程中总结出来的东西了~

猜你喜欢

转载自www.cnblogs.com/FraudulentArtists/p/9835322.html