由于JavaScript
开发者多年的不断抱怨和呼吁,ES6
终于大力度地更新了函数特性,在ES5
基础上进行了许多改进。
函数形参的默认值
ES5形参默认值的实现
在ES5
中,你很可能通过以下这种方式为函数赋予默认值:
function makeRequest(url, timeout, callback) {
timeout = timeout || 2000;
callback = callback || function() {};
// 函数的其余部分
}
对于函数的命名参数,如果不显式传值,则其默认值为undefined
。因此经常是使用逻辑或操作符来为缺失的参数提供默认值。然而这个方式有缺陷,如果给第二个形参timeout
传入值0,尽管这个值是合法的,也会被视为false
值,对函数调用方来说,timeout
非预期的被修改为2000。
更安全的选择是通过typeof
检查参数类型,就像这样:
function makeRequest(url, timeout, callback) {
timeout = (typeof timeout !== 'undefined') ? timeout : 2000;
callback = (typeof callback !== 'undefined') ? callback : function() {};
// 函数的其余部分
}
在流行的JavaScript
库中均使用类似的模式进行默认值补全。
ES6形参默认值的实现
ES6
简化了为形参提供默认值的过程,定义形参时即可指定初始值:
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){ // null是一个合法值
doSomething(body);
});
ES6默认参数表达式
ES6
中,关于默认参数值,还可以是非原始值传参,就像这样:
function getValue() {
return 5;
}
function add(first, second = getValue()) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 6
尤其注意,初次解析函数声明时是不会调用getValue()方法的,只有当调用add()函数且不传入第二个参数时才会被调用。另外,second = getValue()这里的小括号()不要忘记掉,不然最终传入的是对函数的引用,而不是函数调用的结果。
正因为默认参数是在函数调用时求值,所以可以使用先定义的参数作为后定义的参数的默认值,就像这样:
function add(first, second = first) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 2
也可以将参数first
传入一个函数来获得参数second
的值,就像这样:
function getValue(value) {
return value + 5;
}
function add(first, second = getValue(first)) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7
默认参数的临时死区
在引用参数默认值的时,只允许引用前面参数的值,即先定义的参数不能访问后定义的参数:
function add(first = second, second) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // 抛出错误
在讲解let
和const
时介绍了临时死区(TDZ),其实默认参数也有同样的临时死区。上面这个示例,调用add(1, 1)和add(undefined, 1)相当于引擎在背后做了如下事情:
// 表示调用add(1, 1)时的JavaScript代码
let first = 1;
let second = 1;
// 表示调用add(undefined, 1)时的JavaScript代码
let first = second;
let second = 1;
这个示例中,调用add(undefined, 1)函数,因为当first
初始化时second
尚未初始化,所以会导致程序抛出错误,此时second
(已声明,未初始化)尚处于临时死区中,正如讨论let
绑定时所说的那样,任何引用临时死区中的绑定的行为都会报错。
函数参数有自己的作用域和临时死区,其与函数体的作用域是各自独立的,也就是说参数的默认值不可访问函数体内声明的变量。
ES6形参默认值不再影响arguments对象
当使用默认参数值时,arguments
对象的行为与以往有所不同。在ES5
非严格模式下,函数命名参数的变化会体现在arguments
对象中:
function mixArgs(first, second) {
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d";
console.log(first === arguments[0]);
consolo.log(second === arguments[1]);
}
mixArgs("a", "b");
这段代码会输出:
true
true
true
true
在ES5
非严格模式下,命名参数的变化会同步更新到arguments
对象中,所以当first
和second
被赋予新值时,arguments[0]和arguments[1]相应地也就更新了,最终所有===全等结果都为true
。
在ES5
严格模式下,取消了arguments
对象的这个令人感到困惑的行为,无论参数如何变化,arguments
对象不再随之改变。ES6
中,arguments
对象的行为与ES5
严格模式下保持一致。arguments
对象保持与命名参数分离,这个微妙的细节将影响你使用arguments
对象的方式,请看以下代码:
function mixArgs(first, second = "b") {
console.log(arguments.length);
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d";
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a");
这段代码会输出以下内容:
1
true
false
false
false
false
改变first
和second
并不会影响arguments
对象。总是可以通过arguments
对象将参数恢复为初始值。
处理无命名参数
JavaScript
的函数语法规定,无论函数已定义的命名参数有多少,都不限制调用时传入的实际参数数量。当传入更少数量的参数时,默认参数值的特性可以有效简化函数的声明;当传入更多数量的参数时,ES6
同样也提供了更好的方案。
ES5中的无命名参数
早先,我们用JavaScript
提供的arguments
对象来检查函数的所有参数,从而不必定义每一个要用的参数,看下面的例子:
function pick(object) {
let result = Object.create(null);
// 从第二个参数开始
for(let i = 1, len = arguments.length; i < len; i++) {
result[arguments[i]] = object[arguments[i]];
}
return result;
}
let book = {
title: "ECMAScript 6",
author: "Nicholas",
year: 2016
};
let bookData = pick(book, "author", "year");
console.log(bookData.author); // "Nicholas"
console.log(bookData.year); // 2016
这个函数模仿了Underscore.js
库中的pick()方法,返回一个给定对象的副本,包含原始对象属性的特定子集。在这个示例中只定义了一个参数,第一个参数传入的是被复制的源对象,其他参数为被复制属性的名称。
关于pick()函数应该注意这样几件事:首先,并不容易发现这个函数可以接受任意数量的参数,当然,可以定义更多的参数,但是怎么也达不到要求;其次,因为第一个参数为命名参数并已被使用,当你要查找需要拷贝的属性名称时,不得不从索引1而不是索引0开始遍历arguments
对象。牢记真正的索引位置并不难,但这总归是我们需要牵挂的问题。
ES6
中,通过引入不定参数(rest parameters
)的特性可以轻易解决这些问题。
ES6的不定参数
在函数的命名参数前添加三个点(...)就表明这是一个不定参数,该参数为一个数组,包含着自它之后传入的所有参数。使用不定参数重写pick()函数:
function pick(object, ...keys) {
let result = Object.create(null);
for(let i = 0, len = keys.length; i < len; i++) {
result[keys[i]] = object[keys[i]];
}
return result;
}
只需要看一眼就知晓该函数可以处理的参数数量。
注意一下,函数的length
属性统计的是函数命名参数的数量,不定参数的加入不会影响函数length
属性。在本示例中pick()函数的length
值为1,因为只会计算object
。另外每个函数最多只能声明一个不定参数,并且一定要放在末尾。
不定参数对arguments对象的影响
不定参数的设计初衷是代替JavaScript
的arguments
对象的。但是arguments
对象依然存在。如果声明函数时定义了不定参数,则在函数被调用时,arguments
对象包含了所有传入函数的参数,就像这样:
function checkArgs(...args) {
console.log(args.length);
console.log(arguments.length);
console.log(args[0], arguments[0]);
console.log(args[1], arguments[1]);
}
checkArgs("a", "b");
调用checkArgs(),输出以下内容:
2
2
a a
b b
无论是否使用不定参数,arguments
对象总是包含所有传入函数的参数。
展开运算符配合不定参数
展开运算符可以让你指定一个数组,将它们打散后作为各自独立的参数传入函数。正好,不定参数希望的就是指定多个各自独立的参数,并通过整合后的数组来访问:
function max(...args) {
// 计算并返回最大值
}
let values = [10, 20, 30, 40];
// 使用展开运算符打散
max(...values);
// 等价于
// max(10, 20, 30, 40)
构造函数
ES5
及早期版本中的函数具有多重功能,可以结合new
使用,函数内的this
值将指向一个新对象,函数最终会返回这个新对象:
function Person(name) {
this.name = name;
}
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas");
console.log(person); // "[Object Object]"
console.log(notAPerson); // "undefined"
JavaScript
函数有两个不同的内部方法:[[Call]]和[[Construct]]。当通过new
关键字调用函数时,执行的是[[Construct]]函数,它负责创建一个通常被称作实例的新对象,然后再执行函数体,将this
绑定到实例上,并返回这个对象;如果不通过new关键字调用函数,则执行[[Call]]函数,从而直接执行代码中的函数体。具有[[Construct]]方法的函数被统称为构造函数。
不是所有函数都有[[Construct]]方法,因此不是所有函数都可以通过new
来调用,比如ES6
的箭头函数就没有这个[[Construct]]。
ES5判断函数是否用new调用
ES5
中,如果想判断一个函数是否通过new
关键字被调用,最流行的方式是使用instanceof
:
function Person(name) {
if(this instanceof Person) {
this.name = name; // 如果通过new关键字调用
} else {
throw new Error("必须通过new关键字来调用Person。");
}
}
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas"); // 抛出错误
但是这个方法也不完全可靠,因为有一种不通过new
关键字的方法也可以将this
绑定到Person
的实例上:
function Person(name) {
if(this instanceof Person) {
this.name = name;
} else {
throw new Error("必须通过new关键字来调用Person。");
}
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // 有效!
调用Person.call()时将变量person
传入作为第一个参数,相当于在Person
函数里将this
设为了person
实例。对于函数本身,无法区分是通过Person.call()(或者是Person.apply())还是new关键字调用得到的Person
的实例。
ES6判断函数是否用new调用
为了解决判断函数是否通过new关键字调用的问题,ES6
引入了new.target
这个元属性。当调用函数的[[Construct]]方法时,new.target
被赋值为新创建对象实例;如果调用[[Call]]方法,则new.target
的值为undefined
。
可以通过检查new.target是否被定义过来安全的检测一个函数是否是通过new关键字调用的,就像这样:
function Person(name) {
// typeof new.target === Person 可以检查是否被某个特定的构造函所调用
if(typeof new.target !== "undefined") {
this.name = name;
} else {
throw new Error("必须通过new关键字来调用Person。");
}
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // 抛出错误!
箭头函数
在ES6
中,箭头函数是最有趣的新增特性。它与传统的JavaScript
函数有些许不同,主要集中在以下方面:
没有this、super、arguments和new.target绑定
箭头函数中的this、super、arguments及new.target这些值由外围最近一层非箭头函数决定。不能通过new关键字调用
箭头函数没有[[Construct]]方法,所以不能被用作构造函数,如果通过new关键字调用箭头函数,程序会抛出错误。没有原型
由于不可以通过new关键字调用箭头函数,因而没有构建原型的需求,所以箭头函数不存在prototype这个属性。不可以改变this的绑定
函数内部的this值不可被改变,在函数的生命周期内始终保持一致。不支持arguments对象
箭头函数没有arguments绑定,所以你必须通过命名参数和不定参数这两种形式访问函数的参数。不支持重复的命名参数
箭头函数不支持重复的命名参数;而在传统函数的规定中,只有在严格模式下才不能有重复的命名参数。
this
绑定是JavaScript
程序中常见的错误来源,在函数内很容易就对this
的值失去控制,其经常导致程序出现意想不到的行为,箭头函数消除了这方面的烦恼。
箭头函数的语法
单一参数:
let reflect = value => value;
// 相当于
let reflect = function(value) {
return value;
};
两个以上参数:
let sum = (num1, num2) => num1 + num2;
// 相当于
let sum = function(num1, num2) {
return num1 + num2;
};
没有参数:
let getName = () => "Nicholas";
// 相当于
let getName = function(){
return "Nicholas";
};
多表达式组成的更传统的函数体:
let sum = (num1, num2) => {
return num1 + num2;
};
// 相当于
let sum = function(num1, num2) {
return num1 + num2;
};
空函数:
let doNothing = () => {};
// 相当于
let doNothing = function(){};
返回对象字面量:
let getTempItem = id => ({ id: id, name: "Temp" });
// 相当于
let getTempItem = function(id){
return {
id: id,
name: "Temp"
};
};
将对象字面量包裹在小括号中是为了将其与函数体区分开来。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。