1 概述
1.1 声明
function print(s) {
console.log(s);
}
除了 function 命令外,还可以采用变量赋值的写法,声明一个匿名函数。这种写法又称函数表达式。
var print = function(s) {
console.log(s);
};
如果在 function 后加上函数名,那么该函数名只在函数体内有效。这种写法的用处有两个,一是在函数体内调用自身,二是在函数调用栈中显示名称。
var f = function f(s) {
console.log(s);
};
注意,函数表达式要在结尾加上分号,表示语句结束,而函数声明的大括号后面不用加。
第三种方式是构造函数。构造函数可以接收任意数量的参数,只有最后一个参数会被当作函数体。如果只有一个参数,该参数就是函数体。
var add = new Function(
'x',
'y',
'return x + y'
);
// 等同于
function add(x, y) {
return x + y;
}
Function 构造函数甚至可以不使用 new 命令,结果完全一样。总的来说,这种非常不直观的声明函数方式几乎无人使用。
1.2 第一等公民
由于函数与其他数据类型地位平等,所以又称函数为第一等公民。
1.3 函数名提升
JavaScript 引擎将函数名视同变量名,与变量一样,函数会被提升到代码头部。如果一个函数被多次声明,后者覆盖前者。
但是采用函数表达式定义函数时, JavaScript 就会报错。
f();
var f = function () {};
// TypeError: undefined is not a function
// 等同于
var f;
f();
f = function () {};
2 属性和方法
2.1 name
function f1() {}
f1.name // "f1"
var f2 = function () {};
f2.name // "f2"
var f3 = function myName() {};
f3.name // 'myName'
注意,真正的函数名还是 f3 ,而 myName 这个名字只在函数体内部可用。
name 常用于获取参数的函数名。
var myFunc = function () {};
function test(f) {
console.log(f.name);
}
test(myFunc) // myFunc
2.2 length
无论调用时输入了多少参数,它只返回函数定义时的参数个数。
2.3 toString()
返回函数源码,包括注释。利用这一点,可以变相实现多行字符串。
var multiline = function (fn) {
var arr = fn.toString().split('\n');
return arr.slice(1, arr.length - 1).join('\n');
};
function f() {/*
这是一个
多行注释
*/}
multiline(f);
// " 这是一个
// 多行注释"
3 作用域
作用域 scope 指的是变量的有效范围。在 ES5 的规范中, JavaScript 只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。 ES6 新增了区块作用域,本教程不涉及。
函数内部定义的局部变量会在它的作用域内覆盖同名全局变量。
var 命令声明的变量都会被提升到函数体的头部。
函数本身也是一个值,也有自己的作用域。函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。
var x = function () {
console.log(a);
};
function y(f) {
var a = 2;
f();
}
y(x)
// ReferenceError: a is not defined
4 参数
4.1 省略
运行时无论提供多少个参数都不会报错,省略的参数值为 undefined 。不能省略靠前的参数,如果一定要省略,必须显式地传入 undefined 。
再次提醒,函数的 length 属性与实际传入的参数无关个数,只反映预期传入的参数个数。
4.2 传递
原始类型参数的传递方式是值传递。复合类型参数的传递方式是地址传递,也就是说,在函数内修改参数将影响原值。注意,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值。
var obj = [1, 2, 3];
function f(o) {
o = [2, 3, 4];
}
f(obj);
obj // [1, 2, 3]
4.3 同名
函数定义时有同名参数时取后者。即使后者没有值或被省略,也以其为准。
function f(a, a) {
console.log(a);
}
f(1) // undefined
4.4 arguments 对象
arguments 对象包含了函数运行时的所有参数, arguments[0]
就是第一个参数, arguments[1]
就是第二个参数,以此类推。该对象只能在函数体内部使用。通过 arguments 对象的 length 属性,可以判断函数调用时到底传入了几个参数。
正常模式下, 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 很像数组,但它是一个对象,不能直接使用数组的方法!
arguments 对象还有一个 callee 属性,返回原函数,该属性在严格模式下禁用。
5 闭包
正常情况下,函数外部无法读取函数内部声明的变量。
function f1() {
var n = 999;
}
console.log(n) // Uncaught ReferenceError: n is not defined(
于是在函数内部再定义一个函数,并将它作为返回值。因为返回值是一个函数,它可以运行!
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
函数 f2 就是闭包,即能读取其他函数内部变量的函数,变量始终存活在内存中。
6 立即调用的函数表达
原理略。
// 写法一
var tmp = newData;
processData(tmp);
storeData(tmp);
// 写法二
(function () {
var tmp = newData;
processData(tmp);
storeData(tmp);
}());
写法二比写法一更好,避免了对全局变量的污染。
7 eval
eval 命令将字符串参数当作语句执行。
eval('var a = 1;');
a // 1
eval 没有自己的作用域,可能会修改当前作用域下变量的值。
var a = 1;
eval('a = 2');
a // 2
严格模式虽然可以控制 eval 内声明的变量,但对于在 eval 内修改的变量依旧无能为力。更麻烦的是,别名调用五花八门,难以确认。总之,不推荐使用。