资讯专栏INFORMATION COLUMN

你应该要知道的作用域和闭包

JouyPub / 3490人阅读

摘要:写在前面对于一个前端开发者,应该没有不知道作用域的。欺骗词法作用域有两个机制可以欺骗词法作用域和。关于你不知道的的第一部分作用域和闭包已经结束了,但是,更新不会就此止住未完待续

这是《你不知道的JavaScript》的第一部分。

本系列持续更新中,Github 地址请查阅这里。

写在前面

对于一个前端开发者,应该没有不知道作用域的。它是一个既简单有复杂的概念,简单到每行代码都有它的影子,复杂到写过很多的代码依然不一定能完全理解。

最近在看《你不知道的JavaScript》,看完之后不写点什么,好像看的意义就不大了。今天就花点时间,从最简单又复杂的作用域开始缕一缕。

先来份关于《你不知道的JavaScript》第一部分作用域和闭包的目录感受一下:

第1章 作用域是什么

第2章 词法作用域

第3章 函数作用域和块作用域

第4章 提升

第5章 作用域闭包

真正开始写的时候发现好像并没有什么好写了,因为这并不是很难的概念,但是要问自己闭包到底是什么,到底有什么用,什么时候场景用的时候,好像又有点模糊了?闭包这个经常写的代码,居然好像并没有彻底地理解。

开始之前

先问自己几个问题

你真的理解了作用域吗?

什么是动态作用域?什么是词法作用域?

闭包到底是什么?闭包的应用场景有哪些?

怎么实现一个简单的模块依赖加载器?

JS是一门解释型语言,它的编译过程不是发生在构建之前,那么声明提升为什么会发生呢?

如果这些你都知道的话,或许这篇内容并不是很需要看了,如果还有一点困惑,希望它能对你有点帮助。

作用域是什么

在讲作用域之前,我们先来看看什么是编译。在传统编译语言的流程中,编译分为3步:

分词/词法分析:将由字符组成的字符串分解成有意义的代码块(词法单元)

解析/语法分析:将词法单元流转换成一个由元素逐级嵌套所组成的代表程序语法机构的树(抽象语法树,AST)

代码生成:将AST转换成可执行代码

JS是一门解释型语言,它的编译过程不是发生在构建之前,大部分情况下编译发生在代码执行前的几微秒(甚至更短)的时间内,所以在作用域的背后,JS引擎用尽了各种办法来保证性能最佳。

简单了解了编译后,我们再来看看作用域是什么:

作用域负责收集并维护有所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限

上面的话不是很好理解,我们来分解一下下面的代码:

var a = 2;
var b = a;

熟悉JS的同学都知道,包括变量和函数在内的所有声明都会有一个提升的过程,即首先被处理,但是赋值不会提升。所以上面的代码会被如下处理:

var a;
var b;

a = 2;
b = a;

当编译器遇到var a时,编译器会询问作用域是否存在名称为a的变量。如果存在,则忽略声明,继续编译;如果不存在,则声明一个新变量,命名a。(var b;同理)

当遇到a = 2时,引擎会询问作用域,当前作用域中是否存在该变量,如果找不到,就向上一级查找,当抵达最外层的全局作用域时,无论找到还是没找到,查找都会停止,找到了就赋值2给它,没找到就抛出异常。

其中,a = 2这一过程中对a的查找被称为LHS查询,在b = a这一句中,引擎会查找变量a的值和变量b的容器,并将a的值赋值给b,这一查找变量a的值被称为RHS查询。

词法作用域

作用域有两种工作模式,一种词法作用域,一种动态作用域。词法作用域(也叫静态作用域)就是在词法阶段的作用域,词法分析阶段就确定了,不会改变。JS采用的就是词法作用域,但是可以通过一些欺骗词法作用域的方法,在词法分析过后依然可以修改作用域。

词法作用域与动态作用域

我们先来看看词法作用域与动态作用域的区别(因为JS采用的是词法作用域,所以对动态作用域不做过多介绍):

var a = 2;

function foo () {
  console.log(a); // 会输出2还是3?
}

function bar () {
  var a = 3;
  foo();
}

bar();

熟悉JS的同学应该都知道通过RHS引用到了全局作用域中的a,所以输出2。但是如果JS是动态作用域,情况就不一样了,当foo()无法找到a的变量引用时,会在调用foo()的地方查找a,而不是在嵌套的词法作用域上查找,所以会输出3。

下面我们来看看什么是欺骗词法。

欺骗词法作用域

JS有两个机制可以欺骗词法作用域:eval()和with。大多数情况下,它们是不被推荐使用的,因为欺骗词法作用域导致引擎无法在编译时对作用域查找进行优化,所以会导致性能下降;另外,在严格模式下,with被完全禁止使用,间接或非安全的使用eval也被禁止。

eval()这个方法接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。

 function foo (str, a) {
   eval(str);
   console.log(a, b);
 }
 
 var b = 2;
 
 foo("var b =3;", 1); // 1, 3

可以看到 eval() 调用了 var b =3; 导致修改了原本的作用域。

with 通常被当作重复引用同一个对象中的多个属性的快捷方式。

function foo (obj) { 
  with (obj) {
    a = 2; 
  }
}

var o1 = { 
  a: 3
};

var o2 = { 
  b: 3
};

foo(o1);
console.log(o1.a); // 2

foo(o2);
console.log(o2.a); // undefined
console.log(a); // 2——不好,a被泄漏到全局作用域上了!

with 声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域

声明提升

前面有个例子说到了声明提升,我们已经知道引擎会在解释 JavaScript 代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来,这也正词法作用域的核心内容。声明提升在代码中比较常见,相信面试过的朋友肯定对它非常熟悉了,但是它也有几个必须要注意的点:

每个作用域都会进行提升操作,声明会被提升到所在作用域的顶部(只有声明会被提升,赋值或其他运行逻辑会留在原地)

并非所有的声明都会被提升,不同声明提升的权重也不同,具体来说函数声明会被提升,函数表达式不会被提升(就算是有名称的函数表达式也不会提升),通过var定义的变量会提升,而let和const进行的声明不会提升

函数声明和变量声明都会被提升。但是一个值得注意的细节也就是函数会首先被提升,然后才是变量,也就是说如果一个变量声明和一个函数声明同名,那么就算在语句顺序上变量声明在前,该标识符还是会指向相关函数

如果变量或函数有重复声明以会第一次声明为主,但是后面的函数声明还是可以覆盖前面的

闭包

闭包到底是什么?

闭包就是能够读取其他函数内部变量的函数

内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后

闭包这个词的意思是封闭,将外部作用域中的局部变量封闭起来的函数对象称为闭包。被封闭起来的变量与封闭它的函数对象有相同的生命周期

关于闭包的解释,网上有很多,《你不知道的JavaScript》的作者也给出了他的定义:

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行

看起来不是那么生动的解释,仔细看看好像也不是很难理解,不过,作为一个程序员,代码才是王道

function foo () {
  var a = 2;
  function bar () {
    console.log(a);
  }
  return bar;
}
var baz = foo();
baz();

函数bar()的词法作用域可以访问foo()的内部作用域,foo()执行之后,bar()依然持有对foo()内部作用域的引用(也就不会被垃圾回收机制回收),bar()对该作用域的这个引用就被叫做闭包。

再来看看下面这段代码,有没有很熟悉的感觉,看过一些面试题的朋友应该都不会陌生吧,答案是每隔一秒的频率输出五次6

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer () {
    console.log(i)
  }, i * 1000)
}

浏览器运行机制,任务队列之类的我们就不讨论了,我们来看看怎么改进,从闭包的角度出现让它输出1~5

for (var i = 1; i <= 5; i++) {
  (function () {
    setTimeout(function timer () {
      console.log(i)
    }, i * 1000)
  })()
}

上面的代码可以吗?是的,不行,我们只是封闭了什么都没有的空作用域中,依然会向上查找全局的i。怎么实现?写了这么多,我的任务完成了,轮到你动一下脑瓜子了。

模块依赖加载器

require(["a", "b"], callback)这样的模块加载方式有没有勾起你老人家什么回忆呢?作为一个年轻人,ES6的import大法还是比较适合我,不过前辈当时的先进经验还是有很多可以学习的地方的,直接贴代码了

var MyModules = (function Manager () {
  var modules = {};

  function define (name, deps, impl) {
    for (var i = 0; i < deps.length; i++) {
      deps[i] = modules[deps[i]];
    }
    modules[name] = impl.apply(impl, deps);
  }

  function get (name) {
    return modules[name];
  }
  return {
    define: define,
    get: get
  }
})();

上面实现了一个简单的模块加载器,下面是使用它来定义模块

MyModules.define("bar", [], function () {
  function hello (who) {
    return "Let me introduce: " + who;
  }
  return {
    hello: hello
  };
});

MyModules.define("foo", ["bar"], function (bar) {
  var hungry = "hippo";

  function awesome () {
    console.log(bar.hello(hungry).toUpperCase())
  }
  return {
    awesome: awesome
  };
});

var bar = MyModules.get("bar");
var foo = MyModules.get("foo");

console.log(bar.hello("hippo")); // Let me introduce: hippo 
foo.awesome(); // LET ME INTRODUCE: HIPPO
写在最后

不知不觉,篇幅已经不短,怎么总结的更精炼确实是一个技术活,我得好好学学才行。本篇主要从编译、词法作用域、声明提升的角度对JS的作用域进行了介绍,并慢慢打开了闭包的大门,最后展示了一个简单的模块加载器的代码。

关于《你不知道的JavaScript》的第一部分——作用域和闭包已经结束了,但是,更新不会就此止住

未完待续...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94791.html

相关文章

  • 弄懂JavaScript作用域和闭包

    摘要:关于本书,我会写好几篇读书笔记用以记录那些让我恍然大悟的瞬间,本文是第一篇弄懂的作用域和闭包。作用域也可以看做是一套依据名称查找变量的规则。声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域。 《你不知道的JavaScript》真的是一本好书,阅读这本书,我有多次哦,原来是这样的感觉,以前自以为理解了(其实并非真的理解)的概念,这一次真的理解得更加透彻了。关于本书,我会写好...

    everfly 评论0 收藏0
  • javascript作用域和闭包之我见

    摘要:查询是在作用域链中,一级级的往上查找该变量的引用。作用域和作用域链作用域的概念,应该两张图几句话就能解释吧。这个建筑代表程序中的嵌套作用域链。一层嵌一层的作用域形成了作用域链,变量在作用域链中的函数内得到了自己的定义。 javascript作用域和闭包之我见 看了《你不知道的JavaScript(上卷)》的第一部分——作用域和闭包,感受颇深,遂写一篇读书笔记加深印象。路过的大牛欢迎指点...

    SoapEye 评论0 收藏0
  • JavaScript面向对象~ 作用域和闭包

    摘要:大名鼎鼎的作用域和闭包,面试经常会问到。声明理解闭包,先理解函数的执行过程。闭包的基本结构因为闭包不允许外界直接访问,所以只能间接访问函数内部的数据,获得函数内部数据的使用权。 大名鼎鼎的作用域和闭包,面试经常会问到。闭包(closure)是Javascript语言的一个难点,也是它的特色。 声明 理解闭包,先理解函数的执行过程。 代码在执行的过程中会有一个预解析的过程,也就是在代码的...

    WilsonLiu95 评论0 收藏0
  • [JS]《知道Javascript·上》——词法作用域和闭包

    摘要:吐槽一下,闭包这个词的翻译真是有很大的误解性啊要说闭包,要先说下词法作用域。闭包两个作用通过闭包,在外部环境访问内部环境的变量。闭包使得函数可以继续访问定义时的词法作用域。 闭包是真的让人头晕啊,看了很久还是觉得很模糊。只能把目前自己的一些理解先写下来,这其中必定包含着一些错误,待日后有更深刻的理解时再作更改。 吐槽一下,闭包这个词的翻译真是有很大的误解性啊…… 要说闭包,要先说下词法...

    guqiu 评论0 收藏0
  • 重读知道JS (上) 第一节三章

    摘要:如果是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。给函数表达式指定一个函数名可以有效的解决以上问题。始终给函数表达式命名是一个最佳实践。也有开发者干脆关闭了静态检查工具对重复变量名的检查。 你不知道的JS(上卷)笔记 你不知道的 JavaScript JavaScript 既是一门充满吸引力、简单易用的语言,又是一门具有许多复杂微妙技术的语言,即使是经验丰富的 Ja...

    lavor 评论0 收藏0

发表评论

0条评论

JouyPub

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<