概念

函数主要用于减少重复代码。

在 JavaScript 中,函数是头等对象,因为它们可以像任何其他对象一样具有属性和方法。它们与其他对象的区别在于函数可以被调用。简而言之,它们是Function对象。

定义一个函数并不会自动的执行它。定义了函数仅仅是赋予函数以名称并明确函数被调用时该做些什么。调用函数才会以给定的参数真正执行这些动作。

两种定义形式

函数声明

1
2
3
function 函数名 (参数1, 参数2, 参数3, ...) {
// 函数体;
}

函数表达式

  • 命名函数表达式:
1
2
3
4
5
var 变量名 = function 函数名(参数1, 参数2, 参数3, ...) {
// 函数体;
}
// 函数名仅用作函数名称,并不执行,也不一定要和变量名一样
// 变量名.name = 函数名
  • 匿名函数表达式 — 常用,故后续直接叫函数表达式:
1
2
3
4
var 变量名 = function(参数1, 参数2, 参数3, ...) {
// 函数体;
}
// 变量名.name = 函数名

关于函数名和变量名:

1
2
3
4
5
6
7
8
9
var test = function fnName() {

};
console.log(test.name); // fnName

var demo = function() {

};
console.log(demo.name); // demo

表达式的值就是函数本身。函数表达式就是创建一个函数,将其赋值给变量,用变量名()来调用。函数是一个引用类型,将其赋值给某个变量时,变量中保存的是函数的地址。

1
2
3
4
5
6
var f = function foo(){
return typeof(foo);
};

console.log(typeof(foo)); // undefined
console.log(f()); // function

命名函数表达式的识别名(也就是上例的函数名),它的作用域只能在函数的主体(FunctionBody)内部,真正的函数识别名称是被赋值的那个变量识别名(也就是变量名)。

组成形式

函数名称。

参数:不限制数据类型、不限制参数多少。

  • 形式参数:相当于 var 声明变量。形参长度 = 函数名.length

  • 实际参数:实际参数列表 arguments,类似数组,会将所有实际参数存储起来。

1
2
3
4
5
6
7
8
9
function test(a, b) {
console.log(a + b);
}

test(1, 2);// 3

// test -> 函数名称
// a,b -> 形式参数
// 1,2 -> 实际参数

在形式参数和实际参数长度完全相等的时候,arguments和实际参数有一个映射规则,如果arguments内的参数发生变化,相应的实际参数也会改变。反之同理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 实参改变,arguments 内的参数也随之改变
function sum(a, b) {
a = 4;
console.log(arguments[0]); // 4
}

sum(1, 2);

// arguments 内参数改变,实参也随之改变
function sum(a, b) {
arguments[0] = 4;
console.log(a); // 4
}

sum(1, 2);

arguments一开始有多少位参数,就会一直保持这个长度,如果实参数量小于形参,后续增加的实际参数不会添到列表里,即此时不会存在映射关系。

1
2
3
4
5
6
function sum(a, b) {
b = 2;
console.log(arguments[1]); // undefined
}

sum(1);

返回值:需要函数有返回值的时候,可以在函数体里使用return语句。return既可以终止函数,还可以得到函数返回值。

  • 终止函数:
1
2
3
4
5
6
7
8
// 终止函数,console.log('b'); 不会被执行
function sum(a, b) {
console.log('a'); // a
return;
console.log('b');
}

sum(1);
  • 函数返回值:将一个值返回到函数外面。记得要找一个变量接收它。这个时候既终止了函数,又得到了函数返回值。
1
2
3
4
5
6
7
function myNumber(target) {
return +target;
}

var num = myNumber('123');

console.log(typeof(num), num); // number 123

如果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
2
3
fb(5) => fb(4) + fb(3);
fb(4) => fb(3) + fb(2);
fb(3) => fb(2) + fb(1);

找出口:n === 1 || n === 2

1
2
3
4
5
6
7
8
9
function fb(n){
if(n === 1 || n === 2){
return 1;
}
return fb(n - 1) + fb(n - 2);
}

result = fb(5);
console.log(result); // 5

作用域

定义

每个函数都是一个对象,对象中有些属性我们可以访问,有些不可以。这些不可访问的属性仅供 JS 引擎存取,[[scope]]就是其中一个。**[[scope]]指的就是我们所说的作用域,其中存储了运行期(执行期)上下文的集合**。

执行期上下文

当函数执行时,会创建一个称为执行期上下文的内部对象(AO)。一个执行期上下文定义了一个函数执行时的环境,函数==每次执行时对应的执行上下文都是独一无二的==,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,执行上下文被销毁。

作用域链

[[scope]]中所存储的执行期上下文的对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。

查找变量

在哪个函数里查找变量,就从该函数的作用域链的顶端依次向下查找。

例子

函数如下:

1
2
3
4
5
6
7
8
function a() {
function b(){
function c(){}
c();
}
b();
}
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
a defined(定义)    a.[[scope]] -> 0 : GO

a doing(执行) a.[[scope]] -> 0 : aAO
1 : GO

b defined b.[[scope]] -> 0 : aAO
1 : GO

b doing b.[[scope]] -> 0 : bAO
1 : aAO
2 : GO

c defined c.[[scope]] -> 0 : bAO
1 : aAO
2 : GO

c doing c.[[scope]] -> 0 : cAO
1 : bAO
2 : aAO
3 : GO

注意,function 是引用值,即值存储在 heap(堆),而在 stack(栈)里放的是堆地址,也就是引用。

预编译

JS 运行执行步骤

  1. 语法分析(通篇扫描一遍,看看有没有啥错误)
  2. 预编译
  3. 解释执行

全局变量

暗示全局变量(imply global):任何变量,如果未声明就赋值,那么此变量就为全局对象(window)所有。

1
2
3
4
5
6
7
// a 未经声明但直接赋值
a = 123;
// 即:
window = {
a: 123;
}
window.a = 123;

所有声明了的全局变量,也为 window 所有。

1
2
3
4
5
6
7
// b 为声明了的全局变量
var b = 234;
// 即:
window = {
b: 234;
}
window.b = 234;

上面两种都是全局对象 window 的属性(属性名+属性值),window 相当于是全局的域。

1
2
3
4
5
6
7
8
9
10
11
12
var c = 456;
function test() {
var a = b = 123;
}
test()
// 123 赋值给 b
// 声明 a
// b 赋值给 a
// 导致的结果就是:b 未经声明
// 此时:b 归 window 所有
// a 为局部变量
// c 为全局变量,也归 window 所有

预编译过程

函数内

函数内的预编译发生在函数执行的前一刻。

步骤:找形参和变量声明,值为 undefined -> 实参值传给形参 -> 找函数声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function fn(a) { // a 形式参数
console.log(a);

var a = 123; // 变量声明
console.log(a);

function a() {} // 函数声明
console.log(a);

var b = function (){} // 变量声明、函数表达式
console.log(b);

function d() {} // 函数声明
}

fn(1); // 1 实际参数
  • 创建 AO 对象(Activation Object):执行期上下文,可理解为作用域。

  • 找形式参数和变量声明,将形式参数名和变量名作为 AO 属性名,值为 undefined。(变量声明提升)

1
2
3
4
AO = {
a: undefined,
b: undefined
}
  • 将实际参数和形式参数统一。
1
2
3
4
AO = {
a: 1, //undefined
b: undefined
}
  • 在函数体里找函数声明,将函数名作为 AO 属性名,值为自身的函数体(函数声明提升)
1
2
3
4
5
AO = {
a: function a() {}, // 1
b: undefined,
d: function d() {}
}
  • 执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function fn(a) {

console.log(a);
//执行,访问 a,输出此时 AO 对象中 a 的值 -> function a() {}

var a = 123;
//var = a 变量声明已提升,忽略。
//执行赋值操作,将 123 赋值给 a

console.log(a);
// 输出此时 AO 对象中 a 的值 -> 123

function a() {}
// 已提升(提前执行),忽略

console.log(a);
// 输出此时 AO 对象中 a 的值 -> 123

var b = function (){}
//var = b 已提升,忽略。
//执行赋值操作,将 function (){} 赋值给 b

console.log(b);
//输出此时 AO 对象中 b 的值 -> function (){}

function d() {}
}

fn(1);

// AO = {
// a : 123, // function a() {}
// b : function (){}, // undefined
// d : function d() {}
// }

全局内

全局内的预编译发生在全局执行的前一刻。

步骤:找变量声明,值为 undefined -> 找函数声明。

  • 创建 GO 对象(Global Object):全局执行期上下文,可理解为作用域。(GO 就是 window )
  • 找变量声明,将变量名作为 GO 属性名,值为 undefined。(变量声明提升)。
  • 找函数声明,将函数名作为 GO 属性名,值为自身的函数体(函数声明提升)。
  • 执行。

GO 发生在全局执行前,AO 发生在函数执行前,如果 AO 内找不到,会直接在 GO 中找。

补充

通过函数声明定义的函数,会提升到脚本块的顶部。

通过函数声明定义的函数,会成为全局对象的属性。

通过函数表达式定义的函数,它既不会提升,也不会污染全局对象。

例题

例一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// GO = {
// a: 10, // undefined
// c: 234,
// test: function test(){...}
// }

function test() {
console.log(b); // undefined
if (a) {
var b = 100; // 预编译环节,不管是否执行,只先拿出所有该拿的:var = b;
}
console.log(b); // undefined
c = 234;
console.log(c); // 234
}

var a;

// AO = {
// b: undefined
// }

test();
a = 10;
console.log(c); // 234

例二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// GO = {
// a: 100, // undefined
// demo: function demo(e){},
// f: 123
// }

a = 100;
function demo(e) {
function e() {}
arguments[0] = 2;
console.log(e); // 2
if (a) {
var b = 123;
function c() {} // if 语句中不能声明 function
}
var c;
a = 10;
var a;
console.log(b); // undefined
f = 123;
console.log(c); // undefined
console.log(a); // 10
}
var a;

// AO = {
// e: 2, // function e(){} // 1 // undefined
// b: undefined,
// c: undefined,
// a: 10//undefined
// }

demo(1);
console.log(a); // 100
console.log(f); // 123

例三:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 会进行预编译,所以不会按照视觉上的先后顺序运行
// 两个相同的函数,提升后第一个函数会被第二个函数覆盖掉
// 最终都是第二个函数进行的调用

// GO = {
// x: 1, // undefined
// y: 4, // 0, // undefined
// z: 4, // 0,
// add: function add(n){return n = n + 3} // function add(n){return n = n + 1}
// }

var x = 1, y = z = 0;

function add(n) {
return n = n + 1;
}

y = add(x); // 1 + 3 -> y = 4

function add(n) {
return n = n + 3;
}

z = add(x); // 1 + 3 -> z = 4

// AO = {
// n: x // undefined
// }

console.log(x, y, z); // 1, 4, 4