图解Javascript——作用域、作用域链、闭包

什么是作用域?

作用域是一种规则,在代码编译阶段就确定了,规定了变量与函数的可被访问的范围。全局变量拥有全局作用域,局部变量则拥有局部作用域。 js是一种没有块级作用域的语言(包括if、for等语句的花括号代码块或者单独的花括号代码块都不能形成一个局部作用域),所以js的局部作用域的形成有且只有函数的花括号内定义的代码块形成的,既函数作用域。

什么是作用域链?

作用域链是作用域规则的实现,通过作用域链的实现,变量在它的作用域内可被访问,函数在它的作用域内可被调用。

作用域链是一个只能单向访问的链表,这个链表上的每个节点就是执行上下文的变量对象(代码执行时就是活动对象),单向链表的头部(可被第一个访问的节点)始终都是当前正在被调用执行的函数的变量对象(活动对象),尾部始终是全局活动对象。

作用域链的形成?

我们从一段代码的执行来看作用域链的形成过程。

function fun01 () {
 console.log('i am fun01...');
 fun02();
}
function fun02 () {
 console.log('i am fun02...');
}
fun01();

数据访问流程

如上图,当程序访问一个变量时,按照作用域链的单向访问特性,首先在头节点的AO中查找,没有则到下一节点的AO查找,最多查找到尾节点(global AO)。在这个过程中找到了就找到了,没找到就报错undefined。

延长作用域链

从上面作用域链的形成可以看出链上的每个节点是在函数被调用执行是向链头unshift进当前函数的AO,而节点的形成还有一种方式就是“延长作用域链”,既在作用域链的头部插入一个我们想要的对象作用域。延长作用域链有两种方式:

1.with语句

function fun01 () {
 with (document) {
  console.log('I am fun01 and I am in document scope...')
 }
}
fun01();

2.try-catch语句的catch块

function fun01 () {
 try {
  console.log('Some exceptions will happen...')
 } catch (e) {
  console.log(e)
 }
}
fun01();

ps:个人感觉with语句使用需求不多,try-catch的使用也是看需求的。个人对这两种使用不多,但是在进行这部分整理过程中萌发了一点点在作用域链层面的不成熟的性能优化小建议。

由作用域链引发的关于性能优化的一点不成熟的小建议

1.减少变量的作用域链的访问节点

这里我们自定义一个名次叫做“查找距离”,表示程序访问到一个非undefined变量在作用域链中经过的节点数。因为如果在当前节点没有找到变量,就跳到下一个节点查找,还要进行判断下一个节点中是否存在被查找变量。“查找距离”越长,要做的“跳”动作和“判断”动作也就越多,资源开销就越大,从而影响性能。这种性能带来的差距可能少数的几次变量查找操作不会带来太多性能问题,但如果是多次进行变量查找,性能对比则比较明显了。

(function(){
 console.time()
 var find = 1      //这个find变量需要在4个作用域链节点进行查找
 function fun () {
  function funn () {
   var funnv = 1;
   var funnvv = 2;
   function funnn () {
    var i = 0
    while(i <= 100000000){
     if(find){
      i++
     }
    }
   }
   funnn()
  }
  funn()
 }
 fun()
 console.timeEnd()
})()

(function(){
 console.time()
 function fun () {
  function funn () {
   var funnv = 1;
   var funnvv = 2;
   function funnn () {
    var i = 0
    var find = 1      //这个find变量只在当前节点进行查找
    while(i <= 100000000){
     if(find){
      i++
     }
    }
   }
   funnn()
  }
  funn()
 }
 fun()
 console.timeEnd()
})()

在mac pro的chrome浏览器下做实验,进行1亿次查找运算。

实验结果:前者运行5次平均耗时85.599ms,后者运行5次平均耗时63.127ms。

2.避免作用域链内节点AO上过多的变量定义

过多的变量定义造成性能问题的原因主要是查找变量过程中的“判断”操作开销较大。我们使用with来进行性能对比。

(function(){
 console.time()
 function fun () {
  function funn () {
   var funnv = 1;
   var funnvv = 2;
   function funnn () {
    var i = 0
    var find = 10
    with (document) {
     while(i <= 1000000){
      if(find){
       i++
      }
     }
    }
   }
   funnn()
  }
  funn()
 }
 fun()
 console.timeEnd()
})()

在mac pro的chrome浏览器下做实验,进行100万次查找运算,借助with使用document进行的延长作用域链,因为document下的变量属性比较多,可以测试在多变量作用域链节点下进行查找的性能差异。

实验结果:5次平均耗时558.802ms,而如果删掉with和document,5次平均耗时0.956ms。

当然,这两个实验是在我们假设的极端环境下进行的,结果仅供参考!

关于闭包

1.什么是闭包?

函数对象可以通过作用域链相互关联起来,函数体内的数据(变量和函数声明)都可以保存在函数作用域内,这种特性在计算机科学文献中被称为“闭包”。既函数体内的数据被隐藏于作用于链内,看起来像是函数将数据“包裹”了起来。从技术角度来说,js的函数都是闭包:函数都是对象,都关联到作用域链,函数内数据都被保存在函数作用域内。

2.闭包的几种实现方式

实现方式就是函数A在函数B的内部进行定义了,并且当函数A在执行时,访问了函数B内部的变量对象,那么B就是一个闭包。如下:

如上两图所示,是在chrome浏览器下查看闭包的方法。两种方式的共同点是都有一个外部函数outerFun(),都在外部函数内定义了内部函数innerFun(),内部函数都访问了外部函数的数据。不同的是,第一种方式的innerFun()是在outerFun()内被调用的,既声明和被调用均在同一个执行上下文内。而第二种方式的innerFun()则是在outerFun()外被调用的,既声明和被调用不在同一个执行上下文。第二种方式恰好是js使用闭包常用的特性所在:通过闭包的这种特性,可以在其他执行上下文内访问函数内部数据。

我们更常用的一种方式则是这样的:

//闭包实例
function outerFun () {
 var outerV1 = 10
 function outerF1 () {
  console.log('I am outerF1...')
 }
 function innerFun () {
  var innerV1 = outerV1
  outerF1()
 }
 return innerFun //return回innerFun()内部函数
}
var fn = outerFun()  //接到return回的innerFun()函数
fn()     //执行接到的内部函数innerFun()

此时它的作用域链是这样的:

3.闭包的好处及使用场景

js的垃圾回收机制可以粗略的概括为:如果当前执行上下文执行完毕,且上下文内的数据没有其他引用,则执行上下文pop出call stack,其内数据等待被垃圾回收。而当我们在其他执行上下文通过闭包对执行完的上下文内数据仍然进行引用时,那么被引用的数据则不会被垃圾回收。就像上面代码中的outerV1,放我们在全局上下文通过调用innerFun()仍然访问引用outerV1时,那么outerFun执行完毕后,outerV1也不会被垃圾回收,而是保存在内存中。另外,outerV1看起来像不像一个outerFun的私有内部变量呢?除了innerFun()外,我们无法随意访问outerV1。所以,综上所述,这样闭包的使用情景可以总结为:

(1)进行变量持久化。

(2)使函数对象内有更好的封装性,内部数据私有化。

进行变量持久化方面举个栗子:

我们假设一个需求时写一个函数进行类似id自增或者计算函数被调用的功能,普通青年这样写:

 var count = 0
 function countFun () {
  return count++
 }

这样写固然实现了功能,但是count被暴露在外,可能被其他代码篡改。这个时候闭包青年就会这样写:

function countFun () {
 var count = 0
 return function(){
  return count++
 }
}
var a = countFun()
a()

这样count就不会被不小心篡改了,函数调用一次就count加一次1。而如果结合“函数每次被调用都会创建一个新的执行上下文”,这种count的安全性还有如下体现:

function countFun () {
 var count = 0
 return {
  count: function () {
   count++
  },
  reset: function () {
   count = 0
  },
  printCount: function () {
   console.log(count)
  }
 }
}
var a = countFun()
var b = countFun()
a.count()
a.count()
b.count()
b.reset()
a.printCount()  //打印:2 因为a.count()被调用了两次
b.printCount()  //打印出:0 因为调用了b.reset()

以上便是闭包提供的变量持久化和封装性的体现。

4.闭包的注意事项

由于闭包中的变量不会像其他正常变量那种被垃圾回收,而是一直存在内存中,所以大量使用闭包可能会造成性能问题。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持我们!

(0)

相关推荐

  • 深入理解Javascript中的作用域链和闭包

    首先我们回顾下之前一篇关于介绍数组遍历的文章: 请先看上一篇中提到的for循环代码: var array = []; array.length = 10000000;//(一千万) for(var i=0,length=array.length;i<length;i++){ array[i] = 'hi'; } var t1 = +new Date(); for(var i=0,length=array.length;i<length;i++){ } var t2 = +new Date();

  • JS闭包、作用域链、垃圾回收、内存泄露相关知识小结

    补充: 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 闭包的特性 闭包有三个特性: 1.函数嵌套函数 2.函数内部可以引用外部的参数和变量 3.参数和变量不会被垃圾回收机制回收 闭包的定义及其优缺点 闭包 是指有权访问另一个函数作用域中的变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量 闭包的缺点就是常驻内存,会增大内存使用量,使用不当很容易造成内存泄露. 闭包是javascript

  • 深入Javascript函数、递归与闭包(执行环境、变量对象与作用域链)使用详解

    函数表达式 1.JavaScript中定义函数有2钟方法: 1-1.函数声明: 复制代码 代码如下: function funcName(arg1,arg2,arg3){  //函数体} ①name属性:可读取函数名.非标准,浏览器支持:FF.Chrome.safari.Opera. ②函数声明提升:指执行代码之前会先读取函数声明.即函数调用可置于函数声明之前. 1-2.函数表达式: 复制代码 代码如下: var funcName = function(arg1,arg2,arg3){  //函

  • javascript从作用域链谈闭包

    神马是闭包 关于闭包的概念,是婆说婆有理. 闭包是指有权访问另外一个函数作用域中的变量的函数 这概念有点绕,拆分一下.从概念上说,闭包有两个特点: 1.函数 2.能访问另外一个函数作用域中的变量 在ES 6之前,Javascript只有函数作用域的概念,没有块级作用域(但catch捕获的异常 只能在catch块中访问)的概念(IIFE可以创建局部作用域).每个函数作用域都是封闭的,即外部是访问不到函数作用域中的变量. function getName() { var name = "美女的名字&

  • 浅析JavaScript作用域链、执行上下文与闭包

    闭包和作用域链是JavaScript中比较重要的概念,这两天翻阅了一些资料,把相关知识点给大家总结了以下. JavaScript 采用词法作用域(lexical scoping),函数执行依赖的变量作用域是由函数定义的时候决定,而不是函数执行的时候决定.以下面的代码片段举例说明,通常来说(基于栈的实现,如 C 语言) foo 被调用之后函数内的本地变量 scope 会被释放,但是从词法上看 foo 的内嵌匿名函数中 scope 应该指的是 foo 的本地变量 scope ,并且实际上代码的运行结

  • JavaScript中的作用域链和闭包

    作用域 全局作用域 局部作用域 作用域链 执行上下文 活动对象 闭包 闭包优化 JavaScript中出现了一个以前没学过的概念--闭包.何为闭包?从表面理解即封闭的包,与作用域有关.所以,说闭包以前先说说作用域. 作用域(scope) 通常来说一段程序代码中使用的变量和函数并不总是可用的,限定其可用性的范围即作用域,作用域的使用提高了程序逻辑的局部性,增强程序的可靠性,减少名字冲突. 全局作用域(Global Scope) 在代码中任何地方都能访问到的对象拥有全局作用域,以下几种情形拥有全局作

  • 图解JavaScript作用域链底层原理

    目录 前言 作用域 1.什么是作用域 2.[[Scopes]]属性 3.作用域链 4.图解查找变量原理 总结 前言 在学习JavaScript时大家一定都知道,外部空间不能访问内部变量,我们往往只知道这一基本规则,那实现这一基本规则的基本底层原理是什么呢?今天我将从小白的角度来带大家理解作用域链,希望能给大家一些帮助! 作用域 1.什么是作用域 简单来说,作用域(英文:scope)是据名称来查找变量的一套规则,可以把作用域通俗理解为一个封闭的空间,这个空间是封闭的,不会对外部产生影响,外部空间不

  • Javascript的作用域、作用域链以及闭包详解

    一.javascript中的作用域 ①全局变量-函数体外部进行声明 ②局部变量-函数体内部进行声明 1)函数级作用域 javascript语言中局部变量不同于C#.Java等高级语言,在这些高级语言内部,采用的块级作用域中会声明新的变量,这些变量不会影响到外部作用域. 而javascript则采用的是函数级作用域,也就是说js创建作用域的单位是函数. 例如: 在C#当中我们写如下代码: static void Main(string[] args) { for (var x = 1; x < 1

  • 跟我学习javascript的作用域与作用域链

    作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理.今天这篇文章对JavaScript作用域和作用域链作简单的介绍,希望能帮助大家更好的学习JavaScript. 一.JavaScript作用域 任何程序设计语言都有作用域的概念,简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期.在JavaScript中,变量的作用域有全局作用域和局部作用域两种. 1. 全局作用域(Global

  • 全面了解JavaScript的作用域链

    JavaScript的作用域链 这是一个非常重要的知识点了,了解了JavaScript的作用域链的话,能帮助我们理解很多'异常'问题. 下面我们来看一个小例子,前面我说过的声明提前的例子. var name = 'Skylor.min'; function echo() { alert(name); var name = 'mm'; alert(name); alert(age); } echo(); 对于这个例子,没有接触过这方面的时候,第一反应是会纠结下,这第一个的name,到底调用全局变量

  • JavaScript中作用域链的概念及用途讲解

    从零开始讲解JavaScript中作用域链的概念及用途 引言 之前我写过一篇关于JavaScript中的对象的一篇文章,里面也提到了作用域链的概念,相信大家对这个概念还是没有很深的理解,并且这个概念也是面试中经常问到的,因为这个概念实在太重要了,在我们平时写代码时,也可能会因为作用域链的问题,而出现莫名其妙的bug,导致我们花费大量的时间都查找不出原因.所以我就准备单独写一篇关于作用域链的文章,来帮大家更好地理解这个概念. 正文 一.执行环境 首先,我们要引入一个概念,叫做执行环境(下面简称环境

  • 你真的了解JavaScript的作用域与闭包吗

    目录 一.作用域 二.闭包 总结 一.作用域 1.作用域总体来说就是根据名称查找变量的一套规则.JS查找变量的方式有两种:LHS和RHS. LHS(left hand side)大致可以理解为给某个变量赋值,在赋值符(=)的左边:RHS(right hand side)是指找到某个变量的源值,如在赋值符(=)的右边. 举个例子说明,对于 a = 10; a = b; 这两个语句. var a 这里就是LHS查询,查找当前作用域是否有a这个变量来给a进行赋值,如果有则进行赋值:如果没有,在非严格模

  • 网易JS面试题与Javascript词法作用域说明

    调用对象位于作用域链的前端,局部变量(在函数内部用var声明的变量).函数参数及Arguments对象都在函数内的作用域中--这意味着它们隐藏了作用域链更上层的任何同名的属性. 2010年9月14日,我去参加网易网页工程师招聘会,应聘JS工程师职位.有幸参加笔试,然后有幸栽在笔试,呵呵.废话少说,抓出音响极深的一题重新研究研究. 题目大概是:写出如下代码的输出结果并进行分析 复制代码 代码如下: var tt = 'aa'; function test(){ alert(tt); var tt

  • JavaScript词法作用域与调用对象深入理解

    关于 Javascript 的函数作用域.调用对象和闭包之间的关系很微妙,关于它们的文章已经有很多,但不知道为什么很多新手都难以理解.我就尝试用比较通俗的语言来表达我自己的理解吧. 作用域 Scope Javascript 中的函数属于词法作用域,也就是说函数在它被定义时的作用域中运行而不是在被执行时的作用域内运行.这是犀牛书上的说法.但"定义时"和"执行(被调用)时"这两个东西有些人搞不清楚.简单来说,一个函数A在"定义时"就是 functio

  • 一次让你了解全部JavaScript的作用域

    前言 作用域决定了变量的生命周期和可见性,变量在作用域范围之外是不可见的. JavaScript 的作用域包括:模块作用域,函数作用域,块作用域,词法作用域和全局作用域. 全局作用域 在任何函数.块或模块范围之外定义的变量具有全局作用域.可以在程序的任意位置访问全局变量. 当启用模块系统时,创建全局变量会变得困难,但仍然可以做到这一点.可以在 HTML 中定义一个变量,这个变量需要在函数之外声明,这样就可以创建一个全局变量: <script> let GLOBAL_DATA = { value

  • JS作用域作用链及this使用原理详解

    目录 变量提升的原理:JavaScript的执行顺序 第一部分:变量提升部分的代码 第二部分:代码执行部分 代码执行阶段 调用栈:栈溢出的原理 如何利用调用栈 1.使用浏览器查看调用栈的信息 2.小心栈溢出 块级作用域:var.let以及const 作用域 小结 作用域链和闭包 块级作用域中的变量查找 闭包 闭包怎么回收 小练 this:从执行上下文分析this 全局执行上下文的this 函数执行上下文的this 1.通过call 2.通过对象调用 3.通过构造函数设置 this的缺陷以及应对方

随机推荐