Skip to content

深入理解 ES6 #3-函数

🏷️ 《深入理解 ES6》

函数形参的默认值

ES6 中的默认参数值

js
function makeRequest(url, timeout = 2000, callback = function() {}) {

}

可以为任意参数指定默认值,在已指定默认值的参数后可以继续声明无默认值参数。

js
function makeRequest(url, timeout = 2000, callback) {

}

这种情况下,之后当不为第二个参数传入值或者主动为第二个参数传入 undefined 时才会使用 timeout 的默认值

js
// 使用 timeout 的默认值
makeRequest("/foo", undefined, function(body) {
    doSomething(body);
})

// 使用 timeout 的默认值
makeRequest("/foo");

// 不使用 timeout 的默认值
makeRequest("/foo", null, function(body) {
    doSomething(body);
})

第三个调用需要注意。对于默认参数值,null 是一个合法值。

关于 nullundefined 的区别请参照阮一峰的 undefined 与 null 的区别

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

ES5 非严格模式下,函数命名参数的变化会体现在 arguments 对象中。

js
function mixArgs(first, second) {
    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", "b");
// 2
// true
// true
// true
// true

然而在ES5 严格模式下,取消了 arguments 对象这个令人感到困惑的行为。

命名参数与 arguments 对象分离开了。

js
function mixArgs(first, second) {
    "use strict"; // 设置为严格模式
    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", "b");
// 2
// true
// true
// false
// false

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

js
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

mixArgs("a", "b");
// 2
// true
// true
// false
// false

默认参数表达式

默认参数值可以是非原始值传参。

js
function getValue() {
    return 5;
}

function add(first, second = getValue()) {
    return first + second;
}

console.log(add(1, 1)); // 2
console.log(add(1)); // 6

上例中,second 的默认值为 getValue() 的返回值。

默认参数还可以使用先定义的参数作为后定义参数的默认值。

js
function add(first, second = first) {
    return first + second;
}

console.log(add(1, 1)); // 2
console.log(add(1)); // 2

还可以将上面两个例子合起来修改成如下形式:

js
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

在引用参数默认值时,只允许引用前面参数的值,即先定义的参数不能访问后定义的参数。

js
function add(first = second, second) {
    return first + second;
}

console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // 抛出错误
// Uncaught ReferenceError: second is not defined

因为 secondfirst 定义的晚,所以不能作为 first 的默认值。

这里就是所谓默认参数的临时死区(TDZ)。

Note

默认参数有自己的作用域和临时死区,其与函数体的作用域是各自独立的,也就是说参数的默认值不可访问函数体内声明的变量。

处理无命名参数

JS 的函数语法规定,无论函数已定义的命名参数有多少,都不限制调用时传入的实际参数的数量,调用时总是可以传入任意数量的参数。

ES5 中的无命名参数

JS 提供了 arguments 对象来检查函数的所有参数,从而不必定义每一个要用的参数。

js
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: "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() 函数返回一个给定对象的副本,包含原始对象的特定子集。

不定参数

ES6 中提供了不定参数(rest parameters)特性来提供更好的实现方案。

js
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;
}

Note

函数的 length 属性统计的是函数命名参数的数量,不定参数的加入不会影响 length 属性的值。上述 pick 方法的 length 值为 1.

不定参数的使用限制

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

  2. 不定参数不能用于对象字面量 setter 之中。

不定参数对 arguments 对象的影响

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

增强的 Function 构造函数

使用 Function 构造函数创建函数。

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

创建的 add 方法是如下样子的:

js
ƒ anonymous(first,second
/*``*/) {
return first + second
}

ES6 中增强了该构造函数,使其可以支持默认参数和不定参数。

js
var add = new Function("first", "second = first", "return first + second");
console.log(add(1, 1)); // 2
console.log(add(1)); // 2
js
var pickFirst = new Function("...args", "return args[0]");
console.log(pickFirst(1, 2)); // 1

展开运算符

在所有新功能中,与不定参数最相似的是展开运算符。

Math.max() 方法为例,该方法可以接受任意数量的参数并返回最大的那一个。

js
let value1 = 25,
    value2 = 50;

Math.max(value1, value2); // 50

但该方法不支持数组,如果需要从数组中挑出一个最大的时该怎么做呢?

ES5 中可以使用 apply() 方法实现该功能。

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

虽然可以实现该功能,但是难以理解。

ES6 中可以使用展开运算符 (...) 简化上述示例。

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

也可以将展开运算符与其它正常传入的参数混合使用。

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

name 属性

ES6 中所有的函数的 name 属性都有一个合适的值。可以帮助开发更好的追踪问题。

js
function doSomething() {

}

var doAnotherThing = function() {

};

console.log(doSomething.name); // 函数名称 "doSomething"
console.log(doAnotherThing.name); // 匿名函数的变量的名称 "doAnotherThing"

name 属性的特殊情况

js
var doSomething = function doSomethingElse() {

}

var person = {
    get 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); // "get firstName"
  • doSomething.name

    函数本身的名字权重更高

  • person.sayName.name

    其值取自对象字面量

  • person.firstName.name

    person.firstName 实际上是个 getter 函数,自动加上了前缀“get”。
    setter 函数也有其前缀“set”。

    另外通过 bind() 函数创建的函数,其名称带有“bound”前缀;
    通过 Function 构造函数创建的函数,其名称带有“anonymous”前缀。

js
var doSomething = function() {

};

console.log(doSomething.bind().name); // "bound doSomething"
console.log((new Function()).name); // "anonymous"

明确函数的多重用途

ES5 及早期版本中的函数具有多重功能,可以结合 new 使用,函数内的 this 值将指向一个新对象,函数最终返回这个新对象。

js
function Person(name) {
    this.name = name;
}

var person = new Person("JiaJia");
var notAPerson = Person("JiaJia");

console.log(person); // "Person {name: "JiaJia"}"
console.log(notAPerson); // "undefined"
console.log(window.name); // "JiaJia"

如果不同 new 关键字调用 Person 方法,不仅得不到想要的结果,还会在全局作用创建一个 name 属性。

在 ES5 中判断函数被调用的方法

使用 instanceof 操作符判断是会否是通过 new 关键字调用。

js
function Person(name) {
    if (this instanceof Person) {
        this.name = name;
    } else {
        throw new Error("必须通过 new 关键字来调用 Person。");
    }
}

var person = new Person("JiaJia");
var notAPerson = Person("JiaJia"); // 抛出错误

一般来说上述写法是有效的,但是也有例外情况。

因为有一种不依赖 new 关键字的方法也可以将 this 绑定到 person 的实例上。

js
function Person(name) {
    if (this instanceof Person) {
        this.name = name;
    } else {
        throw new Error("必须通过 new 关键字来调用 Person。");
    }
}

var person = new Person("JiaJia");
var notAPerson = Person.call(person, "XKA");
// 没有抛出错误,但也没有得到想要的对象
// 实际修改的是 person 实例的值

元属性 (Metaproperty) new.target

为了解决判断函数是否通过 new 关键字调用的问题,ES6 引入了 new.target 这个元属性。

元属性是指非对象的属性,其可以提供非对象目标的补充信息。

当调用函数的 [[Construct]] 方法时,new.target 被赋值为 new 操作符的目标,通常是新创建对象实例,也就是函数体内 this 的构造函数;

如果调用 [[call]] 方法,则 new.target 的值为 undefined

js
function Person(name) {
    if (typeof new.target !== "undefined") {
        this.name = name;
    } else {
        throw new Error("必须通过 new 关键字来调用 Person。");
    }
}

var person = new Person("JiaJia");
var notAPerson = Person.call(person, "XKA"); // 抛出错误

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

js
function Person(name) {
    if (typeof new.target === Person) {
        this.name = name;
    } else {
        throw new Error("必须通过 new 关键字来调用 Person。");
    }
}

function AnotherPerson(name) {
    Person.call(this, name);
}

var person = new Person("JiaJia");
var anotherPerson = new AnotherPerson("DLPH"); // 抛出错误

Note

在函数外使用 new.target 是一个语法错误。

块级函数

在代码块中声明的函数。

js
"use strict";

if (true) {
    console.log(typeof doSomething); // "function"

    function doSomething() {

    }

    doSomething();
}

console.log(typeof doSomething); // "undefined"

ES6 严格模式下,在定义函数的代码块内,块级函数会被提升至顶部。

块级函数与 let 函数表达式类似,一旦执行过程流出了代码块,函数定义立即被移除。

两者的区别是 let 定义的函数不会被提升。

js
"use strict";

if (true) {
    console.log(typeof doSomething); // 抛出错误
    // Uncaught ReferenceError: doSomething is not defined

    let doSomething = function () {

    }

    doSomething();
}

console.log(typeof doSomething);

非严格模式下的块级函数

在 ES6 的非严格模式下,块级函数会被提升至外围函数或全局作用域的顶部。

js
if (true) {
    console.log(typeof doSomething); // "function"

    function doSomething() {

    }

    doSomething();
}

console.log(typeof doSomething); // "function"

箭头函数

箭头函数是一种使用箭头(=>)定义函数的新语法,但是它与传统的 JS 函数有些不同。

  • 没有 thissuperargumentsnew.target 绑定
  • 不能通过 new 关键字调用
  • 没有原型
  • 不可以改变 this 的绑定
  • 不支持 arguments 对象
  • 不支持重复的命名参数

箭头函数语法

js
let reflect = value => value;

// 实际相当于
let reflect = function(value) {
    return value;
};

如果要传入两个或以上参数,要在参数的两侧添加一对小括号。

js
let sum = (num1, num2) => num1 + num2;

// 实际上相当于
let sum = function(num1, nume) {
    return num1 + num2;
};

如果没有参数,也要在声明的时候写一组没有内容的小括号。

js
let getName = () => "JiaJia";

// 实际上相当于
let getName = function() {
    return "JiaJia";
};

如果函数体是多行,则需要用花括号包裹函数体。

js
let sum = (num1, num2) => {
    return num1 + num2;
}

// 实际上相当于
let sum = function(num1, nume) {
    return num1 + num2;
};

除了 arguments 对象不能使用外,某种程度上你都可以将花括号里的代码视作传统的函数体定义。

如果想创建一个空函数,需要写一对没有内容的花括号。

js
let doNothing = () => {};

// 实际上相当于
let doNothing = function() {};

如果想在箭头函数外返回一个对象字面量,则需要将该字面量包裹在小括号里。

js
let getTempItem = id => ({ id: id, name: "Temp" });

// 实际上相当于
let getTempItem = function(id) {
    return {
        id: id,
        name: "Temp"
    };
};

创建立即执行函数表达式

js
let person = ((name) => {
    return {
        getName: function() {
            return name;
        }
    };
})("JiaJia");

console.log(person.getName()); // "JiaJia"

箭头函数没有 this 绑定

箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值。

如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this

否则 this 的值会被设置为 undefined

js
let PageHandler = {
    id: "123456",

    init: function() {
        document.addEventListener("click", event => this.doSomething(event.type), false);
    },

    doSomething: function(type) {
        console.log("Handling " + type + " fro " + this.id);
    }
};

这里 addEventListener 的第二个参数如果使用匿名函数的形式,则 this 是当前 click 事件目标对象(这里是 document)的引用。

而在本例中,this 就是 PageHandler 对象。

箭头函数缺少正常函数所拥有的 property 属性,所以不能用它来定义新的类型。

如果尝试用 new 关键字调用一个箭头函数,会导致程序抛出错误。

js
var MyType = () => {};
var object = new MyType(); // 抛出错误
// Uncaught TypeError: MyType is not a constructor

箭头函数中的 this 值取决于该函数外部非箭头函数的 this 值,且不能通过 call()apply()bind() 方法来改变 this 的值。

箭头函数和数组

箭头函数的语法简洁,非常适用于数组处理。

js
var result = values.sort(function(a, b) {
    return a - b;
});

可以简化为如下形式

js
var result = values.sort((a, b) => a - b);

箭头函数没有 arguments 绑定

箭头函数没有自己的 arguments 绑定,且无论函数在哪个上下文中执行,箭头函数始终可以访问外围函数的 arguments 对象。

js
function createArrowFunctionReturningFirstArg() {
    return () => arguments[0];
}

var arrowFunction = createArrowFunctionReturningFirstArg(5);

console.log(arrowFunction); // 5

尾调用优化

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

js
function doSomething() {
    return doSomethingElse(); // 尾调用
}

在 ES5 中,尾调用的实现与其它函数调用的实现类似:创建一个新的栈帧(stack frame),将其推入调用栈来表示函数调用。

也就是说,在循环调用中,每一个未用完的栈帧都会保存在内存中,当调用栈变得过大时会造成程序问题。

ES6 中的尾调用优化

ES6 缩减了严格模式下尾调用栈的大小(非严格模式下不受影响),如果满足以下条件,尾调用不再创建新的栈帧,而是清除并重用当前栈帧。

  • 尾调用不访问当前栈帧的变量(也就是说函数不是一个闭包)
  • 在函数内部,尾调用是最后一条语句
  • 尾调用的结果作为函数返回
js
"use strict";

function doSomething() {
    // 可优化
    return doSomethingElse(); // 尾调用
}

以下形式均无法优化

js
"use strict";

function doSomething() {
    // 不可优化
    doSomethingElse();
}
js
"use strict";

function doSomething() {
    // 不可优化
    return 1 + doSomethingElse();
}
js
"use strict";

function doSomething() {
    // 不可优化
    var result = doSomethingElse();
    return result;
}
js
"use strict";

function doSomething() {
    var num = 1,
        func = () => num;
    // 不可优化,该函数是一个闭包
    return func();
}

如何利用尾优化

递归函数是主要的应用场景,此时尾调用优化的效果最显著。

优化前:

js
function factorial(n) {
    if (n <= 1) {
        return 1;
    } else {
        // 无法优化,必须在返回之后执行乘法操作
        return n * factorial(n - 1);
    }
}

优化后:

js
function factorial(n, p = 1) {
    if (n <= 1) {
        return 1 * p;
    } else {
        return factorial(n - 1, n * p);
    }
}

WARNING

通过在谷歌浏览器上测试,好像没有起作用,优化后的代码依然会栈溢出。
这本书作者在写时,这个特性仍在审核中。估计是该优化没有通过 ES6 的审核。