因为最近项目比较少,闲来觉得需要学习《你不知道的JavaScript》;跟大家分享一下;
什么是作用域
需要一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量。这套规
则被称为作用域
执行 var a = 2 发生了什么
1.var a: 编译器会询问作用域是否存在变量a;如果是,编译器会忽略该声明,继续进行编译。否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a;接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理a = 2这个赋值操作。
2.引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果否,引擎就会
使用这个变量;如果不是,引擎会继续查找该变量如果引擎最终找到了a变量,就会将2赋值给它。否则引擎就会举手示意并抛出一个异常!
RHS查询 与 LHS查询
RHS查询:简单地查找某个变量的值
LHS查询:试图找到变量的容器本身,从而可以对其赋值
在概念上最好将其理解为“赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头(RHS)”。
LHS:对哪个 赋值 就对哪个进行LHS引用,可以理解为赋值操作的目标。
RHS:需要 获取 哪个变量的值,就对哪个变量的值进行RHS引用,理解为赋值操作的源头。
作用域嵌套
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法
找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。
遍历嵌套作用域链的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一
级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。
严格模式下的 ReferenceError 与 TypeError
如果RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferenceError异常
在严格模式中LHS查询失败时,并不会创建并返回一个全局变量,引擎会抛出同RHS查询失败时类似的ReferenceError异常。
如果RHS查询找到了一个变量,但是你尝试对这个变量的值进行不合理的赋值,那么引擎会
抛出另外一种类型的异常,叫作TypeError。
ReferenceError同作用域判别失败相关,而TypeError则代表作用域判别成功了,但是对结果的操作
是非法或不合理的。
遮蔽效应
在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。
全局变量会自动成为全局对象(比如浏览器中的window对象)的属性,所以如果要逃避遮蔽效应
可以通过 window对象
window.a //得到的是全局定义的a变量;
全局命名空间
库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象被用作
库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己
的标识符暴漏在顶级的词法作用域中。
函数作用域的问题
我们已经知道,在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外
部作用域无法访问包装函数内部的任何内容。
虽然这种技术可以解决一些问题,但是它并不理想,因为会导致一些额外的问题。首先,必须声明
一个具名函数foo(),意味着foo这个名称本身“污染”了所在作用域(在这个例子中是全局作用域)。
其次,必须显式地通过函数名(foo())调用这个函数才能运行其中的代码。
更加理想的方式
var a = 2;
(function foo(){ // <-- 添加这一行
var a = 3;
console.log( a ); // 3
})(); // <-- 以及这一行
console.log( a ); // 2
以(function...而不仅是以function...开始。函数会被当作函数表达式而不是一个标准的函数声明 来处理。
函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。foo被绑定在函数表达式自身的函数中而不是所在作用域中。
换句话说,(function foo(){ .. })作为函数表达式意味着foo只能在..所代表的位置中被访问,外
部作用域则不行。foo变量名被隐藏在自身中意味着不会非必要地污染外部作用域。
很多人都更喜欢另一个改进的形式:(function(){ .. }())。这两种形式在功能上是一致的。选择哪个全凭个人喜好.
块 作用域
for (var i=0; i<10; i++) {
console.log( i );
}
我们在for循环的头部直接定义了变量i,通常是因为只想在for循环内部的上下文中使用i,而忽
略了i会被绑定在外部作用域(函数或全局)中的事实。
JavaScript的ES3规范中规定try/catch的catch分句会创建一个块作用域,其中声明的变量仅在catch内部有效。
try {
undefined(); // 执行一个非法操作来强制制造一个异常
}c
atch (err) {
console.log( err ); // 能够正常执行!
} c
onsole.log( err ); // ReferenceError: err not found
ES6改变了现状,引入了新的let关键字,提供了除var以外的另一种变量声明方式
let关键字可以将变量绑定到所在的任意作用域中(通常是{ .. }内部)。只要声明是有效的,在声明中的任意位置都可以使用{ .. }括号来为let创建一个用于绑定的块。
为变量显式声明块作用域,并对变量进行本地绑定是非常有用的工具,可以让引擎清楚地知道没有必要继续保存那些变量(当块的变量没有被引用时就销毁);
const
除了let以外,ES6还引入了const,同样可以用来创建块作用域变量,但其值是固定的(常量)。之后
任何试图修改值的操作都会引起错误。
提升
解析两个输出
a = 2;
var a;
console.log( a );//2
console.log( a ); //undefined
var a = 2;
编译器顺序
当你看到var a = 2;时,可能会认为这是一个声明。但JavaScript实际上会将其看成两个声 明:var a;和a =
2;。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执 行阶段。
我们的第二个代码片段实际是按照以下流程处理的:
var a;
console.log( a );
a = 2;
这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动”到了最上面。这个过程就叫作 提升。
换句话说,先有蛋(声明)后有鸡(赋值)。
函数声明会优先于变量声明
foo(); // 1
var foo;
function foo() {
console.log( 1 );
} f
oo = function() {
console.log( 2 );
};
相当于
function foo() {
console.log( 1 );
}
foo(); // 1
foo = function() {
console.log( 2 );
};
闭包
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之
外执行。
词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将 变量和块作用域写在哪里来决定的
function foo() {
var a = 2;
function bar() {
console.log( a );} r
eturn bar;
}
var baz = foo();
baz(); // 2 ———— 朋友, 这就是闭包的效果。
我们将bar()函数本身当作一个值类型进行传递。
函数bar()的词法作用域能够访问foo()的内部作用域。
在foo()执行后,其返回值(也就是内部的bar()函数)赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用调用了内部的函数bar()。
因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去foo()的内容不会再被使用,所以很自然地会考虑对其进行回收。在foo()执行后,通常会期待foo()的整个内部作用域都被销毁。
而闭包的“神奇”之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。
谁在使用这个内部作用域?原来是bar()本身在使用。拜bar()所声明的位置所赐,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()在之后任何时间进行引用。
bar()依然持有对该作用域的引用,而这个引用就叫作闭包。
无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引
用,无论在何处执行这个函数都会使用闭包。本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。只要使用了回调函数,实际上就是在使用闭包!
一个神奇的例子
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
//每秒出现一个6
解析:延迟函数的回调会在循环结束时才执行。事实上,当定时器运行时即使每个迭代中执行的是setTimeout(..,
0),所有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个6出来。我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个i的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i。
改进写法
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})( i );
}
//每秒从1到6依次输出
解析:在迭代内使用IIFE(自执行函数)会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
细说:
for (var i = 0; i <= 5; i++) {
console.log(i)
}
我们都知道上面的代码可以输出1到6;因此可以明白
for (var i=1; i<=5; i++) {
(function(j) { })( i );
}
//这一层,是会不断的获取到正确的i值;
然后,由于延迟函数的回调 使用了闭包;每次闭包都会保存IIFE 的有正确值的作用域;
闭包的应用-- 模块
直接抄代码看:
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
很好理解,coolmodule()返回一个对象,这个对象通过不同的标识符引用调用了内部的函数;这些函数就是coolmodule的闭包,具有访问coolmodule作用域的能力;
除了返回一个对象,还可以返回一个函数
从模块中返回一个实际的对象并不是必须的,也可以直接返回一个内部函数。jQuery就是
一个很好的例子。jQuery和$标识符就是jQuery模块的公共API,但它们本身都是函数(由于函数也是对象,它们本身也可以拥有属性)。因此,jq的方法有两种,通过$.xxx() 运行的是jq的属性方法;通过$() 运行的是jq的函数方法;
闭包的应用-- 实现模块
模块模式需要具备两个必要条件。
必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
单例模式
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
} f
unction doAnother() {
console.log( another.join( " ! " ) );
}return {
doSomething: doSomething,
doAnother: doAnother
};
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3