js作用域通识篇(1)

463 阅读6分钟
原文链接: damobing.com

前言

相信自从es6出来之后,你一定多少知道或者已经在项目中实践了部分的块级作用域,在函数或者类的内部命名变量已经在使用let了,但是你知道它真正的作用是什么吗?又是因为什么我们要用这个块级作用域,本文与你一起探讨块级作用域的场景以及所有细节问题。

es6之前的作用域

特点1 :js只有函数级作用域以及全局两种
特点2 :不通过var声明的变量直接赋值也可以用并且可以访问,原理是直接赋值到了window对象属性变量下,两者如果同时定义,那么会覆盖使用,因为访问方式一样,两者都可以通过window属性值访问到。

//通过var 申明的
var a=13
console.log(window.a)//13
a=14
//通过定义到window下
b=13
console.log(b)//13
var b=14
console.log(a)//14
console.log(b)//14

特点 3:var定义的变量与直接window对象属性变量的区别,window属性变量可以通过delete(window.attrname)删除 ,而通过var申明变量只能内存回收,使用时永久有效。(具体文件报错还有显示报错可以自行尝试,没有定义和没有初始化还是有区别的)

var a
console.log(a)//打印undefined,没有初始化值,(申明过的不会直接文件报错)
a=12
delete(window.a) //删除失败 ,仍然可以访问
b = 12
console.log(b)//12
delete(window.b)//返回true,b不可以再被访问
console.log(b)//Uncaught ReferenceError 文件报错

特点4:语句块是没有局部作用域的,在下面的语句块中,与外部是完全一致的

var a=12
if(true){
  var a=13
  var b=13
  console.log("a:"+a)//13
  console.log("b:"+b)//13
 c=14
 console.log("c:"+c)//14
}
console.log("a:"+a)//13
console.log("b:"+b)//13
console.log("c:"+c)//13

特点5 :函数内有局部作用域,函数内可以访问外部作用域,而全局是不可以访问函数作用域内的变量或者方法的

var a=b=c=12
function demo(){
  var z=13
  console.log(b)//12
  console.log(z)//13

}
demo()
console.log(z)//undefined

特点6:函数内部的变量对于其内部的函数或者对象都是可访问的

function demo(){
  var z=13
  return (function(){
    console.log(z)
  })()

}
demo()//13

特点7:如果函数内部不加var申明,并且全局定义过的,使用的是全局变量并且改变影响的也是全局变量;如果函数内部不加var申明,并且全局没有定义过的,会定义到window对象属性下并造成全局变量的污染;如果加了var申明 那么使用的是自己的变量,不会影响全局变量也不会造成全局变量污染;

var a=b=c=12
function demo(){
  var a=13
  b=13
  z=15
  console.log(a)//13
  console.log(b)//13
  console.log(c)//12
  console.log(z)//15
}
demo()
console.log(a)//12
console.log(b)//13
console.log(c)//12
console.log(z)//15

闭包

为了解决特点5中暴露的问题,也就是外部环境不能使用函数内部变量,而我们实际的场景中是需要的,我们利用特点6可以解决,具体的方案就是闭包的方式。
* 代码案例

function package(){
  var private_t=123
  return (function(){
       return private_t
  })
}
var t=package()
console.log(t())
  • 优点
    保护了函数的私有变量和方法,利用了函数的链接作用域的特点,同时可以对外暴露部分,将我们需要的部分保留在内存中。
  • 缺点
    占用内存,因为一般不用的变量会被gc回收,但是这部分闭包写的变量因为一直被申明占用而不被回收。解决方案是增加一个临时副本,函数结束时将不必要的局部变量删除。代码如下:
    ~~~
    function pack(){
    var private_t=123
    return function(){
    var temp=private_t
    console.log(temp)
    temp=null
    }
    }
    pack()()//123
    //如果你觉得上面写两个方法执行比较麻烦,也可以直接返回自运行函数
    function pack(){
    var private_t=123
    return (function(){
    var temp=private_t
    console.log(temp)
    temp=null
    })()
    }
    ~~~
  • 注意事项
    因为返回方法中可以修改以及得到对应的私有变量,要避免将其作为公用方法而修改私有变量造成其他人使用时出错。这种建议针对私有变量分析清楚是否需要作为可修改属性,还是只读属性。

块级作用域

场景一 循环中的块级作用域

如果我们有一个遍历循环的绑定事件,并且需要把当前的指针绑定到对应方法中。

var arr=[]
for(var i=0;i<10;i++){
  arr[i]=function(){
    console.log(i)
  }
}
arr[2]()//10 
//不是你预想的2,原因是函数执行时,对应的i已经变成了10而不是函数定义时的2
//解决方案 1 闭包 将当时的变量i当做参数传入函数中
var arr=[]
for(var i=0;i<10;i++){
  arr[i]=(function(temp){
   return function(){
     console.log(temp)
   } 
  })(i)
}
arr[2]()//2 
//解决方案 2 块级作用域 ,利用let块级作用域特性,区别就是定义变量时 i是块级变量,所以定义的函数中的变量也是当时的块级作用域,不随外面非块级元素值变化影响
var arr=[]
for(let i=0;i<10;i++){
  arr[i]=function(){
     console.log(i)
  }
}
arr[2]()//1
console.log(i)//undefined

场景二 结合this以及函数运行环境 执行环境的理解,通过这个案例也能更好的理解闭包。

var name='window'
var  demo={
  name:'project',
  sayname:function(){
    return function(){
      console.log(this.name)
  }}
}
var  demo2={
  name:'project',
  sayname:function(){
    var that=this
    return function(){
    console.log(that.name)
  }}
}
demo.sayname()()//window
demo2.sayname()()//project

场景三 多变量在不同包含以及不包含区块重名

我们经常会遇到经常性的用一个或者多个变量名重名导致的各种问题,包括在循环以及不同的语句块中。他们会互相影响,导致我们不得不重新命名或者细致的区分要不要使用原来的变量做某个事情。

if(true){
  let i=2
  console.log(i)//2
  if(true){
    let i=5
    console.log(i)//5,使用的是自己区块变量
  }
  console.log(i)//2,仍然是自己区块变量,内部其他let同名不影响自己
}
if(true){
  let i=3
  console.log(i)//3,其他同级区块不影响自己
  if(true){
    console.log(i)//3,子区块可以链式得到父区块变量
    i=4
    console.log(i)//4 ,子块级直接修改父块级变量
  }
  console.log(i)//4 ,看到修改了的结果,已经变成4
}

变量提升(js-hoisting)

场景一 变量可以先使用后申明,其申明会自动提前

a=12
console.log(a)
var a //这句申明会提前 ,在window环境下不会报错是因为赋值在了window下
//以上代码等效于
var a
a=12

场景二 :变量初始化不会提前(申明提前了,但是赋值没有)

console.log(b)//undefined
var b=12
var j=1;
function demo(){
    console.log(j);//提示undefined ,因为布局有定义变量,先声明了变量,而赋值延迟
    var j=3;
    console.log(j);
}
demo()

综上

建议大家尽量避免变量提升的问题,在变量使用前定义并初始化为你需要的值。

参考文档