《Understanding ES6》Chapter3 functions

202 阅读6分钟

2019-6-11

第三章 函数

(一)函数参数

ES5 中模拟参数默认值:

function makeRequest(url, timeout, callback) {

    timeout = timeout || 2000;
    callback = callback || function() {};

    // the rest of the function

}

缺点:当参数为0时,也会取到默认值。

ES6的参数默认值

1. ES6提供了参数默认值

只有当参数完全等于undefined时,才会使用默认值。拥有默认值的参数被认为是可选的。

2. 参数默认值对arguments对象的影响
// ES5非严格模式下,arguments对象会随出具名参数的变化而变化;
function mixArgs(first, second) {
    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");
// ES5严格模式下,arguments对象与具名参数分离,始终表示初始调用状态;
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]); //false
    console.log(second === arguments[1]); //false
}

mixArgs("a", "b");
// 使用ES6参数默认值:无论是否在严格模式下,表现总是与ES5严格模式一致
function mixArgs(first, second = "b") {
    console.log(arguments.length); //1
    console.log(first === arguments[0]); //true
    console.log(second === arguments[1]); //false
    first = "c";
    second = "d"
    console.log(first === arguments[0]); //false
    console.log(second === arguments[1]); //false
}

mixArgs("a");

3. 参数默认值表达式

参数的默认值不一定是基本数据类型,还可以通过执行函数来产生默认值。注意,在函数声明时,并不会调用参数默认表达式,只有在函数调用未传参时,才会执行参数默认表达式,所以参数的默认值也有可能是可变的。

4. 参数默认值的暂时性死区(TDZ)
//前面的参数,可以被后面的参数引用
function add(first, second = first) {
    return first + second;
}

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

//相当于
// JavaScript representation of call to add(1, 1)
let first = 1;
let second = 1;

// JavaScript representation of call to add(1)
let first = 1;
let second = first;

//前面的参数不能引用后面的参数
function add(first = second, second) {
    return first + second;
}

console.log(add(1, 1));         // 2
console.log(add(undefined, 1)); // throws error

函数参数拥有各自的作用域和暂时性死区,与函数的作用域相分离,这意味着参数的默认值不允许访问在函数体内部声明的任意变量。

使用剩余参数

…具名参数:包含传递给函数的其余参数的一个数组。

  // 函数的length属性值是第一个默认参数之前具名参数的个数
    function example(first, second = "whoo", third) {

    }
    console.log(example.length); //1

	// 剩余参数不会算入函数的参数个数
    function example2(first, ...others) {

    }
    console.log(example2.length); //1

剩余参数的限制条件:

  1. 一个函数只能有一个剩余参数,且必须放在最后面;

2)不能在对象字面量的setter函数中使用,因为对象字面量的setter函数被限定只能有一个参数,而剩余参数是不限制参数数量的。

(二)函数构造器

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

(三)扩展运算符...

扩展运算符将一个数组分割,并将各个项作为分离的参数传给函数,而剩余参数把多个参数合并为一个数组;

用途:作为apply()方法的替代品。

扩展运算符也可以在Function构造器中使用。

(四)函数名称

function doSomething() {
    // ...
}
var doAnotherThing = function() {
    // ...
};
console.log(doSomething.name);          // "doSomething"
console.log(doAnotherThing.name);       // "doAnotherThing"

var doSomething = function doSomethingElse() {
    // ...
};
console.log(doSomething.name);      // "doSomethingElse"

var person = {
    get firstName() {
        return "Nicholas"
    },
    sayName: function() {
        console.log(this.name);
    }
}
console.log(person.sayName.name);   // "sayName"
var descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
console.log(descriptor.get.name); // "get firstName"

var doSomething = function() {
    // ...
};
console.log(doSomething.bind().name);   // "bound doSomething"
console.log((new Function()).name);     // "anonymous"

(五)函数的双重用途

双重用途:根据是否用new调用区分。

  1. Js函数包含两个内部方法:[[Call]] [[Construct]]

不使用new时,执行的是[[Call]],运行的是代码中的函数体

使用new时,执行的是[[Construct]],负责创建一个新对象,且将该对象作为this去执行函数体,最后返回这个新对象

注意:箭头函数没有[[Construct]]方法;

  1. 判断函数如何被调用

ES5中 通过instanceOf来判断,缺点并不一定可靠

function Person(name) {
    if (this instanceof Person) {
        this.name = name;   // using new
    } else {
        throw new Error("You must use new with Person.")
    }
}

var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael");    // works!

ES6 新增new.target元属性判断

function Person(name) {
    if (typeof new.target !== "undefined") {
        this.name = name;   // using new
    } else {
        throw new Error("You must use new with Person.")
    }
}

var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael");    // error!

new.target还可以判断是否使用特定构造器进行了调用:

function Person(name) {
    if (new.target === Person) {
        this.name = name;   // using new
    } else {
        throw new Error("You must use new with Person.")
    }
}

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

var person = new Person("Nicholas");
var anotherPerson = new AnotherPerson("Nicholas");  // error!

(六)块级函数

ES5,严格模式下不允许在代码块内部声明函数,

ES6,会将代码块内部声明的函数视为块级声明

提升位置 作用域
严格模式 所在代码块顶部 当前代码块
非严格模式 所在函数或者全局的顶部 所在函数或者全局

块级函数和let函数表达式的区别:块级函数会被提升,而let函数表达式不会。

(七)箭头函数

箭头函数与传统js函数的区别:

  1. 没有this, super, arguments, new.target, 由外层最接近的非箭头函数来决定;
  2. 不能使用new调用;
  3. 没有原型;
  4. 不能修改this;
  5. 不允许重复的具名参数

使用箭头函数创建立即执行函数时只有一种写法:

(() => {})()

(八)尾调用优化

ES6严格模式下,当满足以下条件时,尾调用优化不会创建新的栈帧,而是清除当前的栈帧并再次利用它:

  1. 不能引用当前栈帧中的变量(意味着该函数不能是闭包);
  2. 返回结果后不能做额外操作;
  3. 尾调用的结果作为当前函数的返回值。
"use strict";

function doSomething() {
    // 可被优化
    return doSomethingElse();
}

function doSomething() {
    var num = 1,
        func = () => num;
    // 未被优化:此函数是闭包
    return func();
}
function doSomething() {
    // 未被优化:在返回以后还执行加法
    return 1 + doSomethingElse();
}
function doSomething() {
    // 未被优化:没有return
    doSomethingElse();
}
function doSomething() {
    // 未被优化:调用不在尾部
    var result = doSomethingElse();
    return result;
}


主要考虑应用在递归函数中,能显著提高性能。

如何控制尾调用优化?以计算阶乘函数为例

function factorial(n) {

    if (n <= 1) {
        return 1;
    } else {

        // not optimized - must multiply after returning
        return n * factorial(n - 1);
    }
}

//尾调用优化
function factorial(n, p = 1) {

    if (n <= 1) {
        return 1 * p;
    } else {
        let result = n * p;

        // optimized
        return factorial(n - 1, result);
    }
}