草庐IT

JS闭包和作用域(必学知识点总结)

chscript 2023-03-28 原文

闭包和作用域


变量声明

var 声明特点

  1. 在使用var声明变量时,变量会被自动添加到最接近的上下文
  2. var存在声明提升var声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前。
  3. 可多次重复声明。而重复的var声明则会被忽略

let 声明特点

  1. let声明存在块级作用域

  2. let声明(创建过程)存在提升。但由于暂时性死区(temporal dead zone),无法在let声明之前去使用变量

  3. 在同一作用域内无法重复声明。重复的let声明会抛出SyntaxError错误

const 声明特点

  1. const声明存在块级作用域
  2. const一旦声明后在其生命周期内都无法重新赋予新值
  3. 其余与let声明一致

变量和函数的声明提升

变量声明与函数声明都存在提升。可以记住以下几个点:

  1. 变量声明中由var定义的变量会提升到其所在作用域的顶部。
  2. 变量声明中letconst提升效果一致,即由其定义的变量都会在创建过程被提升,但在初始化阶段被暂时性死区所扼杀
  3. 函数声明优先于变量声明。而函数表达式则会作为一个变量提升,其提升效果取决于用let还是var定义。

变量和函数的具体声明情况如下:

  • let的「创建」过程被提升了,但是初始化没有提升。
  • var的「创建」和「初始化」都被提升了。
  • function的「创建」「初始化」和「赋值」都被提升了。

来看这样三段代码:

第一段:var 变量声明效果

// 第一段
console.log(a) // 输出:undefined
var a = 10

上面代码运行后的实际情况如下:

var a // y 变量声明提升到其所在作用域的顶部
console.log(a)
a = 10

第二段:let变量声明效果(const与其一致)

// 第二段
{
    console.log(x) // 产生暂时性死区,无法访问变量。
    // 报错内容:Uncaught ReferenceError: Cannot access 'x' before initialization
    // 在值初始化之前无法访问 x ,即变量在初始化阶段被暂时性死区所扼杀
    let x = 10
}

第三段:函数声明与函数表达式声明效果

// 第三段:
var foo = function () {
    console.log('我是函数表达式')
}
function foo() {
    console.log('我是函数声明')
}
foo()
// 按照我们常规思维去思考一下,也许会输出'我是函数声明'。
// 但去执行一下,输出:'我是函数表达式'

上面代码运行后的实际情况如下:

function foo() {  // foo 作为函数声明被提升了
    console.log('我是函数声明')
}
var foo // foo 作为 var 变量声明被提升了 
foo = function () {
    console.log('我是函数表达式')
}
foo()
// 其中函数声明优先于变量声明,这也就解释了为什么不会输出'我是函数声明'。

作用域和作用域链

作用域

作用域分类:

  1. 全局作用域
  2. 函数作用域(function函数体内 )
  3. 块级作用域(letconst声明存在块级作用域)
// 全局作用域

function foo() {
    // 函数作用域
}

{
    let c = 30
    // 块级作用域
}

词法作用域:JavaScript会利用词法分析器分析我们书写的代码,从而依据变量和函数的命名位置来动态生成不同的作用域。即我们在定义变量或函数的时候,就已经决定了它们之间在不同作用域上的关系。

作用域链

作用域链由执行上下文中的变量对象逐级构成。

学习要点:

  1. 作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。
  2. 每个环境都可以逐级向上搜索作用域链查询变量和函数名;但任何环境都不能通过向下搜索作用域链。
  3. 自由变量:未在当前作用域定义的变量。自由变量会按照作用域链的查找机制,逐级向上查找与之对应的变量

执行上下文

执行上下文保存着变量对象,作用域链和this

学习要点:

  1. 所有通过var定义的全局变量会函数都会成为window对象的属性和方法。但使用letconst的声明的全局变量和函数不会定义在全局上下文中。
  2. 每个函数被调用时都会产生一个执行上下文。这个执行上下文会被推入栈。在函数执行完毕后,该执行上下文会在栈弹出,将控制权返还给之前的执行上下文。
  3. 当前作用域链中的第一个变量对象来自上一个上下文,下一个变量对象来自再上一个上下文。以此类推直至全局上下文。

执行上下文分类:

  1. 全局上下文 (window对象)
  2. 函数上下文
  3. eval()上下文

来看这样一段代码:

let a = 10
function sum() {
    let b = 20
    function add() {
        let c = 30
        console.log(a + b + c)
    }
    add()
}
sum()

执行上下文内容如下:

全局执行上下文:
[ 作用域链:[], 变量对象:[ a, sum ], this: window ]

sum 函数执行上下文:
[ 作用域链:[ 全局变量对象:[ a, sum ] ], 变量对象:[ b, add ] , this: window ]

add 函数执行上下文:
[ 作用域链:[ add函数的变量对象: [ b, add ], 全局变量对象:[ a, sum ] ], 变量对象:[c] , this: window ]

入栈过程

  1. 首先调用sum函数将其推入栈,产生了第一个函数执行上下文。

  2. 紧接着sum函数内部又调用add函数,于是又将其函数推入栈,产生了第二个函数执行上下文。

出栈过程

  1. add函数执行完毕后将其弹出栈,控制权交给sum函数。

  2. sum函数执行完毕后将其弹出栈,控制权交给全局上下文。

  3. 当浏览器关闭后,全局上下文会出栈。


闭包

闭包定义:在一个嵌套函数里,内部函数可以访问外部函数的变量。

闭包应用:封装对象的私有属性和方法。即对数据作隐藏和封装,防止污染全局变量

闭包作用:多个闭包可以共享相同的函数定义,但却保存了不同的词法环境。

来看这样三段代码:

// 前置知识:setTimeout在事件循环机制中作为宏任务,for循环属于微任务。
// 宏任务会在微任务之后执行,即我们的for循环会先一步于setTimeout结束。
for (var i = 0; i < 10; i++) {
    setTimeout(function () {
        console.log(i)
    }, 3000)
} // 输出结果:输出10次 10 !
// 每循环一次,都共享了相同的词法环境(全局作用域)。

我们给setTimeout套一个立即执行函数,如下:

for (var i = 0; i < 10; i++) {
    (function (i) { // 我们的闭包函数,相对于全局环境
        setTimeout(function () {
            console.log(i) // 内部函数访问了外部函数的变量
        }, 3000)
    })(i)
} // 输出结果:3秒后输出 0 1 2 3 4 5 6 8 9
// 每循环一次,立即执行函数就创建了不同的词法环境(块级作用域)。

我们换另一种形式去验证一下:

for (var i = 0; i < 10; i++) {
    let a = function (i) { // 我们的闭包函数,相对于全局环境
        setTimeout(function () {
            console.log(i) // 内部函数访问了外部函数的变量
        }, 3000)
    }
    a(i)
} // 输出结果:同样3秒后输出 0 1 2 3 4 5 6 8 9 

特别注意:不能滥用闭包,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。


垃圾回收机制

作用:垃圾回收程序会跟踪记录需要使用的变量和不需使用的变量,自动进行内存管理实现内存分配和闲置资源回收。

内存的生命周期:

  1. 分配你所需要的内存
  2. 使用分配到的内存(读、写)
  3. 不需要时将其释放

在浏览器的发展史上,主要有两种标记策略:引用计数和标记清理。

引用计数

基本原理:当首次声明变量并赋一个引用类型值时,会将这个值的引用次数设定为1。当这个值被赋给其他变量时,这个值的引用次数会再加1。当这个值被其他值所覆盖时,引用次数会减1。直到引用次数为0时,垃圾回收机制则会“上门回收”这个值

来看这样一段代码:

let a = { name: '小红' } // 首次值赋变量,引用计数为 1
let b = a // 值赋变量,引用计数 +1 为 2
let c = a // 值赋变量,引用计数 +1 为 3

c = null // 值被覆盖,引用计数 -1 为 2
b = null // 值被覆盖,引用计数 -1 为 1
a = null // 值被覆盖,引用计数 -1 为 0 被垃圾回收机制回收

循环引用(引用计数的缺陷问题)

来看这样一段代码

function foo() {
    let a = { name: '小红' }
    let b = { name: '小明' }
    a.name = b // b赋值给a对象中的name,b的引用次数为2
    b.name = a // a赋值给b对象中的name,a的引用次数为2
} // 说明:对象属性值作为变量被赋值
foo()

过程解析:函数的变量对象在函数调用完成之后会将每个变量值设为null ,以便垃圾回收机制进行回收。但在引用计数算法的策略中,函数在调用后,循环引用的变量ab依然保留了一次引用次数。也就是说,这两个引用类型的引用次数为1,不会进行回收。

标记清除

基本原理:标志清除算法把“对象不再需要”简化定义为“对象是否可以获得”。垃圾回收器将定期从根(全局对象)开始,找所有从根开始引用的对象,然后找这些对象引用的对象......直到最终垃圾回收器将找到所有可以获取的对象和收集所有不能获取的对象,其中不能获取的对象则会被回收。


参考

MDN-闭包

我用了两个月的时间才理解 let

JavaScript高级程序设计(第4版)

有关JS闭包和作用域(必学知识点总结)的更多相关文章

  1. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  2. 7个大一C语言必学的程序 / C语言经典代码大全 - 2

    嗨~大家好,这里是可莉!今天给大家带来的是7个C语言的经典基础代码~那一起往下看下去把【程序一】打印100到200之间的素数#includeintmain(){ inti; for(i=100;i 【程序二】输出乘法口诀表#includeintmain(){inti;for(i=1;i 【程序三】判断1000年---2000年之间的闰年#includeintmain(){intyear;for(year=1000;year 【程序四】给定两个整形变量的值,将两个值的内容进行交换。这里提供两种方法来进行交换,第一种为创建临时变量来进行交换,第二种是不创建临时变量而直接进行交换。1.创建临时变量来

  3. SPI接收数据异常问题总结 - 2

    SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手

  4. ruby - Ruby 中的闭包和 for 循环 - 2

    我是Ruby的新手,有些闭包逻辑让我感到困惑。考虑这段代码:array=[]foriin(1..5)array[5,5,5,5,5]这对我来说很有意义,因为i被绑定(bind)在循环之外,所以每次循环都会捕获相同的变量。使用每个block可以解决这个问题对我来说也很有意义:array=[](1..5).each{|i|array[1,2,3,4,5]...因为现在每次通过时都单独声明i。但现在我迷路了:为什么我不能通过引入一个中间变量来修复它?array=[]foriin1..5j=iarray[5,5,5,5,5]因为j每次循环都是新的,我认为每次循环都会捕获不同的变量。例如,这绝对

  5. ruby-on-rails - "assigns"在 Ruby on Rails 中有什么作用? - 2

    我目前正在尝试学习RubyonRails和测试框架RSpec。assigns在此RSpec测试中做什么?describe"GETindex"doit"assignsallmymodelas@mymodel"domymodel=Factory(:mymodel)get:indexassigns(:mymodels).shouldeq([mymodel])endend 最佳答案 assigns只是检查您在Controller中设置的实例变量的值。这里检查@mymodels。 关于ruby-o

  6. ruby - 字符串文字前面的 * 在 ruby​​ 中有什么作用? - 2

    这段代码似乎创建了一个范围从a到z的数组,但我不明白*的作用。有人可以解释一下吗?[*"a".."z"] 最佳答案 它叫做splatoperator.SplattinganLvalueAmaximumofonelvaluemaybesplattedinwhichcaseitisassignedanArrayconsistingoftheremainingrvaluesthatlackcorrespondinglvalues.Iftherightmostlvalueissplattedthenitconsumesallrvaluesw

  7. ruby - 为什么这个 eval 在 Ruby 中不起作用 - 2

    你能解释一下吗?我想评估来自两个不同来源的值和计算。一个消息来源为我提供了以下信息(以编程方式):'a=2'第二个来源给了我这个表达式来评估:'a+3'这个有效:a=2eval'a+3'这也有效:eval'a=2;a+3'但我真正需要的是这个,但它不起作用:eval'a=2'eval'a+3'我想了解其中的区别,以及如何使最后一个选项起作用。感谢您的帮助。 最佳答案 您可以创建一个Binding,并将相同的绑定(bind)与每个eval相关联调用:1.9.3p194:008>b=binding=>#1.9.3p194:009>eva

  8. ruby-on-rails - Assets 管道损坏 : Not compiling on the fly css and js files - 2

    我开始了一个新的Rails3.2.5项目,Assets管道不再工作了。CSS和Javascript文件不再编译。这是尝试生成Assets时日志的输出:StartedGET"/assets/application.css?body=1"for127.0.0.1at2012-06-1623:59:11-0700Servedasset/application.css-200OK(0ms)[2012-06-1623:59:11]ERRORNoMethodError:undefinedmethod`each'fornil:NilClass/Users/greg/.rbenv/versions/1

  9. ruby-on-rails - Rails - 理解 application.js 和 application.css - 2

    rails新手。只是想了解\assests目录中的这两个文件。例如,application.js文件有如下行://=requirejquery//=requirejquery_ujs//=require_tree.我理解require_tree。只是将所有JS文件添加到当前目录中。根据上下文,我可以看出requirejquery添加了jQuery库。但是它从哪里得到这些jQuery库呢?我没有在我的Assets文件夹中看到任何jquery.js文件——或者直接在我的整个应用程序中没有看到任何jquery.js文件?同样,我正在按照一些说明安装TwitterBootstrap(http:

  10. ruby-on-rails - Spring 不起作用。 [未初始化常量 Spring::SID::DL] - 2

    我无法运行Spring。这是错误日志。myid-no-MacBook-Pro:myid$spring/Users/myid/.rbenv/versions/1.9.3-p484/lib/ruby/gems/1.9.1/gems/spring-0.0.10/lib/spring/sid.rb:17:in`fiddle_func':uninitializedconstantSpring::SID::DL(NameError)from/Users/myid/.rbenv/versions/1.9.3-p484/lib/ruby/gems/1.9.1/gems/spring-0.0.10/li

随机推荐