实用干货javascript的7个使用技巧(第25节变量作用域及预编译-Javascript-零点程序员-王唯)
本内容是《Web前端开发之Javascript视频》的课件,请配合大师哥《Javascript》视频课程学习。
变量的作用域:
变量的作用域是指一个变量在哪个范围内可以使用,可以分为两种:
全局变量:在所有函数之外定义的变量,其作用范围是整个变量定义之后的所有语句,包括其后定义的函数及其后的<script>中的代码;
局部变量: 定义在函数之内的变量,只有在该函数中才可使用;
如果函数中定义了与全局变量同名的局部变量,会覆盖全局变量;
varmsg="这是全局变量的值";
functionshow(){
varstr="局部变量";
console.log(msg);
console.log(str);
}
show();
console.log(str);//Errorstrisnotdefined
如果函数中使用隐式声明变量,即没有使用var声明,则该变量自动变成全局变量;使用var声明的变量会自动被添加到最接近的环境中;在函数内部,最接近的环境就是函数的局部环境;如:
functionadd(num1,num2){
varsum=num1 num2;
// sum=num1 num2;
returnsum;
}
varresult=add(10,20);
alert(sum);//由于sum不是有效的变量,因此会导致错误
注:在JavaScript中,不声明而直接始初化变量是一个常见的错误做法,因为这样可能会导致意外;建议在初始化变量之前,一定要先声明;并且,在严格模式下,初始化未经声明的变量会导致错误;
ES的变量与其他语言的变量有很大区别;ES变量是松散类型的,这个特点决定了它只是在特定时间用于保存特定值的一个名字而已;由于不存在定义某个变量必须要保存何种数据类型值的规则,所以,变量的值及其数据类型可以在脚本的生命周期内可以被改变;尽管从某种角度看,这可能是一个灵活强大的特性,但同时也是容易出问题的特性;
在实际应用中,ES变量还是比较复杂的;比如:函数的参数,由于参数的数据类型不一致,导致的结果也不致;
嵌套函数:
也称为私有函数 :是指处于局部作用域中的函数;
当函数嵌套定义时,子级函数就是父级函数的私有函数;外界不能调用私有函数,私有函数只能被拥有该函数的函数代码调用;
子级函数可以使用父级函数定义的变量,父级函数不能使用子级函数定义的变量;其他函数不能直接访问子级函数,如此,就实现了信息的隐藏;如:
functionfunA(){
varstrA="funA定义的变量strA";
funB();
functionfunB(){
varstrB="funB定义的变量strB";
console.log(strA);
console.log(strB);
}
}
funA();
预编译:
ES是一种具有函数优先的轻量级解释型或即时编译型的编程语言,其可以不经过编译而直接运行,但是ES存在一个预编译的机制,这也是Java等一些语言中没有的特性,也就正是因为这个预编译的机制,导致了ES中变量提升的一些问题;
JavaScript运行三部曲:
脚本执行期间JS引擎按照以下步骤进行处理:
- 1.语法分析;
- 2.预编译;
- 3.解释执行;
即在执行代码前,还需要两个步骤:
语法分析,就是引擎检查你的代码有没有什么低级的语法错误;
解释执行:就是执行代码;
预编译简单理解就是在内存中开辟一些空间,存放一些变量与函数;
JS预编译发生时刻:
预编译是在脚本执行前就发生了,更确切的说是在函数执行前发生的,也就是说函数执行时,预编译已经结束;
预编译前奏:
imply global暗示全局变量:任何变量,如果未经声明就赋值,这些变量就为全局对象(window)所有(即为window对象的属性);一切声明的全局变量,也是window所有;如:
vara=123;
window.a=123;
functiontest(){
//这里的b是未经声明的变量,所以是归window所有的; 连等的操作也视为无var
vara=b=110;
}
变量声明提升(hosting):
在JavaScript函数里的所有声明(只是声明,不涉及赋值)都被提前到函数体的顶部,预编译时并不会对变量进行赋值(即不会进行初始化),变量赋值是在脚本执行阶段进行的;如:
console.log('before:' a);//before:undefined
vara=1;
console.log('after:' a);//after:1
函数声明整体提升:
函数声明语句将会被提升到外部脚本或者外部函数作用域的顶部,如:
a();//function
console.log(a);//fa(){...}
functiona(){
console.log("function");
}
console.log(a);
a();
在预编译时,function的优先级比var高,如:
//vara=1;//异常,会导致下行的a()异常
a();
vara=1;
functiona(){console.log("function");}
vara;
console.log(typeofa);
此时a的类型是function,而不是number;
函数表达式用的是变量,函数并不会提升:
b();//bisnotafunction
varb=function(){
console.log('functionb');
};
b();
声明同名的函数会覆盖掉之前声明的函数:
functionc(){
console.log('functionc1');
}
c();//functionc2
functionc(){
console.log('functionc2');
}
要理解预编译,只要弄清两点:变量/函数声明 与 变量赋值;在预编译阶段,只进行 变量/函数声明,不会进行变量的初始化(即变量赋值,所有变量的值都是undefined);
变量赋值是在执行阶段才进行的;
预编译步骤:
首先JavaScript的执行过程会先扫描一下整体语法语句,如果存在逻辑错误或者语法错误,那么直接报错,程序停止执行,没有错误的话,开始从上到下解释一行执行一行。
执行器上下文,英文名Activation Object,简称AO,也称为活动对象;
全局对象,英文名Global Object,简称GO;
函数执行前会进行预编译,产生AO;
全局变量在执行前也会有预编译,产生GO;
局部预编译的4个步骤:
创建AO;
- 找形参和变量声明,将变量和形参名作为AO属性名,值为undefined
- 将实参值和形参统一;
- 在函数体里面找函数声明,值赋予函数体;
全局预编译的3个步骤:
- 创建GO对象;
- 查找变量声明,将变量名作为GO属性名,值为undefined;
- 查找函数声明,作为GO属性,值赋予函数体;
由于全局中没有参数的概念,所以省去了实参形参相统一这一步;
注:GO对象是全局预编译,所以它优先于AO对象所创建和执行;
AO对象示例:
functionfn(a){
console.log(a);//fa(){}
//变量声明 变量赋值,但只提升变量声明,不提升变量赋值
vara=123;
console.log(a);//123
//函数声明
functiona(){}
console.log(a);//123
//函数表达式
varb=function(){}
console.log(b);//f(){}
//函数
functionc(){}
}
fn(1);//调用
在进行完预编译后,执行函数则会以AO为基础对函数中的变量进行赋值,函数执行完毕,销毁AO对象。
GO对象的示例:
global=100;
functiontest(){
console.log(global);//undefined
varglobal=200;
console.log(global);//200
varglobal=300;
}
test();
varglobal;
注:关于GO对象和AO对象,它们俩是一个种链式关系,如上例,如果在函数体的内部没有定义global变量,这也意味着AO对象中将有这个global这个属性;如果没有,会去GO对象中寻找,即是就近原则;
另外需要注意的是JS不是全文编译完成再执行,而是块编译,即一个script块中预编译然后执行,再按顺序预编译下一个script块再执行,但是此时上一个script块中的数据都是可用的了,而下一个块中的函数和变量则是不可用的。
执行环境:
执行环境(execution context)是ES中最为重要的一个概念,也称为执行上下文;执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为;每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中;但无法访问这个对象,只有解析器在处理数据时会在后台使用它;
执行环境中有个全局执行环境的概念;
全局执行环境是最外围的一个执行环境;根据ES实现所在的宿主环境不同,表示执行环境的对象也不一样;在Web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的;某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁。
每个函数都有自己的执行环境;当执行流进入一个函数时,函数的环境就会被推入一个环境栈中;而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境;ES程序中的执行流就是由这个的机制控制着;
作用域链:
当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain);作用域链的用途,能够保证对执行环境有权访问的所有变量和函数的有序访问;
作用域的前端,始终都是当前执行的代码所在环境的变量对象;如果这个环境是函数,则将其活动对象作为变量对象;活动对象在最开始时只包含一个对象,即arguments对象;作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境;这样,一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域中的最后一个对象;
functionf(){
vara=10;
functionb(){};
}
varg=100;
f();
查询标识符(在作用域链中查找):
当在某个环境中为了读取或写入而引用一个标识符时,必须通过搜索作用域链来确定该标识符实际代表什么;搜索过程从作用域链的前端开始,向上逐级查询;如果在局部环境中找到了该标识符,搜索过程停止,变量就绪;如果在局部环境中没有找到该变量,则继续沿作用域链向上搜索;搜索过程一直追溯到全局环境的变量对象;如果在全局环境中也没有找到这个标识符,则意味着该变量尚未声明,从而会导致错误发生,如:
varcolor="blue";
functiongetColor(){
// varcolor="red";
returncolor;
}
console.log(getColor());
内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数;这些环境之间的联系是线性的、有次序的;每个环境都可以向上搜索作用域链,以查询变量和函数名;但任何环境都不能通过向下搜索作用域链而进入另一个执行环境,如:
varcolor="blue";
functionchangeColor(){
varanotherColor="red";
functionswapColors(){
//这里可以访问color、anotherColor和tempColor
vartempColor=anotherColor;
anotherColor=color;
color=tempColor;
}
swapColors();//这里可以访问color和anotherColor,但不能访问tempColor
}
changeColor();//这里只能访问color
console.log("coloris" color);//red
注:函数参数也被当作变量来对待,因此其访问规则与执行环境中的其他变量相同;
延长作用域链:
虽然执行环境的类型总共只有两种:全局和局部(函数),但还是有其他办法来延长作用域链;因为有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象在代码执行时,作用域就会加长,在代码执行后被移除:
try-catch语句的catch块及with语句;这两个语句都会在作用域链的前端添加一个变量对象;对with语句来说,会将指定的对象添加到作用域链中;对catch语句来说,会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明;如:
functionbuildUrl(){
varqs="?name=wangwei";
with(location){
varurl=href qs;
}
returnurl;
}
console.log(buildUrl());
作用域中的this对象:
this引用的是函数执行的环境对象,即是调用函数的对象,或者也可以说是this值(当在网页的全局作用域中调用函数时,this对象引用的就是window)。
window.color="red";
varo={color:"blue"};
functionsayColor(){
console.log(this.color);
}
sayColor();//red
o.sayColor=sayColor;
o.sayColor();//blue
没有块级作用域:
ES没有块级作用域;在其他类C的语言中,由花括号封闭的代码块都有自己的作用域(如果用ES的角度来讲,就是它们自己的执行环境),因而支持根据条件来定义变量,如,下面的代码在ES并不会得到想象中的结果:
if(true){
varcolor="blue";
}
console.log(color);//blue;
在ES中,if语句中的变量声明会将变量添加到当前的执行环境(在这里是全局环境)中;在使用for语句时表现的最为明显,如:
functionoutputNumber(count){
for(vari=0;i<count;i ){
console.log(i);
}
//vari;
console.log("最后的值:" i)//5
}
outputNumber(5);
可以模拟块级作用域,使用立即执行函数进行模拟;
立即执行函数:
立即执行函数也称为自运行函数,其没有声明,本质上就是匿名函数;其在一次执行后立即释放;
适合做一些初始化的工作或者模拟块级作用域;
(function(){
console.log("这里是立即执行函数");
})();
立即执行函数不允许使用函数声明方式,但是如果在function前加一个 号即可,同时在控制台中,该函数名也会被忽略,如:
functionmyFun(){
console.log("这里是立即执行函数");
}();
在function前加上 、!、-、~等一元操作符,也是立即执行函数的写法,等同上面的立即执行函数,如果没有这些符号,解析器会把function认为为一个函数声明;
同理,只要在function前加上其他的表达式语句,都可以,如:
true&&functionmyFun(){
console.log("这里是立即执行函数");
}();
//或
0,function(){
console.log("ok");
}();
立即执行函数可以传值,也可以有返回值,如:
//传值
(function(x,y,z){
console.log(x y z)
})(1,2,3);//6
//返回值
varresult=(function(x,y,z){
varsum=x y z;
returnsum;
})(1,2,3);
console.log(result);//6
特例:
functionmyFun(){
console.log("这里是立即执行函数");
}(1,2,3);
// 此时,不会报错,函数也存在,但不会立即执行,原因是解析器会把它拆分成两条语句;如:
functionmyFun(){console.log("这里是立即执行函数");};
(1,2,3);
这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数;
一般来说,应该尽量少向全局作用域中添加变量和函数;在一个由很多开发人员共同参与的大型应用程序中,过多的全局变量和函数很容易导致命名冲突,而通过创建私有作用域,每个开发人员都可以使用自己的变量,而不必担心搞乱全局作用域;
(function(){
varnow=newDate();
if(now.getMonth()==0&&now.getDate()==1){
console.log("HappynewYear");
}
})();
说明:变量now是匿名函数的局部变量;
立即执行函数也是后面要讲的闭包的基本形式,但闭包有个问题,就是内存占用的问题,此种方法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用,只要函数执行完毕,就立即销毁其作用域链了;
思考两个小示例:
varfoo=(
functionf(){return"1";},
functiong(){return2;}
)();
console.log(typeoffoo);//number
varx=1;
//(functionfun(){})因为在括号中,所以是一个表达式,
//运行完后就消失了,所以typeoffun就是undefined
if(functionfun(){}){
x =typeoffun;
console.log(typeoffun);//undefined
}
console.log(x);//1undefined
Web前端开发之Javascript-零点程序员-王唯
,免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com