JavaScript之深入函数(一)
在任何编程语言中,函数的功能都是十分强大的,javascript也不例外。之前已经讲解了函数的一些基本知识,诸如函数定义,函数执行和函数返回值等,今天就带大家升入了解javascript中函数的原理及执行过程。
一 函数参数
1,声明函数时可以添加参数,相当于在函数内部隐式的声明了变量。它的学名叫形式参数,简称形参,在执行函数使实际传递的值叫实际参数,简称实参。
1 function add(a,b){ 2 return a+b; 3 } 4 add(1,2);//3 5 //a和b就是形参,1和2就是实参
2,js中函数的参数不限制个数和数据类型,这意味着函数的形参和实参个数可以不相等,都可以是无限多个。
1 function add(a,b){ 2 return a + b; 3 } 4 add(1);//nan 1+undefined 5 add(1,2,3);//3 忽略3 6 add(1,"3");//"13"
3,函数甚至可以没有形参,函数的arguments属性,以类数组的形式存储了函数执行时的实参。
4,函数有一个length属性,表示函数的形参个数。
1 function test(){ 2 console.log(arguments); 3 } 4 test(1,2,3);//[0:1,1:2,2:3,length:3,...] 5 //返回一个类数组
5,arguments中的实参和形参是相互绑定的,修改其中一个,另一个也会改变,但形参和实参实际是两个变量。实际上实参列表的个数是不可更改的,函数执行时传递几个就是几个。
1 function test(a,b){ 2 console.log(arguments); 3 console.log(b); 4 b = 2; 5 console.log(arguments); 6 console.log(b); 7 a = 10; 8 console.log(arguments); 9 } 10 test(1); 11 /* 12 [0:1,length:1,...] 13 undefined 14 [0:1,length:1,...] 15 2 16 [0:10,length:1,...] 17 */
二 预编译
前面介绍javascript时,提到它是单线程,解释性语言,即读到一行就执行一行。其实这只是它的表象,实际上javascript执行代码分为了3个大的步骤:
1, 语法分析
语法分析的工作大体就是检测是否有语法错误,是否符合版本规则等等。如果么有问题则进入预编译阶段,如果遇到问题则会抛出错误。
2, 预编译
在理解预编译之前,我们应该先明白两个概念:
a:如果变量未申明即访问将报错,但是变量未声明即赋值,那么该变量将自动升级成全局对象(window)的属性。
b:在全局申明的变量,也会自动升级成window的属性。
程序预编译发生在即将执行之前,分为三步:
1) 创建一个go(global object)对象(也称为全局作用域),实际上就是window对象。
2) 查找变量声明,并把他们作为go对象的属性,值为undefined。
3) 查找是否有函数声明,若有,则把函数名作为go对象的属性,并把函数体赋值给该属性,若函数名和变量名相同,则会覆盖他们。
函数也有预编译过程,函数的预编译发生在函数即将执行之前,分为四步:
1) 创建一个ao(active object)对象(即执行期上下文,也叫函数作用域或本地作用域)。
2) 查找形参和变量声明,并把他们作为ao对象的属性,值为undefined。
3) 将实参赋值给形参。
4) 查找函数内部是否有函数声明,若有,则把函数名作为ao对象的属性,并把函数体赋值给该属性,若函数名和形参或变量名相同,则会覆盖他们。
总结下来,可以简单概括为:变量声明时,声明提升。函数声明时,整体提升。函数声明优先级大于变量和形参。
1 function fn(a) { 2 console.log(a); //ƒ a() {} 3 console.log(b); //undefined 4 console.log(c); //ƒ c() {} 5 var a = 123; 6 console.log(a); //123 7 function a() {} 8 console.log(a); //123 9 var b = function b() {}; 10 console.log(b); //ƒ b() {} 11 function c() {} 12 } 13 fn(1); 14 /* 15 go --> {fn:fn} 16 17 ao 18 第一步:ao --> {} 19 第二部:ao --> {a:undefined,b:undefined,c:function c() {}} 20 第三部:ao --> {a:1,b:undefined,c:function d() {}} 21 第四部:ao --> {a:function a() {},b:undefined,c:function c() {}} 22 23 注意这里只是预编译过程,函数真正执行时,ao中的属性值会动态改变。所以: 24 第一行代码直接打印fn a 25 第二行打印undefined 26 第三行打印fn c 27 第四行声明变量a已经被提前执行了,这里直接赋值123 28 第五行打印123 29 第六行函数声明被整体提前了,这一行代码将被直接跳过 30 第七行依然打印123 31 第八行只执行赋值操作,b == fn b 32 第九行则打印fn b 33 第十行声明函数已经被整体提升了 34 */
3, 解释执行
根据预编译后的代码顺序,一条一条的执行。
三 作用域和作用域链
每一个函数都有一个隐式的属性[[scope]],它存储的是函数在执行时创建的执行期上下文集合,即一堆ao和go对象,当然他们是有顺序的,类似一个数组,它只能被系统调用,而不能被我们访问和使用。
当一个函数在全局被创建时(没有被执行,这时还没有产生它自身的ao对象),[[scope]]将被插入go对象。当函数执行时(这时已经创建了自己的ao对象),[[scope]]的头部将被插入一个自己的ao对象,类似数组的unshift()方法。现在[[scope]]中已经有两个执行期上下文的对象了:第0位的ao,第1位的go。
1 function fn(){} 2 //fn.[[scope]] --> {0:go} 3 fn(); 4 //fn.[[scope]] --> {0:ao,1:go}
如果全局函数的内部定义了一个子函数,那么该子函数的[[scope]]属性类似的会存储:第0位父函数的ao,第1位go(因为只有父函数执行才会产生子函数的定义,所以子函数被定义时就已经有两个执行期上下文对象了),这时子函数还没被执行,所以它只会存储这两个对象。当他被执行时,子函数[[scope]]属性的头部将被插入它自己的ao对象。如果子函数内部还定义的有其他函数,那么它的[[scope]]属性生成方式和上面相同。
1 function fn(){ 2 function son(){}; 3 } 4 //fn.[[scope]] --> {0:go} 5 //son.[[scope]] --> {} fn还没执行,son都还没声明 6 fn(); 7 //fn.[[scope]] --> {0:ao(fn),1:go} 8 //son.[[scope]] --> {0:ao(fn),1:go} 这里只是声明了函数son,所以并没有ao(son),如果function son(){}后面还有一行代码:son();那么当执行到这一行时,son.[[scope]] --> {0:ao(son),1:ao(fn),2:go}
这样就形成了函数的作用域链。当我们在函数内部访问变量时,实际上是在函数的[[scope]]属性里依次查找(从第0位开始),直到全局go(window)对象。函数作用域链的最终表现就是:子函数可以访问父函数的变量,父函数不能访问子函数的变量。
每次函数执行产生的执行期上下文都是独一无二的,当函数执行完毕,它自己的ao将被永远销毁,并更新自己的[[scope]]属性。下一次执行将产生一个新的ao对象,并添加到[[scope]]属性中。
下一次将更新将介绍javascript函数中另外两个重要的应用:立即执行函数和闭包。
上一篇: cmake
下一篇: 胆囊炎不能吃什么?再不知道就晚了!
推荐阅读
-
一些常用的JS功能函数代码_javascript技巧
-
JavaScript函数式编程(一)
-
深入浅出Python——Python高级语法之函数
-
JavaScript之深入对象(一)
-
php之GD库的函数(一)
-
深入理解JavaScript系列(29):设计模式之装饰者模式详解_javascript技巧
-
Javascript-Mozilla和IE中的一个函数直接量的问题分析_javascript技巧
-
JavaScript学习总结之正则的元字符和一些简单的应用
-
使用JavaScript实现一个小程序之99乘法表
-
深入理解JavaScript系列(4) 立即调用的函数表达式_javascript技巧