小飘不懂JS冒险记(二)

作用域与闭包

一.什么是作用域?

它定义如何在某些位置存储变量,以及如何 在稍后找到这些变量。我们称这组规则为:作⽤域。

编译器理论

JavaScript是⼀个 编译型语⾔。

传统的编译型语⾔处理中,在它被执⾏ 之前 通常将会经 历三个步骤,⼤致被称为**“编****译”**:

  1. **分词/词法分析:**将一连串字符打断成有意义的片段,称为token(记号)。举例来说,考虑这段程序: var a = 2; 。这段程序很可能会被 打断成如下 token: var , a , = , 2 ,和 ; 。空格也许会被保留为⼀个 token,这要看它是否是有意义的。

注意:
在变量中存储值和取出值的能⼒,给程序赋予了 状态
分词分析:token 是以 无状态的方式被识别。
词法分析:token 是以 有状态的方式被识别。

  1. **解析:**将⼀个 token 的流(数组)转换为⼀个嵌套元素的树,它综合地表示了程序的语法结构。

  2. 代码⽣成: 这个处理将抽象语法树转换为可执⾏的代码。

理解作用域

  1. 引擎:负责从始至终编译和执行我们的JavaScript 程序。
  2. 编译器:处理有所有的解析和代码生成的重活。
  3. 作用域:收集并维护一张所有被声明的标识符(变量)的列表,并对当前执行中的代码如何访问这些变量强制实施一组严格的规则
反复

var a = 2;

  1. 遇到 var a ,编译器 让 作⽤域 去查看对于这个特定的作⽤域集合,变量 a 是否已经存在了。如果是,编译器 就忽略这个声明并继续前进。否则,编译器 就让 作⽤域 去为这个作⽤域集合声明⼀个称为 a 的新变量

  2. 然后 编译器 为 引擎 ⽣成稍后要执⾏的代码,来处理赋值 a = 2 。引擎 运⾏的 代码⾸先让 作⽤域 去查看在当前的作⽤域集合中是否有⼀个称为 a 的变量可以 访问。如果有,引擎 就使⽤这个变量。如果没有,引擎 就查看 其他地⽅。

总结:对于⼀个变量赋值,发⽣了两个不同的动作:第⼀,编译器 声明⼀个变量 (如果先前没有在当前作⽤域中声明过),第⼆,当执⾏时,引擎 在 作⽤域 中查询 这个变量并给它赋值,如果找到的话。

复制的目标(LHS)和赋值的源(RHS)

RHS :简单地查询某个变量的
LHS :查询是试着找到变量容器本身,以便 它可以赋值。

注意: LHS 和 RHS 意味着“赋值的左/右⼿边”未必像字⾯上那样意味着“= 赋值操作符的左/右边”。

console.log( a );   
//这个指向 a 的引⽤是⼀个 **RHS** 引⽤,因为这⾥没有东⻄被赋值给 a 。

var a = 2;
//这⾥指向 a 的引⽤是⼀个 **LHS** 引⽤,因为我们实际上不关⼼当前的值是什么,我们 只是想找到这个变量,将它作为 = 2 赋值操作的⽬标。

function foo(a) {
	console.log( a ); // 2
} 
foo( 2 );
//1.调⽤ foo(..) 的最后⼀⾏作为⼀个函数调⽤要求⼀个指向 foo 的 **RHS** 引⽤
//2.隐含的 a = 2 。进 ⾏了⼀个 LHS 查询。
//3.console.log(..) 需要⼀个引⽤来执⾏,a 的值的 RHS 引⽤。

错误

ReferenceError 是关于 作⽤域 解析失败的。
TypeError 暗示着 作⽤域 解析成功,但是试图对这个结果进⾏了⼀个⾮法/不可能的动作。⽐如将⼀个⾮函数的值作为函数运⾏,或者引⽤ null 或者 undefined 值的 属性。

注意:

  1. 未被满⾜的 RHS 引⽤会导致 ReferenceError 被抛出。
  2. 未被满⾜的 LHS 引⽤会导致 ⼀个⾃动的,隐含地创建的同名全局变量(如果不是“Strict模式”)。
  3. 未被满⾜的 LHS 引⽤会导致⼀个 ReferenceError (如果是“Strict模式”)。

二.词法作用域(JavaScript 实际上没有动态作⽤域)

作⽤域”定义为⼀组规则,它主宰着 引擎 如何通过标识符名称在 当前的 作⽤域,或者在包含它的任意 嵌套作⽤域 中来查询⼀个变量。

作⽤域的⼯作⽅式:

词法作用域(最常见)和动态作用域

补充:动态作用域
它的作⽤域是在运⾏时被确定的,它的作⽤域是在运⾏ 时被确定的。
动态作⽤域本身关⼼ 它们是从何处被调⽤的

词法作用域

词法作⽤域是在词法分析时被定义的作⽤域。词法作用域是基于你在写程序时,**变量和作用域的块儿在何处被编写决定的,**因此 它在词法分析器处理你的代码时(基本上)是固定不变的。

查询

⼀旦找到第⼀个匹配,作⽤域查询就停⽌了。

**遮蔽:**相同的标识符名称可以在嵌套作⽤域的 多个层中被指定(内部的标识符“遮蔽”了外部的标识 符)。

**注意:**全局变量也是全局对象(在浏览器中是window,等等)的属性。

window.a

这种技术给出了访问全局变量的⽅法,没有它全局变量将因为被遮蔽⽽不可访问。

欺骗词法作用域

欺骗词法作⽤域会导致更低下的性能。

eval

JavaScript 中的 eval(…) 函数接收⼀个字符串作为参数值,并将这个字符串的内容 看作是好像它已经被实际编写在程序的那个位置上。

eval(…) 通常被⽤于执⾏动态创建的代码。

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

注意: 当 eval(…) 被⽤于⼀个操作它⾃⼰的词法作⽤域的 strict 模式程序时,在 eval(…) 内部做出的声明不会实际上修改包围它的作⽤域

function foo(str){
	"uesr strict";
	eval(str);
	console.log(a);   // ReferenceError: a is not defined
}
foo("var a = 2");
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;
}

with 语句接收⼀个对象,这个对象有0个或多个属性,并将这个对象视为好像它是 ⼀个完全隔离的词法作⽤域

function foo(obj){
	with(obj){
		a = 2;
	}
}
var obj1 = {
	a:3;
};
var obj2 = {
	b:2;
}

foo(obj1);
console.log(obj1.a);    // 2
foo( obj2 ); 
console.log( obj2.a ); // undefined
console.log( a ); // 2 -- 哦,全局作⽤域被泄漏了

注意: 尽管⼀个 with 块⼉将⼀个对象视为⼀个词法作⽤域,但是在 with 块⼉内 部的⼀个普通 var 声明将不会归于这个 with 块⼉的作⽤域,⽽是归于包含它的函 数作⽤域。

总结:

  • eval(…) 函数接收⼀个含有⼀个或多个声明的代码字符串,它就会修改现存的 词法作⽤域 。
  • with 语句实际上是从你传递给它的对象中凭空制造了⼀个 全新的词 法作⽤域

三.函数与块儿作用域

函数中的作用域

JavaScript 拥有基于函数的作⽤域
函数作⽤域⽀持着这样的想法:所有变量都属于函数,⽽且贯穿整个函数始终都可以 使⽤和重⽤(⽽且甚⾄可以在嵌套的作⽤域中访问)。

隐藏于普通作用域

它们主要是由⼀种称为“最低权限原则”的 软件设计原则引起的,有时也被称为“最低授权”或“最少曝光”。
⽐如⼀个模块/对象的API,你应当只暴露所需要的最低限 度的东⻄,⽽“隐藏”其他的⼀切。

function doSomething(a){
	function doSomethingElse(a){
		return a-1;
	}
	var b;
	b = a + doSomethingElse(a*2);
	console.log(b*3);    
}

doSomething(2);    //15

好处一:
更安全。将这些私有细节隐藏在 doSomething(…) 的作⽤域内部,b 和 doSomethingElse(…) 对任何外界影响都是不可访问的,⽽是仅仅由 doSomething(…) 控制。
好处二:
避免冲突。避免两个同名但⽤处不同的 标识符之间发⽣⽆意的冲突。

全局“名称空间”

变量冲突(很可能)发⽣的⼀个特别强有⼒的例⼦是在全局作⽤域中。

在全局作⽤域中使⽤⼀个⾜够独特的名称来创建⼀个单独的变量声 明,它经常是⼀个对象。然后这个对象被⽤作这个库的⼀个“名称空间”,所有要明确 暴露出来的功能都被作为属性挂在这个对象(名称空间)上。

var MyReallyCoolLibrary = { 
	awesome: "stuff",
	doSomething: function() { 
		// ...
    }, 
	doAnotherThing: function() { 
	 	// ...
    }
};
模块管理

另⼀种回避冲突的选择是通过任意⼀种依赖管理器,使⽤更加现代的“模块”⽅式。没有库可以向全局作⽤域添加任何标识符,取⽽代之的是使⽤依赖管理器的各种机制,要求库的标识符被明确地导⼊到另⼀个指定的作⽤域中。。

函数作为作用域

**注意:**区分声明与表达式的最简单的方法是,这个语句中"function"一词的位置。如果"function"是这个语句中的第一个东西,那么他就是一个函数声明。否则,他就是一个函数表达式。

函数表达式:

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

将名称 foo 隐藏在它⾃⼰内部意味着它不会没必要地污染外围作⽤域。

匿名与命名

回调函数:(匿名函数表达式)

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

**注意:**函数表达式可以是匿名的,但是函数声明不能省略名称 。

匿名函数的缺点:

  1. 没有可用的名称表示,这可能使得调试更加困难;
  2. 自引用时,递归等⽬的引⽤它⾃⼰或者在被触发后想要把⾃⼰解除绑定。
  3. ⼀个描述性的名称 可以帮助代码⾃解释。

内联函数表达式 很强⼤且很有⽤ ,最佳的⽅法是总是命名你的函数表达式。

立即调用函数表达式(IIFE)

变种一:我们可以从外围作用域传入任何你想要的东西,⽽且你可以将参数命名为任何适合你的名称。

IIFE 的另一种变种,IIFE只是函数调用的事实。
例如:

var a = 2;
(function IIFE(global){
	var a = 3;
	console.log(a);     //3
	conslole.log(global.a);    //2
})(window);

console.log(a);    //2

传⼊ window 对象引⽤,参数为global。

变种二:可以用来保证在⼀个代码块中 undefined 标识符确实是是⼀个未定义的值。

默认的 undefined 标识符的值也许会被不正确地覆盖掉,⽽导致意外的结果。
通过将参数命名为 undefined ,同时不为它传递 任何参数值

(function IIFE(undefined){
	var a;
	if(a===undefined){
		console.log("Undefined is safe here!");
	}
})();

变种三:将事情的顺序倒了过来,要被执⾏的函数在调⽤和传递给它的参数之后给出。

这种模式被⽤于 UMD( 统⼀模 块定义)项⽬。

def 函数表达式作为一个参数被传递给在代码前半部分定义的IIFE 函数。参数def(函数)被调用,并将window 作为 global 参数传入。

var a = 2;
(function IIFE(def){
	def(window);
})(function def(global){
	var a = 3;
	console.log(a);    // 3
	console.log(global.a);   //2
});

块儿作为作用域

补充:let-er 是⼀个编译期代码转译器,它唯⼀的任务就是找到 let 语句形式并转译它们。

for (var i=0; i<10; i++){
 	 console.log( i );
}

我们很可能认为是仅在这个 for 循环内部的上下⽂环境中使⽤ i ,⽽实质上忽略了这个***变量实际上将⾃⼰划⼊了外围 作⽤域中(函数或全局)***的事实。

为什么要⽤仅将在这个 for 循环中使⽤的变量 i 去污染⼀个函数的整个作⽤域呢?

with

它从对象中创建的作⽤域***仅存在于这个 with 语句的⽣命周期中***,⽽不再外围作⽤域中。

try/catch

catch ⼦句 中声明的变量,是属于 catch 块⼉的块⼉作⽤域的。

try{
	undefined();   //⽤⾮法的操作强制产⽣⼀个异常!
}catch(err){
	console.log( err ); // 好⽤!
}

console.log( err ); // ReferenceError: `err` not found

注意:如果你在同⼀个作⽤域中有两个或多个 catch ⼦句,⽽ 它们⼜各⾃⽤***相同***的标识符名称声明了它们表示错误的变量时,可以:
1. 将 catch 变量命名为 err1 , err2 ,等等。
2. ⼲脆关闭 linter 对重复变量名的检查。

let

let 关键字将变量声明附着在它所在的任何块⼉(通常是⼀个 { … } )的作⽤域 中。
换句话说, let 为它的变量声明隐含地劫持了任意块⼉的作⽤域

var foo = true;
if (foo) { 
  {  //  <-- 明确的块儿
	let bar = foo * 2; 
	bar = something( bar );
    console.log( bar );
   }
} 

console.log( bar ); // ReferenceError

使⽤ let 做出的声明将 不会 在它们所出现的整个块⼉的作⽤域中提升

{
	console.log(bar);    //ReferenceError!
	let bar = 2;
}
垃圾回收

块儿作用域的有用指出之一:关于闭包和释放内存的垃圾回收

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 );
let循环
for (let i=0; i<10; i++) {
	console.log( i );
}

console.log( i ); // ReferenceError

const

const ,它也创建⼀个块⼉作⽤域变量,但是它的值 是固定的(常量)

四.提升

变量和函数声明被从它们在代码流中出现的位置**“移动”到代码的顶端**。

关于出现在⼀个作⽤域内各种位置的声明如何附着在作⽤域上。

编译过程的⼀部分就是找到所有的声明,并将它们关联在合适的作⽤域上

var a = 2;

**第⼀个语句:var a;**声明,是在编译阶段被处理的。
**第⼆个语句:a = 2;**赋值,为了执⾏阶段⽽留在 原处

注意:

  • 提升是 以作⽤域为单位的。
  • 函数声明会被提升,就像我们看到的。但是函数表达式不会。
foo();
function foo() { 
	console.log( a ); // undefined 
	var a = 2;
}
foo(); // 不是 ReferenceError, ⽽是 TypeError!

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

这个代码段可以(使⽤提升)更准确地解释为:

var foo;
foo(); // TypeError 
bar(); // ReferenceError
foo = function() { 
	var bar = ...self... 
	// ...
}
函数优先

函数会⾸先被提升,然后才是变量
虽然多个/重复的 var 声明实质上是被忽略的,但是后续的函数声明确实会覆盖前⼀ 个

foo(); // 1 

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

五.作用域闭包

闭包就是函数能够记住并访问它的词法作⽤域,即使当这个函数在它的词法作⽤域之外执⾏时

在 JavaScript 中闭包⽆所不在,闭包是依赖于词法作⽤域编写代码⽽产⽣的结果。

function foo() { 
	var a = 2;
	function bar() { 
		console.log( a );
} 
	return bar;
} 
var baz = foo();
 baz(); // 2 -- 看到闭包了。

bar() 依然拥有对 foo() 作⽤域的引⽤,⽽这个引⽤称为闭包。

函数可以被作为值传递:

var fn;
function foo() { 
	var a = 2;
	function baz() { 
		console.log( a );
	} 
	fn = baz; // 将`baz`赋值给⼀个全局变量
}
function bar() { 
	fn(); // 看到闭包了!
} 
foo();
bar();   // 2

计时器、事件处理器、Ajax请求、跨窗 ⼝消息、web worker、或者任何其他的异步(或同步!)任务,当你传⼊⼀个 回调函 数,你就在它周围悬挂了⼀些闭包!

循环+闭包

错误:

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

输出的结果反映的是 i 在循环终结后的最终值。
缺少循环的每次迭代都“捕捉”⼀份对 i 的拷 ⻉。由于作⽤域的⼯作⽅ 式,它们 都闭包在同⼀个共享的全局作⽤域上,⽽它事实上只有⼀个 i

**纠正:**使用IIFE

如果没有var j = i; 将不会好用,因为 拥有⼀个被闭包的 空的作⽤域 是不够的。

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

**升级:**使用块儿作用域

for (let i=1; i<=5; i++) { 
	setTimeout( function timer(){ 
		console.log( i );
	}, i*1000 ); 
}
模块
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

⾏使模块模式有两个“必要条件”:

  1. 必须有一个外部的外围函数,而且他必须至少被调用一次(每次创建一个新的模块实例)。
  2. 外围的函数必须至少返回一个内部函数,这样这个内部函数才拥有私有作用域的闭包,并且可以访问和/或修改这个私有状态。

模块可以传递参数;
模块作为公有API返回的对象命名:

var foo = (function CoolModule(id) {
	function change() { 
	// 修改公有 API 
	publicAPI.identify = identify2;
	}
	function identify1(){
		console.log( id );
	}
	function identify2(){
		console.log( id.toUpperCase() );
	}
	var publicAPI = {
		change:change,
		identify:identify1
	};
	return publicAPI;
})("foo module");

foo.identify1();      // foo module
foo.change();
foo.identify2();      // FOO MODULE
未来的模块

ES6 将⼀个⽂件 视为⼀个独⽴的模块。每个模块可以导⼊其他的模块或者特定的API成员,也可以导出它们⾃⼰的公有API成员

ES6 模块API是静态的(这些API不会在运⾏时改变,在(⽂件加载和)编译期间检查⼀个指向被导⼊模 块的成员的引⽤是否 实际存在。如果API引⽤不存在,编译器就会在编译时抛出⼀ 个“早期”错误。

ES6 模块 没有 “内联”格式,它们必须被定义在⼀个分离的⽂件中每个模块⼀个)。

import 在当前的作⽤域中导⼊⼀个模块的API的⼀个或多个成员,每个都绑定到⼀个变量
module 将整个模块的API导⼊到⼀个被绑定的变量
export 为当前模块的公有API导出⼀个标识符(变量,函数)

例如:
bar.js

funtion hello(who){
	return "Let me introduce :"+who;
}
export hello;   // 导出函数

foo.js

import hello from "bar";

var hungry = "hippo";
function asome(){
	console.log(hello(hungry).toUpperCase());
}
export asome;
module foo from "foo"
module bar from "bar"

console.log(bar.hello("piao"));    // Let me introduce : piao
foo.asome();              //Let me introduce : HIPPO

词法this

ES6为函数声明增加了一种特殊的语法形式,称为**“箭头函数”**(匿名函数)

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

=> 是function关键字的缩写,但不仅仅是可以少打⼀些“function”那么简单。

var obj = {
	id:"asome",
	cool:function coolFn(){
		console.log(this.id);
	}
};
var id = "not asome";
obj.cool();     // asome
setTimeout(obj.cool,100);    // not asome

cool()函数上丢失了this绑定,解决方法:

  1. var self = this;
    console.log(self.id);
    self 变成了⼀个可以通过词法作⽤域和闭包解析的标识符。
var obj = { 
	count: 0, 
	cool: function coolFn() { 
		var self = this;
		if (self.count < 1) { 		
				setTimeout( function timer(){ 
					self.count++; 
					console.log( "awesome?" );
				}, 100 ); } }
}; 
obj.cool(); // awesome?
  1. 箭头函数,称为“词法this”的行为。
    当箭头函数遇到它们的 this 绑定时,将它们的⽴即外围词法作⽤域作 为 this 的值,⽆论它是什么。
var obj = { 
	count: 0, 
	cool: function coolFn() { 
		if (this.count < 1) { 	
			setTimeout( () => { // 箭头函数能好⽤? 
				this.count++; 
				console.log( "awesome?" );
			}, 100 ); 
		} 
	}
}; 
obj.cool(); // awesome?
  1. 正确地使⽤并接受 this 机制,使用bind(this)绑定。
var obj ={
	count:0,
	cool:function coolFn(){
		if(this.count<1){
			setTimeout(function timer(){
				this.count++;   // `this` 因为 `bind(..)` 所以安全
				console.log("more awesome");
			}.bind(this),100);      //使用 bind
		}
	}
};
obj.cool();   // more awesome
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值