全局作用域
所有浏览器都支持 window 对象,它表示浏览器窗口,JavaScript 全局对象、函数以及变量均自动成为 window 对象的成员。所以,全局变量是 window 对象的属性,全局函数是 window 对象的方法,甚至 HTML DOM 的 document 也是 window 对象的属性之一。
全局变量是JavaScript里生命周期(一个变量多长时间内保持一定的值)最长的变量,其将跨越整个程序,可以被程序中的任何函数方法访问。
在全局下声明的变量都会在window对象下,都在全局作用域中,我们可以通过window对象访问,也可以直接访问。
1 var name = "jeri"; 2 console.log(window.name); // 输出:jeri 3 console.log(name); // 输出:jeri
在JS中任何位置,没有使用var关键字声明的变量也都是全局变量。
1 function fun() { 2 name = "jeri"; 3 alert(name); 4 } 5 6 console.log(name); // 输出:jeri
全局变量存在于整个函数的生命周期中,然而其在全局范围内很容易被篡改,我们在使用全局变量时一定要小心,尽量不要使用全局变量。在函数内部声明变量没有使用var也会产生全局变量,会为我们造成一些混乱,比如变量覆盖等。所以,我们在声明变量的任何时候最好都要带上var。
全局变量存在于程序的整个生命周期,但并不是通过其引用我们一定可以访问到全局变量。
词法作用域
词法作用域:函数在定义它们的作用域里运行,而不是在执行它们的作用域里运行。也就是说词法作用域取决于源码,通过静态分析就能确定,因此词法作用域也叫做静态作用域。with和eval除外,所以只能说JS的作用域机制非常接近词法作用域(Lexical scope)。词法作用域也可以理解为一个变量的可见性,及其文本表述的模拟值。
1 var name = "global"; 2 3 function fun() { 4 var name = "jeri"; 5 return name; 6 } 7 8 console.log(fun()); // 输出:jeri 9 console.log(name); // 输出:global
在通常情况下,变量的查询从最近接的绑定上下文开始,向外部逐渐扩展,直到查询到第一个绑定,一旦完成查找就结束搜索。就像上例,先查找离它最近的name="jeri",查询完成后就结束了,将第一个获取的值作为变量的值。
动态作用域
在编程实践中,最容易低估和过度滥用的概念就是动态作用域,因为很少有语言支持这种方式为绑定解析方案。
动态作用域与词法作用域相对而言的,不同于词法作用域在定义时确定,动态作用域在执行时确定,其生存周期到代码片段执行为止。动态变量存在于动态作用域中,任何给定的绑定的值,在确定调用其函数之前,都是不可知的。
在代码执行时,对应的作用域链常常是保持静态的。然而当遇到with语句、call方法、apply方法和try-catch中的catch时,会改变作用域链的。以with为例,在遇到with语句时,会将传入的对象属性作为局部变量来显示,使其便于访问,也就是说把一个新的对象添加到了作用域链的顶端,这样必然影响对局部标志符的解析。当with语句执行完毕后,会把作用域链恢复到原始状态。实例如下:
1 var name = "global"; 2 3 // 使用with之前 4 console.log(name); // 输出:global 5 6 with({name:"jeri"}){ 7 console.log(name); // 输出:jeri 8 } 9 10 // 使用with之后,作用域链恢复 11 console.log(name); // 输出:global
在作用域链中有动态作用域时,this引用也会变得更加复杂,不再指向第一次创建时的上下文,而是由调用者确定。比如在使用apply或call方法时,传入它们的第一个参数就是被引用的对象。实例如下:
1 function globalThis() { 2 console.log(this); 3 } 4 5 globalThis(); // 输出:Window {document: document,external: Object…} 6 globalThis.call({name:"jeri"}); // 输出:Object {name: "jeri"} 7 globalThis.apply({name:"jeri"},[]); // 输出:Object {name: "jeri"}
因为this引用是动态作用域,所以在编程过程中一定要注意this引用的变化,及时跟踪this的变动。
函数作用域
函数作用域,顾名思义就是在定义函数时候产生的作用域,这个作用域也可以称为局部作用域。和全局作用域相反,函数作用域一般只在函数的代码片段内可访问到,外部不能进行变量访问。在函数内部定义的变量存在于函数作用域中,其生命周期随着函数的执行结束而结束。实例如下:
1 var name = "global"; 2 3 function fun() { 4 var name = "jeri"; 5 console.log(name); // 输出:jeri 6 7 with ({name:"with"}) { 8 console.log(name); // 输出:with 9 } 10 console.log(name); // 输出:jeri 11 } 12 13 fun(); 14 15 // 不能访问函数作用域 16 console.log(name); // 输出:global
没有块级作用域
不同于其他编程语言,在JavaScript里并没有块级作用域,也就是说在for、if、while等语句内部的声明的变量与在外部声明是一样的,在这些语句外部也可以访问和修改这些变量的值。实例如下:
1 function fun() { 2 3 if(0 < 2) { 4 var name = "jeri"; 5 } 6 console.log(name); // 输出:jeri 7 name = "change"; 8 console.log(name); // 输出:change 9 } 10 11 fun();
作用域链
JavaScript里一切皆为对象,包括函数。函数对象和其它对象一样,拥有可以通过代码访问的属性和一系列仅供JavaScript引擎访问的内部属性。其中一个内部属性是作用域,包含了函数被创建的作用域中对象的集合,称为函数的作用域链,它用来保证对执行环境有权访问的变量和函数的有序访问。
当一个函数创建后,它的作用域链会被创建此函数的作用域中可访问的数据对象填充。在全局作用域中创建的函数,其作用域链会自动成为全局作用域中的一员。而当函数执行时,其活动对象就会成为作用域链中的第一个对象(活动对象:对象包含了函数的所有局部变量、命名参数、参数集合以及this)。在程序执行时,Javascript引擎会通过搜索上下文的作用域链来解析诸如变量和函数名这样的标识符。其会从作用域链的最里面开始检索,按照由内到外的顺序,直到完成查找,一旦完成查找就结束搜索。如果没有查询到标识符声明,则报错。当函数执行结束,运行期上下文被销毁,活动对象也随之销毁。实例如下:
1 var name = 'global'; 2 3 function fun() { 4 console.log(name); // output:global 5 name = "change"; 6 // 函数内部可以修改全局变量 7 console.log(name); // output:change 8 // 先查询活动对象 9 var age = "18"; 10 console.log(age); // output:18 11 } 12 13 fun(); 14 15 // 函数执行完毕,执行环境销毁 16 console.log(age); // output:Uncaught ReferenceError: age is not defined