JavaScript
中,函数是头等 ( first-class
)对象,因为它们可以像任何其他对象一样具有属性和方法。它们与其他对象的区别在于函数可以被调用。简而言之,它们是 Function
对象。
一、函数概览
在 JavaScript
中,每个函数其实都是一个Function
对象。
return
语句,则它默认返回undefined
。return
语句来指定一个要返回的值。(使用new
关键字调用一个构造函数除外)。调用函数时,传递给函数的值被称为函数的实参(值传递),对应位置的函数参数名叫作形参。
this
关键字
this
关键字并不会指向正在运行的函数本身,而是指向调用该函数的对象。arguments.callee
属性 (严格模式下不可用),如果该函数是一个匿名函数,则你只能使用后者。 /* 定义函数 myFunc */
function myFunc(theObject)
{
//实参 mycar 和形参 theObject 指向同一个对象。
theObject.brand = "Toyota";
}
/*
* 定义变量 mycar;
* 创建并初始化一个对象;
* 将对象的引用赋值给变量 mycar
*/
var mycar = {
brand: "Honda",
model: "Accord",
year: 1998
};
/* 弹出 'Honda' */
window.alert(mycar.brand);
/* 将对象引用传给函数 */
myFunc(mycar);
/*
* 弹出 'Toyota',对象的属性已被修改。
*/
console.log(mycar.brand);
二、函数定义
定义函数有多种方法:
语法:function name([param[, param[, ... param]]]) { statements }
test(); //正常执行,弹出1
function test() {
alert(1)
}
函数表达式和函数声明非常相似,它们甚至有相同的语法。一个函数表达式可能是一个更大的表达式的一部分。可以定义函数“名字”(例如可以在调用堆栈时使用)或者使用“匿名”函数。函数表达式不会提升,所以不能在定义之前调用。
语法:
var myFunction = function name([param[, param[, ... param]]]) { statements }
test2() //不能正常执行,报错
var test2 = function() {
alert(2)
}
当函数只使用一次时,通常使用IIFE (Immediately Invokable Function Expressions)
。
(function() {
statements
})();
IIFE
是在函数声明后立即调用的函数表达式。
函数声明有一种特殊的语法:
function* name([param[, param[, ...param]]]) { statements }
function*
这种声明方式 (function
关键字后跟一个星号)会定义一个生成器函数 (generator function
),它返回一个 Generator
对象。
function* generator(i) {
yield i;
yield i + 10;
}
const gen = generator(10);
console.log(gen.next().value);
// Expected output: 10
console.log(gen.next().value);
// Expected output: 20
生成器函数在执行时能暂停,后面又能从暂停处继续执行。
iterator
)对象。next()
方法被首次(后续)调用时,其内的语句会执行到第一个(后续)出现yield
的位置为止, yield
后紧跟迭代器要返回的值。yield*
(多了个星号),则表示将执行权移交给另一个生成器函数(当前生成器暂停执行)。next()
方法返回一个对象,这个对象包含两个属性:value
和 done
,value
属性表示本次 yield
表达式的返回值,done
属性为布尔类型,表示生成器后续是否还有 yield
语句,即生成器函数是否已经执行完毕并返回。yield
关键字使生成器函数执行暂停,yield 关键字后面的表达式的值返回给生成器的调用者。它可以被认为是一个基于生成器的版本的 return
关键字。语法:function* [name]([param] [, param] [..., param]) { statements }
name:函数名。在声明匿名函数时可以省略。函数名称只是函数体中的一个本地变量。
paramN:传入函数的一个参数名。一个函数最多有 255 个参数。
statements:函数体。
function*
表达式和function*
声明比较相似,并具有几乎相同的语法。function*
表达式和function*
声明之间主要区别就是函数名,即在创建匿名函数时,function*表达式可以省略函数名。
var x = function*(y) {
yield y * y;
};
语法:([param] [, param]) => { statements } param => expression
param:参数名称。零参数需要用 () 表示。只有一个参数时不需要括号。(例如 foo => 1
)
statements or expression
:多个声明 statements
需要用大括号括起来,而单个表达式时则不需要。表达式 expression
也是该函数的隐式返回值。
不推荐使用 Function
构造函数创建函数,因为它需要的函数体作为字符串可能会阻止一些 JS
引擎优化,也会引起其他问题。
语法:new Function (arg1, arg2, ... argN, functionBody)
arg1, arg2, … argN
函数使用零个或多个名称作为正式的参数名称。每一个必须是一个符合有效的 JavaScript 标识符规则的字符串或用逗号分隔的字符串列表,例如“x”,“theValue”或“a,b”。
functionBody
一个构成的函数定义的,包含 JavaScript 声明语句的字符串。
把 Function
的构造函数当作函数一样调用 (不使用 new
操作符) 的效果与作为 Function
的构造函数调用一样。
在 JavaScript
中,生成器函数实际上都是 GeneratorFunction
对象。注意,GeneratorFunction
并不是一个全局对象,但你可以通过下面的代码创建 GeneratorFunction()
构造函数。
const GeneratorFunction = function* () {}.constructor;
GeneratorFunction constructor
) 创建函数,因为它需要的函数体作为字符串可能会阻止一些 JS
引擎优化,也会引起其他问题。语法:new GeneratorFunction (arg1, arg2, ... argN, functionBody)
arg1, arg2, … argN
函数使用零个或多个名称作为正式的参数名称。每一个必须是一个符合有效的 JavaScript 标识符规则的字符串或用逗号分隔的字符串列表,例如“x”,“theValue”或“a,b”。
functionBody
一个构成的函数定义的,包含 JavaScript 声明语句的字符串。
三、函数参数
如果没有值或传入了未定义的值,默认函数参数允许形式参数使用默认值初始化。
function multiply(a, b = 1) {
return a * b;
}
console.log(multiply(5, 2));
// Expected output: 10
console.log(multiply(5));
// Expected output: 5
剩余参数语法允许将数量不限的参数描述成一个数组。
function sum(...theArgs) {
let total = 0;
for (const arg of theArgs) {
total += arg;
}
return total;
}
console.log(sum(1, 2, 3));
// Expected output: 6
console.log(sum(1, 2, 3, 4));
// Expected output: 10
四、arguments对象
arguments
对象是所有(非箭头)函数中都可用的局部变量(一个包含了传递给当前执行函数参数的类似于数组的对象)。你可以使用arguments
对象在函数中引用函数的参数。此对象包含传递给函数的每个参数,第一个参数在索引 0 处。
五、方法函数定义
你可以在支持添加新属性的任何标准的内置对象或用户定义的对象内定义 getter
(访问方法) 和 setter
(设置方法)。使用对象字面量语法定义 getters
和 setters
方法。
const obj = {
log: ['a', 'b', 'c'],
get latest() {
return this.log[this.log.length - 1];
}
};
console.log(obj.latest);
// Expected output: "c"
const language = {
set current(name) {
this.log.push(name);
},
log: []
};
language.current = 'EN';
language.current = 'FA';
console.log(language.log);
// Expected output: Array ["EN", "FA"]
从 ECMAScript 6
开始,你可以用更短的语法定义自己的方法,类似于 getters
和 setters
var obj = {
foo() {},
bar() {}
};
六、构造函数 vs 函数声明 vs 函数表达式
对比下面的例子:
Function
构造函数定义的函数,被赋值给变量 multiply
:var multiply = new Function('x', 'y', 'return x * y');
multiply
的函数声明:function multiply(x, y) {
return x * y;
} // 没有分号
multiply
: var multiply = function(x, y) {
return x * y;
};
func_named
的函数的函数表达式,被赋值给变量multiply:var multiply = function func_name(x, y) {
return x * y;
};
虽然有一些细微的差别,但所起的作用都差不多:
var y = function x() {};
alert(x); // throws an error
当函数是通过 Function’s toString()
方法被序列化时,函数名同样也会出现。
另一方面,被函数赋值的变量仅仅受限于它的作用域,该作用域确保包含着该函数被声明时的作用域。正如第四个例子所展示的那样,函数名与被函数赋值的变量是不相同的。彼此之间没有关系。函数声明同时也创建了一个和函数名相同的变量。因此,与函数表达式定义不同,以函数声明定义的函数能够在它们被定义的作用域内通过函数名而被访问到:
new Function
定义的函数没有函数名。 然而,在 SpiderMonkey JavaScript
引擎中,其函数的序列化形式表现的好像它拥有一个名叫"anonymous"
的名称一样。比如,使用 alert(new Function())
输出:function anonymous() {
}
anonymous
不是一个可以在函数内被访问到的变量。例如,下面的例子将会导致错误: var foo = new Function("alert(anonymous);");
foo(); // Uncaught ReferenceError: anoymous is not defined
Function
构造函数定义的函数不同,函数声明定义的函数可以在它被声明之前使用。举个例子:foo(); // alerts FOO!
function foo() {
alert('FOO!');
}
函数表达式定义的函数继承了当前的作用域。换言之,函数构成了闭包。另一方面,Function
构造函数定义的函数不继承任何全局作用域以外的作用域 (那些所有函数都继承的)。
通过函数表达式定义的函数和通过函数声明定义的函数只会被解析一次,而 Function
构造函数定义的函数却不同。也就是说,每次构造函数被调用,传递给 Function 构造函数的函数体字符串都要被解析一次。虽然函数表达式每次都创建了一个闭包,但函数体不会被重复解析,因此函数表达式仍然要快于"new Function(...)"
。所以 Function
构造函数应尽可能地避免使用。
有一点应该要注意的,在通过解析 Function
构造函数字符串产生的函数里,内嵌的函数表达式和函数声明不会被重复解析。例如:
var foo = (new Function("var bar = \'FOO!\';\nreturn(function() {\n\talert(bar);\n});"))();
foo(); // 函数体字符串"function() {\n\talert(bar);\n}"的这一部分不会被重复解析。
// 函数声明
function foo() {}
// 函数表达式
(function bar() {})
// 函数表达式
x = function hello() {}
if (x) {
// 函数表达式
function world() {}
}
// 函数声明
function a() {
// 函数声明
function b() {}
if (0) {
//函数表达式
function c() {}
}
}
七、块级函数
从 ECMAScript 6
开始,在严格模式下,块里的函数作用域为这个块。ECMAScript 6
之前不建议块级函数在严格模式下使用。
'use strict';
function f() {
return 1;
}
{
function f() {
return 2;
}
}
f() === 1; // true
// f() === 2 in non-strict mode
一句话:不要用。
在非严格模式下,块中的函数声明表现奇怪。例如:
if (shouldDefineZero) {
function zero() { // DANGER: 兼容性风险
console.log("This is zero.");
}
}
ECMAScript 6
中,如果shouldDefineZero
是 false
,则永远不会定义 zero
,因为这个块从不执行。然而,这是标准的新的一部分。这是历史遗留问题,无论这个块是否执行,一些浏览器会定义 zero
。
在严格模式 下,所有支持 ECMAScript 6
的浏览器以相同的方式处理:只有在 shouldDefineZero
为 true
的情况下定义 zero
,并且作用域只是这个块内。
有条件地定义一个函数的一个更安全的方法是把函数表达式赋给一个变量:
var zero;
if (0) {
zero = function() {
console.log("This is zero.");
};
}
八、示例
下面的函数返回一个字符串,其中包含了一个格式化的、以一个由 0 开头并填充的数字。
// 这个函数返回一个由 0 开头并填充的字符串
function padZeros(num, totalLen) {
var numStr = num.toString(); // 用字符串返回值进行初始化
var numZeros = totalLen - numStr.length; // 计算 zeros 顺序
for (var i = 1; i <= numZeros; i++) {
numStr = "0" + numStr;
}
return numStr;
}
//执行
var result;
result = padZeros(42,4); // returns "0042"
result = padZeros(42,2); // returns "42"
result = padZeros(5,4); // returns "0005"
你可以通过 typeof
操作符检测一个函数是否存在。在下面的例子中,用一个测试来演示检测 window
对象是否拥有一个 noFunc
函数的属性。如果存在,那就使用它;否则就采取其他的一些操作。(注意在 if 语句中,使用了 noFunc
的引用 – 在函数名的后面没有括号“()”
,所以实际函数并没有被调用。)
if ('function' === typeof window.noFunc) {
// use noFunc()
} else {
// do something else
}