《深入理解ES6》之函数

函数形参的默认值

JavaScript函数语法规定,无论在函数定义中声明了多少形参,调用时可以传入任意数量的参数。可以在定义时,当已定义的形参无对应的传入参数时为其指定一个默认值。

在ES5中模拟默认参数

ES5和早期版本中,可能通过如下方法模拟默认参数

1
2
3
4
5
6
function makeRequest(url, timeout, callback){
timeout = (typeof timeout !== 'undefined')? timeout: 2000;
callback = (typeof callback !== 'undefined')? callback: function(){};
//函数其余部分
}

示例中,timeoutcallback为可选参数,如果没有传入相应的参数,会有一个默认值。对于函数命名参数,如果不显示传值,默认为undefined。在模拟默认参数时,最好不要这样写timeout = timeout || 2000,因为,如果想给timeout传入0,即使这个值合法,也会被视为false,最终timeout赋值为2000。

ES6中的默认参数

1
2
3
function makeRequest(url, timeout=2000, callback=function() {}){
//函数其余部分
}

ES6简化了为形参提供默认参数的过程,声明函数时,可以为任意参数指定默认值,在已指定默认值的参数后面,可以继续声明无默认值参数。只有不为参数传入值或者 传入值为undefined 时,对应参数才会使用默认值。

1
2
3
4
5
6
7
8
9
10
11
12
//使用timeout默认值
makeRequest('/foo',undefined,function(body){
doSomething(body)
})
//使用timeout默认值
makeRequest('/foo')
//不使用timeout默认值
makeRequest('/foo', null, function(body){
doSomething(body)
})

默认参数值对arguments对象的影响

在ES5非严格模式中,命名参数的变化会同步更新到arguments对象,严格模式中arguments对象不随命名参数变化而变化。

1
2
3
4
5
6
7
8
9
10
function mixArgs(first, second){
// 'use strict'
console.log(first === arguments[0]); //true
console.log(second === arguments[1]); //true
first = 'c';
second = 'd';
console.log(first === arguments[0]); //true
console.log(second === arguments[1]); //true
}
mixArgs('a', 'b');

当定义为严格模式时,输出为truetruefalsefalse

ES6中,函数使用了默认参数,无论是否显式定义了严格模式,arguments对象行为都与ES5中的严格模式保持一致。

1
2
3
4
5
6
7
8
9
function mixArgs(first, second='b'){
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')

以上输出为truefalsefalsefalsefirstsecond并不会影响arguments对象,其中arguments[1]===undefined

默认参数表达式

函数默认参数值非原始值传参时,默认参数是在函数调用时求值,也就是说在函数声明时,参数默认值是不确定的。

1
2
3
4
5
6
7
8
9
10
let value = 5;
function getValue(){
return value++
}
function add(first, second = getValue()){
return first + second
}
console.log(add(1,1)); //2
console.log(add(1)); //6
console.log(add(1)); //7

示例中,调用add不给second传值,就会调用getValue()second求默认值,所以任何时候都可以改变默认值。

因为默认参数是函数调用时求值,所以可以使用先定义的参数做为后定义参数的默认值。

1
2
3
4
5
function add(first, second = first){
return first + second
}
console.log(add(1,1));
console.log(add(1));

先定义的参数访问了后定义的参数,会抛出错误。

1
2
3
4
5
function add(first=second, second){
return first + second
}
console.log(add(1,1)); //2
console.log(add(undefined, 1)); //抛出错误

这是由于函数参数有自己的临时死区,定义参数时会为每个参数创建一个新的标识符绑定,该绑定在初始化之前不可被引用。

上面示例中调用add(1, 1)add(undefined, 1)时,相当于

1
2
3
4
5
6
7
//调用add(1, 1)
let first = 1;
let second = 1;
//调用add(undefined, 1)
let first = second;
let second = 1;

first初始化时second尚未初始化,此时second处于临时死区中,所以会导致程序抛出错误。

处理无命名参数

早先,利用arguments来检查函数的所有参数,而不必定义每个要用的参数。ES6中,通过引入不定参数的特性来解决这些问题。

不定参数

在函数的命名参数前添加三个点(...)就表明这是一个不定参数,该参数是一个数组,包含着自它之后传入的所有参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
}
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

不定参数keys包含的是object之后传入的所有参数。

函数的length属性统计的是函数命名参数的数量,不包括不定参数。pick.length=1

注意

  • 每个函数最多只能声明一个不定参数,而且要放在所有参数的末尾。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //语法错误:不定参数后不能有其他命名参数
    function pick(object, ...keys, last){
    let result = Object.create(null);
    for(let i = 0, len = keys.length; i < len; i++){
    result[keys[i]] = object[keys[i]]
    }
    return result
    }
  • 不定参数不能用户对象字面量setter之中,因为对象字面量setter的参数有且只能有一个。

  • 无论是否使用不定参数,arguments对象总是包含所有传入函数的参数。

增强的Function构造函数

Function构造函数通常用来动态创建新的函数,该构造函数接受字符串形式的参数,分别为函数的参数及函数体。

1
2
var add = new Function('first', 'second', 'return first + second');
console.log(add(1, 1)); //2

ES6增强了Function构造函数的功能,支持在创建函数时定义默认参数和不定参数。

1
2
3
4
5
6
var add = new Function('first', 'second = first', 'return first + second');
console.log(add(1, 1)); //2
console.log(add(1)); //2
var pickFirst = new Function('...args', 'return args[0]');
console.log(pickFirst(1, 2)); //1

展开运算符

展开运算符可以将一个数组打散后作为各自独立的参数传入函数。例如,要从一个数组的中找出最大值,我们可以利用Math.max()方法,但Math.max()方法不允许传入数组。在ES5及早期版本,可能实现如下

1
2
let values = [25, 50, 75, 100];
console.log(Math.max.apply(Math, values)); //100

在ES6中,在数组前添加...符号,就会将参数数组分割为各自独立的参数依次传入

1
2
let values = [25, 50, 75, 100];
console.log(Math.max(...values)); //100

此外,展开运算符还可以与其他正常传入的参数混合使用

1
2
let values = [-25, 50, -75, 100];
console.log(Math.max(...values, 0)); //100

name属性

ES6给所有函数新增了name属性,用来辨别函数。

1
2
3
4
5
6
7
8
9
function doSomething() {
//空函数
}
let doAnotherThing = function() {
//空函数
}
console.log(doSomething.name); // 'doSomething'
console.log(doAnotherThing.name); //'doAnotherThing'

函数声明函数name属性对应着声明时的函数名称,匿名函数表达式函数name属性对应着被赋值变量的名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let doSomething = function doSomethingElse() {
//空函数
}
var person = {
get firstName() {
return 'Nicholas'
},
sayName: function() {
console.log(this.name);
}
}
var descriptor = Object.getOwnPropertyDescriptor(person, 'firstName');
console.log(doSomething.name); //'doSomethingElse'
console.log(person.sayName.name); //'sayName'
console.log(descriptor.get.name); //'get firstName'

函数表达式有一个名字,该名字比函数本身被赋值的变量权重高,所以doSomething.name的值为doSomethingElsefirstName是一个getter函数,他的名称会有get前缀,setter函数名称也有前缀set

1
2
3
4
5
var doSomething = function() {
//空函数
}
console.log(doSomething.bind().name); //'bound doSomething'
console.log((new Function()).name); //'anonymous'

通过bind()函数创建的函数,其名称带有’bound’前缀,使用Function构造函数创建的函数,其名称为’anonymous’。

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/name

明确函数的多重用途

ES5及早起版本,函数具有多重功能,可以结合new作为构造函数使用,返回一个对象。

函数有两个不同的内部方法:[[Call]][[Construct]]。通过new关键字调用函数时,执行的是[[Construct]]函数,它负责创建实例,然后执行函数体,将this绑定到实例上。如果不是通过new关键字调用,则执行[[Call]]函数,从而执行代码中的函数体。

具有[[Construct]]方法的函数统称为构造函数,不是所有函数都有[[Construct]]方法,比如箭头函数。

ES5中判断函数是否作为构造函数调用

在ES5中,判断函数是否通过new关键字被调用,流行的方式如下

1
2
3
4
5
6
7
8
9
10
11
function Person(name){
if(this instanceof Person){
this.name = name
} else {
throw new Error('必须通过new关键字来调用Person')
}
}
var person = new Person('Nicholas');
var notAPerson1 = Person('Nicholas'); //抛出错误
//var notAperson = Person.call(person, 'Michael'); //有效

这个方法并不完全可靠,因为可以通过call()或者apply()方法将this绑定到Person的实例上。

元属性new.target

元属性是指非对象属性,其可以提供非对象目标的补充信息(例如new)。当函数调用[[Construct]]方法时,new.target被赋值为new操作符的目标,通常是新创建实例的构造函数。当函数调用[[Call]]方法,则new.target的值为undefined

1
2
3
4
5
6
7
8
9
10
function Person(name){
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'); //抛出错误

也可以检查是否被某个特定构造函数所调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name){
if(new.target === Person){
this.name = name
} else {
throw new Error('必须通过new关键字来调用Person')
}
}
function AnotherPerson(name){
Person.call(this, name)
}
var person = new Person('Nicholas');
var anotherPerson = new AnotherPerson('Nicholas'); //抛出错误

new AnotherPerson('Nicholas')调用时,真正的调用Person.call(this, name)没有使用关键词,因此new.target的值为undefined抛出错误。

块级函数

ES5中,严格模式下,当在代码块内部声明函数时,程序抛出错误。ES6中,严格模式,在代码块中的声明的函数,会被提升块级作用域顶部,函数表达式不会被提升,一旦代码块结束执行,函数销毁。

1
2
3
4
5
6
7
8
9
10
11
12
13
'use strict'
if(true) {
console.log(typeof doSomething); //'function'
//console.log(typeof doAnotherThing); //抛出错误
function doSomething() {
//空函数
}
let doAnotherThing = function() {
//空函数
}
}
console.log(typeof doSomething); //'undefined'

当执行到typeof doAnotherThing时,由于尚未执行let语句声明,doAnotherThing还在当前块作用域的临时死区,程序被迫终端执行。

ES6规定,函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。

非严格模式下,可以在外围函数或全局作用域访问和调用(定义后?)。

1
2
3
4
5
6
7
8
console.log(typeof doSomething); //'undefined' node v8.9.0
if(true) {
console.log(typeof doSomething); //'function'
function doSomething() {
//空函数
}
}
console.log(typeof doSomething); //'function'

箭头函数

箭头函数是一种使用箭头(=>)定义函数的新语法,它与以前的函数有些许不同。

  • 没有thissuperargumentnew.target绑定,这些值都由外围最近一层非箭头函数决定。
  • 不能通过new关键字调用,因为箭头函数没有[[Construct]]方法。
  • 没有原型,由于不能通过new关键字调用,因而没有构建原型的需求,所以箭头函数不存在prototype这个属性
  • 不支持重复命名参数,无论在严格模式还是非严格模式。传统函数只有在严格模式下才不支持。

箭头函数语法

当箭头函数只有一个参数时,可以直接写参数名,箭头紧随其后,箭头右侧的表达式被求值后便立即返回。当箭头函数有多个参数时,要给参数加上括号。

1
2
3
4
5
6
7
8
9
10
11
12
13
let reflect = value => value;
//相当于
let reflect = function(value){
return value
}
let sum = (num1, num2) => num1 + num2;
//相当于
let sum = function(num1, num2){
return num1 + num2;
}

当函数没有参数时,也要在声明的时候写一对空括号。当函数体有多个表达式时,需要用花括号包裹函数体,如果需要返回值,就得显示定义一个返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let getName = () => 'Nicholas';
//相当于
let getName = function() {
return 'Nicholas'
}
let min = (num1, num2) => {
if(num1 > num2){
return num2
}
return num1
}
//相当于
let nim = function(num1, num2){
if(num1 > num2){
return num2
}
return num1
}

当函数体只有一个表达式,并且返回一个对象字面里时,需要将其包裹在小括号内,以区分函数体。

1
let getTemItem = id => ({id: id, name: 'Temp'})

尾调优化

尾调用指的是函数做为另一个函数的最后一条语句调用

1
2
3
function doSomething() {
return doSomethingElse()
}

在ES5的引擎中,尾调用的实现与其他函数调用的实现类似:创建一个新的栈帧,将其推入调用栈来表示函数调用。也就是说在循环调用中,每一个未用完的栈帧都会保存在内存中,当调用栈变得过大时会造成程序问题。

ES6中的尾调优化

ES6,在严格模式下,如果满足以下条件,JavaScript引擎自动优化(主要看引擎支持),尾调用不再创建新的栈帧,而是清除并重用当前栈帧。

  • 尾调用不妨问当前栈帧的变量(也就是说函数不是一个闭包)
  • 在函数内部,尾调用时最后一条语句
  • 尾调用结果作为函数值返回
    1
    2
    3
    4
    5
    'use strict'
    function doSomething() {
    //优化后
    return doSomethingElse()
    }

这个函数中,尾调用doSomethingElse的结果立即返回,不调用任何局部作用域变量。

1
2
3
4
5
'use strict'
function doSomething() {
//无法优化,无返回
doSomethingElse()
}

这个函数,不返回最终结果,无法被优化

1
2
3
4
5
'use strict'
function doSomething() {
//无法优化,返回值执行其他操作
return 1 + doSomethingElse()
}

在尾调用返回后执行其他操作,无法被优化

1
2
3
4
5
6
'use strict'
function doSomething() {
//无法优化,调用不再尾部
let result = doSomethingElse()
return result
}

把函数调用的结果存储在一个变量里,最后再返回这个变量,由于没有立即返回doSomethingElse函数的值,无法被优化

1
2
3
4
5
6
7
'use strict'
function doSomething() {
let num = 1,
func = () => num;
//无法优化,该函数是一个闭包
return func();
}

func函数访问局部变量num,无法被优化

利用尾调用优化

1
2
3
4
5
6
7
8
function factorial(n){
if(n <= 1){
return 1;
}else{
//无法优化
return n * factorial(n - 1)
}
}

上面是一个阶乘函数,由于在递归调用前执行了乘法操作,所以阶乘函数无法被优化

1
2
3
4
5
6
7
8
9
function factorial(n, p = 1){
if(n <= 1){
return p * 1;
}else{
//优化后
let result = n * p;
return factorial(n - 1, result)
}
}

这个重写后的factorial函数,参数p用来保存乘法结果,下一次迭代可以取出它用于计算。在线ES6引擎就可以优化递归调用了。

ES6的尾调优化,最终还是取决于JavaScript引擎是否支持。