实用干货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

实用干货javascript的7个使用技巧(第25节变量作用域及预编译-Javascript-零点程序员-王唯)(1)

Web前端开发之Javascript-零点程序员-王唯

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页