JavaScript笔记 | 作用域和闭包 |《你不知道的JavaScript(上卷)》第一部分

JavaScript | 作用域和闭包 | 读书笔记

读书笔记(自用)

来自《你不知道的JavaScript(上卷)》第一部分 作用域和闭包

1 作用域是什么

1.1编译的3个步骤

(1)分词/词法分析(Tokenizing/Lexing)
–将字符组成的字符串分解为对编程语言来说有意义的代码块(词法单元(token))
(2)解析/语法分析(Parsing)
–将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树(抽象语法树(Abstract Syntax Tree,AST))
(3)代码生成
–将AST转换为可执行代码(机器指令)的过程

1.2理解作用域

(1)LHS
当变量出现在赋值操作的左侧时进行LHS查询
LHS查询试图找到变量容器本身
如果查找的目的是对变量进行赋值,就会使用LHS
(2)RHS
当变量出现在赋值操作的右侧时进行RHS查询
RHS查询就是简单的查找某个变量的值
如果目的是获取变量,就会使用RHS
参考链接:
更详细的知识点内容如下:笔记1link ,笔记2 link,笔记3 link

1.3作用域嵌套

1.4异常

举例

function foo(a){
   console.log(a+b);
   b=a;
}
foo(2)

–ReferenceError异常–
指作用域判别失败

(1)RHS查询
由于b是一个未声明的变量,第一次对b进行的RHS查询在所有嵌套的作用域中都遍寻不到所需的变量,所以引擎会抛出ReferenceError异常。
(2)非严格模式下的LHS查询
当引擎执行LHS查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎。
(3)严格模式下的LHS查询
严格模式禁止自动或隐式地创建全局变量,因此在严格模式中LHS查询失败时,并不会创建并返回一个全局变量,此时,引擎会抛出同RHS查询失败时类似的ReferenceError异常。

–TypeError异常–
指在作用域判别成功的情况下,对结果的操作是非法或不合理的。
如果RHS查询到了一个变量,但你尝试对这个变量的值进行不合理的操作,例如试图对一个非函数类型的值进行函数调用,或引用null或undefined类型的值中的属性,那么引擎会抛出TypeError异常。

2.词法作用域

2.1词法阶段

举例:找出3个逐级嵌套的作用域
图片来自《你不知道的JavaScript(上卷)》

2.2欺骗词法

如果词法作用域完全由写代码期间函数所声明的位置来定义,怎样才能在运行时来“修改”(欺骗)词法作用域呢?

(1)eval
–JS中的eval()函数可以接受一个字符串作为参数,可以在你写的代码中用程序生成代码并运行,就好像代码是写在那个位置的一样。

–在执行eva()之后的代码时,引擎并不“知道”或“在意”前面的代码是以动态形式插入进来,并对词法作用域的环境进行修改的,引擎指挥如往常地进行词法作用域查找。

举例:

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

图片来自《你不知道的JavaScript(上卷)》
–注意:在严格模式下,eval()中的声明无法修改所在的作用域。
举例:

function foo(str){
    "use strict";
    eval(str);
    console.log(a);//ReferenceError:a is not defined
}
foo("var a=2");       

(2)with
–通常被当做重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。
图片来自《你不知道的JavaScript(上卷)》
图片来自《你不知道的JavaScript(上卷)》![图片来自《你不知道的JavaScript(上卷)》](https://img-blog.csdnimg.cn/299cf90768b848f192ee1fd3e9525fea.jpeg#pic_center

(3)性能
eval()和with会在运行时修改或创建新的作用域,一次欺骗其他在书写时定义的词法作用域。
大量使用eval()或with会导致运行起来会很慢。

3.函数作用域和块作用域

3.1函数中的作用域

3.2隐藏内部实现

好处:

  • 最小暴露/授权原则,如果把所有变量和函数都放在全局作用域中,可能会暴露或泄露过多的本该是私有的变量或函数。
  • 规避冲突,避免同名标识符之间的冲突,而冲突会导致变量的值被意外覆盖。
    –>全局命名空间
    –>模块管理

3.3函数作用域

函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。(另外,函数表达式可以是匿名的,但在JS中函数声明不可以省略函数名)

3.3.1匿名和具名

匿名函数表达式:
function()…没有名称标识符。

setTimeout( function() {
    console.log("I waited 1 second!");
}, 1000 )

(1)优点:

  • 写起来简单快捷,很多库和工具也倾向于鼓励这种代码风格。

(2)缺点:

  • 调试困难,因为匿名函数在栈追踪中不会显示有意义的函数名。
  • 如果没有函数名,当函数需要引用自身时只能使用已经过期的darguments.callee 引用,比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑自身。
  • 不利于代码的可读性。

(3)解决方案:

  • 给函数表达式指定一个函数名可以有效解决以上问题

3.3.2立即执行函数表达式

** IIFE(Immediately Invoked Function Expression)**

var a = 2;
(function foo() {
   var a = 3;
   console.log( a ); // 3
})();
console.log( a ); // 2

由于函数被包含在一对 ( ) 括号内部,因此成为了一个表达式,通过在末尾加上另外一个( ) 可以立即执行这个函数,比如 (function foo(){ … })()。第一个 ( ) 将函数变成表达式,第二个 ( ) 执行了这个函数。

3.4块作用域

3.4.1with

3.4.2try/catch

JavaScript 的 ES3 规范中规定 try/catch 的 catch 分句会创建一个块作用域,其中声明的变量仅在 catch 内部有效。

try {
    undefined(); // 执行一个非法操作来强制制造一个异常
}
catch (err) {
    console.log( err ); // 能够正常执行!
}
console.log( err ); // ReferenceError: err not found;

3.4.3let

ES6引入let,let 关键字可以将变量绑定到所在的任意作用域中(通常是 { … } 内部)。换句话说,let为其声明的变量隐式地了所在的块作用域。
(1)垃圾收集
举例

function process(data) {
    // 在这里做点有趣的事情
}
var someReallyBigData = { .. };
process( someReallyBigData );
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt) {
    console.log("button clicked");
}, /*capturingPhase=*/false );

click 函数的点击回调并不需要someReallyBigData 变量。理论上这意味着当 process(…) 执行后,在内存中占用大量空间的数据结构就可以被垃圾回收了。但是,由于 click 函数形成了一个覆盖整个作用域的闭包,JavaScript 引擎极有可能依然保存着这个结构(取决于具体实现)。
块作用域可以打消这种顾虑,可以让引擎清楚地知道没有必要继续保存 someReallyBigData 了:

function process(data) {
// 在这里做点有趣的事情
}
// 在这个块中定义的内容可以销毁了!
{
    let someReallyBigData = { .. };
    process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );

3.4.4const

var、let、const知识点详细版 后续补充

4.提升

4.2编译器

foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
    // ...
};

这个代码片段经过提升后,实际上会被理解为以下形式

var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function() {
    var bar = ...self...
// ...
}

4.3函数优先

这一部分后续补充

5.作用域闭包

5.3循环和闭包

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

正常情况下,我们对这段代码行为的预期是分别输出数字 1~5,每秒一次,每次一个。
但实际上,这段代码在运行时会以每秒一次的频率输出五次 6。这个循环的终止条件是 i 不再 <=5。条件首次成立时 i 的值是6。因此,输出显示的是循环结束时 i 的最终值。
解决方案:
我们使用 IIFE 在每次迭代时都创建一个新的作用
域。换句话说,每次迭代我们都需要一个块作用域。第 3 章介绍了 let 声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。

for (var i=1; i<=5; i++) {
    let j = i; // 是的,闭包的块作用域!
    setTimeout( function timer() {
        console.log( j );
}, j*1000 );
}

5.5 模块

function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];
    function doSomething() {
        console.log( something );
}
    function doAnother() {
        console.log( another.join( " ! " ) );
}
    return {
        doSomething: doSomething,
        doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

这个模式在 JavaScript 中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露,这里展示的是其变体。
模块模式需要具备两个必要条件:

  • 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
  • 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

一个具有函数属性的对象本身并不是真正的模块。从方便观察的角度看,一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块。

附录A:动态作用域

附录B:块作用域的替代方案

附录A:this词法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值