A.任何程序设计语言都有作用域的概念。作用域(scope)就是变量(variable)与函数(function)的可访问范围,换句话说,scope控制着variable和function的可见性和生命周期。
- 全局作用域
所有的浏览器(如果用原生js,必须解决浏览器的兼容问题)都支持window对象,它表示着浏览器窗口。所以javacript中的全局对象 函数 变量都会自动成为window对象的成员,函数成为window对象的方法,变量成为window对象的属性。在代码任何地方都能访问到的对象拥有全局作用域,通常以下3种情形拥有全局作用域:
(1)最外层函数和在最外层函数外面定义的变量全局作用域,例如:
var a = 1; //全局变量
function f1() {
alert(a); //alerts '1'
function inner() {
alert(a); //alerts '1'
}
}
f1(); //输出1 1;
inner(); //脚本错误
(2)在JS任何位置不使用var关键字声明的变量也拥有全局作用域:
function f1() {
var firstName = "James";
lastName = "Camelo";
alert(firstName); //alerts 'James'
alert(secondName); //alerts 'Camelo'
}
f1(); //输出'James'和'Camelo'
alert(firstName); //脚本错误
alert(secondName); //输出'Camelo'
变量secondName拥有全局作用域,而firstName不能在函数外部被访问.
(3)所有window对象的属性拥有全局作用域.
总结:
全局变量存在于整个函数的生命周期中,然而其在全局范围内很容易被篡改;声明变量不带上var很容易造成混乱。少用全局变量,声明带上var。
全局变量存在于程序的整个生命周期,但并不是通过其引用我们一定可以访问到全局变量。
2.局部作用域/函数作用域
和全局作用域相反,函数作用域一般只在函数的代码片段内可访问到,外部不能进行变量访问。在函数内部定义的变量存在于函数作用域中,其生命周期随着函数的执行结束而结束。例如:
var name = 'James';
function getName(){
var name = 'Camelo';
alert(name); //alerts 'Camelo'
}
alert(name); //alerts ‘James'
3.词法作用域/静态作用域
词法作用域:函数是在定义它们的作用域里运行,而不是在执行它们的作用域里运行。换句话说,词法作用域是由书写代码时函数声明的位置来决定的(eval() 和 with 可以欺骗词法作用域)。还是通过一个例子来了解词法作用域:
var name = "James";
function f1(){
alert(name); //alerts 'undefined'
var name = 'Camelo';
alert(name); //alerts 'Camelo'
}
f1(); //输出 'undefined'和'Camelo'
js解释器在执行任何代码之前都会先创建一个全局对象,全局变量相当于这个全局对象的一个属性。对于f1这个函数,就会生成一个叫做调用对象的东西,局部变量 函数参数 和 Arguments都是这个对象的一部分。
更致命的是,调用对象位于作用域链的前端,这就意味着全局对象的属性中与调用对象同名的属性将会被隐藏(变量的查询从最接近的绑定上下文开始,向外部逐渐扩展,直到查询到第一个绑定,一旦完成查找就结束搜索)。
所以代码片段中的,f1函数内部 "var name = 'Camelo';" 使得“var name = 'James';"被隐藏,并且第一个alert(name)处于"var name = 'Camelo';"之前,所以才会输出"undefined";函数定义完毕后,name就已经添加到作用域里了,所以alert()能找到name这个属性,但是恶心的事 name并未被赋值。
犀牛书中写道javascript函数“在定义它们的作用域里运行,而不是在执行它们的作用域里运行”,多么抽象而又精髓的一句总结。经过一番讲解后,上述代码等价于下述代码:
var name = "James";
function f1(){
var name;
alert(name); //alerts 'undefined'
name = 'Camelo';
alert(name); //alerts 'Camelo'
}
f1(); //输出 'undefined'和'Camelo'
javascript中的函数可以先写调用再写定义,但是这样做很不好,会养成不好的习惯 并且 难维护。
var name = "James";
f1(); //输出 'undefined'和'Camelo'
function f1(){
var name;
alert(name); //alerts 'undefined'
name = 'Camelo';
alert(name); //alerts 'Camelo'
}
4.动态作用域
与词法作用域不同于在定义时确定,动态作用域在执行时确定,其生存周期到代码片段执行为止。动态变量存在于动态作用域中,任何给定的绑定的值,在确定调用其函数之前,都是不可知的。
在代码执行的时候,对应的作用域通常是静态的,但是我们可以通过一些方法或者语句改变作用域链。例如with语句(with语句执行完毕后,会把作用域链恢复到原始状态,但是一般禁用with语句,因为太耽搁性能):
var name = 'James'
alert(name); //alerts 'James'
with({name; 'Camelo'}){
alert(name); //alerts'Camelo'
}
alert(name); //alerts'James'
当然还有call方法、apply方法和try-catch中的catch也能修改作用域链,但是很重要的一点就是,当作用域链中存在动态作用域时,this引用会变得更复杂,不再指向第一次创建的上下文,而是由调用者决定。
5.没有块级作用域(ES6新增的let能创建块级作用域)
不同于C那些编程语言,在JavaScript中没有块级作用域,换句话说,在块级语句内部里声明的变量在与外部声明的变量是一样的,在这些块级语句外部也能访问和修改这些变量的值。例如:
function f1(){
if( 1 < 3 ){
var name = 'James';
}
alert(name); //alerts'James'
name = 'Camelo';
alert(name); //alerts'Camelo'
}
f1(); //输出'James'和'Camelo'
B.作用域链
两种模式:
词法作用域只关心函数和作用域是在哪里定义的,而不关心在哪执行。作用域链通常为嵌套作用域链。可以将作用域想成一条绳子,当前作用域位于绳头,全局作用域位于绳尾。RHS和LHS引用会从绳头开始进行查找直到绳尾,找到就用,找不到就返回异常(严格模式下)。
动态作用域并不关心函数和作用域是如何声明以及在哪里声明。作用域链是基于调用栈的,而不是代码中的作用域嵌套。
function foo() {
alert(a); // 3
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();