JS闭包和作用域

更新日期: 2023-01-27阅读: 630标签: 字符

变量声明

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. 变量声明中let和const提升效果一致,即由其定义的变量都会在创建过程被提升,但在初始化阶段被暂时性死区所扼杀
  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. 块级作用域(let和const声明存在块级作用域)
// 全局作用域

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

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

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

作用域链

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

学习要点:

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

执行上下文

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

学习要点:

  1. 所有通过var定义的全局变量会函数都会成为window对象的属性和方法。但使用let和const的声明的全局变量和函数不会定义在全局上下文中。
  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 ,以便垃圾回收机制进行回收。但在引用计数算法的策略中,函数在调用后,循环引用的变量a和b依然保留了一次引用次数。也就是说,这两个引用类型的引用次数为1,不会进行回收。

标记清除

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

链接: https://fly63.com/article/detial/12332

js判断字符串是否含有特殊字符和emoji表情?以及如何过滤

原生js判断字符串是否含有特殊字符和emoji表情,js禁止输入框输入特殊符号或emoji表情

JS转换HTML转义符

去掉html标签,普通字符转换成转意符,转意符换成普通字符,&nbsp;转成空格,回车转为br标签,去除开头结尾换行,并将连续3次以上换行转换成2次换行,将多个连续空格合并成一个空格

Unicode字符集的由来

Unicode于乱世出生逐渐成为标准统一字符世界,至今仍持续发展,造福了社会,极大的提升了生产效率,虽未与ASCII并列与IEEE里程碑,但也是计算机科学史中一件举足轻重的大事记。

javascript如何判断字符是否是中文?

有时需要判断一个字符是不是汉字,比如在用户输入含有中英文的内容时,需要判断是否超过规定长度就要用到。用Javascript判断通常有两种方法。

Js如何判断字符串中是否包含某个字符串?

JavaScript中要判断字符串中是否包含某个字符串有多种方法,下面我们来看一下使用indexOf()方法、search()方法、match()方法来判断字符串中是否包含某个字符串。

javascript怎么判断字符是否是中文?

有时我们需要判断一个字符是不是汉字,比如在用户输入含有中英文的内容时,需要判断是否超过规定长度就要用到。那么如何判断?下面本篇文章就来给大家介绍一下判断方法

javascript中字符串如何转换成数组?

javascript中字符串如何转换成数组?下面本篇文章就来给大家介绍一下使用javascript将字符串转换成数组的方法,在javascript中,可以使用split()方法来将字符串转换成数组。split()方法用于把一个字符串分割成字符串数组

javascript如何添加前置0?

很多时候为了显示格式,需要在某一字符串不满位的情况下进行前补0操作。下面这篇文章就给大家主要介绍了javascript添加前置0(补零)的方法。

Javascript怎样提取字符个数?

javascript中获取字符串的字符个数主要是根据字符串的变量,然后通过.length就可以获取到字符串的长度。JavaScript中可以使用length属性来提取字符个数。

JavaScript 专题之花式表示 26 个字母

我们之所以拼出 toString,是因为利用 toString 这个方法可以表示出 26个 字母!这时候,就要隆重介绍下这个平时看起来不起眼,但是在这里确实最终主角的 toString 方法!

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!