轻松应对大厂面试-彻底理解js作用域和闭包

免费视频在B站
轻松应对大厂面试-彻底理解js作用域和闭包_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili

一. 预编译

1 概念

1) 什么是预编译

首先, 我们要知道Javascript是解释性语言

  • 解释性: 逐行解析, 逐行执行

那么, 什么是预编译呢?

在Javascript真正被解析之前, js解析引擎会首先把整个文件进行预处理, 以消除一些歧义. 这个预处理的过程就被称为预编译

示例

console.log(a)
var a = 123
console.log(a)
function a() {
  console.log(a)
}
a()

这是一段奇怪的代码, 大家可以先思考一下, 三个console.log分别会打印出什么

如果要完全理解, 我们就需要深入的分析js引擎到底是如何工作的!!!

2) 全局对象GO

全局对象

全局对象(Global Object):

  • 在浏览器环境中, js引擎会整合<script>标签中的内容, 产生window对象, 这个window对象就是全局对象
  • 在node环境中, 会产生global对象

全局变量

<script>标签中声明的变量为全局变量, 全局变量会作为window对象的属性存在!!

示例

var a = 100
console.log(a)
console.log(window.a)

这里打印a实际上相当于打印window对象的a属性

扩展

啥叫整合?

示例

<script>
  var a = 100
  console.log(a)
  console.log(window.a)
</script>
<script>
  // 在这里能访问到a吗???
  console.log(a)
</script>
  • 可以, 因为js引擎会把所有的<script>标签整合到一起, 生成一个window对象

全局函数

<script>标签中声明的函数为全局函数, 全局函数会作为window对象的方法存在!!

示例

function a() {
  console.log('111')
}
console.log(window.a)

那么问题来了, 当同时定义变量a和函数a时, 会发生什么呢?

就像我们看到的奇怪代码里一样, 而预编译就是为了处理类似的这些冲突

3) 活动对象AO

活动对象

活动对象(Activation Object): 也叫激活对象

  • 在函数被调用时产生, 用来保存当前函数内部的执行环境(Execution Context), 也叫执行期上下文
  • 在函数调用结束时销毁

局部变量

在函数内部声明的变量叫局部变量, 局部变量做为AO对象的属性存在

示例

function a() {
  var i = 0
  console.log(i)
}
a()

如何理解局部

函数a的外部, 不能访问变量i, 变量i只在函数a的范围内才能使用. 其实, 这也就是作用域的由来, skr~

  • 如果不执行函数, 不会产生AO对象, 就不会存在i属性
  • 如果执行函数, 就会产生AO对象, 并将变量i作为AO对象的属性
  • 函数执行完后, AO对象被销毁, 也就意味着不能使用i属性

局部函数

在函数内部声明的函数叫局部函数, 局部函数做为AO对象的方法存在

示例

function a() {
  function b() {
    console.log(222)
  }
  b()
}
a()

2 全局预编译

1) 流程

  1. 查找变量声明, 作为GO对象的属性名, 值为undefined
  2. 查找函数声明, 作为GO对象的属性名, 值为function

变量声明

通过var关键字声明变量

var a // 变量声明
var a = 111 // 变量声明+变量赋值

函数声明

通过function关键字声明函数

function a () {} // 函数声明
var a = function () {} // 函数表达式, 不是函数声明

示例

console.log(a)
var a = 100
console.log(a)
function a() {
  console.log(111)
}
console.log(a)

2) 结论

如果存在同名的变量和函数, 函数的优先级高

3 函数预编译

1) 流程

  1. 在函数被调用时, 为当前函数产生AO对象
  2. 查找形参和变量声明作为AO对象的属性名, 值为undefined
  3. 使用实参的值改变形参的值
  4. 查找函数声明, 作为AO对象的属性名, 值为function

2) 示例

示例一

function a(test) {
  var i = 0
  function b() {
    console.log(222)
  }
  b()
}
a(1)

函数a的AO对象中, 存在三个属性

  • test: 形参, 值为1
  • i: 局部变量, 值为0
  • b: 局部函数

示例二

function a(test) {
  console.log(b)
  var b = 0
  console.log(b)
  function b() {
    console.log(222)
  }
}
a(1)

当局部变量与局部函数同名时, 函数的优先级高

示例三

function a(b, c) {
  console.log(b)
  var b = 0
  console.log(b)
  function b() {
    console.log(222)
  }
  console.log(c)
}
a(1)

示例四

function a(i) {
  var i
  console.log(i)
}
a(1)

3) 结论

只要声明了局部函数, 函数的优先级最高

没有声明局部函数, 实参的优先级高

整体来说: 局部函数 > 实参 > 形参和局部变量

二. 作用域与作用域链

1 概念

1) 域

域: 范围, 区域

在js中, 作用域分为全局作用域局部作用域

  • 全局作用域: 由<script>标签产生的区域, 从计算机的角度可以理解为window对象
  • 局部作用域: 由函数产生的区域, 从计算机的角度可以理解为该函数的AO对象

2) 作用域链

在js中, 函数存在一个隐式属性[[scopes]], 这个属性用来保存当前函数在执行时的环境(上下文), 由于在数据结构上是链式的, 也被称为作用域链. 我们可以把它理解成一个数组

函数类型存在[[scopes]]属性

function a() {}

console.dir(a) // 打印内部结构

输出

[[scopes]]属性在函数声明时产生, 在函数被调用时更新

[[scopes]]属性记录当前函数的执行环境

在函数被调用时, 将该函数的AO对象压入到[[scopes]]中

示例

function a() {
  console.dir(a)
  function b() {
    console.dir(b)
    function c() {
      console.dir(c)
    }
    c()
  }
  b()
}
a()

[[scopes]]属性是一个数组的形式

0: 是函数b的AO对象

1: 是GO对象

2 作用

作用域链有什么作用呢?

在访问变量或者函数时, 会在作用域链上依次查找, 最直观的表现是:

  • 内部函数可以使用外部函数声明的变量

示例

function a() {
  var aa = 111
  function b() {
    console.log(aa)
  }
  b()
}
a()
  • 在函数a中声明定义了变量aa
  • 在函数b中没有声明, 却可以使用

思考

如果在函数b中, 也定义同名变量aa会怎样

示例

function a() {
  var aa = 111
  function b() {
    var aa = 222
    console.log(aa)
  }
  b()
}
a()

第一个问题: 函数a和函数b里的变量aa是不是同一个变量?

第二个问题: 函数b里打印的aa是用的谁?

结论

内部函数可以使用外部函数的变量

外部函数不能使用内部函数的变量

三. 闭包

如果在内部函数使用了外部函数的变量, 就会形成闭包. 闭包保留了外部环境的引用

如果内部函数被返回到了外部函数的外面, 在外部函数执行完后, 依然可以使用闭包里的值

1 闭包的形成

在内部函数使用外部函数的变量, 就会形成闭包, 闭包是当前作用域的延伸

示例

function a() {
  var aa = 100
  function b() {
    console.log(aa)
  }
  b()
}
a()

从代码的角度看, 闭包也是一个对象, 闭包里包含哪些东西呢?

在内部函数b中使用了外部函数a中的变量, 这个变量就会作为闭包对象的属性!!

思考

function a() {
  var aa = 100
  function b() {
    console.log(b)
  }
  b()
}
a()
  1. 会不会形成闭包?
  2. 如果形成, 闭包里有什么?

答案

会形成闭包, 由于b的声明是在外部函数a中的, 在内部函数b中使用了b, 会形成闭包

闭包里存放了一个属性, 就是b函数

思考

function a() {
  var aa = 100
  function b() {
    var b = 200
    console.log(b)
  }
  b()
}
a()
  1. 会不会形成闭包?

答案

不会形成闭包, 由于在b函数内部定义了变量b, 打印时直接使用的是内部函数里的变量b, 不会形成闭包

2 闭包的保持

如果希望在函数调用后, 闭包依然保持, 就需要将内部函数返回到外部函数的外部

示例

function a() {
  var num = 0
  function b() {
    console.log(num++)
  }
  return b
}
var demo = a()
console.dir(demo)
demo()
demo()

第8行, 调用a函数, 将内部函数b返回, 保存在函数a的外部

第9行, 调用demo函数, 实质上是调用内部函数, 在函数b的[[scopes]]属性中可以找到闭包对象, 从而访问到里面的值

3 总结

使用闭包要满足两个条件

  1. 闭包要形成: 在内部函数使用外部函数的变量
  2. 闭包要保持: 内部函数返回到外部函数的外面

四. 闭包的应用

1 闭包的两面性

任何事物都有两面性

好处: 一般来说, 在函数外部是没办法访问函数内部的变量的, 设计闭包最主要的作用就是为了解决这个问题.

坏处: 有时不注意使用了闭包, 会导致出现意想不到的结果

2 闭包的应用

  1. 在函数外部访问私有变量
  2. 实现封装
  3. 防止污染全局变量

示例

在函数外部访问私有变量

function a() {
  var num = 0
  function b() {
    console.log(num++)
  }
  return b
}
var demo = a()
console.dir(demo)
demo()

本来在函数a的外部(全局)不能直接访问内部变量num, 通过闭包就可以使用num变量了

示例

function Person() {
  var uname
  function setName(uname) {
    this.uname = uname
  }
  function getName() {
    return this.uname
  }
  return {
    getName: getName,
    setName: setName,
  }
}

var xiaopang = Person()
xiaopang.setName('xiaopang')
var name = xiaopang.getName()
console.log(name)

定义了一个函数Person, 一个内部变量uname, 两个内部函数

返回内部函数, 也是使用了闭包特性

这样在Person函数的外部, 通过get和set方法对变量uname进行操作, 这就是面向对象里的封装的思想

3 闭包的问题

在很多时候, 我们写的代码会无意识的用了闭包, 但是这并不是我们想要的结果.

这种情况应该尽量避免, 或者说遇到了这类bug时, 我们应该知道如何解决

示例

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

arr[0]()
  1. 会不会形成闭包?
  2. 打印结果是什么?
  3. 为什么

示例

var arr = []
function a() {
  for (var i = 0; i < 10; i++) {
    arr[i] = function () {
      console.log(i)
    }
  }
}
a()
arr[0]()
  1. 会不会形成闭包?
  2. 打印结果是什么?
  3. 为什么

虽然看起来结果一样, 但是执行的过程有很大的差异~

现在问题是如果希望依然打印0~9, 该怎么解决

五. 立执行函数

其实, 不管是上述哪种情况, 根本的问题在于js中没有块作用域

1 块作用域

{}形成的作用区域

示例

for (var i = 0; i < 10; i++) {
  console.log(i)
}
console.log('for执行完了, i=' + i)

首先, 我们从预编译的角度来分析一下

  1. var i属于变量声明, 会在window对象添加一个属性i
  2. 执行for循环, 没有函数, 不会产生局部作用域, 在for循环结束后, i依然可以访问

一般情况下, 我们更希望循环变量i只在循环体内有效, 也就是{}里有效. 这个就是块作用域

如何解决呢

2 立执行函数

由于函数会产生作用域, 我们可以尝试在for循环中写一个函数, 并调用函数

示例

for (var i = 0; i < 10; i++) {
  function a(j) {
    console.log(j)
  }
  a(i)
}

像这种声明了马上执行的函数就是立执行函数, 可以合并写在一起

示例

for (var i = 0; i < 10; i++) {
  (function (j) {
    console.log(j)
  })(i)
}

前面的()表示函数的声明

后面的()表示函数的执行

示例

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

3 函数表达式

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容