目录
function - 函数
每个编程人员在编写代码时都必须遵循的基本原则是高内聚、低耦合。
高内聚 -> 我们开发一个模块,它的内部代码一定是紧密联系度比较强的,我们希望一个模块能够独立的去完成一个任务,而这个任务完成的好坏,是跟高内聚有关系的。里面的代码一定是紧密联系的。
低耦合 -> 假如我们有多个地方编写到了相同的功能代码,那么我们应该把这部分重复编写到的代码给抽离出来,抽离出来后把它给组成一个独立的模块。这样可以减少相同代码的重复率,也减少了我们重复编写相同代码所浪费的时间。
而function就是为我们处理这个问题的,我们可以把具有相同功能的代码抽离到function里,然后在对应的时机里调用(函数只有被调用的时候才会执行相关代码)。
函数定义
在JavaScript中,定义函数的方式有两种。
第一种 - 函数声明
function 函数名称 () {
...函数内部执行代码
}
第二种 - 函数表达式
var 变量名 = function () {
...函数内部执行代码
}
函数调用
如果我们想要调用一个函数,让它执行函数内部的代码,我们可以通过函数名() 的方式来调用。
function test () {
console.log(1);
}
test(); // 1
这里有一点要注意,如果你是通过函数声明的方式定义的函数,那么你在声明之前调用,是不会报错的,它会正常运行。(因为这里涉及到函数声明提升的概念,后面会讲到)
test(); // 1
function test () {
console.log(1);
}
如果是通过函数表达式定义的函数,则必须在定义之后才可调用,否则会报错
test(); // 报错
var test = function () {
console.log(1);
}
函数返回
假如我们现在在函数内部定义了一个变量,那么这个变量就是一个局部变量,它只存在于函数内部的作用域链里,函数外部无法访问到此变量(这里涉及到GO和AO的知识点,后面会讲解到)。此时如果我们希望外部可以访问到此变量的值,那么我们可以通过return来把值返回给外部。
function test () {
return 1 + 2; // 返回 1 + 2 的和给外部
}
var total = test(); // 接收test函数的返回值并赋值给一个新的全局变量
console.log(total); // 3
当JavaScript执行到达return语句的时候,会认为函数已经执行完毕,并把结果返回,从而停止执行后续代码。因此我们可以通过利用这点进行一些判断处理或者特殊处理。
function test () {
var num = 3;
if (num > 1) {
console.log('num 大于 1');
return;
}
console.log('num 小于 1');
}
test(); // num 大于 1
如果我们现在函数内部没有return语句或者没有给予一个return出去的值,那么函数会默认返回undefined。
function test1 () {
console.log(1);
}
var result = test1(); // 1
console.log(result); // undefined
形参、实参
在实际开发中,变量的值往往是未知的。它可以是用户输入的,也可以是通过请求获取到的,我们不可能在一开始就知道要处理的变量的值是多少,所以就有了参数的存在。
参数分为形参和实参
形参
形参是存在于函数内部的局部变量,它通常以函数名(形参){ ... }的方式表现,在有传入实参的时候,JavaScript会根据两者在()里的顺序来为他们匹配赋值。如果没有给予形参默认值并且没有实参的话,这时获取形参的值为undefined。
function test (name) {
console.log(name);
}
test(); // undefined
实参
实参指传递给函数的实际的值,它通常以函数名(实参)的方式表现,它传入的实参会根据顺序对应匹配赋值给形参。
function test (name, age) {
console.log(name);
console.log(age);
}
test('jack', 18); // jask 18
arguments、test
JavaScript还为我们提供了两个关键字(arguments、test),它们只在函数内部起作用。我们可通过它们来获取实参、形参的长度与值。
arguments
arguments指向当前函数所传入的所有实参,它类似Array但又不是Array(也可以称为类数组),它本身并不能调用Array方法。
function test () {
for (let i = 0; i < arguments.length; i++) {
console.log(arguments[i]);
}
}
test(1, 2, 3); // 1 2 3
test
test指向当前函数的所有形参,它同样是一个类数组。
function test (a, b) {
console.log(test.length);
}
test(); // 2
参数默认值
如果我们希望在没有实参的时候,形参能够有一个默认值的话,我们可以通过以下方式设置。
function test (a = 1) {
console.log(a);
}
test(); // 1
假如我们现在有两个形参, 我们希望第一个参数选取默认值,第二个参数选取实参的值。可以采用以下写法。
function test (a = 1, b) {
console.log(a);
console.log(b);
}
test(undefined, 2); // 1 2
这里传递undefined是因为函数在初始化参数的时候,如果argument的值为undefined的话,它就会进入test里查到,从而为它匹配上默认值。
不过以上是es6的写法,在IE8里会有兼容问题,所以我们推荐使用以下兼容写法
// 第一种写法
function test(a, b) {
var a = argument[0] || 1;
var b = argument[1] || 2;
console.log(a);
console.log(b);
}
// 第二种写法
function test(a, b) {
var a = typeof(argument[0]) !== 'undefined' ? argument[0] || 1; // 这里的写法是三元运算符,后面会学到
var b = typeof(argument[1]) !== 'undefined' ? argument[1] || 2;
console.log(a);
console.log(b);
}
test() // 1 2
暗示全局变量
在JavaScript中,未声明就直接赋值的变量称为暗示全局变量,它被挂载到window下。(window是作用于全局的)
function test () {
a = 1;
}
test();
console.log(a); // 1
预编译
JavaScript的执行过程并不是直接解释一行就执行一行的,而是按照以下步骤进行
1 -> 它会先进行全部代码的语法分析,如果你当前这一作用域内的代码存在语法错误,那么这一作用域的所有代码将不执行,直接报错。
2 -> 语法检测完成没有错误后,它会进行全局预编译
3 -> 在全局预编译后,解释一行执行一行
4 -> 当执行到函数调用那行时,会先进行函数预编译,再往下执行
全局预编译
1. 创建全局上下文GO对象(window)
2. 将所有变量的声明放到最前面,作为window对象的属性,并赋值undefined。(声明提升,赋值不提升)
3. 将所有的函数声明放到最前面,作为window对象的属性,若函数名与变量名相同,函数名会将变量名覆盖,并将其值改为函数体。
函数预编译
1. 创建一个执行期上下文AO对象(Active Object)
2. 将形参和变量声明提前,作为AO对象的属性,并赋值undefined。(声明提升,赋值不提升)
3. 将实参赋值给形参
4. 将所有的函数声明放到最前面,作为AO对象的属性,若函数名与变量名相同,函数名会将变量名覆盖,并将其值改为函数体。
作用域
一个函数在被声明定义的时候,它会生成一个js内部的隐式属性[[scope]](也就是我们所说的作用域)。[[scope]]用来存储该函数的作用域链。作用域链的第0位存储着当前环境下的全局执行期上下文GO,GO里存储着全局下的变量与函数。
假如我们现在声明定义了一个函数a
var num = 1;
function a (){
console.log(num)
}
那么此时函数a的里存储着的会如以下所示:
function a(){} | |
[[scope]] | Scope Chain 作用域链 |
Scope Chain 作用域链 | |
0 | GO 全局执行期上下文 |
GO 全局执行期上下文 | |
this | window |
window | (object) |
document | (object) |
a | (function) |
num | 1 |
作用域链
作用域链可以理解成是一个存放AO、GO的容器, 它会把AO、GO从上到下排列起来,形成一个链式关系。
当一个函数被执行时(前一刻),它会生成一个AO(Activation Object),也叫执行期上下文。AO会被存储到作用域链的第0位,GO会被存储到作用域的第1位。在执行完函数之后,AO会被销毁,GO重新存储到第0位。(这就是函数内部可以访问到外部,可是外部却不能访问到函数内部的原因)
以下面的代码为例
var c = 1;
function a () {
function b(){}
var a = 1;
}
a();
函数a的作用域链变化过程会如以下所示:
function a(){} | |
[[scope]] | Scope Chain 作用域链 |
Scope Chain 作用域链 | |
0 | GO 全局执行期上下文 |
执行前一刻(Scope Chain发生变化)
Scope Chain 作用域链 | |
0 | AO 执行期上下文 |
1 | GO 全局执行期上下文 |
AO 执行期上下文 | |
this | window |
arguments | [] |
b | (function) |
a | 1 |
GO 全局执行期上下文 | |
this | window |
window | (object) |
document | (object) |
a | (function) |
c | 3 |
执行完后(Scope Chain再次发生变化)
Scope Chain 作用域链 | |
0 | GO 全局执行期上下文 |
通过上面的演示,我们可以清晰的看到一个函数的作用域链从被声明定义到执行完成的整个变化过程,从而理解到什么是作用域链。然而我们还需要知道的一点就是函数内部在查找一个变量的时候,会从作用域链的顶端开始依次向下查找,如果在AO里找到了要查找的变量,那么它就会停止继续向下查找。比如AO和GO里都存在着a,那么它会优先取AO里a的值。