1 函数的创建
函数实际上是对象,每个函数都是Function类型的实例,而Function也有属性和方法,跟其他引用类型一样,因为函数是对象,所以函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定。函数通常以函数声明的方式定义:
function sum(num1,num2){
return num1+num2;
}
注意函数定义最后没有加分号,另一种定义函数的语法是函数表达式:
let sum = function(num1,num2){
return num1+num2;
};
这里代码定义了一个变量sum并将其初始化为一个函数,注意这里的函数末尾是有分号的,与任何变量初始化语句一样,还有一种定义函数的方式与函数表达式很像,叫做“箭头函数(arrow function):
let sum = (num1,num2)=>{
return num1+num2;
};
最后一种定义函数的方式是使用Function构造函数,这个构造函数接收任意多个字符串参数,最后一个参数始终会被当成函数体,而之前的参数都是新函数的参数:
let sum = new Function("num1","num2","return num1+num2");
我们不推荐使用这种语法来定义函数,因为这段代码会被解释两次:第一次将它当作常规的ECMAScript代码,第二次解释传给构造函数的字符串,这显然会影响性能。
2 箭头函数
ECMAScript 6新增了使用胖箭头(=>)语法定义函数表达式的能力,很大程度上,箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的,任何可以使用函数表达式的地方,都可以使用箭头函数:
let arrowSum = (a,b)=>{
return a+b;
};
let functionExpressionSum = function(a,b){
return a+b;
};
console.log(arrowSum(5,8));//13
console.log(functionExpressionSum(5,8));//13
箭头函数简洁的语法非常适合嵌入函数的场景:
let ints = [1,2,3];
console.log(ints.map(function(i){return i+1;}));//[2,3,4]
console.log(ints.map((i)=>{return i+1}));//[2,3,4]
如果只有一个参数,那也可以不用括号,只有没有参数或者多个参数的情况下,才需要使用括号:
//以下两种写法都有效
let double = (x) => {return 2*x;};
let triple = x => {return 3*x;};
//没有参数需要括号
let getRandom = () => {return Math.random();};
//多个参数需要括号
let sum = (a,b) => {return a+b;};
在箭头函数中如果不用大括号,则箭头后面就只能有一行代码,比如赋值或一个表达式,而且省略大括号会隐式返回这行代码的值:
//以下两种写法都有效,而且返回相应的值
let double = (x) => {return 2*x;};
let triple = (x) => 3*x;
//可以赋值
let value = {};
let setName = (x) => x.name = "Matt";
setName(value);
console.log(value.name);//"Matt"
箭头函数虽然简洁,但有很多场合不使用,箭头函数不能使用arguments、super和new.target,也不能用作构造函数,此外箭头函数也没有prototype属性。
3 函数名
因为函数名就是指向函数的指针,所以它们跟其他包含对象指针的变量具有同样的行为,这意味着一个函数可以有多个名称:
function sum(num1,num2){
return num1+num2;
}
console.log(sum(10,10));//20
let anotherSum = sum;
console.log(anotherSum(10,10));//20
sum = null;
console.log(anotherSum(10,10));//20
4 理解参数
ECMAScript函数的参数跟大多数其他语言不同,ECMAScript函数既不关心传入参数个数,也不关心这些参数的数据类型。定义函数时要接收两个参数,并不意味着调用时就传两个参数,可以传1个、3个甚至一个也不传,解释器都不会报错。之所以会这样,主要是因为ECMAScript函数的参数在内部表现为一个数组,函数被调用时总会接收一个数组,但函数并不关心这个数组中包含什么,如果数组中什么也没有,那没问题,如果数组的元素超出了要求,那也没问题。事实上在使用function关键字(非箭头)定义函数时,可以在函数内部访问arguments对象,从中取得传进来的每个参数值。
arguments对象是一个类数组对象(但不是Array的实例),因此可以使用中括号语法访问其中的元素(第一个参数是arguments[0],第二个参数是arguments[1])。而要确定传进来多少个参数可以访问arguments.length属性。
在下面的例子中,sayHi()函数的第一个参数叫name:
function sayHi(name,message){
console.log("Hello "+name+", "+message);
}
可以通过arguments[0]取得相同的参数值,因此把函数重写成不声明参数也可以:
function sayHi(){
console.log(`Hello ${arguments[0]} , ${arguments[1]}`);
}
也可以通过arguments对象的length属性检查传入的参数个数:
function howManyArgs(){
console.log(arguments.length);
}
howManyArgs("string",45);//2
howManyArgs();//0
howManyArgs(12);//1
arguments对象的另一个有意思的地方就是它的值始终会与对应的命名参数同步:
function doAdd(num1, num2) {
arguments[1] = 10;
console.log(arguments[0] + num2);
}
console.log(doAdd(1, 2)); //11
这个doAdd()函数把第二个参数的值重写为10,因为arguments对象的值会自动同步到对应的命名参数,所以会修改arguments[1]也会修改num2的值,因此二者的值都是10,但这不意味着它们都访问同一个内存地址,它们在内存中还是分开的,只不过会保持同步而已。另外还需要记住一点:如果只传了一个参数,然后把arguments[1]设置为某个值,那么这个值并不会反映到第二个命名参数。这是因为arguments对象的长度是根据传入的参数个数,而非定义函数时给出的命名参数个数确定的。对于命名参数而言,如果调用函数时没有传入这个参数,那么它的值就是undefined。
5 默认参数
在ECMAScript5.1及之前,实现默认参数的一种常用方式就是检测某个参数是否等于undefined,如果是则意味着没有传这个参数,那就给它赋一个值:
function makeKing(name){
name = (typeof name !== 'undefined')?name:'Henry';
return `King ${name} VIII`;
console.log(makeKing());//'King Henry Viii'
console.log(makeKing('Louis'));//'King Louis VIII'
ES6之后就不用这么麻烦了,因为它支持显式定义默认参数值,只要在函数定义中的参数后面用=就可以为参数赋一个默认值:
function makeKing(name = 'Henry'){
return `King ${name} Viii`;
}
console.log(makeKing('Louis'));//'King Louis VIII'
console.log(makeKing());//'King Henry VIII'
给参数传undefined相当于没有传值,不过这样可以利用多个独立的默认值:
function makeKing(name = 'Henry',numerals = 'VIII'){
return `King ${name} ${numerals}`;
}
console.log(makeKing());//'King Henry VIII'
console.log(makeKing('Louis'));//'King Louis VIII'
console.log(makeKing(undefined,'VI'));/'King Henry VI'
在使用默认参数是,arguments对象的值不反映参数的默认值,只反映传给函数的参数:
function makeKing(num1 = 'heal', num2) {
console.log(arguments[0]);
console.log(num1);
}
doAdd()
默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值:
let romanNumerals = ['I','II','III','IV','V','VI'];
let ordinality = 0;
function getNumerals(){
//每次调用后递增
return romanNumerals[ordinality++];
}
function makeKing(name = 'Henry',numerals = getNumerals()){
return `King ${name} ${numerals}`;
}
console.log(makeKing());//'King Henry I'
console.log(makeKing('Louis','XVI'));//'King Louis XVI'
console.log(makeKing());//'King Henry II'
console.log(makeKing());//'King Henry II'
函数的默认参数只有调用时才会求值,不会在函数定义时求值,而且计算默认值的函数只有在调用函数但未传相应参数才会才会被调用。
6 参数扩展及收集
ECMAScript6新增了扩展操作符,使用它可以非常简洁地操作和组合集合数据。扩展操作符最有用的场景就是函数定义中的参数列表,在这里它可以充分利用这门语言的弱类型及参数长度可变的特点。扩展操作符既可以用于调用函数时传参,也可以用于定义函数参数。
(1)扩展参数
在给函数传参时,有时候可能不需要传一个数组,而是要分别传入数组的元素:
let values = [1,2,3,4];
function getSum(){
let sum = 0;
for (let i = 0;i<arguments.length;++i){
sum+=arguments[i];
}
return sum;
}
在ECMAScript中,可以通过扩展操作符极为简洁的将函数这里的数组拆分,并将迭代返回的每个值单独传入,比如使用扩展操作符可以将前面例子中的数组像这样直接传给函数:
console.log(getSum(...Values));//10
因为数组长度已知,所以在使用扩展操作符传参的时候,并不妨碍在其前面或后面再传其他的值,包括使用扩展操作符传其他参数:
console.log(getSum(-1,...values));//9
console.log(getSum(...values,5));//15
console.log(getSum(-1,...values,5));//14
console.log(getSum(...values,..[5,6,7]));//28
对函数中的arguments对象而言,它并不知道扩展操作符的存在,而是按照调用函数时传入的参数接收每一个值:
let values = [1,2,3,4];
function countArguments(){
console.log(arguments.length);
}
countArguments(-1,...values);//5
countArguments(...values,5);//5
countArguments(-1,...values,5);//6
countArguments(...values,...[5,6,7]);//7
(2)收集参数
在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组,这有点类似arguments对象的构造机制,只不过收集参数的结果会得到一个Array实例:
function getSum(...values){
//顺序累加values中的所有值
//初始值的总和为0
return values.reduce((x,y)=>x+y,0);
}
console.log(getSum(1,2,3));//6
收集参数的前面如果还有命名参数,则只会收集其余的参数,如果没有则会得到空数组,因为收集参数的结果可变,所以只能把它作为最后一个参数:
//不可以
function getProduct(...values,lastValue){}
//可以
function ignoreFirst(firstValue,...values){
console.log(values);
}
ignoreFirst();//[]
ignoreFirst(1);//[]
箭头函数虽然不支持arguments对象,但支持收集参数的定义方式,因此也可以实现与arguments一样的逻辑:
let getSum = (...values) => {
return values.reduce((x,y)=>x+y,0);
};
console.log(getSum(1,2,3);//6
另外使用收集参数并不影响arguments对象,它仍然反映调用时传给函数的参数。
7 函数声明与函数表达式
JavaScript引擎在任何代码执行之前,会读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一步,才会在执行上下文中生成函数定义:
//没问题
console.log(sum(10,10)):
function sum(num1,num2){
return num1+num2;
}
//会出错
console.log(sum(10,10));
let sum = function(num1,num2){
return num1+num2;
};
以上代码可以正常运行是因为函数声明会在任何代码执行之前先被读取并添加到执行上下文。这个过程叫函数声明提升。上面代码会出错是因为这个函数定义包含在一个变量初始化语句中,而不是函数声明中,这意味着代码如果没有执行到函数表达式哪一行,那么执行上下文中也没有函数定义,所以上面代码会出错,这并不是因为使用let导致的,使用var关键字也会碰到同样的问题:
console.log(sum(10,10));
var sum = function(num1,num2){
return num1+num2;
}
除了函数什么时候真正有定义以外,这两种语法是等价的。
8 函数作为值
因为函数名在ECMAScript中就是变量,所以函数可以用在任何可以使用变量的地方,这意味着不仅可以把函数作为参数传给另一个函数,而且还可以在一个函数中返回另一个函数:
function callSomeFunction(someFunction,someArgument){
return somFunction(someArgument);
}
这个函数接收两个参数,第一个参数应该是一个函数,第二个参数应该是要传给这个函数的值,任何函数都可以像下面这样作为参数传递:
function add10(num){
return num+10;
}
let result1 = callSomeFunction(add10,10);
console.log(result1);//20
function getGreeting(name){
return "Hello, "+name;
}
let result2 = callSomeFunction(getGreeting,"Nicholas");
console.log(result2);//"Hello,Nicholas"
从一个函数中返回另一个函数也是可以的,而且非常有用,例如假设有一个包装对象的数组,而我们想按照任意对象属性对数组进行排序,为此可以定义一个sort()方法需要的比较函数,它接收两个参数,即要比较的值,但这个比较函数还需要想办法确定根据哪个属性来排序,这个问题可以通过定义一个根据属性名来创建比较函数的函数来解决:
function createComparisonFunction(propertyName){
return function(object1,object2){
let value1 = object1[propertyName];
let value2 = object2[propertyName];
if(value1<value2){
return -1;
}else if(value2>value1){
return 1;
}else{
return 0;
}
};
}
这个函数的语法乍一看比较复杂,但实际上就是在一个函数中返回另一个函数,注意那个return操作符,内部函数可以访问propertyName参数,并通过中括号语法取得要比较的对象的相应属性值,取得属性值以后,再按照sort()方法的需要返回比较值就行了:
let data = [
{name:"Zachary",age:28}
{name:"Nicholas",age:29}
];
data.sort(createComparisonFunction("name"));
console.log(data[0].name);//Nicholas
data.sort(createComparisonFunction("age"));
console.log(data[0].name);//Zachary