1 基本概念及语法
简单地说,函数就是一段可以反复调用的代码块。
1.1 函数的声明
函数的声明(Function Declaration)有3种方法:
-
function
命令:function
命令声明的代码区块就是一个函数。写法如下:function 函数名(参数...){ 函数体 }
-
函数表达式:采用的是变量赋值的写法,声明的是一个匿名函数,
function
命令后不带函数名,且大括号末尾需加上分号(注意:上面第一种方法声明的函数末尾则不用加分号)。由于等号右侧只能放表达式,因此匿名函数又被称为函数表达式。
var 变量=function (参数...){ 函数体 };
若采用函数表达式声明函数时加上了函数名,则函数名仅在函数体内部有效,并且函数名指代函数表达式本身。而函数名在函数体外部是无效的。
-
Function
构造函数:如下例子,若有多个参数,则最后一个参数为函数体,前面的为参数。若仅有一个参数,则该参数就是函数体。var add=new Function( 'x', 'y', 'return x+y' ); //等同于 function add(x,y){ return x+y; }
总结:前两种声明方式经常使用,第三种声明方式几乎无人使用。
注:若同一个函数被多次声明,后面的声明会覆盖前面的声明。
1.2 函数的调用及返回值
-
调用函数要使用圆括号运算符,圆括号中还可以加入函数的参数。调用函数的写法为:
函数名(参数...);
,例如调用加法运算函数add(1,2);
-
函数体内部的
return
语句,表示返回,return
语句后面的表达式或值就是函数的返回值。 -
return
语句不是必须的,若没有则表示该函数不返回任何值,或者返回undefined
。 -
若
return
语句后面还有语句,则不会得到执行。
1.3 递归
函数可以调用自身,这就是递归(recursion)。
1.4 函数的本质
- JavaScript将函数看作一种值,和数值、字符串、布尔值一样。
- 除了可以将函数赋值给变量或对象的属性,还可以将其当作参数传入其他函数,或者作为函数的结果返回。
1.5 函数名的提升
在JavaScript基础语法-学习笔记_lovewhoilove-CSDN博客一文中提到过变量提升,而采用function
命令声明函数时,函数就跟变量声明一样,被提升到代码的头部,这便是函数名的提升。因此,函数的声明可以写在调用函数语句的后面。
若采用函数表达式(即匿名函数)的声明方式则不会产生函数名提升的效果,此时若在匿名函数声明前面调用函数则会报错。
f();
var f=function(){};
//上面的代码实际执行效果如下所示,f只是被声明而未被赋值
var f;
f();
f=function(){};
再来看一个特例:
var f = function () {
console.log('1');
}
function f() {
console.log('2');
}
f()//输出结果为1
上面实际执行的代码为:
function f(){
console.log('2');
}
var f;
f=function(){
console.log('1');
};
f();
函数表达式声明的变量f
首先被提升,然后function
命令声明的函数又进行函数提升,后提升的反而到了第一行而被先执行,然后执行的的反而是后声明的匿名函数,又由于为同名函数,后声明的函数会覆盖前面声明的函数,因此,最终执行的的反而是后声明的匿名函数。
2 函数的属性和方法
2.1 name属性
函数的name
属性返回函数的名字。
2.2 length属性
函数的length
属性返回函数预期传入的参数个数,即函数定义之中的参数个数。
拓展知识:
length
属性提供了一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的“方法重载”(overload)。
2.3 toString()方法
函数的toString()
方法返回一个字符串,内容是函数的源码,包含换行符在内。
注:针对JavaScript 引擎提供的原生函数,
toString()
方法会返回原生代码的提示。
3 作用域
作用域(scope)指的是变量存在的范围。
3.1 作用域与变量
- 在ES5规范中,JavaScript有两种作用域:
-
全局作用域:变量在整个程序中一直存在,所有地方都可以读取;
-
函数作用域:变量只在函数内部存在。
-
注:在ES6中,新增了块级作用域。
-
根据作用域,可将其内的变量分为:全局变量与局部变量:
- 对于顶层函数来说,函数外部声明的变量就是全局变量(global variable),它可以在函数内部读取。
- 而在函数内部定义的变量,函数外部是无法读取的,称为局部变量(local variable)。
-
函数内部定义的变量,会在该作用域内覆盖同名全局变量。
测试如下:变量
v
同时在函数的外部和内部有定义,此时局部变量v
覆盖了全局变量v
。var v=1; function f(){ var v=2; console.log(v); } f() //输出结果:2 v //输出结果:1
-
对于var命令来说,局部变量只能在函数内部声明,在其他区块中的声明,一律是全局变量。
测试如下:变量
x
在条件判断区块之中声明,结果表明它就是一个全局变量,可以在区块之外读取。if(true){ var x=5; } console.log(x); //输出结果:5
3.2 函数内部的变量提升
函数作用域内部也会产生“变量提升”现象。var
命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。
function f(x) {
if (x > 100) {
var tmp = x - 100;
}
}
上面的代码等同于:
function f(x) {
var tmp;
if (x > 100) {
tmp = x - 100;
};
}
3.3 函数自身的作用域
由于函数自身也是一个值,因而它的作用域与变量是一样的,就是与其声明时所在的作用域有关,但注意与其运行时所在的作用域无关。
而在函数体内声明的函数,作用域会绑定在函数体内部。
测试如下:在函数foo
内部声明了一个函数bar
,此时bar
的作用域就绑定在foo
函数体内部;当在foo
外部取出bar
函数执行时,变量x
指向的是foo
内部的x
,而不是foo
外部的x
。也正是这种机制才构成了所谓的闭包现象。
function foo() {
var x = 1;
function bar() {
console.log(x);
}
return bar;
}
var x = 2;
var f = foo();
f() // 1
4 闭包
4.1 闭包的定义
简单地说,闭包(closure)就是定义在一个函数内部的函数。
举个例子:我们为了能在f1
函数外部读取到其内部的变量n
,那么我们在函数f1
内部定义一个函数f2
,那么只要将f2
作为返回值,这样就可以了。
闭包就是f2
,它是能读取f1
函数内部变量的函数。因此本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 输出结果为:999
4.2 闭包的作用
-
通过上一节的例子可以知道:闭包的作用是可以读取外层函数内部的变量;
-
而另一个作用是:闭包可以让外层函数的变量始终保持在内存中,即闭包可以使得它诞生的环境一直存在。
来看一个具体实例就明白了:即通过闭包,
n
的状态被保留了。具体来说就是:闭包f2
用到了外层变量n
,导致外层函数f1
不能从内存中释放,则f1
的内部变量n
就始终保存着当前值,供闭包读取,每一次调用都是在上一次调用的基础上进行计算。function f1() { var n = 999; return function f2() { return n++; } } var result = f1(); result() // 999 result() // 1000 result() // 1001
-
我们可以利用闭包来封装对象的私有属性和私有方法。
如下代码所示:函数
Person
的内部变量_age
,通过闭包getAge
和setAge
,变成了返回对象p1
的私有变量。function Person(name) { var _age; function setAge(n) { _age = n; } function getAge() { return _age; } return { name:name, getAge:getAge, setAge:setAge }; } var p1=Person('张三'); p1.setAge(25); p1.getAge() //25
但由于外层函数的每次执行都会生成一个新的闭包,进而保留外层函数的的内部变量,这使得内存消耗很大,因此不能滥用闭包。
4.3 立即调用的函数表达式
我们将圆括号()
跟在函数名之后,表示调用该函数。但在利用function
命令定义函数后立即调用,则会产生语法错误。当用函数表达式创建函数时,则可以在定义后直接加圆括号调用:
var f = function f(){ return 1}();
f // 1
或者让解释器以表达式来处理函数定义的方法:
var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();
!function () { /* code */ }();
~function () { /* code */ }();//~为位运算符,表示反转所有位
-function () { /* code */ }();
+function () { /* code */ }();
但最简单的处理方法是:将函数放在圆括号里面:
(function(){ /* code */ });
//或者写为
(function(){ /* code */ })();
上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表达式,而不是函数定义语句,所以就避免了错误。这就叫做立即调用的函数表达式(Immediately-Invoked Function Expression),简称 IIFE。
使用立即调用立即执行的函数表达式目的一般有两个:
- 不必为函数命名,避免了污染全局变量;
- IIFE内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。
举例如下:写法2要更好,它完全避免了污染全局变量。
//写法1
var tmp=newData;
processData(tmp);
storeData(tmp);
//写法2
(function(){
var tmp=newData;
processData(tmp);
storeData(tmp);
}());
5 参数
像function square(x) {return x * x;}
中,x
就是square
函数的参数,每次调用时,都需要提供这个值。
5.1 省略参数
若参数不是必须的,则允许省略:如下参数a时必须的,则加上第3个参数和省略第2个参数都是可以的,但都省略则会出错。
function f(a, b) {
return a;
}
f(1, 2, 3) // 1
f(1) // 1
f() // undefined
f(,1) //省略第一个参数会报错
f.length // 2
需要注意的是:函数的length
属性与实际传入的参数个数无关,只反映函数预期传入的参数个数。
5.2 参数传递方式
参数传递方式分为传值传递(passes by value)和传址传递(pass by reference)两种。
-
若函数参数为原始类型的值(如数值、字符串、布尔值),传递方式是传值传递;
var p = 2; function f(p) { p = 3; } f(p); p // 2
如上代码所示:变量
p
是一个原始类型的值,传入函数f
的方式是传值传递。因此,在函数内部,p
的值是原始值的拷贝,无论怎么修改,都不会影响到原始值。 -
若函数参数为符合类型的值(如数组、对象、其他函数),传递方式则是传址传递。也就是说,传入函数的原始值的地址,若在函数内部修改对象的属性,将会影响到原始值。
-
倘若在函数内部修改的不是对象的某个属性,而是替换掉整个参数对象,则只是将对象指向新的地址,保存在原地址上的值自然不受影响。
测试如下:
var obj = [1, 2, 3]; function f(o) { o = [2, 3, 4]; } f(obj); obj // [1, 2, 3]
5.3 同名参数
如果有同名的参数,则取最后出现的那个值。测试如下:
function f(a, a) {
console.log(a);
}
f(1, 2) // 2
若要获得第一个a
的值,则可以借助arguments
对象:
function f(a, a) {
console.log(arguments[0]);
}
f(1,2) // 1
5.4 arguments对象
由于JavaScript允许函数有不定数目的参数,因此需要有一种机制,可以在函数体内部读取所有参数,这时就需要arguments
对象了。
arguments
对象包含了函数运行时的所有参数,像arguments[0]
就是第一个参数,arguments[1]
就是第二个参数,以此类推,并且arguments
对象仅在函数体内部可以使用。
5.4.1 arguments对象的使用模式
arguments
对象的使用分两种模式:
-
正常模式下:
arguments
对象可以在运行时修改参数:var f = function(a, b) { arguments[0] = 3; arguments[1] = 2; return a + b; } f(1, 1) // 5
-
严格模式下:修改
arguments
对象不会影响到实际的函数参数:var f = function(a, b) { 'use strict'; // 开启严格模式 arguments[0] = 3; arguments[1] = 2; return a + b; } f(1, 1) // 2
通过
arguments
对象的length
属性,可以判断函数调用时带的参数个数。function f() { return arguments.length; } f(1, 2, 3) // 3 f(1) // 1 f() // 0
这里再顺便对比下函数的
length
属性:函数的length
属性与实际传入的参数个数无关,只反映函数预期传入的参数个数。
5.4.2 将arguments对象转为数组
虽然arguments很像数组,但其实并非数组,要想让其使用数组的一些专有方法,则需要将其先转换为数组。以下为2种常用的转换方法:
-
使用
slice
方法:var args=Array.prototype.slice.call(arguments);
-
逐一填入新数组:
var args=[]; for (var i=0;i<arguments.length;i++){ args.push(arguments[i]); }