作用域与闭包

87 阅读5分钟
作用域

作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合

主要用于减少名字冲突 增强程序的可靠性

全局作用域

任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问

局部作用域 (函数作用域)

函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问

函数作用域也曾作为一种模块化的实现方式,通过函数作用域解决命名冲突、污染全局的问题

(function (a) {
    // 在这里面声明各种变量、函数都不会污染全局作用域
})(a)
块级作用域

ES6引入了letconst关键字,和var关键字不同,在大括号中使用letconst声明的变量存在于块级作用域中。在大括号之外不能访问这些变量

{
  // 块级作用域中的变量
  let greeting = 'Hello World!';
  var lang = 'English';
  console.log(greeting); //  'Hello World!'
}
console.log(lang); //  'English'
​
console.log(greeting);// 报错:Uncaught ReferenceError: greeting is not defined
​
​
暂时性死区

在块级作用域内,let命令声明变量之前,该变量不可用,在语法上称为暂时性死区

console.log(a)  // Uncaught ReferenceError: a is not defined
let a = 1 
var xye = 1;
(function () {
    console.log(xye)
    xye = 2
    let xye = 4
    console.log(this.xye)
    console.log(xye)
​
})()
​

以上代码会输出

image-20220903181902359.png

为什么要引入块级作用域的概念?
问题1 出现一些难以预料的变量提升
var myname = 'Hello'
function showName() {
    console.log(myname)
    if (0) {
        var myname = 'World'
    }
    console.log(myname)
}
showName()
​

showName函数中if块呢的代码变量提升了,所以这个函数作用域就先使用了这个函数作用域中的myname了,而内部的myname还没有赋值,是undefined,注意了啊,if(0)虽然是表示里面的代码不会执行,但是编译的前期阶段是不会判断if里面到底是true还是false的,只要遇到了var声明的变量就直接变量提升了,只是后来执行代码阶段的时候不区进行myname的赋值操作,所以下一个myname也是undefined

问题2 本该销毁的i变量并没有被销毁 这就造成了一些内存泄漏问题
function foo(){
    for(var i=0;i<7;i++){
    }
    console.log(i) //7
}
​
作用域链

当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域

如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错

闭包

什么是闭包?

函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。

经典面试题,循环中使用闭包解决 var 定义函数的问题

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}
解决方案1 利用let 的块级作用域解决
for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}
​
//可以将使用var声明的for循环看作
var i = 1; 
i++ // i=2
i++ // i=3
i++ // i=4
i++ // i=5
i++ // i=6 i>5 for () {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
} // 输出6 6 6 6 6
​
​
//可以将使用let声明的for循环看作
{
    let i =1;
    setTimeout(function timer() {
    console.log(i)
    }, i * 1000)
}
{
    let i =2;
    setTimeout(function timer() {
    console.log(i)
    }, i * 1000)
}
{
    let i =3;
    setTimeout(function timer() {
    console.log(i)
    }, i * 1000)
}
{
    let i =4;
    setTimeout(function timer() {
    console.log(i)
    }, i * 1000)
}
{
    let i =5;
    setTimeout(function timer() {
    console.log(i)
    }, i * 1000)
}
解决方案2 利用闭包
for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j)
    }, j * 1000)
  })(i)
}
闭包的优缺点
优点

1、可以从内部函数访问外部函数的作用域中的变量,且访问到的变量长期驻扎在内存中,可供之后使用。

2、避免变量污染全局。

3、把变量存到独立的作用域,作为私有成员存在。

缺点

1、对内存消耗有负面影响,因内部函数保存了对外部变量的引用,导致无法被垃圾回收,增大内存使用量,所以使用不当会导致内存泄漏。

2、对处理速度具有负面影响。闭包的层级决定了引用的外部变量在查找时经过的作用域长度。

内存泄漏与垃圾回收

浏览器一般采用标记清除 引用计数两种算法来进行垃圾回收,找出那些不在继续使用的变量,然后释放其内存

内存泄漏一般是指不再用到的内存,没有及时释放 闭包使用不当会出现内存泄漏问题

function foo(){
let value=123;
    function bar(){
        alert(value)
    }
    return bar
}
​
let mybar=foo()
​
// 这种情况变量value会保存在内存中,如果加上 mybar=null value就会被清除
闭包应用场景
1.缓存
//比如求和操作,如果没有缓存,每次调用都要重复计算,采用缓存已经执行过的去查找,查找到了就直接返回,不需要重新计算    
var fn=(function(){
  var cache={};//缓存对象
  var calc=function(arr){//计算函数
      var sum=0;
      //求和
      for(var i=0;i<arr.length;i++){
          sum+=arr[i];
      }
      return sum;
  }
​
  return function(){
      var args = Array.prototype.slice.call(arguments,0);//arguments转换成数组
      var key=args.join(",");//将args用逗号连接成字符串
      var result , tSum = cache[key];
      if(tSum){//如果缓存有   
          console.log('从缓存中取:',cache)//打印方便查看
          result = tSum;
      }else{
          //重新计算,并存入缓存同时赋值给result
          result = cache[key]=calc(args);
          console.log('存入缓存:',cache)//打印方便查看
      }
      return result;
  }
})();
fn(1,2,3,4,5);
fn(1,2,3,4,5);
fn(1,2,3,4,5,6);
fn(1,2,3,4,5,8);
fn(1,2,3,4,5,6);
2.访问私有变量
function fn(){
    var name="hello";
    return function(){
        return name;
    }
}
var fnc = fn();
console.log(fnc())//hello
3.单例模式
class Person{
   getName(){
       console.log('张三')
   }
}
​
const getSingle = (function(fn){
    var result; 
    return function(){
        if(result) return result;
        return result=new Person();
    }
})()
​
const instance1= getSingle()
const instance2= getSingle()
instance1===instance2 
​
​
4.闭包经典问题
function fun(n,o){
    console.log(o);
    return{
        fun:function(m){
            return fun(m,n)
        }
    }
}
var a = fun(0)
a.fun(1)
a.fun(2)
a.fun(3)
var b = fun(0).fun(1).fun(2).fun(3)
var c = fun(0).fun(1)
c.fun(2)
c.fun(3) 
​
​
​
​
​
// undefined 0 0 0 undefined 0 1 2 undefined 0 1 1