JS中函数(一)(箭头函数、函数参数、扩展操作符)
??本博文按照 JavaScript 高级程序设计第10章详细总结函数的相关知识,防止内容过长,分了两部分,这是第一部分。设计到函数的四种创建方式、箭头函数、函数的参数、扩展符操作在函数中的运用等知识。 本章内容
- 函数表达式、函数声明及箭头函数
- 默认参数及扩展操作符
- 使用函数实现递归
- 使用闭包实现私有变量
??在 ECMAScript 中,每个函数都是 Function 类型的实例,都是对象,拥有属性和方法。函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定。
??定义函数通常有下面四种方法:① 函数声明 ② 函数表达式 ③ 箭头函数 ④ Function 构造函数
function sum (num1,num2){
return num1 + num2;
}
let sum = function (num1,num2){
return num1 + num2;
};
let sum = (num1,num2)=> {
return num1 + num2;
};
- 使用 Function 构造函数,接收任意多个字符串参数,最后一个当成函数体。
let sum = new Function("num1","num2","return num1 + num2");
10.1、箭头函数
??ECMAScript6 中新增了箭头(=>)函数,它与函数表达式创建的函数对象行为相同。
let arrowSum = (a,b) => {
return a + b;
}
let functionExpressionSum = function(a,b){
return a + b;
}
console.log(arrowSum(5,8));
console.log(functionExpressionSum(5,8));
??箭头函数更简洁,非常适合嵌入函数的场景:
let ints = [1,2,3];
console.log(ints.map(function(i) {return i + 1;}));
console.log(ints.map((i) => {return i + 1;}));
??上述代码中用到了数组的 map() 方法,不熟悉的可以参考博文JS中数组大总结
注意:
- 如果函数只有一个参数,可以省略参数的括号,多个或者没有参数时不能省略。
- 如果函数体的内容代码就只有一行代码,那么可以省略函数体的大括号和 return 关键字以及后面的分号。比如一个赋值操作或者一个表达式,省略大括号会隐式返回这行代码的值:
let a = (x) => {return 2*x;};
let b = x => 4 * x;
console.log(a(2));
console.log(b(2));
let value = {};
let setName = x =>x.name = "dog";
setName(value);
console.log(value.name);
let c = (a,b) => return a * b;
console.log(c(1,2));
?? 箭头函数虽然语法简洁,但也有很多场合不适用,主要有以下几个特点:涉及到其他内容会在其他博文中讲解
(1) 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2) 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3) 不可以使用 arguments 对象,该对象在箭头函数体内不存在。如果要用,可以用 rest 参数代替。
(4) 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。
(5) 没有 prototype 属性,其他任何函数形式都有 prototype 属性指向其对应的原型对象。这一点可以在原型内容中找到答案
10.2、函数名
??函数名就是指向函数的指针,跟其他对象指针的变量一样。
function sum(a,b){
return a + b;
}
let anotherSum = sum;
console.log(sum(1,2));
console.log(anotherSum(2,3));
sum = null;
console.log(anotherSum(3,4));
??注意,使用不带括号的函数名会访问函数指针,而不会执行函数。(上述代码第4行)。此时,anotherSum 和 sum 都指向同一个函数。
??ECMAScript6 的所有函数对象都会暴露一个只读的 name 属性,其中包含关于函数的信息。多数情况下,这个属性中保存的就是一个函数标识符,或者说是一个字符串化的变量名。即使函数没有名称,也会如实显示成空字符串。如果它是使用 Function 构造函数创建的,则会标识成 “anonymous”:
function foo() {}
let bar = function() {};
let baz = () => {};
console.log(foo.name);
console.log(bar.name);
console.log(baz.name);
console.log((() => {}).name);
console.log((new Function()).name);
??如果函数是一个获取函数、设置函数(访问器属性的知识),或者使用 bind() 实例化,那么标识符前面会加上一个前缀:
function foo() {}
console.log(foo.bind(null).name);
let dog = {
years: 1,
get age() {
return this.years;
},
set age(newAge) {
this.years = newAge;
}
}
let propertyDescriptor = Object.getOwnPropertyDescriptor(dog, 'age');
console.log(propertyDescriptor.get.name);
console.log(propertyDescriptor.set.name);
10.3、理解参数
??ECMAScript 函数既不关心传入的参数个数,也不关心这些参数的数据类型。定义函数时要接收两个参数,但你可以传一个、三个, 甚至一个也不传,解释器都不会报错。
??因为 ECMAScript 函数的参数在内部表现为一个数组。函数被调用时总会接收一个数组,但函数并不管这个数组中包含什么。在使用 function 关键字定义(非箭头)函数时,可以在函数内部访问 arguments 对象,从中取得传进来的每个参数值。箭头函数中不包含 arguments 对象。
??arguments 对象是一个类数组对象(但不是 Array 的实例),因此可以使用中括号语法访问其中的元素 (例如 arguments[0])。访问 arguments.length 属性确定传进来多少个参数。
function sayHi(name, message) {
console.log("Hello " + name + ", " + message);
}
function sayHi() {
console.log("Hello " + arguments[0] + ", " + arguments[1]);
}
??上述两段代码可起到一样的功能,表明,ECMAScript 函数的参数只是为了方便才写出来的,并不是必须写出来的。ECMAScript 不存在验证命名参数的机制。
??通过 arguments.length 属性检查传入的参数个数:
function howManyArgs() {
console.log(arguments.length);
}
howManyArgs("string", 45);
howManyArgs();
howManyArgs(12);
??开发者可以想传多少参数就传多少参数,用于弥补 ECMAScript 在函数重载方面的缺失:
function doAdd() {
if (arguments.length === 1) {
console.log(arguments[0] + 10);
} else if (arguments.length === 2) {
console.log(arguments[0] + arguments[1]);
}
}
doAdd(10);
doAdd(30, 20);
??arguments 对象可以跟命名参数混合使用,比如:
unction doAdd(num1, num2) {
if (arguments.length === 1) {
console.log(num1 + 10);
} else if (arguments.length === 2) {
console.log(arguments[0] + num2);
}
}
??arguments 对象始终与传入参数的命名参数同步:
function doAdd(num1, num2) {
arguments[1] = 10;
console.log(arguments[0] + num2);
}
??这个 doAdd() 函数把第二个参数的值重写为10。因为 arguments 对象的值会自动同步到对应的命名参数,所以修改 arguments[1] 也会修改 num2 的值,因此两者的值都是10。但这并不意味着它们都访问同一个内存地址,它们在内存中还是分开的,只不过会保持同步而已。还有一点:如果只传了一个参数,num2 的值就是undefined。然后把 arguments[1] 设置为某个值,那么这个值并不会反映到第二个命名参数。这是因为 arguments 对象的长度是根据传入的参数个数,而非定义函数时给出的命名参数个数确定的。
??严格模式下,arguments 有一些变化。首先,像前面那样给 arguments[1] 赋值不会再影响num2 的值。就算把 arguments[1] 设置为 10,num2 的值仍然还是传入的值。其次,在函数中尝试重写 arguments 对象会导致语法错误。
??如果函数是使用箭头语法定义的,那么传递的参数将不能使用 arguments 关键字访问,而只能通过定义的命名参数访问。
function foo() {
console.log(arguments[0]);
}
foo(5);
let bar = () => {
console.log(arguments[0]);
};
bar(5);
??箭头函数中没有 arguments 对象,但可以在包装函数中把它提供给箭头函数:
function foo() {
let bar = () => {
console.log(arguments[0]);
};
bar();
}
foo(5);
**【注意】:**参数都是按值传递的。如果把对象作为参数传递,那么传递的值就是这个对象的引用。
10.4、没有重载
??ECMAScript 函数不能像传统编程那样重载。如 Java 中,一个函数可以有两个定义,只要签名(接收参数的类型和数量)不同就行。ECMAScript 函数没有签名,因为参数是由包含零个或多个值的数组表示的。没有函数签名,自然也就没有重载。
??如果在 ECMAScript 中定义了两个同名函数,则后定义的会覆盖先定义的。
??虽然 JS 中的函数没有重载,可以通过检查参数的类型和数量,然后分别执行不同的逻辑来模拟函数重载。
10.5、默认参数值
??在 ECMAScript5.1 及以前,用检测某个参数是否等于undefined 来实现默认参数,如果是则意味着没有传这个参数,那就给它赋一个值:
function makeKing(name) {
name = (typeof name !== 'undefined') ? name : 'Henry';
return `King ${name} VIII`;
}
console.log(makeKing());
console.log(makeKing('Louis'));
??ECMAScript 6 之后支持显式定义默认参数了。在函数定义中的参数后面用=就可以为参数赋一个默认值:
function makeKing(name = 'Henry') {
return `King ${name} VIII`;
}
console.log(makeKing('Louis'));
console.log(makeKing());
??给参数传 undefined 相当于没有传值,不过这样可以利用多个独立的默认值:下面代码第6行可以实现利用第一个默认值而不使用第二个默认值。
function makeKing(name = 'Henry', numerals = 'VIII') {
return `King ${name} ${numerals}`;
}
console.log(makeKing());
console.log(makeKing('Louis'));
console.log(makeKing(undefined, 'VI'));
??在使用默认参数时,arguments 对象的值不反映参数的默认值,只反映传给函数的参数。当然,跟 ES5 严格模式一样,修改命名参数也不会影响 arguments 对象,它始终以调用函数时传入的值为准:
function makeKing(name = 'Henry') {
name = 'abc';
return `King ${arguments[0]}`;
}
console.log(makeKing());
console.log(makeKing('Louis'));
??默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值:
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());
console.log(makeKing('Louis', 'XVI'));
console.log(makeKing());
console.log(makeKing());
??函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值。而且,计算默认值的函数只有在调用函数但未传相应参数时才会被调用如上代码第10行。
??箭头函数同样也可以这样使用默认参数,但只有一个参数时,也必须使用括号:
let makeKing = (name = 'Henry') => `King ${name}`;
console.log(makeKing());
??在求值默认参数时可以定义对象,也可以动态调用函数,所以函数参数肯定是在某个作用域中求值的。参数属于函数内部的局部变量,给多个参数定义默认值实际上跟使用 let 关键字顺序声明变量一样:
function makeKing(name = 'Henry', numerals = 'VIII') {
return `King ${name} ${numerals}`;
}
function makeKing() {
let name = 'Henry';
let numerals = 'VIII';
return `King ${name} ${numerals}`;
}
?? 因此,默认参数可以引用先定义的参数,先定义的参数不能引用后定义的参数或函数内部将定义的变量,这边是参数的暂时性死区:
function makeKing1(name = 'Henry', numerals = name) {
return `King ${name} ${numerals}`;
}
function makeKing2(name = numerals, numerals = 'VIII') {
return `King ${name} ${numerals}`;
}
function makeKing3(name = 'Henry', numerals = defaultNumeral) {
let defaultNumeral = 'VIII';
return `King ${name} ${numerals}`;
}
10.6、参数扩展与收集
??ECMAScript6 新增了扩展操作符(…),使用它可以非常简洁地打散和组合集合数据。扩展操作符最有用的场景就是函数定义中的参数列表,充分利用 JS 语言的弱类型及参数长度可变的特点。扩展操作符既可以用于调用函数时传参,也可以用于定义函数参数。
10.6.1 扩展参数
??如果 let values = [1,2,3] ,那么 ...values 就相当于 1,2,3 即把数组打散成一个一个元素。
??假设我们有一个函数getSum() 用于将传进的参数累加起来。而我们拥有的数据是一个数组values。如下:
let values = [1,2,3,4];
function getSum() {
let sum = 0;
for (let i = 0; i < arguments.length; ++i) {
sum += arguments[i];
}
return sum;
}
??不使用扩展操作符,想把定义在这个函数上面的数组拆分,那么就得求助于 apply() 方法:了解apply()、call()、bind()三个函数
console.log(getSum.apply(null, values));
??在 ES6 中,可以通过扩展操作符极为简洁地实现这种操作。 对可迭代对象应用扩展操作符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入。
console.log(getSum(...values));
??使用扩展操作符传参的时候,并不妨碍在其前面或后面再传其他的值,包括使用扩展操作符传其他参数:
console.log(getSum(-1, ...values));
console.log(getSum(...values, 5));
console.log(getSum(-1, ...values, 5));
console.log(getSum(...values, ...[5,6,7]));
??对函数中的 arguments 对象而言,它并不知道扩展操作符的存在,而是按照调用函数时传入的参数接收每一个值:
let values = [1,2,3,4]
function countArguments() {
console.log(arguments.length);
}
countArguments(-1, ...values);
countArguments(...values, 5);
countArguments(-1, ...values, 5);
countArguments(...values, ...[5,6,7]);
??arguments 对象只是消费扩展操作符的一种方式。在普通函数和箭头函数中,也可以将扩展操作符用于命名参数,当然同时也可以使用默认参数:
function getProduct(a, b, c = 1) {
return a * b * c;
}
let getSum = (a, b, c = 0) => {
return a + b + c;
}
console.log(getProduct(...[1,2]));
console.log(getProduct(...[1,2,3]));
console.log(getProduct(...[1,2,3,4]));
console.log(getSum(...[0,1]));
console.log(getSum(...[0,1,2]));
console.log(getSum(...[0,1,2,3]));
10.6.2 收集参数
??在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组。这有点类似 arguments 对象的构造机制,只不过收集参数的结果会得到一个 Array 实例。
function getSum(...values) {
return values.reduce((x, y) => x + y, 0);
}
console.log(getSum(1,2,3));
??上述代码中 reduce() 是数组的一个归并方法,了解数组的reduce() 方法。
??扩展操作符用在函数定义是作为命名参数时,表示要把函数调用时传入的参数组合成数组放入命名参数中,即表示收集参数。
??收集参数的前面可以有命名参数,后面不能有,调用函数传入的实参会先分配给收集参数前的命名参数,剩下的才会收集到数组中。如果没有剩下的则会得到空数组。因为收集参数的结果可变,所以只能把它作为最后一个参数:
function getProduct(...values, lastValue) {}
function ignoreFirst(firstValue, ...values) {
console.log(values);
}
ignoreFirst();
ignoreFirst(1);
ignoreFirst(1,2);
ignoreFirst(1,2,3);
??箭头函数虽然不支持 arguments 对象,但支持收集参数的定义方式,因此也可以实现与使用arguments 一样的逻辑:
let getSum = (...values) => {
return values.reduce((x, y) => x + y, 0);
}
console.log(getSum(1,2,3));
??另外,使用收集参数并不影响 arguments 对象,它仍然反映调用时传给函数的参数:
function getSum(...values) {
console.log(arguments.length);
console.log(arguments);
console.log(values);
}
console.log(getSum(1,2,3));
10.7、函数声明与函数表达式
??一直到现在还没有把函数声明和函数表达式区分得很清楚。 JS 引擎在加载数据时(执行代码前的预处理阶段)对它们是区别对待的。JS 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义(这便是函数声明提升)。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。
console.log(sum(10, 10));
function sum(num1, num2) {
return num1 + num2;
}
??函数声明提升:在执行代码时,JS 引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。所以在函数声明定义前就可以调用函数了。
??如果把函数声明改为等价的函数表达式,则会出错:
console.log(sum(10, 10));
let sum = function(num1, num2) {
return num1 + num2;
};
??除了函数什么时候真正有定义这个区别之外,函数声明和函数表达式这两种语法是等价的。即函数声明有提升,函数表达式没有提升。
【注意】: 使用函数表达式初始化变量时,也可以给函数一个名称,比如 let sum = function sum() {}; 。这一点在 10.11 节讨论函数表达式时会再讨论。
10.8、函数作为值
??因为函数名在 ECMAScript 中就是变量,所以函数可以用在任何可以使用变量的地方。可以把函数作为参数传给另一个函数,可以在一个函数中返回另一个函数。
function callSomeFunction(someFunction, someArgument) {
return someFunction(someArgument);
}
function add10(num) {
return num + 10;
}
let result1 = callSomeFunction(add10, 10);
console.log(result1);
function getGreeting(name) {
return "Hello, " + name;
}
let result2 = callSomeFunction(getGreeting, "Nicholas");
console.log(result2);
??从一个函数中返回另一个函数也是可以的,而且非常有用。
??假设有一个包含对象的数组,而我们想按照任意对象属性对数组进行排序。 为此,可以定义一个 sort() 方法需要的比较函数,它接收两个参数,即要比较的值。但这个比较函数还需要想办法确定根据哪个属性来排序。 这个问题可以通过定义一个根据属性名来创建比较函数的函数来解决。 比如:
function createComparisonFunction(propertyName) {
return function(object1, object2) {
return object1[propertyName]-object2[propertyName];
};
}
let data = [
{name: "A", age: 30},
{name: "B", age: 29}
];
data.sort(createComparisonFunction("name"));
console.log(data[0].name);
data.sort(createComparisonFunction("age"));
console.log(data[0].name);
|