块级作用域的理解

块级作用于的概念

由一对花括号{}中的语句集都属于一个块,在这个{}里面包含的块内定义的所有变量在代码块外都是不可见的,因此称为块级作用域。
作用域永远都是任何一门语言的重中之中,因为它控制着变量和参数的可见性和生命周期。讲到这里,首先要理解两个概念:块作用域和函数作用域。什么是块级作用域呢?

任何一对花括号({})中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的,我们称之为块级作用域。

函数作用域就更好理解了定义在函数中的参数和变量在函数外是不可见的。

作用域分析


function fn1(x) {
  var a = 0;
  let b = 0;
  {
    var c = 1; // 未用let或const,不形成c的块作用域,依然是bar的函数作用域
    let d = 1; // d形成块作用域,只可在当前代码块中访问
  }
  function fn() {
    var e = 2;
    let g = 2;
    console.log('a:', a)
    console.log('b:', b)
    console.log('c:', c) // 访问外部作用域中的a/b/c,形成闭包
    // console.log('d:', d) // d is undefined
  }
  f()
}
fn1(5)


代码执行进入fn1函数时的作用域如下:

  1. 在fn1中用var定义的a
  2. 在fn1中用let定义的b
  3. 在代码块中用var定义的c
  4. 在fn1中定义的函数fn
  5. fn1形参x


当前执行上下文栈是 [全局执行上下文, fn1执行上下文]

代码进入代码块时的作用域

当前执行上下文栈是 [ 全局执行上下文,fn1执行上下文, 块作用域1]

代码进入fn函数时的作用域

在fn中用var声明的e
在fn中用let声明的f
在fn中有对fn1变量对象的引用,形成闭包


当前执行上下文栈是 [ 全局执行上下文,fn1执行上下文(闭包), fn执行上下文]

for遍历中的var与let


function bar() {
  var fnArr1 = []
  var fnArr2 = []
  var fnArr3 = []
  for(var i = 0; i < 5; i++) {
    fnArr1.push(function() {
      return i
    })
  }
  for(var k = 0; k < 5; k++) {
    (function(k) {
      fnArr2.push(function() {
        return k
      })
    })(k)
  }
  for(let j = 0; j < 5; j++) {
    fnArr3.push(function() {
      return j
    })
  }
  console.log('i:', i) // 5
  console.log('fnArr1:', fnArr1.map(x => x())) // [4, 4, 4, 4, 4]
  console.log('--------')
  console.log('k:', k) // 5
  console.log('fnArr2:', fnArr1.map(x => x())) // [0, 1, 2, 3, 4]
  console.log('--------')
  console.log('j:', j)
  console.log('fnArr3:', fnArr1.map(x => x()))
}


在for循环中使用var声明表达式变量


var声明变量不具有块作用域特性,它声明的变量作用域为当前作用域,在循环中i会被反复覆盖,所以当循环遍历结束后,i的值为最后一次遍历的值,即在这里为5。

var fnArr1 = []
for(var i = 0; i < 5; i++) {
  fnArr1.push(function() {
    return i
  })
}
console.log('fnArr1:', fnArr1.map(x => x())) // [5, 5, 5, 5, 5]

在for循环中使用var声明表达式变量且用立即执行函数

通过传递参数到立即执行函数,传递的参数是非引用类型变量,所以已然切割了与外面变量k的联系,即第一次循环传递的是数字0, 第二次循环传递的是数字1 … 以此类推,所以遍历执行数组的函数会返回一个递增的数组。 

var fnArr2 = []
for(var k = 0; k < 5; k++) {
  (function(k) {
    fnArr2.push(function() {
      return k
    })
  })(k)
}
console.log('fnArr2:', fnArr1.map(x => x())) // [0, 1, 2, 3, 4]
 

 在for循环中使用let声明表达式变量


let声明的变量会有块级作用域的特点,所以在for循环表达式中使用let其实就等价于在代码块中使用let,也就是说

for(let j = 0; j < 5; j++) 这句话的圆括号之间,有一个隐藏作用域
for(let j = 0; j < 5; j++) { 循环体 } 在每次执行循环体之前,js引擎会把j在循环体的上下文中重新声明及初始化一次


var fnArr3 = []
for(let j = 0; j < 5; j++) {
  fnArr3.push(function() {
    return j
  })
}

// js引擎会处理成
for(let j = 0; j < 5; j++) {
  let t = j
  fnArr3.push(function() {
    return t
  })
}
console.log('fnArr3:', fnArr1.map(x => x())) // [0, 1, 2, 3, 4]


拓展


function fn() {
  var fnArr = []
  for (let p = { i: 0 }; p.i < 5; p.i++) {
    fnArr.push(function() {
      return p.i
    })
  }
  console.log(fnArr.map(x => x())) // [5, 5, 5, 5, 5] 为什么??
}
fn()


按照上面的理解打印出来的应该是[0, 1, 2, 3, 4]才对,但是为什么与期望不符呢?这与浅拷贝/深拷贝的问题有关了

js引擎会把上面的循环处理为以下代码:

for (let p = { i: 0 }; p.i < 5; p.i++) {
  let k = p // 这里为引用类型变量的赋值
  fnArr.push(function() {
    return k.i
  })
}


js引擎在循环体中用let声明了一个变量k=p,这里p为引用类型变量*{ i: 0 }*, 即这里声明的k是对p对象的引用。所以执行fnArr中的函数,最终返回的是p的i属性;而p.i在一次次循环后已经自增为5,所以最终打印结果都是5。

那如何改写上面代码来实现想要的结果呢?

function fn() {
  var fnArr = []
  var o = { i: 0 }
  for (let p = o.i; p < 5; p++) {
    fnArr.push(function() {
      return p
    })
  }
  console.log(fnArr.map(x => x())) // [0, 1, 2, 3, 4]
}
fn()

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/159741.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

搭建第一个区块链网络与一键部署WeBASE步骤

官网 搭建第一个区块链网络 — FISCO BCOS v2 v2.9.0 文档 (fisco-bcos-documentation.readthedocs.io) 一键部署 — WeBASE v1.5.5 文档 (webasedoc.readthedocs.io) 步骤 默认如MySQL、Python、java等依赖已经引入 1.创建操作目录, 下载安装脚本 创建操作目录 cd ~ &a…

​实现1个电脑打开多个微信​

实现1个电脑打开多个微信&#xff1a;1、快速双击打开微信&#xff0c;可打开多个微信。2、按住回车键&#xff0c;双击打开微信&#xff0c;并快速放开回车键即可打开多个微信。3、用命令符也可打开多个微信。4、建立一个批处理文件实现打开多个微信。 方法一&#xff1a;最简…

故障诊断模型 | Maltab实现SVM支持向量机的故障诊断

效果一览 文章概述 故障诊断模型 | Maltab实现SVM支持向量机的故障诊断 模型描述 Chinese: Options:可用的选项即表示的涵义如下   -s svm类型:SVM设置类型(默认0)   0 – C-SVC   1 --v-SVC   2 – 一类SVM   3 – e -SVR   4 – v-SVR   -t 核函数类型:核函…

CoDeSys系列-4、基于Ubuntu的codesys运行时扩展包搭建Profinet主从环境

CoDeSys系列-4、基于Ubuntu的codesys运行时扩展包搭建Profinet主从环境 文章目录 CoDeSys系列-4、基于Ubuntu的codesys运行时扩展包搭建Profinet主从环境一、前言二、资料收集三、Ubuntu18.04从安装到更换实时内核1、下载安装Ubuntu18.042、下载安装实时内核&#xff0c;解决编…

如何卸载干净 IDEA(图文讲解)windows和Mac教程

大家好&#xff0c;我是sun~ 很多小伙伴会问 Windows / Mac 系统上要怎么彻底卸载 IDEA 呢&#xff1f; 本文通过图片文字&#xff0c;详细讲解具体步骤&#xff1a; 如何卸载干净 IDEA&#xff08;图文讲解&#xff09; Windows1、卸载 IDEA 程序2、注册表清理3、残留清理 M…

Openssl生成证书-nginx使用ssl

Openssl生成证书并用nginx使用 安装openssl yum install openssl -y创库目录存放证书 mkdir /etc/nginx/cert cd /etc/nginx/cert配置本地解析 cat >>/etc/hosts << EOF 10.10.10.21 kubernetes-master.com EOF10.10.10.21 主机ip、 kubernetes-master.com 本…

Android各版本对应的SDK及JDK版本要求

1、Android Gradle 插件版本说明 | Android 开发者 | Android Developers 2、 3、Android Gradle 插件 7.3.0&#xff08;2022 年 9 月&#xff09; | Android 开发者 | Android Developers 4、 5、参考 Android中Gradle版本和Gradle插件版本 - 简书

QCC TX 音频输入切换+提示声音

QCC TX 音频输入切换提示声音 QCC蓝牙芯片&#xff08;QCC3040 QCC3056 等等&#xff09;&#xff0c;AUX、I2S、USB输入 蓝牙音频输入&#xff0c;模拟输出是最常见的方式。 也可以再此基础上动态切换输入方式。 针对TX切换EQ,调节音量不能出提示声音问题&#xff0c;可以增…

路由器基础(八):策略路由配置

在实际网络应用中&#xff0c;策略路由也是一种重要的技术手段。尽管 在考试并不注重策略路由&#xff0c;但是实际上应用较多&#xff0c;建议考生除了掌握基本的静态路由协议IP route-static, 动态路由协议RIP 、OSPF的基础配置外&#xff0c;还要掌握如何配置策略路由。…

解决ECharts柱形图自定义单个柱子颜色图例无法显示

legend里data和series里的name需要对应series里对象需要设置stack属性&#xff0c;属性值都一样即可显示单柱重点在于series里对象data属性设置&#xff0c;必须使用&#xff0c;否则影响柱体上数值显示tooltip的值需要自定义&#xff0c;否则会显示堆叠柱状图的tooltip格式&am…

Java Web 学习笔记(二) —— JDBC

目录 1 JDBC 概述2 JDBC 快速入门3 JDBC API 详解3.1 DriverManager3.2 Connection3.3 Statement3.4 ResultSet3.5 PreparedStatement3.5.1 代码模拟 SQL 注入3.5.2 PreparedStatement 的使用3.5.3 PreparedStatement 原理 4 数据库连接池4.1 数据库连接池概述4.2 数据库连接池…

牛客项目(五)-使用kafka实现发送系统通知

kafka入门以及与spring整合 Message.java import java.util.Date;public class Message {private int id;private int fromId;private int toId;private String conversationId;private String content;private int status;private Date createTime;public int getId() {retur…