Tags:默认参数、不定参数、展开运算符、name
属性、元属性 new.target
、箭头函数、尾调用优化、
函数形参的默认值
JavaScript 函数的特点:无论在函数定义中声明了多少形参,都可以传入任意数量的参数,也可以在定义函数时添加针对参数数量的处理逻辑,当已定义的形参无对应的传入参数时为其指定一个默认值。
在 ECMAScript 5 中模拟默认参数
创建函数,并为参数赋予默认值。
function makeRequest(url, timeout, callback) {
// timeout、callback 为可选参数
// 如果未传值,则通过逻辑或操作符 || 为缺失的参数设置默认值
timeout = timeout || 2000;
callback = callback || function() {};
// 函数的其余部分
}
缺陷:如果我们想给 makeRequest
函数的第二个形参 timeout
传入值 0,即使这个值是合法的,也会被视为一个假值,并最终为 timeout
赋值 2000。
优化方案:通过 typeof
操作符检查参数类型:
function makeRequest(url, timeout, callback) {
// timeout、callback 为可选参数
// 如果未传值,默认值参数值为 "undefined"
timeout = (typeof timeout !== "undefined") ? timeout : 2000;
callback = (typeof callback !== "undefined") ? callback : function() {};
// 函数的其余部分
}
ECMAScript 6 中的默认参数值
ECMAScript 6 简化了为形参提供默认值的过程,如果没有为参数传入值,则为其提供一个初始值。
function makeRequest(url, timeout = 2000, callback = function() {}) {
// 函数的其余部分
}
声明函数时,可以为任意参数指定默认值,在已指定默认值的参数后可以继续声明无默认值参数。
function makeRequest(url, timeout = 2000, callback) {
// 函数的其余部分
}
此时,只有当不为第二个参数传入值或主动为第二个参数传入 undefined
时才会使用 timeout 的默认值:
// 使用 timeout 的默认值
makeRequest("/foo", undefined, function(body) {
doSomething(body);
});
// 使用 timeout 的默认值
makeRequest("/foo");
// 不使用 timeout 的默认值
makeRequest("/foo", null, function(body) {
doSomething(body);
});
默认参数值对 arguments
对象的影响
- 在 ECMAScript 5 非严格模式下,函数命名参数的变化会同步更新到
arguments
对象中。 - 在 ECMAScript 5 严格模式下,无论参数如何变化,
arguments
对象不再随之改变。 - 在 ECMAScript 6 中,无论是否处于严格模式下,只要一个函数使用了默认参数值,
arguments
对象都不随参数改变。
默认参数表达式
因为默认参数是在函数调用时求值,所以可以使用先定义的参数作为后定义参数的默认值。
在引用参数默认值时,只允许引用前面参数的值。
function add(first, second = first) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 2
处理无命名参数
在 JavaScript 函数中,无论函数已定义的命名参数有多少,都不限制调用时传入的实际参数数量,调用时总是可以传入任意数量的参数。
当传入更少数量的参数时,默认参数值的特性可以有效简化函数声明的代码。
当传入更多数量的参数时,ECMAScript 6 同样也提供了更好的方案。
ECMAScript 5 中的无命名参数
使用 arguments
对象来检查函数的所有参数。
// 返回一个给定对象的副本,包含原始对象属性的特定子集
function pick(object) {
let result = Object.create(null);
// 从第二个参数开始
for (let i = 1; i < arguments.length; i++) {
// 将 object 对象的属性值逐个赋值给第二个开始的参数
result[arguments[i]] = object[arguments[i]];
}
return result;
}
let book = {
title: 'Understanding ECMAScript 6',
author: 'Nicholas C. Zakas',
year: 2016
};
let bookData = pick(book, 'author', 'year');
console.log(bookData.author); // Nicholas C. Zakas
console.log(bookData.year); // 2016
不定参数
在函数的命名参数前添加三个点(...
)就表明这是一个不定参数,该参数为一个数组,包含着自它之后传入的所有参数,通过这个数组名即可逐一访问里面的参数。
使用不定参数重写 pick()
函数。
// 不定参数 keys 包含的是 object 之后传入的所有参数。
function pick(object, ...keys) {
let result = Object.create(null);
for (let index = 0; index < keys.length; index++) {
result[keys[i]] = object[keys[i]];
}
return result;
}
不定参数的使用限制
- 每个函数最多只能声明一个不定参数,而且一定要放在所有参数的末尾。
- 不定参数不能用于对象字面量
setter
之中。
let object = {
// 语法错误:不可以在 setter 中使用不定参数
set name(...value) {
// 执行一些逻辑
}
};
不定参数对 arguments
对象的影响
没有影响。
无论是否使用不定参数,arguments
对象总是包含所有传入函数的参数。
function checkArgs(...args) {
console.log(args.length); // 2
console.log(arguments.length); // 2
console.log(args[0], arguments[0]); // a a
console.log(args[1], arguments[1]); // b b
}
checkArgs('a', 'b');
增强的 Function 构造函数
Function 构造函数很少用到,通常我们用它来动态创建新的函数。
// Function 构造函数接受字符串形式的参数,分别为函数的参数和函数体。
var add = new Function('first', 'second', 'return first + second');
console.log(add(1, 2)); // 3
ECMAScript 6 增强了 Function 构造函数的功能,支持在创建函数时定义默认参数和不定参数。
定义默认参数:在参数名后添加一个等号及一个默认值。
var add = new Function('first', 'second = first', 'return first + second');
console.log(add(1, 2)); // 3
console.log(add(1)); // 2
定义不定参数,只需要在最后一个参数前添加...
:
var pickFirst = new Function('...args', 'return args[0]');
console.log(pickFirst(1, 2)); // 1
对于 Function 构造函数,新增的默认参数和不定参数特性,使其具备了与声明式创建函数相同的能力。
展开运算符
- 不定参数可以让你指定多个各自独立的参数,并通过整合后的数组来访问。
- 展开运算符可以让你指定一个数组,将它们打散后作为各自独立的参数传入函数。
JavaScript 内建的 Math.max()
方法可以接受任意数量的参数,并返回值最大的那一个。
let value1 = 25,
value2 = 50;
console.log(Math.max(value1, value2)); // 50
如果想从一个数组中挑选出最大的那个值应该怎么做?而 Math.max()
方法又不允许传入数组。
使用 apply()
方法手动遍历:
let array = [25, 50, 75, 100];
console.log(Math.max.apply(Math, array)); // 100
使用 ECMAScript 6 中的展开运算符:向 Math.max()
方法中传入一个数组,再在数组前添加不定参数使用符号...
:
let array = [25, 50, 75, 100];
console.log(Math.max(...array)); // 100
可以将展开运算符与其他正常传入的参数混合使用:
let array = [-25, -50, -75, -100];
// 传入限定值 0 ,保证最小值为正数
console.log(Math.max(...array, 0)); // 0
综上,展开运算符可以简化使用数组给函数传参的编码过程。
name
属性
ECMAScript 6 为所有函数新增了 name
属性用于辅助辨别函数。
💡💡💡 函数 name
属性的值不一定引用同名变量,它只是协助调试用的额外信息,所以不能使用 name
属性的值来获取对函数的引用。
如何选择合适的名称
// 函数
function doSomething() {
// 空函数
}
// 函数表达式
var doAnotherThing = function() {
// 空函数
};
console.log(doSomething.name); // doSomething,对应声明的函数名
console.log(doAnotherThing.name); // doAnotherThing,对应被赋值为该匿名函数的变量名
name
属性的特殊情况
// 函数表达式的名字权重比变量名高
var doSomething = function doSomethingElse() {
// 空函数
};
var person = {
firstName() {
return 'Nicholas';
},
sayName: function() {
console.log(this.name);
}
};
console.log(doSomething.name); // doSomethingElse
console.log(person.sayName.name); // sayName
console.log(person.firstName.name); // firstName
- 通过
bind()
函数创建的函数,其名称将带有 “bound” 前缀。 - 通过 Function 构造函数创建的函数,其名称将是 “anonymous”。
明确函数的多重用途
function Person(name) {
this.name = name;
}
var person = new Person('Andy');
var notAPerson = Person('Andy');
console.log(person); // Person { name: 'Andy' }
console.log(notAPerson); // undefined
JavaScript 函数有两个不同的内部方法:[[Call]]
和 [[Construct]]
。
当通过 new
关键字调用函数时,执行的是 [[Construct]]
函数,它负责创建一个通常被称作实例的新对象,然后再执行函数体,将 this
绑定到实例上;
如果不通过 new
关键字调用函数,则执行 [[Call]]
函数,从而直接执行代码中的函数体。
具有 [[Construct]]
方法的函数被统称为构造函数。
在 ECMAScript 5 中判断函数被调用的方法
在 ECMAScript 5 中,如果想确定一个函数是否通过 new
关键字被调用(或者说,判断该函数是否作为构造函数被调用),最流行的方式是使用 instacneof
:
function Person(name) {
// 检查 this 值是否是构造函数的实例
// 因为 [[Construct]] 方法会创建一个 Person 的新实例,并将 this 绑定到新实例上。
if (this instanceof Person) {
this.name = name; // 如果通过 new 关键字调用
} else {
throw new Error('必须通过 new 关键字来调用 Person');
}
}
var person = new Person('Andy');
var notAPerson = Person('Andy'); // 抛出错误
// 例外:不依赖 new 方法也可以将 this 绑定到 Person 的实例上
var stillWorkPerson = Person.call(person, 'Andy'); // 有效!!!
console.log(person); // Person { name: 'Andy' }
console.log(notAPerson);
console.log(stillWorkPerson); // undefined
元属性 new.target
当调用函数的 [[Construct]]
方法时,new.target
被赋值为 new
操作符的目标。通常是新创建对象实例,也就是函数体内 this
的构造函数;
如果调用 [[Call]]
方法,则 new.target
被赋值为 undefined
。
function Person(name) {
if (typeof new.target !== "undefined") {
this.name = name; // 如果通过 new 关键字调用
} else {
throw new Error('必须通过 new 关键字来调用 Person');
}
}
块级函数
在 ES5 的严格模式下,在代码块内部声明函数时程序会抛出错误。ES6 中则视为块级函数,从而可以在定义该函数的代码块内访问和调用。
在代码块中,块级函数会被提升至块的顶部,而用 let
定义的函数表达式不会被提升。
在 ES6 的非严格模式下,块级函数不再被提升至代码块的顶部,而是提升至外围函数或全局作用域的顶部。
箭头函数
- 没有
this
、super
、arguments
和new.target
绑定。 - 不能通过
new
关键字调用。 - 没有原型。
- 不可以改变
this
的绑定。 - 不支持
arguments
对象。 - 不支持重复命名的参数。
箭头函数语法
- 当箭头函数只有一个参数时,可以直接写参数名,箭头紧跟其后,箭头右侧的表达式被求值后便立即被返回。
// 一个参数
let reflect = value => value;
// 实际上相当于
let reflect = function (value) {
return value;
}
- 如果要传入两个或两个以上的参数,要在参数的两侧添加一对小括号。
// 多个参数
let sum = (num1, num2) => num1 + num2;
// 实际上相当于
let sum = function (num1, num2) {
return num1 + Number;
}
- 如果函数没有参数,也要在声明的时候写一组没有内容的小括号。
- 如果函数体由多个表达式组成,需要用花括号
{}
包裹函数体。 - 箭头函数没有
this
绑定,必须通过查找作用域链来决定其值。如果箭头函数被非箭头函数包含,则this
绑定的是最近一层非箭头函数的this
。否则,this
的值会被设置为全局对象。
尾调用优化
尾调用指的是函数作为另一个函数的最后一条语句被调用。
在 ES5 中,尾调用会创建一个新的栈帧(stack frame),而在 ES 6 中,尾调用会清除并重用当前栈帧。
ES 6 尾调用需要满足的条件:
- 尾调用不访问当前栈帧的变量(也就是说函数不是一个闭包)。
- 在函数内部,尾调用是最后一条语句。
- 尾调用的结果作为函数的返回值。
尾调用优化的主要应用场景:递归函数
// 阶乘函数
function factorial(n) {
if (n <= 1) {
return 1;
} else {
// 无法优化,因为这里在返回后还执行了乘法操作
// 也就是说,如果在尾调用返回后还执行了其他操作,即无法得到尾调用优化
return n * factorial(n - 1);
}
}
// 通过默认参数将乘法操作移出 return 语句
// 用 p 来保存乘法结果,下一次迭代中取出用于计算,不再需要额外的函数调用
function factorial(n, p = 1) {
if (n <= 1) {
return 1 * p;
} else {
let result = n * p;
return factorial(n-1, result);
}
}
网友评论