Skip to content

深入理解 ES6 #3-函数

函数形参的默认值

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 的审核。

Page Layout Max Width

Adjust the exact value of the page width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the page layout
A ranged slider for user to choose and customize their desired width of the maximum width of the page layout can go.

Content Layout Max Width

Adjust the exact value of the document content width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the content layout
A ranged slider for user to choose and customize their desired width of the maximum width of the content layout can go.