小黄书(《你不知道的JavaScript上卷》)学习笔记之作用域和闭包

2021/7/16 22:35:07

本文主要是介绍小黄书(《你不知道的JavaScript上卷》)学习笔记之作用域和闭包,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一、 作用域是什么

1. 编译原理

与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植
JavaScript 是先编译,编译完就会立即执行。JavaScript 引擎用尽了各种办法(比如 JIT,可以延迟编译甚至实施重编译)来保证性能最佳。

2. 理解作用域

作用域是根据名称查找变量的一套规则。

  • 理解作用域
    首先需要了解对var a = 2;进行处理的演员们
    1. 引擎:从头到尾负责整个 JavaScript 程序的编译及执行过程。
    2. 编译器:引擎的好朋友之一,负责语法分析及代码生成等脏活累活(详见前一节的内容)。
    3. 作用域:引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查
      询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

处理过程分为两个部分,编译和执行

编译器首先会将这段程序分解成词法单元,然后将词法单元解析成一个树结构。然后编译器第二步就会生成代码,接着引擎
开始执行它,执行过程会进行查询。有两种查询方式

简单概况:如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;
如果目的是获取变量的值,就会使用 RHS 查询。
赋值操作符会导致 LHS 查询。=操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。

在查询过程中,LHS 和 RHS 查询都会在当前执行作用域中开始,没找到就会一直向上级作用域查找,最后达到全局。
引擎执行 LHS 查询时,如果在顶层(全局作用域)中也无法找到目标变量,
全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下。

如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。
比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的属性,
那么引擎会抛出另外一种类型的异常,叫作 TypeError。

测验答案(自己做的)

lsh:1. 引擎找c  var c
    	   2. 引擎找a  函数传递形参时,a作为形参
  	     3. 引擎找b

rhs: 1. foo函数rhs引用
   	     2.a的rhs引用 将a赋予b
    	    3. b的rhs引用   a+b
     	   4..a的rhs引用    a+b

二、词法作用域

1、词法阶段

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

在多层的嵌套作用域中可以定义同名的 标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应,
作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见 第一个匹配的标识符为止。

2、欺骗词法

  1. eval()函数:接受一个字符串(程序代码)并执行,将字符串定位在eval(函数所在位置)
    setTimeout(…) 和setInterval(…) 的第一个参数可以是字符串,与eval函数类似。
function foo(str, a) {
eval( str ); // 欺骗!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3```
  1. with通常被当作重复引用同一个对象的多个属性的快捷方式。 with 可以将一个没有或有多个属性的对象处理为一个
    完全隔离的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符。
    (变量会暴露到全局作用域中)
var obj = {
a: 1,
b: 2,
c: 3
};
// 单调乏味的重复 "obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式
with (obj) {
a = 3;
b = 4;
c = 5;
}
//可以这样理解,当我们传递 o1 给 with 时,with 所声明的作用域是 o1,而这个作用域中含
//有一个同 o1.a 属性相符的标识符。但当我们将 o2 作为作用域时,其中并没有 a 标识符,
//因此进行了正常的 LHS 标识符查找(查看第 1 章)。
//o2 的作用域、foo(..) 的作用域和全局作用域中都没有找到标识符 a,因此当 a=2 执行
//时,自动创建了一个全局变量(因为是非严格模式)。
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 被泄漏到全局作用域上了
  • eval(…) 函数如果接受了含有一个或多个声明的代码,就会修改其所处的词法作用域,而
    with 声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域
  • 这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。
    使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。

三、函数作用域和块级作用域

1、函数作用域

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复
用(事实上在嵌套的作用域中也可以使用)

1.1隐藏内部实现

可以使用函数作用域实现隐藏内部实验保护其内部的函数和变量。避免暴露更多的变量和函数,减少对全局的污染。

1.2规避冲突

“隐藏”作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突,
两个标识符可能具有相同的名字但用途却不一样,无意间可能造成命名冲突。冲突会导致
变量的值被意外覆盖

  1. 全局命名空间
    当程序中加载了多个第三方库时,如果它们没有妥善地将内部私有的函数或变量隐藏起来,就会很容易引发冲突
var MyReallyCoolLibrary = {
awesome: "stuff",
doSomething: function() {
// ...
},
doAnotherThing: function() {
// ...
}
};
  1. 模块管理
    从众多模块管理器中挑选一个来使用。使用这些工具,任何库都无需将标识符加入到全局作用域中,而是通过依赖管理器
    的机制将库的标识符显式地导入到另外一个特定的作用域中

立即执行函数表达式

IIFE:立即执行函数表达式;三种形式:

1. (function IIFE(){

 	})();   
2.  (function IIFE(){
	
	}()); 

3.  (function IIFE( def ) {
		def( window );
	})(function def( global ) {
		...
	});

块作用域:

  1. with :看上文
  2. try/catch
    可以捕获程序的异常,然后程序并不会结束,而是会继续执行
    在一些特殊情况下非常好用
try {
undefined(); // 执行一个非法操作来强制制造一个异常
}
catch (err) {
console.log( err ); // 能够正常执行!
}
console.log( err ); // ReferenceError: err not found
  1. let ,let的特点:
    1. let可以将变量绑定到所在任意作用域中。在声明中的任意位置都可以使用 { … } 括号来为 let 创建一个用于绑定的块。
    2. 但是使用 let 进行的声明不会在块作用域中进行提升。声明的代码被运行之前,声明并不“存在”。var可以先赋值在声明。
  • 一个 let 可以发挥优势的典型例子就是之前讨论的 for 循环
  1. const用来固定值,变为常量,(任何修改都会引起错误),const在声明的时候必须要进

四、提升

先有鸡还是先有蛋

a = 2;
var a;
console.log( a );
//你认为 console.log(..) 声明会输出什么呢?

编译器:先编译(声明等等)在执行(赋值等等执行语句);
包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。

函数声明会被提升,但是函数表达式却不会被提升。函数会首先被提升,然后才是变量。
这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。
如下图代码会输出 1 而不是 2

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

可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的
最顶端,这个过程被称为提升

五、闭包

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

可以理解为闭包是指有权访问另一个函数作用域中的变量的函数。

分析闭包

例子1:

function foo() {
	var a = 2;
	function bar() {
		console.log( a );
	}
	return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。

由于baz对foo函数的引用,导致foo函数内部的变量不会被销毁。它拥有涵盖 foo() 内部作用域的闭包,
使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。
在上面例子中,bar作为foo函数内部和外部之间的一座桥梁。

  • 我的简单点理解,只要是函数嵌套函数,而且在父函数外面的作用域有对子函数的引用就产生了闭包。

有聪明的同学就会不理解如果不是闭包为什么变量会被销毁
因为js每次调用函数时,都会创建一个新的对象来保存局部变量,然后将这个对象传入作用域链,接着在执行阶段就可以访问
在函数调用完返回时,就会将这个对象从作用域链中删除,除非有特殊情况(嵌套的函数等)。
在解释一下,因为子元素也会有一个对象来保存局部变量,但不止局部变量,他父函数中的变量也会被他保存下来。

例子2:

function foo() {
	var a = 2;
	function baz() {
		console.log( a ); // 2
	}
	bar( baz );
}
function bar(fn) {
	fn(); // 妈妈快看呀,这就是闭包!
}

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用 域的引用,无论在何处执行这个函数都会使用闭包

函数在定义时的词法作用域以外的地方被调用,闭包使得函数可以继续访问定义时的词法作用域。
把内部函数 baz 传递给 bar,当调用这个内部函数时(现在叫作 fn),它涵盖的 foo() 内部
作用域的闭包就可以观察到了,因为它能够访问 a

例子3:

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

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

例子4

//关于IIFE模式
var a = 2;
(function IIFE() {
console.log( a );
})();

虽然这段代码可以正常工作,但严格来讲它并不是闭包。为什么?因为函数(示例代码中的 IIFE)并不是在它本身的词法作用域以外执行的.
尽管 IIFE 本身并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是最常用来创建可以被封闭起来的闭包的工具。因此 IIFE 的确同闭包息息相关,即使本身并不会真的使用闭包.

  • 看完上面几个例子,读者你应该会有一点自己的理解,这时候你可以回顾一下你最近写过的代码,判断一下那些使用了闭包

循环和闭包

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

正常情况下,我们对这段代码行为的预期是分别输出数字 1~5,每秒一次,每次一个。
但实际上,这段代码在运行时会以每秒一次的频率输出五次 6

  • 这是因为每次调用的setTimeout函数都是共享一个变量i,然而延迟函数的回调会在循环结束时才执行,所有全都是6.

那要怎么解决呢?
从根源上发现,出现的原因为他们都共享一个变量i,所以我们需要块级作用域将i包裹起来。
刚好IIFE可以起到封闭的作用

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

但如上图这个样包裹并没有效果,因为这里还是共享一个i,可以在IIFE括号中、函数上面加一个局部变量j来储存i。

重返块级作用域

上面那个IIFE例子貌似解决了问题,但很麻烦。这时候块级作用域正好满足解决问题。

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

for 循环头部的 let 声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随
后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
将上面的例子变化一下

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

这里let创建了一个类似于{}的块级作用域,函数类似于块级作用域。

模块

  • 模块有两个主要特征:
    1. 为创建内部作用域而调用了一个包装函数;
    2. 包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。


这篇关于小黄书(《你不知道的JavaScript上卷》)学习笔记之作用域和闭包的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程