代码改变世界

【读书笔记】【深入理解ES6】#3-函数

2017-11-23 16:52  佳佳的博客  阅读(205)  评论(0编辑  收藏  举报

函数形参的默认值

ES6中的默认参数值

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

第三个调用需要注意。对于默认参数值,null是一个合法值。
关于 null 和 undefined 的区别请参照阮一峰的 undefined与null的区别

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

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

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 对象分离开了。

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严格模式下保持一致。

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

默认参数表达式

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

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() 的返回值。

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

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

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

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

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)); // 抛出错误
// Uncaught ReferenceError: second is not defined

因为second比first定义的晚,所以不能作为fist的默认值。
这里就是所谓默认参数的临时死区(TDZ)。

Note

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

处理无命名参数

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

ES5中的无命名参数

JS提供了 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: "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)特性来提供更好的实现方案。

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 构造函数创建函数。

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

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

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

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

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()方法为例,该方法可以接受任意数量的参数并返回最大的那一个。

let value1 = 25,
    value2 = 50;

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

但该方法不支持数组,如果需要从数组中挑出一个最大的时该怎么做呢?
ES5中可以使用apply()方法实现该功能。

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

虽然可以实现该功能,但是难以理解。
ES6中可以使用展开运算符(...)简化上述示例。

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

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

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

name 属性

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

function doSomething() {

}

var doAnotherThing = function() {

};

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

name 属性的特殊情况

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”前缀。
var doSomething = function() {

};

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

明确函数的多重用途

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

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关键字调用。

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的实例上。

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。

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 是否被某个特定构造函数所调用

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 是一个语法错误。

块级函数

在代码块中声明的函数。

"use strict";

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

    }
    
    doSomething();
}

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

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

块级函数与let函数表达式类似,一旦执行过程流出了代码块,函数定义立即被移除。
两者的区别是let定义的函数不会被提升。

"use strict";

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

    }
    
    doSomething();
}

console.log(typeof doSomething);

非严格模式下的块级函数

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

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

    }
    
    doSomething();
}

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

箭头函数

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

  • 没有 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, nume) {
    return num1 + num2;
};

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

let getName = () => "JiaJia";

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

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

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

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

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

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

let doNothing = () => {};

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

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

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

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

创建立即执行函数表达式

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

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

箭头函数没有this绑定

箭头函数中没有this绑定,必须通过查找作用域链来决定其值。
如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this;
否则this的值会被设置为undefined。

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关键字调用一个箭头函数,会导致程序抛出错误。

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

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

箭头函数和数组

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

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

可以简化为如下形式

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

箭头函数没有arguments绑定

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

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

var arrowFunction = createArrowFunctionReturningFirstArg(5);

console.log(arrowFunction); // 5

尾调用优化

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

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

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

ES6中的尾调用优化

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

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

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

以下形式均无法优化

"use strict";

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

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

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

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

如何利用尾优化

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

优化前:

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

优化后:

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

Warning

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