函数表达式
var print = function (s) {
console.log(s)
}
这个写法是将一个匿名函数赋值给一个变量,这时这个匿名函数又称为函数表达式,因为赋值语句等号的右侧只能放表达式。
递归
函数调用自身的这种行为就叫递归,例如:
function fib (num) {
if (num === 0) return 0
if (num === 1) return 1
return fib (num - 1) + fib (num - 2)
}
fib (6) // 8
第一等公民
JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。
由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民。
函数名的提升
JavaScript 引擎将函数名视同变量名,所以采用function
命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。所以,下面的代码不会报错。
f();
function f() {}复制代码
严格模式下会报错
不能在条件语句中声明函数
根据 ES5 的规范,不得在非函数的代码块中声明函数,最常见的情况就是if
和try
语句。
if (foo) {
function x() {}
}
try {
function x() {}
} catch(e) {
console.log(e);
}复制代码
上面代码分别在if
代码块和try
代码块中声明了两个函数,按照语言规范,这是不合法的。但是,实际情况是各家浏览器往往并不报错,能够运行。
但是由于存在函数名的提升,所以在条件语句中声明函数,可能是无效的,这是非常容易出错的地方。
函数的属性和方法
1.name 属性
函数的name
属性返回函数的名字。
function f1() {}
f1.name // "f1"复制代码
2.length 属性
函数的length
属性返回函数预期传入的参数个数,即函数定义之中的参数个数。
function f(a, b) {}
f.length // 2复制代码
上面代码定义了空函数f
,它的length
属性就是定义时的参数个数。不管调用时输入了多少个参数,length
属性始终等于2。
3.toString()
函数的toString
方法返回一个字符串,内容是函数的源码。
函数内部的变量提升
与全局作用域一样,函数作用域内部也会产生“变量提升”现象。var
命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。
function foo(x) {
if (x > 100) {
var tmp = x - 100;
}
}
// 等同于
function foo(x) {
var tmp;
if (x > 100) {
tmp = x - 100;
};
}复制代码
同名参数
如果有同名的参数,则取最后出现的那个值。
function f(a, a) {
console.log(a);
}
f(1) // undefined复制代码
上面代码中,函数f
有两个参数,且参数名都是a
。取值的时候,以后面的a
为准,即使后面的a
没有值或被省略,也是以其为准。
调用函数f
的时候,没有提供第二个参数,a
的取值就变成了undefined
。这时,如果要获得第一个a
的值,可以使用arguments
对象。
function f(a, a) {
console.log(arguments[0]);
}
f(1) // 1复制代码
arguments 对象
(1)定义
由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是arguments
对象的由来。
arguments
对象包含了函数运行时的所有参数,arguments[0]
就是第一个参数,arguments[1]
就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用。
var f = function (one) {
console.log(arguments[0]);
console.log(arguments[1]);
console.log(arguments[2]);
}
f(1, 2, 3)
// 1
// 2
// 3复制代码
正常模式下,arguments
对象可以在运行时修改。
var f = function(a, b) {
arguments[0] = 3;
arguments[1] = 2;
return a + b;
}
f(1, 1) // 5复制代码
上面代码中,函数f
调用时传入的参数,在函数内部被修改成3
和2
。
严格模式下,arguments
对象是一个只读对象,修改它是无效的,但不会报错。
var f = function(a, b) {
'use strict'; // 开启严格模式
arguments[0] = 3; // 无效
arguments[1] = 2; // 无效
return a + b;
}
f(1, 1) // 2复制代码
上面代码中,函数体内是严格模式,这时修改arguments
对象就是无效的。
通过arguments
对象的length
属性,可以判断函数调用时到底带几个参数。
function f() {
return arguments.length;
}
f(1, 2, 3) // 3
f(1) // 1
f() // 0复制代码
(2)与数组的关系
arguments
是一个对应于传递给函数的参数的类数组对象。
arguments
对象不是一个 Array
。它类似于Array
,但除了length属性和索引元素之外没有任何Array
属性。例如,它没有 pop 方法。但是它可以被转换为一个真正的Array。
下面是两种常用的转换方法:slice
方法和逐一填入新数组。
var args = Array.prototype.slice.call(arguments);
// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}复制代码
(3)callee 属性
arguments
对象带有一个callee
属性,返回它所对应的原函数。
var f = function () {
console.log(arguments.callee === f);
}
f() // true复制代码
可以通过arguments.callee
,达到调用函数自身的目的。这个属性在严格模式里面是禁用的,因此不建议使用。
函数的其他知识点
闭包
由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。
function createIncrementor(start) {
return function () {
return start++;
};
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7复制代码
上面代码中,start
是函数createIncrementor
的内部变量。通过闭包,start
的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc
使得函数createIncrementor
的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。
为什么会这样呢?原因就在于inc
始终在内存中,而inc
的存在依赖于createIncrementor
,因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。
闭包的另一个用处,是封装对象的私有属性和私有方法。
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复制代码
上面代码中,函数Person
的内部变量_age
,通过闭包getAge
和setAge
,变成了返回对象p1
的私有变量。
注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。
立即调用的函数表达式
在 Javascript 中,圆括号()
是一种运算符,跟在函数名之后,表示调用该函数。比如,print()
就表示调用print
函数。
有时,我们需要在定义函数之后,立即调用该函数。function
这个关键字即可以当作语句,也可以当作表达式。为了避免解析上的歧义,JavaScript 引擎规定,如果function
关键字出现在行首,一律解释成语句。
解决方法就是不要让function
出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面。
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();复制代码
上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就避免了错误。这就叫做“立即调用的函数表达式”,简称 IIFE。
注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,可能就会报错。
eval 命令
eval
命令的作用是,将字符串当作语句执行。
eval('var a = 1;');
a // 1复制代码
上面代码将字符串当作语句运行,生成了变量a
。
eval
没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题。
var a = 1;
eval('a = 2');
a // 2复制代码
上面代码中,eval
命令修改了外部变量a
的值。由于这个原因,eval
有安全风险。
eval
的命令字符串不会得到 JavaScript 引擎的优化,运行速度较慢。这也是一个不应该使用它的理由。