作用域和闭包

作用域是什么

正是这种储存和访问变量的值的能力将状态带给了程序。

js编译原理

一、与传统的编译语言不同,js不是提前编译的,编译结果也不能在分布式系统中进行移植。
二、在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤:分词/词法分析、解析/语法分析、代码生成,统称为编译
三、任何JavaScript代码片段在执行前都要进行编译(通常就在执行前)

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

这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。

例如:var a = 2;,这段程序通常会被分解成为下面这些词法单元:vara=2;。空格是否会被当作词法单元,取决于空格在这门语言中是否具有意义。

分词和词法分析的主要差异在于词法单元的识别是通过有状态还是无状态的方式进行的。如果词法单元生成器在判断a是一个独立的词法单元还是其他词法单元的一部分时,调用的是有状态的解析规则,那么这个过程就被称为词法分析;如果调用的是无状态的解析规则,则被称为分词
有状态的解析规则?无状态的解析规则?

解析/语法分析(Parsing)

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

代码生成

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

理解作用域

一、参与到对程序var a = 2;进行处理的过程所需:

  1. 引擎:从头到尾负责整个程序的编译及执行过程。
  2. 编译器:负责语法分析及代码生成等脏活累活。
  3. 作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

二、var a = 2;在引擎眼里存在两个完全不同的声明:一个由编译器在编译时处理,另一个则由引擎在运行时处理。
三、var a = 2;处理过程:编译器首先会将其分解成词法单元,然后将词法单元解析成一个树结构,最后进行代码生成:

  1. 遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a
  2. 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理a = 2这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。如果引擎最终找到了a变量,就会将2赋值给它。否则引擎就会抛出一个异常!

总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。

编译器有话说

一、编译器在编译过程的第二步中生成了代码,引擎执行它时,会通过查找变量a来判断它是否已声明过。查找的过程由作用域进行协助,那么引擎如何执行查找?这其中有两种查询方式:LHS查询、RHS查询。L、R分别代表赋值操作的左侧和右侧。换句话说,当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询。讲得更准确一点,RHS查询与简单地查找某个变量的值别无二致,而LHS查询则是试图找到变量的容器本身,从而可以对其赋值。从这个角度说,RHS并不是真正意义上的“赋值操作的右侧”,更准确地说是“非左侧”。你可以将RHS理解成retrieve his source value(取到它的源值),这意味着“得到某某的值”。

概念上最好将其理解为“赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头(RHS)”。

注意:console.log(..)也会进行查询:对console对象进行RHS查询,并且检查得到的值中是否有一个叫作log的方法。

作用域嵌套

一、遍历嵌套作用域链的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。
二、把作用域链比喻成一个建筑:
在这里插入图片描述
这个建筑代表程序中的嵌套作用域链。第一层楼代表当前的执行作用域,也就是你所处的位置。建筑的顶层代表全局作用域。LHS和RHS引用都会在当前楼层进行查找,如果没有找到,就会坐电梯前往上一层楼,如果还是没有找到就继续向上,以此类推。一旦抵达顶层(全局作用域),可能找到了你所需的变量,也可能没找到,但无论如何查找过程都将停止。

异常

一、为什么区分LHS和RHS?因为在变量还没有声明(在任何作用域中都无法找到该变量)的情况下,这两种查询的行为是不一样的。如果RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferenceError异常。当引擎执行LHS查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下。
二、严格模式禁止自动或隐式地创建全局变量。因此,在严格模式中LHS查询失败时,并不会创建并返回一个全局变量,引擎会抛出同RHS查询失败时类似的ReferenceError异常。
三、如果RHS查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,比如试图对一个非函数类型的值进行函数调用,或者引用nullundefined类型的值中的属性,那么引擎会抛出另外一种类型的异常,叫作TypeErrorReferenceError同作用域判别失败相关,而TypeError则代表作用域判别成功了,但是对结果的操作是非法或不合理的。

小结

一、作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。
二、赋值操作符会导致LHS查询。=操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。
三、JavaScript引擎首先会在代码执行前对其进行编译,在这个过程中,像var a = 2这样的声明会被分解成两个独立的步骤:

  1. 首先,var a在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。
  2. 接下来,a = 2会查询(LHS查询)变量a并对其进行赋值。

词法作用域

作用域共有两种主要的工作模型。

  1. 第一种是最为普遍的,被大多数编程语言所采用的词法作用域
  2. 第二种叫作动态作用域,仍有一些编程语言在使用(比如Bash脚本、Perl中的一些模式等)。

词法阶段

一、词法作用域意味着作用域是由书写代码时函数声明的位置来决定的
二、没有任何函数的气泡可以(部分地)同时出现在两个外部作用域的气泡中,就如同没有任何函数可以部分地同时出现在两个父级函数中一样。
三、作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应,作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。
四、词法作用域查找只会查找一级标识符,比如abc。如果代码中引用了foo.bar.baz,词法作用域查找只会试图查找foo标识符,找到这个变量后,对象属性访问规则会分别接管对barbaz属性的访问。

欺骗词法

一、如果词法作用域完全由写代码期间函数所声明的位置来定义,怎样才能在运行时来“修改”(也可以说欺骗)词法作用域呢?使用欺骗词法
二、欺骗词法作用域会导致性能下降。

eval

一、eval(..)函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码
二、考虑如下代码:

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

理解:

  1. eval(..)调用中的var b = 3;这段代码会被当作本来就在那里一样来处理。由于那段代码声明了一个新的变量b,因此它对已经存在的foo(..)的词法作用域进行了修改。
  2. 这段代码实际上在foo(..)内部创建了一个变量b,并遮蔽了外部(全局)作用域中的同名变量。当console.log(..)被执行时,会在foo(..)的内部同时找到ab,但是永远也无法找到外部的b。因此会输出1, 3而不是正常情况下会输出的1, 2

三、在上面这个例子中,我们给eval(..)传递进去的“代码”字符串是固定不变的。而在实际情况中,可以根据程序逻辑动态地将字符拼接在一起之后再传递进去。
四、默认情况下,如果eval(..)中所执行的代码包含声明(无论是变量还是函数),就会对eval(..)所处的词法作用域进行修改。
五、在严格模式的程序中,eval(..)在运行时有其自己的词法作用域,意味着其中的声明无法修改所在的作用域。

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

with

一、with通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。

var obj = {
	a: 1,
	b: 2,
	c: 3
};
// 单调乏味的重复“obj”
obj.a = 2;
obj.b = 3;
obj.c = 4;
//使用with,简单快捷
with(obj){
	a = 3;
	b = 4;
	c = 5;
}

二、考虑如下代码:

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(o1.a);// undefined
console.log(a);// 2,a被泄漏到全局作用域上了

实际上a = 2赋值操作创建了一个全局的变量a。这是怎么回事?with可以将一个对象处理为一个完全隔离的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符,但是这个块内部正常的var声明并不会被限制在这个块的作用域中,而是被添加到with所处的函数作用域中。

eval(..)函数和with的区别:

  1. eval(..)函数如果接受了含有一个或多个声明的代码,就会在运行时修改其所处的词法作用域,
  2. with声明实际上是根据你传递给它的对象,在运行时凭空创建了一个全新的词法作用域,将对象的属性当作作用域中的标识符来处理

性能

JavaScript引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行过程中快速找到标识符。但如果引擎在代码中发现了eval(..)with,它只能简单地假设关于标识符位置的判断都是无效的,因为无法在词法分析阶段明确知道eval(..)会接收到什么代码,这些代码会如何对作用域进行修改,也无法知道传递给with用来创建新词法作用域的对象的内容到底是什么。最悲观的情况是如果出现了eval(..)with,所有的优化可能都是无意义的,因此最简单的做法就是完全不做任何优化。

函数作用域和块作用域

函数中的作用域

一、JavaScript具有基于函数的作用域,意味着每声明一个函数都会为其自身创建一个作用域气泡,而其他结构都不会创建。
二、函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)

函数作用域

一、对函数的传统认知就是先声明一个函数,然后再向里面添加代码。但反过来想也可以带来一些启示:从所写的代码中挑选出一个任意的片段,然后用函数声明对它进行包装,实际上就是把这些代码“隐藏”起来了。考虑以下代码:

var a = 2;
function foo() {// 添加这一行
	var a = 3;
	console.log(a);// 3
}// 添加这一行
foo();// 添加这一行
console.log(a)// 2

但这样会出现一些额外的问题:

  1. 必须声明一个具名函数foo(),意味着foo这个名称本身“污染”了所在作用域(本例中是全局作用域)。
  2. 必须显式地通过函数名(foo())调用这个函数才能运行其中的代码。

二、如果函数不需要函数名(或者至少函数名可以不污染所在作用域),并且能够自动运行,这将会更加理想。幸好,JavaScript提供了能够同时解决这两个问题的方案

var a = 2;
(function foo() {// 添加这一行
	var a = 3;
	console.log(a);// 3
})();// 添加这一行
console.log(a);// 2

首先,包装函数的声明以(function...而不是function...开始,这样函数会被当作函数表达式而不是一个标准的函数声明来处理。

如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
函数表达式可以是匿名的,而函数声明则不可以省略函数名

三、函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处:比较一下前面两个代码片段。第一个片段中foo被绑定在所在作用域中,可以直接通过foo()来调用它。第二个片段中foo被绑定在函数表达式自身的函数中。换句话说,(function foo(){ .. })作为函数表达式意味着foo只能在..所代表的位置中被访问,外部作用域则不行。foo变量名被隐藏在自身中意味着不会非必要地污染外部作用域。

立即执行函数表达式

一、IIFE代表立即执行函数表达式(Immediately Invoked Function Expression),考虑如下代码:

var a = 2;
(function foo() {// 添加这一行
	var a = 3;
	console.log(a);// 3
})();// 添加这一行
console.log(a);// 2

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

  1. 上述代码是IIFE最常见的用法,即使用一个匿名函数表达式。除了上面那种写法,很多人也喜欢这样写:(function(){ .. }()),二者功能上没有区别
  2. IIFE的另一个用法是把它们当作函数调用并传递参数进去。考虑如下代码,我们将window对象的引用传递进去,但将参数命名为global
var a = 2;
(function iife(global){
	var a = 3;
	console.log(a);// 3
	console.log(global.a);// 2
})(window);
console.log(a);// 2
  1. 另外一个应用场景是解决undefined标识符的默认值被错误覆盖导致的异常(虽然不常见)。
undefined = true;// 不要这样做
(function iife(undefined) {
	var a;
	if(a === undefined) {
		console.log("undefined id safe here!");
	}
})();
  1. 倒置代码的运行顺序,将需要运行的函数放在第二位,在IIFE执行之后当作参数传递进去
var a = 2;
(function iife(def) {
	def(window);
})(function def(global) {
	var a = 3;
	console.log(a);// 3
	console.log(global.a);// 2
})

块作用域

with

with从对象中创建出的作用域仅在with声明中而非外部作用域中有效。

try/catch

catch分句会创建一个块作用域,其中声明的变量仅在catch内部有效。

let

一、提升是指声明会被视为存在于其所出现的作用域的整个范围内。使用let进行的声明不会在块作用域中进行提升。声明的代码被运行之前,声明并不“存在”。
二、考虑以下代码:

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

i会被绑定在外部作用域(函数或全局),解决方法:

  1. 使用let
for(let i = 0; i < 10; i++){
	console.log(i);
}
  1. 每次迭代时进行重新绑定:
let j;
for(j = 0; j < 10; j++){
	let i = j;
	console.log(i);
}

const

其值是固定的(常量)

提升

先有鸡还是先有蛋

JavaScript代码在执行时不是由上到下一行一行执行的

编译器再度来袭

一、引擎会在解释JavaScript代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来
二、包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理,函数会首先被提升,然后才是变量。
三、当你看到var a = 2;时,可能会认为这是一个声明。但JavaScript实际上会将其看成两个声明:var a;a = 2;。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。
四、这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动”到了最上面。这个过程就叫作提升。
五、只有声明本身会被提升,而赋值或其他运行逻辑会留在原地
六、每个作用域都会进行提升操作
七、函数声明会被提升,但是函数表达式却不会被提升。即使是具名的函数表达式,名称标识符在赋值之前也无法在所在作用域中使用:

// 函数声明会被提升,但是函数表达式却不会被提升
foo();// TypeError
// 即使是具名的函数表达式,名称标识符在赋值之前也无法在所在作用域中使用
bar();// ReferenceError
var foo = function bar(){
	// ...
};

函数优先

一、考虑如下代码:

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

这个代码片段会被引擎理解为如下形式:

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

var foo尽管出现在function foo()...的声明之前,但它是重复的声明(因此被忽略了),因为函数声明会被提升到普通变量之前。
二、尽管重复的var声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的。

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

三、一个普通块内部的函数声明通常会被提升到所在作用域的顶部,这个过程不会像下面的代码暗示的那样可以被条件判断所控制:

foo();// "b"
var a = true;
if(a) {
	function foo() {console.log("a")}
}else{
	function foo() {console.log("b")}
}

作用域闭包

实质问题

一、闭包:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。考虑以下代码:

function foo() {
	var a = 2;
	function bar() {
		console.log(a);// 严格来说这里并不是闭包,这里应该算作词法作用域的查找规则,这些规则只是闭包的一部分
	}
	return bar;
}
var baz = foo();
baz();// 闭包

执行完foo(),垃圾回收器会释放他的内存空间,而bar()依然持有对该作用域的引用,这个引用就叫作闭包。
二、无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

function foo() {
	var a = 2;
	function baz() {
		console.log(a);
	}
	bar(baz);
}
function bar(fn) {
	fn();// 闭包
}
var fn;
function foo() {
	var a = 2;
	function baz() {
		console.log(a);
	}
	fn = baz;
}
function bar() {
	fn();// 闭包
}
foo();
bar();

三、考虑以下代码:

var a = 2;
(function iife() {
	console.log(a);
})();

严格来讲它并不是闭包。因为IIFE函数并不是在它本身的词法作用域以外执行的。它是在定义时所在的作用域中执行(而外部作用域,也就是全局作用域也持有a)。a是通过普通的词法作用域查找而非闭包被发现的。
四、尽管技术上来讲,闭包是发生在定义时的,但并不非常明显

循环和闭包

一、考虑如下代码:

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

二、解决方案:

  1. 在循环的过程中每个迭代都需要一个闭包作用域。前面我们知道IIFE会通过声明并立即执行一个函数来创建作用域,那么我们可以给这个作用域创建他自己的变量j,用来在每个迭代中储存i的值
for(var i = 1; i <= 5; i++) {
	(function() {
		var j = i;
		setTimeout(function timer() {
			console.log(j);// 输出1 2 3 4 5
		}, j * 1000);
	})();
}
  1. 改进一下上面的代码,直接将i传递进去
for(var i = 1; i <= 5; i++) {
	(function(j) {
		setTimeout(function timer() {
			console.log(j);// 输出1 2 3 4 5
		}, j * 1000);
	})(i);
}
  1. let声明可以用来劫持块作用域,并且在这个块作用域中声明一个变量
for(var i = 1; i <= 5; i++) {
	let j = i;// 闭包的块作用域
	setTimeout(function timer() {
		console.log(j);// 输出1 2 3 4 5
	}, j * 1000);
}
  1. for循环头部的let声明还会有一个特殊的行为:指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量
for(let i = 1; i <= 5; i++) {
	setTimeout(function timer() {
		console.log(i);// 输出1 2 3 4 5
	}, i * 1000);
}

模块

一、以下代码在JavaScript中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露,这里展示的是其变体:

function CoolModule() {
	var something = "cool";
	var another = [1, 2, 3];
	function doSomething() {
		console.log(something);
	}
	function doAnother() {
		console.log(another.jion("!"));
	}
	// 从模块中返回一个实际的对象并不是必须的,也可以直接返回一个内部函数
	return {// 可以将这个对象类型的返回值看作是模块的公共API。
		doSomething: doSomething,
		doAnother: doAnother
	};
}
var foo = CoolModule();
foo.doSomething();
foo.doAnother();

doSomething()doAnother()函数具有涵盖模块实例内部作用域的闭包(通过调用CoolModule()实现)。当通过返回一个含有属性引用的对象的方式来将函数传递到词法作用域外部时,我们已经创造了可以观察和实践闭包的条件。
二、模块模式需要具备两个必要条件:

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

三、上述示例代码中有一个叫作CoolModule()的独立的模块创建器,可以被调用任意多次,每次调用都会创建一个新的模块实例。当只需要一个实例时,可以将模块函数转换成了IIFE,立即调用这个函数并将返回值直接赋值给单例的模块实例标识符foo

var foo = (function CoolModule() {
	var something = "cool";
	var another = [1, 2, 3];
	function doSomething() {
		console.log(something);
	}
	function doAnother() {
		console.log(another.jion("!"));
	}
	// 从模块中返回一个实际的对象并不是必须的,也可以直接返回一个内部函数
	return {// 可以将这个对象类型的返回值看作是模块的公共API。
		doSomething: doSomething,
		doAnother: doAnother
	};
})();
foo.doSomething();
foo.doAnother();

四、模块也是普通的函数,因此可以接受参数

function CoolModule(id) {
	function identify() {
		console.log(id);
	}
	return {
		identify: identify
	};
}
var foo = CoolModule("foo1");

五、模块模式另一个用法是命名将要作为公共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.identify();// foo module
foo.change();
foo.identify();// FOO MODULE

通过在模块实例的内部保留对公共API对象的内部引用,可以从内部对模块实例进行修改,包括添加或删除方法和属性,以及修改它们的值。

小结

一、当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
二、模块有两个主要特征:

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

动态作用域

一、词法作用域是一套关于引擎如何寻找变量以及会在何处找到变量的规则。词法作用域最重要的特征是它的定义过程发生在代码的书写阶段(假设你没有使用eval()with)。考虑以下代码:

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

词法作用域让foo()中的a通过RHS引用到了全局作用域中的a,因此会输出2。
二、动态作用域让作用域作为一个在运行时就被动态确定的形式,而不是在写代码时进行静态确定的形式。
三、动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。因此,如果JavaScript具有动态作用域,理论上,下面代码中的foo()在执行时将会输出3

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

为什么会这样?因为当foo()无法找到a的变量引用时,会顺着调用栈在调用foo()的地方查找a,而不是在嵌套的词法作用域链中向上查找。由于foo()是在bar()中调用的,引擎会检查bar()的作用域,并在其中找到值为3的变量a
四、事实上JavaScript并不具有动态作用域。它只有词法作用域
五、词法作用域和动态作用域的主要区别:

  1. 词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。(this也是!)
  2. 词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。

动态作用域

一、工具可以将ES6的代码转换成能在ES6之前环境中运行的形式。你可以使用块作用域来写代码,并享受它带来的好处,然后在构建时通过工具来对代码进行预处理,使之可以在部署时正常工作。
二、Google维护着一个名为Traceur的项目,该项目正是用来将ES6代码转换成兼容ES6之前的环境(大部分是ES5,但不是全部)

隐式和显式作用域

一、下面这种被称作let作用域或let声明

let(a = 2) {
	console.log(a);// 2
}
console.log(a);// ReferenceError

同隐式地劫持一个已经存在的作用域不同,let声明会创建一个显示的作用域并与其进行绑定。

性能

为什么不直接使用IIFE来创建作用域?

  1. try/catch的性能的确很糟糕,但技术层面上没有合理的理由来说明try/catch必须这么慢,或者会一直慢下去。
  2. IIFE和try/catch并不是完全等价的,因为如果将一段代码中的任意一部分拿出来用函数进行包裹,会改变这段代码的含义,其中的thisreturnbreakcontine都会发生变化。IIFE并不是一个普适的解决方案,它只适合在某些情况下进行手动操作。

this词法

一、考虑以下代码:

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

问题在于cool()函数丢失了同this之间的绑定。解决这个问题有好几种办法,但最常用的就是var self = this;

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

或者用箭头函数解决:

var obj = {
	count: 0,
	cool: function coolFn(){
		if(self.count < 1){
			setTimeout(() => {
				this.count++;
				console.log("awesome");
			},100)
		}
	}
},
obj.cool();// awesome
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值