“老司机”也会在闭包上翻车

“老司机”也会在闭包上翻车

前言:

 闭包是 JavaScript 中最基本也是最重要的概念之一,很多开发者都对它“了如指掌”。可是,闭包又绝对不是一个单一的概念:它涉及作用域、作用域链、执行上下文、内存管理等多重知识点。 不管是新手还是“老司机”,经常会出现“我觉得我弄懂了闭包,但还是会在一些场景下翻车” 的情况。本篇将对这个话题进行梳理,并通过“应试题"来强化理解闭包。

基础知识

 如同前面所说的,闭包不是一个单一的概念,在直击闭包概念之前,我们先来了解一-下与之相关的必备知识点。

作用域

 作用域可以被理解为某种规则下的限定范围,该规则用于指导开发者如何在特定场景下查找变量。任何语言中都有作用城的概念,同一种语言在演进过程中也会不断完善其作用域规则。比如、在 JavaScript 中,ES6 出现之前,一般来说只有函数作用域和全局作用域之分。

函数作用域和全局作用域

​  大家应该非常熟悉函数作用域了,例如,执行以下 foo 函数时,变量 a 在函数 foo 的作用域内,因此可以在函数体内正常访问该变量,并输出 bar。

function foo() {
  var a = 'bar'
  console.log(a)
}
foo()

 对以上代码稍加改动,使其变为如下形式。

var b = 'bar'
function foo() {
  console.log(b)
}
foo()

 执行以上代码时,foo 函数在自身函数作用城内并未找到 b 变量,但是它会继续向外扩大查找范围,于是便在全局作用城中找到了变量b,井输出 bar。

 如果我们再对代码稍加改动,使其变成如下形式,结果又将如何呢?

function bar() {
  var b = 'bar'
}

function foo() {
  console.log (b) 
}
foo()

 在以上代码中,foo 和 bar 分属于两个彼此独立的函数作用城,foo 函数无法访问 bar 函数中定义的变量 b,且其作用域链内(直到上层全局作用城中)也不存在相应的变量,因此执行这段代码会报错 Uncaught ReferenceError: b is not defined。

 简单总结下,在 JavaScript 中执行某个函数时,如果遇见变量且需要读取其值,就会“就近”先在函数内部查找该变量的声明或赋值情况。这里涉及“变量声明方式”及“变量提升”等知识点, 后面的篇章会做进一步讲解。 如果在函数内无法找到该变量,就要跳出函数作用域,到更上层作用域中查找。这里的“更上层作用域”可能也是一个函数作用域。下面来看一个具体示例。

function bar() {
  var b = 'bar'
  function foo() {
    console.log (b) 
  }
  foo()
}

bar()

 在 foo 函数执行时,变量 b 的声明或赋值情况是在其上层函数 bar 的作用域中获取的。另外,更上层作用域也可以顺着作用域范围向外扩散,一直到全局作用域,示例如下。

var b = 'bar'
function bar() {
  function foo() {
    console.log (b) 
  }
  foo()
}

bar()

 执行以上代代码回输出 bar 这是因为执行 foo 函数时,在其作用城链上最终找到了全局作用城下的变量 b。我们看到,变量作用城的查找是一个扩 散过程,就像各个环节相扣的链条,逐次递进,这就是“作用城链”的由来。

块级作用域和暂时性死区

 作用域概念不断演进,ES6 中增加了通过 let 和 const 声明变量的块级作用域,使得 JavaScript 中的作用城内涵更加丰富。块级作用域,顾名思义,是指作用域范围限制在代码块中,这个概念在其他语言中也普遍存在。当然,这些新特性的出现也增加了一定的复杂度,带来了新的概念,比如暂时性死区。这里有必要对此概念稍做展开,说到暂时性死区,还需要从“变量提升”说起,请参看以下代码。

function foo() {
  console.log(bar)
  var bar = 3
}
foo()

 执行以上代码会输出 undefined,原因是变量 bar 在函数内进行了提升。以上代码与以下代码是等价的。

function foo() {
  var bar
  console.log(bar)
  bar = 3
}
foo()

 但是,在使用 let 对 bar 进行声明时(如下所示则会报错 Uncaught ReferenceError: bar is not defined。

function foo() {
  console.log(bar)
  let bar = 3
}
foo()

 我们知道,用 let 或 const 声明变量时会针对这个变量形成个封间的块级作用城, 在这个块级作用城中,如果在声明变量前访问该变量,就会报 refernceError 错误:如果在声明变量后访问该变量,则可以正常获取变量值,示例如下。

function foo() {
  let bar = 3
  console.log(bar)
}
foo()

 以上代码将正常输出 3。因此,在相应花括号形成的作用域中存在一个“死区”,起始于函数开头。终止于相关变量声明语句的所在行。在这个范围内无法访问使用 let 或 const 声明的变量。这个 “死区”的专业名称为 TDZ( Temporal Dead Zone ),相关语言规范的介绍可参考ECMAScript 2015 Language Specification,喜欢刨根问底的读者可以了解一下。

暂时性死区,有一种比较极端的情况是,函数的参数默认值设置也会受到它的影响,实例代码如下。

function foo(argl = arg2, arg2) {
  console.1og(`${arg1} ${arg21}`)
}

foo('arg1', 'arg2')

//返回 arg1 arg2

 在上面的 foo 函数中,如果没有传人第一个参数, 则会使用第二个参数作为第一个实参。调用以上代码,返回内容正常;但是当第一个参数为默认值时, 执行 ag1 = ag2 会被当作暂时性死区处理,示例如下。

function foo(argl = arg2, arg2) {
  console.1og(`${arg1} ${arg21}`)
}

foo(undefined, 'arg2')

// Uncaught ReferenceError: arg2 is not defined

 以上代码的输出结果存在问题是因为除了块级作用域,函数参数默认值也会受到暂时性死区的影响。那么,我在这里再“抖个机灵”,大家猜一猜执行下面的代码会输出什么。

function foo(argl = arg2, arg2) {
  console.1og(`${arg1} ${arg21}`)
}

foo(null, 'arg2')

 答案是,输出 null arg2。这就涉及 undefined 和 null 的区别了。在执行 foo(null, arg2) 时,不会认为“函数第一个参数为默认值” ,而会直接接收null作为第一个参数的值。

 这个知识点已经不属于本篇的主题了,undefined 和 null 的具体区别会在后续篇章中提到。

 既然已经偏题,那索性再分析个场景,顺便引出新的知识点。猜猜以下代码的输出结果是什么。

function foo(argl) {
  let arg1
}

foo('arg1')

 实际上,执行这段代码会报错 Uncaught SyntaxError: Identifier ‘arg1’ has already been declared,这是由函数参数名出现在其“执行上下文/作用城”中导致的。

 函数的第一行便已经声明了 arg1 这个变量,函数体再用 let 声明就会报错(这是用 let 声明变量的特点,也是 ES6 的基础内容,这里不再展开),就像下面的代码一样。

function foo(argl) {
  var arg1
  let arg1
}

执行上下文和调用栈

代码执行的两个阶段
  • 代码预编译阶段
  • 代码执行阶段
  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值