ECMAScript笔记(三)函数
概念
函数主要用于减少重复代码。
在 JavaScript 中,函数是头等对象,因为它们可以像任何其他对象一样具有属性和方法。它们与其他对象的区别在于函数可以被调用。简而言之,它们是Function
对象。
定义一个函数并不会自动的执行它。定义了函数仅仅是赋予函数以名称并明确函数被调用时该做些什么。调用函数才会以给定的参数真正执行这些动作。
两种定义形式
函数声明
1 | function 函数名 (参数1, 参数2, 参数3, ...) { |
函数表达式
- 命名函数表达式:
1 | var 变量名 = function 函数名(参数1, 参数2, 参数3, ...) { |
- 匿名函数表达式 — 常用,故后续直接叫函数表达式:
1 | var 变量名 = function(参数1, 参数2, 参数3, ...) { |
关于函数名和变量名:
1 | var test = function fnName() { |
表达式的值就是函数本身。函数表达式就是创建一个函数,将其赋值给变量,用变量名()
来调用。函数是一个引用类型,将其赋值给某个变量时,变量中保存的是函数的地址。
1 | var f = function foo(){ |
命名函数表达式的识别名(也就是上例的函数名),它的作用域只能在函数的主体(FunctionBody)内部,真正的函数识别名称是被赋值的那个变量识别名(也就是变量名)。
组成形式
函数名称。
参数:不限制数据类型、不限制参数多少。
形式参数:相当于 var 声明变量。
形参长度 = 函数名.length
实际参数:实际参数列表 arguments,类似数组,会将所有实际参数存储起来。
1 | function test(a, b) { |
在形式参数和实际参数长度完全相等的时候,arguments
和实际参数有一个映射规则,如果arguments
内的参数发生变化,相应的实际参数也会改变。反之同理。
1 | // 实参改变,arguments 内的参数也随之改变 |
arguments
一开始有多少位参数,就会一直保持这个长度,如果实参数量小于形参,后续增加的实际参数不会添到列表里,即此时不会存在映射关系。
1 | function sum(a, b) { |
返回值:需要函数有返回值的时候,可以在函数体里使用return
语句。return
既可以终止函数,还可以得到函数返回值。
- 终止函数:
1 | // 终止函数,console.log('b'); 不会被执行 |
- 函数返回值:将一个值返回到函数外面。记得要找一个变量接收它。这个时候既终止了函数,又得到了函数返回值。
1 | function myNumber(target) { |
如果
return
后面不跟任何数据,或者函数中没有return
语句,则该函数会在末尾自动 return undefined。如果找一个变量接收它,就会得到 undefined 的结果。
递归
递归只是让函数更加简洁,并没有减少响应时间,所以特别复杂的程序不能使用递归。
例子
写一个函数,实现斐波那契数列(1、1、2、3、5、8……)。
找规律:fb(n) = fb(n - 1) + fb(n - 2);
- 发现可以采用递归 -> return 公式:
return fb(n - 1) + fb(n - 2);
- 测试公式
1 | fb(5) => fb(4) + fb(3); |
找出口:n === 1 || n === 2
1 | function fb(n){ |
作用域
定义
每个函数都是一个对象,对象中有些属性我们可以访问,有些不可以。这些不可访问的属性仅供 JS 引擎存取,[[scope]]
就是其中一个。**[[scope]]
指的就是我们所说的作用域,其中存储了运行期(执行期)上下文的集合**。
执行期上下文
当函数执行时,会创建一个称为执行期上下文的内部对象(AO)。一个执行期上下文定义了一个函数执行时的环境,函数==每次执行时对应的执行上下文都是独一无二的==,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,执行上下文被销毁。
作用域链
[[scope]]
中所存储的执行期上下文的对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。
查找变量
在哪个函数里查找变量,就从该函数的作用域链的顶端依次向下查找。
例子
函数如下:
1 | function a() { |
a 函数在被定义时,a.[[scope]]
里第 0 位存储了全局的执行期上下文(GO)。
a 函数执行时,它的执行期上下文(AO)放入a.[[scope]]
里的顶端,即第 0 位,GO 下移一位,到第 1 位。
此时若在 a 函数里查找一个变量,就需要在 a 的作用域链里查找,先在第 0 位 AO 中找,没有的话再去第 1 位 GO 中找。
而对于 a 函数中的 b 函数,==a 函数执行时,b 函数被定义==,此时 b 函数的全局执行期上下文就是 a 的作用域链,即b.[[scope]] === a.[[scope]]
(同一个作用域链,是同一个房间内的)。
b 函数执行时,它的执行期上下文放入b.[[scope]]
的第 0 位,原有的依次下移一位。
b 函数执行完毕后,销毁它自己的 AO(链接被砍断,也就是删除引用),b 又回到被定义状态,等待下一次被执行。
a 函数执行完毕后,销毁它自己的 AO,由于引用被删除,AO 里面的 b 函数和 c 函数都被砍断,所以这俩函数都没了。a 回到了被定义状态,等待下一次被执行。当 a 函数需要再次执行时,又会生成新的 AO(里面又会有新的 b 函数和 c 函数出现)。
1 | a defined(定义) a.[[scope]] -> 0 : GO |
注意,function 是引用值,即值存储在 heap(堆),而在 stack(栈)里放的是堆地址,也就是引用。
预编译
JS 运行执行步骤
- 语法分析(通篇扫描一遍,看看有没有啥错误)
- 预编译
- 解释执行
全局变量
暗示全局变量(imply global):任何变量,如果未声明就赋值,那么此变量就为全局对象(window)所有。
1 | // a 未经声明但直接赋值 |
所有声明了的全局变量,也为 window 所有。
1 | // b 为声明了的全局变量 |
上面两种都是全局对象 window 的属性(属性名+属性值),window 相当于是全局的域。
1 | var c = 456; |
预编译过程
函数内
函数内的预编译发生在函数执行的前一刻。
步骤:找形参和变量声明,值为 undefined -> 实参值传给形参 -> 找函数声明。
1 | function fn(a) { // a 形式参数 |
创建 AO 对象(Activation Object):执行期上下文,可理解为作用域。
找形式参数和变量声明,将形式参数名和变量名作为 AO 属性名,值为 undefined。(变量声明提升)
1 | AO = { |
- 将实际参数和形式参数统一。
1 | AO = { |
- 在函数体里找函数声明,将函数名作为 AO 属性名,值为自身的函数体(函数声明提升)
1 | AO = { |
- 执行。
1 | function fn(a) { |
全局内
全局内的预编译发生在全局执行的前一刻。
步骤:找变量声明,值为 undefined -> 找函数声明。
- 创建 GO 对象(Global Object):全局执行期上下文,可理解为作用域。(GO 就是 window )
- 找变量声明,将变量名作为 GO 属性名,值为 undefined。(变量声明提升)。
- 找函数声明,将函数名作为 GO 属性名,值为自身的函数体(函数声明提升)。
- 执行。
GO 发生在全局执行前,AO 发生在函数执行前,如果 AO 内找不到,会直接在 GO 中找。
补充
通过函数声明定义的函数,会提升到脚本块的顶部。
通过函数声明定义的函数,会成为全局对象的属性。
通过函数表达式定义的函数,它既不会提升,也不会污染全局对象。
例题
例一:
1 | // GO = { |
例二:
1 | // GO = { |
例三:
1 | // 会进行预编译,所以不会按照视觉上的先后顺序运行 |