深入理解函数内部原理(1)——函数定义、调用、解析、执行

来源:互联网 发布:打谱软件什么意思 编辑:程序博客网 时间:2024/05/22 08:17

在阅读本博客之前先阅读:
执行环境: http://blog.csdn.net/wmaoshu/article/details/60466990
引用规范类型:http://yanhaijing.com/es5/#80
本系列博客主要说一下一个函数从定义到调用到解析到执行的过程,以便于更好的理解后续介绍的闭包、this等概念。先介绍内部原理,然后通过一个实例说明一下这个原理。然后是一些对这个原理一部分的应用技巧。



内部原理

函数定义

官方文档:http://yanhaijing.com/es5/#237
过程一:执行函数定义
函数定义的方式有两种,一种是函数声明的方式,一种是函数表达式的方式,其中又分为匿名的函数表达式和具名的函数表达式。
对于这三种定义函数的方式有许多的不同,现在先介绍一个不同之处就是在函数定义的时候不同。
无论是哪一种函数定义方式,到最后都调用创建函数对象这么一个过程,在这个过程中传入一个参数为scope,这个参数的目的就是为了赋给内部属性[[scope]]。
(1)对于函数声明来说传入的scope为函数所在的执行环境的变量环境。
(2)对于匿名的函数表达式来说传入的scope为函数所在的执行环境的词法环境。
(3)对于具名的函数表达式来说与上述两种方式不同,sope为一个创建的词法环境,在这个词法环境中外部词法环境为所在的执行环境的词法环境,而环境记录项为声明式环境记录项,声明式环境记录项中会有一个属性,这个属性就是这个函数表达式的名字并且与创建的这个函数对象绑定在一起。函数的递归就是对这个应用。

过程二:调用创建函数对象过程
在这个过程中,创建一个空对象,添加各种内部属性比如[Class] [Put] [Get] [Prototype]等最重要的是将有一个[Scope]内部属性赋值为传入的scope。然后添加一些函数特有的内部方法[Call]、[Construct]、[HasInstance]。同时还会定义一些属性比如length为形式参数的个数、constructor为只想这个函数对象的指针、prototype为一个空对象,目的是为了实现够当构造函数时实现原型继承。


函数调用

官方文档:http://yanhaijing.com/es5/#164
过程一:令 ref 为解释执行 MemberExpression 的结果,取值
在这里的MemberExpression 可能有三种类型一种是标识符、一种是属性访问表达式、一种是一个函数表达式可能是匿名的后者也有可能是具名的。下面分别介绍这三种方式:

(1)使用函数标识符的方式
在这里要介绍一下标识符解析的过程,不只是函数调用中的标识符,对于所有的标识符解析都试用。
官方文档:http://lzw.me/pages/ecmascript/#144
其实标识符解析的过程就是在作用域链中从下到上(从内到外)匹配的过程。GetIdentifierReference 函数就是这么做的。把当前的执行环境中的词法环境和标识符字符串已经是否严格模型传入GetIdentifierReference(lex, name, strict)内部函数中,然后开始执行:
1:如果传入的词法环境lex为null,则返回一个引用{基值:undefined,引用名称:name,严格引用:strict}
2:得到lex中环境记录项的值为env
3:经过搜索匹配env中存在这个标识符,则返回一个引用{基值:env,引用名称:name,严格引用:strict}
4:不存在的话让lex为传入的lex这个词法环境的外部词法环境,继续调用GetIdentifierReference 。
无论这个标识符在作用域链中存不存在都会返回一个引用类型。通过getValue函数取得该值

<script>function add(){};add();</script>

这里add标识符经过解析返回的是{base:window,name:add,strict:false}

(2)使用属性访问表达式的方式
先了解一下属性访问表达式内部工作原理。
官方文档:http://lzw.me/pages/ecmascript/#162
可以看出返回的也是一个引用类型,这个引用类型的基值为属性访问操作对象,引用名字为操作字符串,严格引用视情况而定。通过getValue函数取得该值

<script>var o = {    add: function(){    }};o.add();</script>

这里o.add属性访问标识符经过解析返回的是{base:o,name:add,strict:false}

(3)使用函数表达式
比如如下:

<body>(function add(){    })();</body>

当解析器遇到这个函数调用的时候,回西安之行add函数表达式,然后根据定义的步骤会返回一个add函数的对象,所以函数调用操作符“()”前面的操作数不和前面两种情况一样是引用了,而是一个函数对象,但是用过getValue得到的值仍然是这个函数对象。

过程二:判断是否是一个可被调用的对象
判断函数调用操作符“()”前面的操作对象的返回值引用或者非引用经过getValue之后的到一个值,如果这个值是一个对象并且有[Call]内部函数的话,那么就认为这个对象可以被调用。

过程三:确定this的值
经过过程一可以得知,得到的可能是引用类型或者非引用类型,对于非引用类型,则直接将this设置为undefined。对于引用类型,则取决于基值得值:
如果基值为一个(Object、number、string、boolean)类型之一的话,那么将this赋值为基值。
如果基值是一个环境记录项,那么this为ImplicitThisValue 结果,一般如果是全局对象的话由于全局对象中provideThis为false,所以为undefined。

过程四:调用[Call]
将上述第三过程中确定的this和参数列表传入函数的[Call]方法中,这个方法会创建一个执行环境push逻辑栈中为进入函数代码做准备。


进入函数代码

官方文档:http://lzw.me/pages/ecmascript/#150
过程一:确定最终的this
如果说处于严格模式下,那么执行环境中this属性将直接是用过[Call]传入this的值即使是undefined。
如果不是严格模式的话,如果传入的this为null或者undefined,会用全局对象来替代。
如果传入的this不是对象类型,那么就尽可能的转化为对象,最终在复制给执行环境中this属性,这个执行环境中this属性将作为在这个执行代码块中this标识符的值。所以说this的值是在运行的时候确定的。

过程二:确定词法环境和变量环境
先创建一个词法环境,然后让外部词法环境这一属性值为该函数的[Scope]内部属性的值。然后将这个创建的词法环境复制给执行环境的词法环境属性和变量环境属性

过程一:执行定义绑定初始化
开始预览一边代码,执行如下操作:
如果有参数,则将参数和值绑定到环境记录项中。
如果有函数声明,则将函数与定义后创建的返回的函数对象绑定到环境记录项中。
并且创建一个arguments属性然后将传入的实参作为值列表复制给她。
如果遇到变量表达式即使是函数表达式,也是仅仅将变量名和undefined绑定到环境记录项中,并不是与实际的值,实际的值在接下来执行代码阶段进行解析赋值。
从这里可以看出函数声明和函数表达式第二个不同之处:
那就是在执行直线函数声明会将函数名和值绑定到环境记录项中,而函数表达式仅仅是将函数名和undefined绑定到环境记录项中,所以函数声明的函数可以在声明语句之前调用,这就是所谓的函数提升,而函数表达式则不能。



最终函数开始执行。

0 0
原创粉丝点击