资讯专栏INFORMATION COLUMN

《你不知道的javascript》笔记_作用域与闭包

galaxy_robot / 523人阅读

摘要:建筑的顶层代表全局作用域。实际的块级作用域远不止如此块级作用域函数作用域早期盛行的立即执行函数就是为了形成块级作用域,不污染全局。这便是闭包的特点吧经典面试题下面的代码输出内容答案个如何处理能够输出闭包方式方式下一篇你不知道的笔记

下一篇:《你不知道的javascript》笔记_this

写在前面

这一系列的笔记是在《javascript高级程序设计》读书笔记系列的升华版本,旨在将零碎未知的知识总结

一、基本概念 1.1 编译

在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为编译

1. 分词/词法分析(Tokenizing/Lexing)

这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。例如,考虑程序var a = 2;。这段程序通常会被分解成为下面这些词法单元:var、a、=、2 、;。空格是否会被当作词法单元,取决于空格在这门语言中是否具有意义

2. 解析/语法分析(Parsing)

这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为抽象语法树(Abstract Syntax Tree,AST)

3. 代码生成

将 AST 转换为可执行代码的过程称被称为代码生成。抛开具体细节,简单来说就是有某种方法可以将var a = 2;的 AST 转化为一组机器指令,用来创建一个叫作 a 的变量(包括分配内存等),并将一个值储存在 a 中

相对于上面的流程,javascript在语法分析和代码生成阶段有特定的步骤来对运行性能进行优化,包括对冗余元素进行优化等。

1.2 组成
引擎:从头到尾负责整个JavaScript程序的编译及执行过程

编译器:负责语法分析及代码生成等脏活累活

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

1.3 查询&异常

RHS 查询:当变量出现在赋值操作的右侧时进行 RHS 查询
LHS 查询:当变量出现在赋值操作的左侧时进行 LHS 查询(赋值、传参、函数执行)

RHS查询异常:RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常
LHS查询异常:非严格模式下,LHS 查询失败会在全局创建变量;在严格模式中 LHS 查询失败时,并不会创建并返回一个全局变量,引擎会抛出同 RHS 查询失败时类似的 ReferenceError 异常

1.4 思考

javascript引擎执行代码var a = 2;的过程?

编译阶段:var a;,如果作用域内已存在变量 a,则忽略;若不存在,则在该作用域内声明
执行阶段:a = 2;,对 a 进行 LHS 引用,并对其赋值

二、作用域

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

通俗的说,作用域是维护变量并确定访问权限的一套规则

2.1 词法作用域
词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)

下面有个简单的作用域嵌套的例子:

【1】包含着整个全局作用域,其中只有一个标识符: foo。
【2】包含着 foo 所创建的作用域,其中有三个标识符: a、bar 和 b,可访问全局作用域变量。
【3】包含着 bar 所创建的作用域,其中只有一个标识符: c,可访问foo和全局作用域变量。

另外有两个比较特殊的欺骗词法机制:

eval(..) 函数

with 关键字

这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们
2.2 作用域查找规则

书中对作用域链和作用域查找做了一个非常形象的比喻,如下图

这个建筑代表程序中的嵌套作用域链。第一层楼代表当前的执行作用域,也就是你所处的位置。建筑的顶层代表全局作用域。

LHS 和 RHS 引用都会在当前楼层进行查找,如果没有找到,就会坐电梯前往上一层楼, 如果还是没有找到就继续向上,以此类推。一旦抵达顶层(全局作用域),可能找到了你所需的变量,也可能没找到,但无论如何查找过程都将停止

作用域查找会在找到第一个匹配的标识符时停止

2.2 块级作用域

早期的javascript语句中块级作用域就是函数块,这是在读本书之前我粗浅的认识。实际的块级作用域远不止如此

块级作用域:
(1)函数作用域
早期盛行的立即执行函数(IIFE)就是为了形成块级作用域,不污染全局。常用的写法有:

(function(形参){函数体})(实参)

(function(形参){函数体}(实参))

!function(形参){函数体}(实参)

(2) with关键字

(3) try/catch语句
Google 维护着一个名为 Traceur 的项目,该项目正是用来将 ES6 代码转换成兼容 ES6 之前 的环境(大部分是 ES5,但不是全部),下面是用来兼容低版本创建块级作用域的写法:

{
    try {
        throw undefined;
    } catch (a) {
        a = 2;
        console.log( a );
    }
}

(4) let/const关键字

三、变量提升

在之前的两篇文章中对变量提升(预解析)有比较充分的说明:
《javascript高级程序设计》笔记:变量对象与预解析
《javascript高级程序设计》笔记:内存与执行环境

四、闭包 4.1 什么是闭包
本书中定义:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

MDN定义:闭包是函数和声明该函数的词法环境的组合

个人理解:当外部能够访问到某个函数的私有变量时,就会产生闭包(不严谨,仅用于理解)

两个经典的闭包例子:

function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name);
    }
    return displayName;
}

var myFunc = makeFunc();
myFunc(); // "Mozilla"

思考myFunc是执行makeFunc时创建的displayName函数实例的引用,为什么执行myFunc时会打印出makeFunc中私有变量name呢?

解释闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

分析:按照闭包能暂存变量的思路,执行makeAdder时,会把参数暂存在所return的函数中,当再次执行函数时,会把两次的参数之和输出

4.2 应用

闭包在js编程中随处可见,书中有这样一个结论:

在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!

定时器闭包案例:

function wait(message) {
    setTimeout( function timer() {
        console.log( message );
    }, 1000 );
}
wait( "Hello, closure!" );

事件监听闭包案例:

function setupBot(name, selector) {
    $(selector).click( function activator() {
        console.log( "Activating: " + name );
    });
}
setupBot( "Closure Bot 1", "#bot_1" );
setupBot( "Closure Bot 2", "#bot_2" );

上面的案例中,有个相同的特点:先定义函数,后执行函数时能够调用到函数中的私有变量或者实参。这便是闭包的特点吧

4.3 经典面试题

(1)下面的代码输出内容?

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

答案:5个6

(2)如何处理能够输出1~5

// 闭包方式
for (var i=1; i<=5; i++) {
    (function(index) {
        setTimeout( function timer() {
            console.log( index );
        }, index*1000 );
    })(i)
}

// ES6 方式
for (let i=1; i<=5; i++) {
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
}

下一篇:《你不知道的javascript》笔记_this

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

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

相关文章

  • 你不知道JavaScript上卷之作用域与闭包·读书笔记

    摘要:的分句会创建一个块作用域,其声明的变量仅在中有效。而闭包的神奇作用是阻止此事发生。依然持有对该作用域的引用,而这个引用就叫做闭包。当然,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。 date: 16.12.8 Thursday 第一章 作用域是什么 LHS:赋值操作的目标是谁? 比如: a = 2; RHS:谁是赋值操作的源头? 比如: conso...

    Raaabbit 评论0 收藏0
  • 你不知道javascript笔记_this

    下一篇:《你不知道的javascript》笔记_对象&原型 写在前面 上一篇博客我们知道词法作用域是由变量书写的位置决定的,那this又是在哪里确定的呢?如何能够精准的判断this的指向?这篇博客会逐条阐述 书中有这样几句话: this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式当一个函数被调用时...

    cpupro 评论0 收藏0
  • 先有蛋还是先有鸡?JavaScript 作用域与闭包探析

    摘要:而闭包的神奇之处正是可以阻止事情的发生。拜所声明的位置所赐,它拥有涵盖内部作用域的闭包,使得该作用域能够一直存活,以供在之后任何时间进行引用。依然持有对该作用域的引用,而这个引用就叫闭包。 引子 先看一个问题,下面两个代码片段会输出什么? // Snippet 1 a = 2; var a; console.log(a); // Snippet 2 console.log(a); v...

    elisa.yang 评论0 收藏0
  • Js基础知识(三) - 作用域与闭包

    摘要:是词法作用域工作模式。使用可以将变量绑定在所在的任意作用域中通常是内部,也就是说为其声明的变量隐式的劫持了所在的块级作用域。 作用域与闭包 如何用js创建10个button标签,点击每个按钮时打印按钮对应的序号? 看到上述问题,如果你能看出来这个问题实质上是考对作用域的理解,那么恭喜你,这篇文章你可以不用看了,说明你对作用域已经理解的很透彻了,但是如果你看不出来这是一道考作用域的题目,...

    lemanli 评论0 收藏0
  • Js基础知识(三) - 作用域与闭包

    摘要:是词法作用域工作模式。使用可以将变量绑定在所在的任意作用域中通常是内部,也就是说为其声明的变量隐式的劫持了所在的块级作用域。 作用域与闭包 如何用js创建10个button标签,点击每个按钮时打印按钮对应的序号? 看到上述问题,如果你能看出来这个问题实质上是考对作用域的理解,那么恭喜你,这篇文章你可以不用看了,说明你对作用域已经理解的很透彻了,但是如果你看不出来这是一道考作用域的题目,...

    XFLY 评论0 收藏0

发表评论

0条评论

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