likes
comments
collection
share

浅谈JavaScript作用域和闭包

作者站长头像
站长
· 阅读数 18

作用域(scope)

一个变量的作用域是程序源代码中的定义这个变量的区域 通俗的说就是定义一个变量,所有能访问到该变量的所有区域

执行上下文/执行环境(execution context)

在介绍作用域之前,先了解一下执行上下文

执行上下文定义了定义了变量或函数有权访问的其他数据,决定了它们各种的行为

每个执行上下文都有一个变量对象,该对象保存了该环境中定义的变量和函数。

执行上下文和作用域的关系 上下文关联了变量对象决定了函数可以访问哪些数据,而作用域则是决定了数据访问的规则

全局作用域和函数作用域

全局作用域,全局都能访问 函数作用域,在函数内部创建的变量的作用域是函数内部

var a =1 ;// a的作用域是全局的

function fun (){
    // 可以在fun中访问a,a是全局变量
    var b= 2 ; // 只能在fun中访问b
     // 在这里能不能访问c呢
     console.log(c)
    if(a<0){
        var c = 3;// c在if中
    }
}

上述代码中有一个疑问,这个疑问可以分为两个部分

  1. 变量c 是在if(){}中声明的,能不能在花括号外使用花括号中声明的变量?
  2. 在上述位置中变量c还没有声明,能不能使用呢?如果可以使用,使用时c的值是多少呢?

代码中的打印结果为 undefined,说明在该位置中c 是可以被访问的,它的值是undefined

关于第一个问题就是解决函数作用域的概念

在c语言中{} 花括号会封闭一个作用域,花括号外部是不能访问花括号内部的变量,这就是块级作用域

但是var 声明的变量的作用域是函数作用域 也就是说不使用{} 封装作用域,而是使用函数定义function 或者()=>{} 来封装作用域

当我们在函数外部使用函数内部的值时,会出现ReferenceError

var a =1 ;// a的作用域是全局的
console.log(b)// ReferenceError
function fun (){
    // 可以在fun中访问a,a是全局变量
    var b= 2 ; // 只能在fun中访问b
     // 在这里能不能访问c呢
     console.log(c)
    if(a<0){
        var c = 3;// c在if中
    }
}

变量提升(提前声明)

关于第二个问题,是变量提升的问题

JavaScript的函数作用域指的是一整个函数体都可以访问变量 所以c的访问范围是这样的

浅谈JavaScript作用域和闭包

而不是这样的

浅谈JavaScript作用域和闭包

所以在函数内部变量声明代码前可以写访问变量的代码,那实际上函数声明是什么时候呢? 用var 声明的变量会被提前到函数的顶部,就是在调用该函数时,其他都先不管,把变量都给找出来声明了。

所以上述代码是可以访问到c 的 然而我们访问到的c的值是undefined ,声明被提升了,但是赋值没有被提升,所以c在初始化前调用值为undefined 那如果出现了下面的情况,它的值是多少呢

function fun (){
    c =2
    console.log(c) // 2
    if(1){
        var c =1;
        console.log(c) // 1
    }
}

块级作用域

上面提到用{} 封装的作用域是块作用域 在ES6 之前JavaScript是没有块作用域的,ES6中有两个标识符可以声明块作用域的变量——let const

把上面的代码的var 全部换成 let

let a =1 ;// a的作用域是全局的
console.log(b)// ReferenceError
function fun (){
    // 可以在fun中访问a,a是全局变量
    let b= 2 ; // 只能在fun中访问b
     
    if(a<0){
        let c = 3;// c在if中
    }
    // 在这里能不能访问c呢
     console.log(c)
}

此时打印变量c的结果是ReferenceError ,在{} 外部不能访问内部的变量,这个是块级作用域

那用let 声明的变量会不会有变量提升呢,修改一下代码,把if条件删掉

function fun (){
    let b= 2 ; 
    console.log(c)
    let c = 3;// c在if中
}

依旧是referenceError ,用let声明的变量不会进行变量提升

那在for中小括号定义的变量的作用域是哪里呢

function fun (){
for(let i=0;i<2;i++){
    // for 循环内部显然是可以访问i的
    console.log(i)
}
// 在外部能不能访问i呢?
console.log(i)
}

结果是referenceError ,显然i是属于其内部的作用域。

经典例子

function fun (){
    var a=[];  
    for(var i = 0;i<10;i++){  
       a[i]=function(){console.log(i)}  
    }  
    a[0]()   // 10
}

在这个例子中,i是函数作用域,也就是从头到尾都只有一个变量i,在调用a[0]() 之前,i在循环过程中已经赋值为10了,此时console.log(i) 打印出来的就是i的值10

如果把var换成let,每次循环都会声明一个变量并赋值i,数组中函数存储的i都是不相同的,就可以打印出其当次循环的i

作用域链

JavaScript 是基于词法作用域的语言:通过阅读包含变量定义在内的数行源码就能知道变量的作用域。

作用域链就是当使用一个变量时,查找变量的路径。从最里层的作用域开始查找,如果没有找到就往上一级查找,当查找到顶层作用域还是没有找到这个变量就会抛出ReferenceError 错误

this 关键字

  • 在方法中,this 表示该方法所属的对象。
  • 如果单独使用,this 表示全局对象。
  • 在函数中,this 表示全局对象。
  • 在函数中,在严格模式下,this 是未定义的(undefined)。
  • 在事件中,this 表示接收事件的元素。
  • 类似 call() 和 apply() 方法可以将 this 引用到任何对象

闭包

函数对象可以通过作用域相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性叫做闭包

个人理解就是在函数中嵌套一个函数,这个子函数可以访问父函数的局部变量叫做闭包

哪有什么奇怪的? 在使用一个变量的时候会沿着作用域链查找变量,子函数肯定可以访问父函数的局部变量。

但是如果把子函数返回,放在另一个作用域链中执行呢?

看以下代码

let value = 'global'
function fun1 (){
    let value = 'fun1'
    function fun2 (){
        console.log(value)
    }
    return fun2
}

fun1()() // 会打印什么呢

fun1() 就等于 fun2 。 问题是fun2 它的作用域链是什么呢

fun2->fun1->global还是 fun2->global

实际上是fun2->fun1->global 打印的结果是fun1。

这就是闭包的作用,可以从内部函数访问外部函数的作用域

从垃圾回收的角度理解,一般来说一个函数执行完就会被当做垃圾回收,但是如果这个函数定义嵌套的函数,并且将嵌套的函数作为返回值或者存储在某处的属性里,这时就会有一个外部的引用指向这个嵌套的函数,它就不会被当作垃圾回收。

实现一个闭包

根据上述垃圾回收的理解,创建一个闭包要有以下的条件

  • 在函数内部定义函数
  • 返回嵌套的函数,或者把这个嵌套的函数作为属性传递到外面的作用域链中

前面提到的例子就是属于返回嵌套的函数,作为属性传递就更简单了

let obj ={}

function setGetNameF (obj,name){
   obj.getNmae = ()=>{
       return name
   }
}

setGetNameF (obj,'xiaoming')

obj.getName() // -> xiaoming

闭包的使用场景

在什么场景下需要使用闭包

  • 创建私有变量
  • 延长变量的生命周期

闭包的使用有很多,但是容易造成内存泄漏。此处不再举例

个人拙见,欢迎讨论。

转载自:https://juejin.cn/post/7194023024541040698
评论
请登录