函数和作用域
概述
在数学上,我们会遇到各式各样的函数,比如经典的二次函数,它常常被写成 f(x) = x2,当我们代入不同的 x 值,函数的值也会跟随变化。
在程序中,也有函数。程序中的函数可以完成比数学函数更复杂的功能,它是一系列程序语句的组合。
在 JS 中,函数被按照下列方式声明一个二次函数:
function f(x) {
var y = x * x;
return y;
}
其中:
function
:是 函数声明 的关键字,类似变量声明的var
关键字;f
:是 函数名,由标识符组成,和变量名类似;(x)
:是函数的输入部分,x
被称作函数的 参数,更细致的说,它读作 形参,它用来让函数接收外部的数据;{...}
:花括号是函数声明的一部分,花括号内部是函数的实现代码;reutrn
:是函数的输出部分,描述了函数的 返回值,它用来从返回函数内的数据并停止后续代码的执行;
现在,让我们来执行以下这个函数:
var re = f(9);
console.log(re); // 81
执行函数的时候,我们直接使用函数名,并在函数的后面添加圆括号表示执行,圆括号内写入的数据也称作 实参。
我们一直用来打印数据的 console.log
也是一个函数,它由 JS 内部引擎实现,用来打印数据和调试。
函数就像一个独立的程序,它拥有自己的输入输出和内部逻辑。
备注:在 JS 中,函数属于引用类型的数据,它被用来存储 JS 代码。
函数声明与函数表达式
函数声明
现在我们来反推一下函数的声明。
一个最简单的函数声明如下:
function foo(/* 参数部分 */) {
/*主体代码*/
}
function bar() {}
上面的函数没有任何功能,只是一个空壳,但是它们是 可以执行的,只是没有任何逻辑的处理。
函数是使用 function
关键字来声明的,函数名和变量名的声明规则相同,一般使用大小写字母数字下划线 _
或者美元符 $
组成。比如:
function _() {}
function $() {}
function $foo() {}
function _foo() {}
上面四个函数的声明都是有效的。
函数名后紧接函数的参数,参数部分使用一个圆括号包裹,主体代码使用花括号包裹,这两个括号都不可省略,比如:
function foo;
function bar ();
function baz {};
上面三种方式都是错误的,圆括号和花括号不可省略。
重复声明
查看一下代码:
function foo() {
console.log("one");
}
function foo() {
console.log("tow");
}
foo();
上述代码会打印 tow
,和变量的重复声明不一样:
- 变量重复声明无效。
- 函数的重复声明会覆盖之前的声明。
函数执行
编写一个简单的函数并执行:
function foo() {
console.log("running!");
}
foo(); // "running!"
上述代码中,最后一行的 foo
标识符是一个 函数表达式,函数表达式可以被执行。我们可以在函数表达式末尾加圆括号执行一个函数表达式,被执行的函数表达式会执行其内部代码。
参数
可以使用函数参数向函数内部传递数据,比如:
function foo(bar) {
console.log("bar :", bar);
}
foo("ohh"); // "bar :" "ohh"
上面代码中:
- 字符串
ohh
被称作 实参,标识符bar
被称作 形参; foo
函数把字符串ohh
赋值给了它的形参bar
,形参在函数内部等同于一个变量;- 接着
foo
的内部程序开始执行;
一个函数可以传递多个参数:
function foo(x, y) {
console.log(x + y);
}
foo(1, 2); // 3
foo(10, 20); // 30
函数的参数传递是有顺序的,需要按顺序传递。
函数表达式
函数也是数据,它就是之前我们介绍的引用类型的其中一种,既然他是数据,那么自然就可以让它赋值给变量:
var foo = function () {
console.log("foo run");
};
foo();
上面代码中,赋值给 foo
变量的函数是一个 函数表达式。函数表达式的函数名是可以省略的,这种省略函数名的函数,被叫做匿名函数。
函数表达式的函数名也可以不省略,比如:
var foo = function bar() {
console.log("inner:", foo, bar);
};
foo();
console.log("outer:", foo, bar);
函数表达式的函数名只能在函数内部使用,上述代码中,bar
只能在函数内部访问,外部访问会报错。
如何区分函数声明和函数表达式?
- 函数声明的
function
关键字在语句的开头,前面没有任何逻辑。 - 函数表达式是可以执行的,函数声明不行。
比如:
function foo() {alert("run");}();
上面的代码会报错,因为函数声明不可执行,但是函数表达式可以:
var foo = function () {alert('run')}();
函数在声明后,再次使用函数名就可以得到对应函数的表达式,所以函数执行时,执行的是函数表达式:
function foo() {
alert(1);
}
// 此时使用的 foo 也是一个函数表达式
foo();
作用域
思考一下代码会发生什么:
var a = 1;
function foo() {
var a;
console.log(a);
}
foo();
上述代码会打印出 undefined
,如果按照变量重复声明无效的原则,第二个 var a
无效,上面的代码的运行结果和此原则产生了冲突。
这里你需要理解一个新的概念,作用域。不论变量还是函数声明,它们都有自己可以被使用的区域,这个区域 就是就是变量自身所在的作用域。
因为函数类似一个小型的程序,它有自己的输入输出和逻辑,所以 foo
函数内部形成了一个新的作用域。
因为 foo
内部的变量 a
是在函数内部的作用域声明的,两个变量 a
并不在同一个作用域内,当我们在函数内部访问变量 a
的时候,它会优先寻找当前作用域内的变量 a
,于是打印一个没有初始化的变量 a
。
查看下列代码:
var a = 1;
function foo() {
console.log(a);
}
foo();
上述代码会打印出 1
,此时 console.log
去当前作用域寻找变量 a
的时候是找不到的,那么程序就会向外层的作用域继续寻找,因为外层作用域存在变量 a
,所以最后打印出 a
的值。
继续深入,查看下列代码:
var a = 1;
function foo() {
var a = 2;
function bar() {
console.log(a);
}
bar();
}
foo();
上述代码中,我们在 foo
函数内部又声明了一个函数 bar
,并且执行了它。bar
内部依旧是打印变量 a
,此时形成了 3 个作用域,foo
外部作用域,foo
内部作用域和 bar
内部作用域。
当程序打印变量 a
的时候,它会执行以下操作:
- 在
bar
内部作用域寻找变量a
,找不到,去外层寻找 - 在
foo
内部作用域寻找变量a
,找到了,它的值是2
,打印2
作用域术语
上面一节中,程序在寻找变量时,是一层一层作用域去寻找的,这种作用域之间的嵌套,被叫做 作用域链。
程序末端的作用域,被叫做 全局作用域,也就是最外层的作用域。
作用域链的单向访问
作用域链中,外层的作用域范围内不可访问内层作用域的数据,比如:
function foo() {
var a = 1;
}
foo();
console.log(a); // error
当我们打印变量 a
的时候,只会在当前作用域以及上层作用域寻找变量 a
,不会向某个函数内部寻找。
内存清理
函数执行后,一般情况下,都不会保存执行时产生的数据,每次重复执行函数,都是一次新的执行,比如:
function foo() {
var a = 0;
console.log(a);
a++;
}
foo(); // 0
foo(); // 0
函数并不能获取上一次声明了变量,每次执行结束后,内存都会被清理,并不会保留上一次运行产生的数据。
全局作用域的数据比较特殊,全局作用域的数据不会被清理,程序不会因为运行结束,就清理所有全局的数据或函数,除非关闭页面。
变量的隐式声明与作用域
之前说过,变量可以不声明直接做赋值操作,程序会自动隐式声明变量。这在全局作用域上,并没有任何不妥,但是如果在函数的作用域内,就会发生异样:
function foo() {
a = 1;
}
foo();
console.log(a);
上述代码会打印出 1
,变量被隐式声明到了全局作用域。
这是因为当在函数内部隐式声明一个变量的时候,程序会按照作用域内去寻找变量,如果找不到,向上层作用域寻找,如果一直到全局作用域都找不到,那么就把变量隐式声明到全局作用域上。
除非你非常清楚的知道,你在隐式声明一个全局变量,否则不要那么做,这样很可能增加程序的调试和维护难度。
函数没有隐式声明,所以不会出现函数隐式声明到到全局作用域上。
函数参数
实参与形参
函数的参数被区分为 实参 和 形参,实参是真实传入的实际参数,形参是帮助描述函数声明的形式参数。
例:
function foo(a, b) {
console.log(a);
console.log(b);
}
foo(1, 2);
上述代码中,a
和 b
是形参,函数内部都使用形参来编写逻辑,字面量 1
和 2
是实参,它是函数执行时实际运行的参数。
实参的值传递给形参的过程叫做 参数传递,它类似于赋值操作。上述代码中,实参 1
就赋值给了形参 a
,实参 2
也赋值给了形参 b
。
实参也不一定是字面量,可以把变量当做实参:
var c = 1,
d = 2;
foo(c, d);
形参的作用域是在函数内部,因为作用域的存在,实参和形参可以同名而不产生冲突:
function foo(a, b) {
console.log(a);
console.log(b);
}
var a = 1,
b = 2;
foo(a, b);
上述代码中,foo
声明外部的 a
和 b
标识符是全局变量,最后一行代码 foo(a, b)
中,a
和 b
被当做实参传入函数内部。
函数内部的标识符 a
和 b
是形参,它们的作用域在函数内部,所以函数打印参数 a
和 b
的时候,会打印参数,而不是全局变量。
如果很难理解,就修改外部变量名,防止混淆。
参数无限制
JS 中的函数参数的限制完全取决于开发者本身,一个函数的实参数量可以不等同于形参数量,比如:
function foo(a, b) {
console.log(a);
console.log(b);
}
foo();
foo(1);
foo(1, 2);
foo(1, 2, 3);
上述执行 foo
函数的方式都不会报错,形参的默认值是 undefined
,所以上述代码中,前两次执行时,没有传入的参数会打印出 undefined
。
多余传入的参数并不会被丢弃,可以通过其他关键字获得,如:
function foo(a, b) {
console.log(arguments);
}
foo(1, 2, 3);
其中,arguments
是一个特殊的关键字,它可以获取到整个参数列表的值,它是一个引用类型,在介绍完引用类型后,我会详细介绍它。
返回值
每个函数至多可以拥有一个返回值,它会作为执行函数的结果,返回值使用 return
关键字描述:
function foo() {
return 1;
}
var a = foo();
console.log(a);
上述代码中,foo
函数的返回值是数字 1
。
计算值 空值 不返回值
函数的返回值也可是一个计算值:
function foo(n) {
return n * 10;
}
console.log(foo(5)); // 50
还可以返回一个空值或者不返回值:
function foo() {
return;
}
function bar() {}
console.log(foo(), bar()); // undefined undefined
上述代码中,空值返回和不返回值,得到的结果都是 undefined
。
结束运行
在函数中,当执行到 return
关键字后,函数会结束 return
后的语句运行:
function foo() {
console.log("start");
return;
console.log("end");
}
foo(); // start
上述代码中,不会打印 end
字符串,函数会在执行完 return
语句后停止执行。