《你不知道的JavaScript》学习笔记
作用域
作用域嵌套
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。
function foo(a) {
console.log( a + b );
}
var b = 2;
foo( 2 );//4
//在函数foo作用域中无法找到b变量,window是foo外层作用域,变量b在window(全局作用域)中,所以找到该变量,返回4(其中b为2)
遍历嵌套作用域链的规则:引擎从当前的执行作用域开始查找变量,如果找不到,
就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都
会停止。
异常
ReferenceError
同作用域判别失败相关,而TypeError
则代表作用域判别成功了,但是对
结果的操作是非法或不合理的。
词法作用域
作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法
作用域,我们会对这种作用域进行深入讨论。另外一种叫作动态作用域,仍有一些编程语
言在使用(比如Bash 脚本、Perl 中的一些模式等)。
词法阶段
作用域查找会在找到第一个匹配的标识符时停止。
全局变量会自动成为全局对象(比如浏览器中的window 对象)的属性,因此
可以不直接通过全局对象的词法名称,而是间接地通过对全局对象属性的引
用来对其进行访问。
window.a
通过这种技术可以访问那些被同名变量所遮蔽的全局变量。但非全局的变量
如果被遮蔽了,无论如何都无法被访问到。
无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处
的位置决定。
欺骗词法
欺骗词法作用域会导致性能下降。
eval
eval(..)
函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。
function foo(str, a) {
eval( str ); // 欺骗!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
eval(..)
调用中的 var b = 3;
这段代码会被当作本来就在那里一样来处理。由于那段代码声明了一个新的变量b
,因此它对已经存在的foo(..)
的词法作用域进行了修改。事实上,和前面提到的原理一样,这段代码实际上在foo(..)
内部创建了一个变量b
,并遮蔽了外部(全局)作用域中的同名变量。当console.log(..)
被执行时,会在foo(..)
的内部同时找到a
和b
,但是永远也无法找到外部的b
。因此会输出1, 3
而不是正常情况下会输出的1, 2
。
在严格模式的程序中,eval(..)
在运行时有其自己的词法作用域,意味着其中的声明无法修改所在的作用域。
function foo(str) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2" );
with
先跳过
小结
词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。
JavaScript 中有两个机制可以“欺骗”词法作用域:eval(..)
和with
。前者可以对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在 运行时)。后者本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。
这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。
函数作用域和块作用域
函数中的作用域
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。
函数作用域
在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐
藏”起来,外部作用域无法访问包装函数内部的任何内容。
//foo 被绑定在所在作用域中,可以直接通过foo() 来调用它。
var a = 2;
function foo() { // <-- 添加这一行
var a = 3; console.log( a ); // 3
} // <-- 以及这一行
foo(); // <-- 以及这一行
console.log( a ); // 2
虽然这种技术可以解决一些问题,但是它并不理想,因为会导致一些额外的问题。首先,必须声明一个具名函foo()
,意味着foo
这个名称本身“污染”了所在作用域(在这个例子中是全局作用域)。其次,必须显式地通过函数名(foo())调用这个函数才能运行其 中的代码。
如果函数不需要函数名(或者至少函数名可以不污染所在作用域),并且能够自动运行,这将会更加理想。
//foo 被绑定在函数表达式自身的函数中而不是所在作用域中。
var a = 2;
(function foo(){
var a = 3;
console.log(a);//3
})();
console.log(a);//2
区分函数声明和表达式最简单的方法是看function
关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果function
是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。
匿名和具名
立即执行函数表达式
由于函数被包含在一对( )
括号内部,因此成为了一个表达式,通过在末尾加上另外一个( )
可以立即执行这个函数,比如(function foo(){ .. })()
。第一个( )
将函数变成表达式,第二个( )
执行了这个函数。
作者:掘金-《你不知道的JavaScript》学习笔记