前言:JavaScript 中的函数是什么
一般来说,一个函数是可以通过外部代码调用的一个“子程序”。像程序本身一样,一个函数由称为函数体的一系列语句组成。值可以传递给一个函数,函数将返回一个值。
但在 JavaScript 中比较特殊的是,函数实际上也是一个对象。每个函数都是Function
类型的实例,所以和其他对象一样具有属性和方法,区别在于函数可以被调用。
一、函数定义
定义函数有多种方法,主要分为函数声明和函数表达式。
1.1 函数声明
function Identifier ( FormalParameterList ) { FunctionBody }
Identifier
:函数标识符(函数名)FormalParameterList
:参数列表FunctionBody
:函数体
1.2 函数表达式
(1)具名函数表达式
const myFunction = function Identifier ( FormalParameterList ) { FunctionBody }
(2)匿名函数表达式
const myFunction = function ( FormalParameterList ) { FunctionBody }
1.3 Function 构造函数
new Function (arg1, arg2, ... argN, functionBody)
arg1, arg2, ... argN
:函数使用零个或多个名称作为正式的参数名称。每一个必须是一个符合有效的JavaScript标识符规则的字符串或用逗号分隔的字符串列表functionBody
:一个构成的函数定义的,包含JavaScript声明语句的字符串。
注意:MDN官方不推荐使用
Function
构造函数创建函数,因为可能会影响JavaScript引擎优化。
1.4 箭头函数表达式
([param] [, param]) => { statements }
param => expression
param
:参数名称. 零参数需要用()表示. 只有一个参数时不需要括号. (例如foo => 1
)statements or expression
:多个声明statements需要用大括号括起来,而单个表达式时则不需要。表达式expression也是该函数的隐式返回值。
有关箭头函数后文详细介绍。
函数声明、函数表达式、Function构造函数的区别
假设我们要定义一个函数multiply
计算两个数的乘积,那么通过不同的方法定义如下:
// 函数声明
function multiplyDeclaration(x, y) {
return x * y;
} // 注意:函数声明结尾一般没有分号
// 匿名函数表达式
const multiplyAnonymousExpression = function (x, y) {
return x * y;
};
// 具名函数表达式
const multiplyNamedExpression = function funcName(x, y) {
return x * y;
};
// Function 构造函数
const multiplyFunctionConstructor = new Function('x', 'y', 'return x * y');
(1)函数声明提升
JavaScript引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行时,才会在执行上下文中生成函数定义。
console.log(multiplyDeclaration(3, 7)); // 21
console.log(multiplyAnonymousExpression(3, 7)); // ReferenceError: Cannot access 'multiplyAnonymousExpression' before initialization
// 函数声明
function multiplyDeclaration(x, y) {
return x * y;
} // 注意:函数声明结尾一般没有分号
// 匿名函数表达式
const multiplyAnonymousExpression = function (x, y) {
return x * y;
};
可以看出,在函数声明之前调用该函数并不会出错,但是函数表达式会出现ReferenceError。
函数声明和函数表达式最大的区别在于:函数声明会提升
(2)性能问题
通过函数表达式定义的函数和通过函数声明定义的函数只会被解析一次,而Function构造函数定义的函数却不同。每次构造函数被调用,传递给Function构造函数的函数体字符串都要被解析一次 。 所以Function构造函数应尽可能地避免使用。
二、函数名称
函数名是指向函数的指针,可以通过 function.name
属性访问函数实例的名称。若函数没有名称,则会返回一个空字符串。 函数名称是不可写、不可迭代、可配置的属性,即{writable: false, enumerable: false, configurable: true}
(1)函数声明的名称
name
属性返回一个函数声明的名称。
function foo() {
}
console.log(foo.name);
(2)通过new Function()
创建的函数
使用new Function(...)
语法创建的函数,其名称为“anonymous”。
(new Function).name; // "anonymous"
(3)匿名函数表达式
可以从句法位置推断匿名函数的名称
const foo = function(){}
const obj = {
bar: function() {
},
baz() {
}
}
console.log(foo.name); // foo
console.log(obj.bar.name) // bar
console.log(obj.baz.name) // baz
console.log((function() {}).name) // ''
注意:箭头函数的名称相同
(4)具名函数表达式
const foo = function funcName(){}
console.log(foo.name); // funcName
(5)getter 和 setter 函数
当通过 get
和 set
访问器来存取属性时, “get” 或 “set” 会出现在函数名称前。
var o = {
get foo(){},
set foo(x){}
};
var descriptor = Object.getOwnPropertyDescriptor(o, "foo");
descriptor.get.name; // "get foo"
descriptor.set.name; // "set foo";
(6)绑定函数的函数名
Function.bind()
所创建的函数将会在函数的名称前加上"bound "
function foo() {};
foo.bind({}).name; // "bound foo"
(7)Symbol作为函数名称
如果Symbol
被用于函数名称,并且这个symbol具有相应的描述符,那么方法的名字就是方括号中的描述符。
var sym1 = Symbol("foo");
var sym2 = Symbol();
var o = {
[sym1]: function(){},
[sym2]: function(){}
};
o[sym1].name; // "[foo]"
o[sym2].name; // ""
有关函数名需要注意:
- 函数名和函数的变量存在着差别。函数名不能被改变,但函数的变量却能够被再分配。函数名只能在函数体内使用。倘若在函数体外使用函数名将会导致错误
const foo = function bar() { }
bar(2); // ReferenceError: bar is not defined
- 通过
new Function()
创建的函数,实际上并没有名称,anonymous
不是一个可以在函数内被访问到的变量
const foo = new Function("console.log(anonymous);");
foo(); // ReferenceError: anonymous is not defined
三、函数参数
ECMAScript 函数的参数和大多数其他语言有所不同。
- ECMAScript 函数不关心传入的参数个数,也不关心参数的数据类型。即实参和形参的类型和个数不一定要匹配。这是因为在 ECMAScript 中函数的参数在内部表现为一个数组。事实上,在使用function关键字定义函数时,可在函数内部访问
arguments
对象,从中获取传进来的每个参数值。 - ECMAScript 函数中所有参数都是按值传递。不能按引用传递参数,如果把对象当作参数传递,那么传递的值就是这个对象的引用。
3.1 Arguments 对象
arguments
是一个对应于传递给函数的参数的类数组对象。
arguments
对象是所有(非箭头)函数中都可用的局部变量。你可以使用arguments
对象在函数中引用函数的参数。此对象包含传递给函数的每个参数,第一个参数在索引0处。可以通过如下方法访问:
arguments[0]
arguments[1]
arguments[2]
此外,Arguments
对象还有以下几个属性:
arguments.length
:传递给函数的参数数量。
function foo(arg1, arg2) {
console.log(arguments.length);
}
foo(1); // 1
foo(1, 2); // 2c
arguments.callee
:指向参数所属的当前执行的函数。(但是该属性已经被弃用,而且严格模式下禁用)arguments.callee.caller
:返回调用指定函数的函数.(该属性与Function.caller
意义相同,都被弃用)
关于为啥会被弃用,可参考MDN
3.2 默认参数
在ES5.1之前,实现默认参数的方法,即在函数内部检测参数是否为undefined
,如果为undefined
则说明该参数未传,然后设置默认值。例如:
function foo(arg) {
arg = arg !== undefined ? arg: 0;
console.log(arg);
}
foo(); // 0
foo(1); // 1
但是在ES6之后,就可以在函数定义中的参数后用 “=” 为参数赋默认值了。例如:
function foo(arg = 0) {
console.log(arg);
}
foo(); // 0
foo(1); // 1
有关默认参数,需要注意以下几点:
arguments
对象的值不反映参数的默认值,只反映传递给函数的参数。
function foo(arg = 0) {
console.log(arguments[0]);
}
foo(); // undefined
foo(1); // 1
- 默认参数的作用域:多个参数定义默认值实际上与使用
let
关键字顺序声明变量一样。所以后定义默认值的参数可以引用先定义的参数。例如:
function foo(arg1 = 0, arg2 = arg1) {
console.log(arg1, arg2);
}
foo(); // 0 0
但是由于let
的暂时性死区,先定义默认值的参数不可以引用后定义的参数。
- JavaScript中带有默认值的参数可以定义在任意位置(不像python中,带默认值的参数必须定义在不带默认值参数之后),传递实参时是按位置传递的。
function foo(arg1 = 0, arg2, arg3 = 10) {
console.log(arg1, arg2,arg3);
}
foo(); // 0 undefined 10
// 实参1 传递给 agr1
foo(1); // 1 undefined 10
// 实参1 传递给 agr1, 实参2 传递给 agr2
foo(1, 2) // 1 2 10
// 实参1 传递给 agr1, 实参2 传递给 agr2, 实参3 传递给 agr3
foo(1, 2, 3) // 1 2 3
3.3 剩余参数
剩余参数语法允许我们将一个不定数量的参数表示为一个数组。(常用于参数可变的情况)
如果函数的最后一个命名参数以...
为前缀,则它将成为一个由剩余参数组成的真数组,其中从0
(包括)到theArgs.length
(排除)的元素由传递给函数的实际参数提供。
function(a, b, ...theArgs) {
// ...
}
举个例子,计算多个变量的和:
function add(...values) {
return values.reduce((pre, cur) => {
return pre + cur;
}, 0);
}
console.log(add(1, 2, 3)); // 6
console.log(add(1, 2, 3, 4)); // 10
此外,我们也可以通过剩余操作符(…)将传递的参数放在一个数组中,然后通过解构赋值,传递参数
function foo(a, b, c) {
console.log(a, b, c);
}
foo(...[1, 2]); // 1 2 undefined
foo(...[1, 2, 3]); // 1 2 3
注意:\color{red}{注意:}注意:剩余参数必须作为最后一个参数。否则就会报语法错误。
// SyntaxError: Rest parameter must be last formal parameter
function foo(...values, a) {
console.log(a, values)
}
剩余参数 和 arguments
对象的区别
- 剩余参数只包含哪些没有对应形参的实参,而
arguments
对象包含了传给函数的所有实参。 arguments
对象是一个类数组对象,而剩余参数是Array
对象实例arguments
对象还有一些附加属性(例如:callee
属性)
3.4 没有重载
例如Java一类的编程语言,在同一个类中,允许存在一个以上的同名函数,只要参数个数或者参数类型不同即可,也称为函数重载。但是JavaScript不关心函数参数个数和类型,自然也没有函数重载一说。
当JavaScript中出现同函数时,后定义的函数会覆盖先定义的函数。例如:
function foo() {
console.log('before')
}
function foo() {
console.log('after')
}
foo(); // after
但是,如果时通过函数表达式的形式定义,并赋值给let
或const
声明的函数变量。若出现同名函数变量,则会报语法错误
let foo = function() {
console.log('before')
}
// SyntaxError: Identifier 'foo' has already been declared
let foo = function() {
console.log('after')
}
3.5 按位置传递(位置参数)
这里关于JavaScript中参数传递多嘴一句,也是消除自己的一些疑惑。前面已经说了参数是按值传递的了,那这里的按位置传递又是什么呢。
其实按位置传递,指的是实参和形参的对应关系,即实参的值传递给哪个形参。
学过python的人在调用函数的时候肯定写过类似于f(a=0, b=1)
的代码,这种参数称为关键字参数。即通过形参的名称来传递对应的参数,无需保证传递参数的位置顺序。但是在JavaScript并不存在这一机制,JavaScript中的参数只能按形参定义的位置顺序进行传递。
说的可能有点模糊,我们看如下的例子:
function foo(a, b) {
console.log(`a=${a}, b=${b}`);
}
foo(b=1, a=2); // a=1, b=2
foo(b=3, a=2); // a=3, b=2
foo(2, 3); // a=2, b=3
上述代码中foo(b=1, a=2);
虽然指定了b=1, a=2
但是传递参数时,仍然是按位置传递,最终输出的是a=1, b=2
。所以JavaScript中的参数是按位置传递的,并不存在类似于关键字参数这样的机制。
四、函数调用的内部方法
4.1 [[call]]
当用一个 this 值、一个参数列表调用函数对象 F
的 [[Call]] 内部方法,采用以下步骤:
- 用
F
的[FormalParameters]]
部属性值、参数列表args
、 this 值来建立函数代码的一个新执行环境,令funcCtx
为其结果。 - 令
result
为functionBody
(也就是F
的[Code]]
部属性)解释执行的结果。如果 F 没有[Code]]
内部属性或其值是空的unctionBody
则result
是 (normal, undefined, empty)。 - 退出
funcCtx
运行环境,恢复到之前的执行运行环境。 - 如果
result
.type 是 throw 则抛出result
.value。 - 如果
result
.type 是 return 则返回result
.value。 - 否则
result
.type 必定是 normal。返回 undefined。
其中4、5、6也说明JavaScript中退出函数的三种方式:抛出错误、返回值、代码执行完毕正常结束饭返回undefined。
4.2 [[Construct]]
当以一个可能的空的参数列表调用函数对象 F
的 [[Construct]] 内部方法,采用以下步骤:
- 令
obj
为新创建的 ECMAScript 原生对象。 - 依照
obj
的所有内部方法。 - 设定
obj
的[[Class]]
内部属性为 “Object” 。 - 设定
obj
的 `[[Extensible]]内部属性为 true。 - 令
proto
为以参数 “prototype” 调用F
的[[Get]]
内部属性的值。 - 如果 Type(
proto
) 是 Object,设定obj
的[[[Prototype]]
内部属性为proto
。 - 如果 Type(
proto
) 不是 Object,设定obj
的[[Prototype]]
内部属性为标准内置的 Object 原型对象。 - 以
obj
为 this 值,调用[[Construct]]
的参数列表为args
,调用F
的[[Call]]
内部属性,令result
为调用结果。 - 如果 Type(
result
) 是 Object,则返回result
。 - 返回
obj
。
下面介绍一个与构造函数调用相关且可以在函数体内部访问到的属性:
- new.target
ES6中新增了检测函数是否使用new
关键字调用的new.target
属性。如果函数是正常调用的,则new.target
的值是undefined
;如果是使用new
关键字调用的,则new.target
将引用被调用的构造函数。
let foo = function() {
console.log(new.target);
}
foo(); // undefined
new foo(); // ƒ () { console.log(new.target); }
小结:
以上两个内部方法都是函数被调用时执行的,不同的是:
- 函数被某一对象当作普通函数调用时,会执行
[[Call]]
内部方法 - 函数被
new
运算符当作构造函数调用时,会执行[[Construct]]
内部方法
当然[[Call]]
和[[Construct]]
是函数的内部方法,无法被访问。
五、函数递归
递归函数:通常的形式是一个函数通过名称调用自己。
function foo(n){
if (n < 0)
return 0;
else
return foo(n - 1) + 1;
}
console.log(foo(1)); // 2
在JavaScript中一个函数可以指向并调用自身,有三种方法可以达到这个目的:
- 函数名
arguments.callee
- 作用域下的一个指向该函数的变量名
那么在如下的代码中:bar()
、arguments.callee()
、foo()
三者等价。
var foo = function bar() {
// statements go here
};
但是在递归函数中,通过作用下的一个指向该函数的变量名去访问函数本身的话,当该变量的值发生改变时,可能会出现以下问题:
function foo(n){
if (n < 0)
return 0;
else
return foo(n - 1) + 1;
}
console.log(foo(1)); // 2
let foo1 = foo;
foo = null;
foo1(1); // Uncaught TypeError: foo is not a function
对于这种问题,可以通过 arguments.callee
来解决:
function foo(n){
if (n < 0)
return 0;
else
return arguments.callee(n - 1) + 1;
}
console.log(foo(1)); // 2
let foo1 = foo;
foo = null;
console.log(foo1(1)); // 2;
但是arguments.callee
以及被弃用,并且在严格模式下被禁用,所以我们可以通过命名表达式的方式,通过函数名去访问函数本身:
let foo = function f(n){
if (n < 0)
return 0;
else
return f(n - 1) + 1;
}
console.log(foo(1)); // 2
let foo1 = foo;
foo = null;
console.log(foo1(1)); // 2;
综上所述:在实现递归时,尽量使用函数声明或者命名表达式。
可以从 FunctionExpression 的 FunctionBody 里面引用 FunctionExpression 的 Identifier,以允许函数递归调用自身。然而不像 FunctionDeclaration,FunctionExpression 的 Identifier 不能被范围封闭的 FunctionExpression 引用,也不会影响它。 —— ES5规范
六、箭头函数
6.1 什么是箭头函数
箭头函数表达式的语法比函数表达式更简洁.箭头函数表达式更适用于那些本来需要匿名函数的地方.
- 没有自己的
this
,arguments
,super
或new.target
。 - 不能用作构造函数。
- 没有
prototype
属性 yield
关键字通常不能在箭头函数中使用
6.2 箭头函数语法
(1)基础语法
- 箭头后面的语句有花括号
{}
包裹,则括号中的内容即为函数体 - 箭头后面的语句若没有花括号
{}
包裹,则执行该表达式,并返回该表达式的结果
(param1, param2, …, paramN) => { statements }
(param1, param2, …, paramN) => expression
//相当于:(param1, param2, …, paramN) =>{ return expression; }
(2)当只有一个参数时,圆括号可选
// 当只有一个参数时,圆括号是可选的:
(singleParam) => { statements }
singleParam => { statements }
(3)当没有参数时,圆括号必修写
// 没有参数的函数应该写成一对圆括号。
() => { statements }
(4)返回对象字面量表达式必须用圆括号()
包裹
//加括号的函数体返回对象字面量表达式:
params => ({foo: bar})
(5)支持剩余参数和默认参数,以及参数列表解构
//支持剩余参数和默认参数
(param1, param2, ...rest) => { statements }
(param1 = defaultValue1, param2, …, paramN = defaultValueN) => {
statements }
//同样支持参数列表解构
let f = ([a, b] = [1, 2], {x: c} = {x: a + b}) => a + b + c;
f(); // 6
6.3 箭头函数中的this
箭头函数不会创建自己的this
,它只会从自己的作用域链的上一层继承this
。
七、立即调用函数表达式(IIFE, Immediately Invoked Function Expression)
IIFE( 立即调用函数表达式)是一个在定义时就会立即执行的 JavaScript 函数。
(function () {
statements
})();
这是一个被称为 自执行匿名函数 的设计模式,主要包含两部分。第一部分是包围在圆括号运算符()
里的一个匿名函数,这个匿名函数拥有独立的词法作用域。这不仅避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域。
第二部分再一次使用 ()
创建了一个立即执行函数表达式,JavaScript 引擎到此将直接执行函数。
(function(n){
console.log(n)
})(1)
以上就是个人在学习JavaScript函数的一点记录啦。
主要参考
[1] 《JavaScript高级程序设计(第四版)》 [2] MDN