ES6 函数

269 阅读6分钟

ES6 在函数特性上做出了许多改进的地方,让函数使用起来更灵活。

形参默认值

JS函数定义中无论声明了多少形参,都可以传入任意数量的参数。当已定义的形参无对应的传入参数是为其指定一个默认值。

对比 ES5 与 ES6 默认值方式

//  ES5示例:
function fun (arg1) {
    arg1 = (typeof arg1 !== "undefined") ? arg1 : "hello ES5";
    // 执行代码
}


//  ES6示例:
function fun (arg1 = "hello ES6") {
    // 执行代码
}

ES5 中通过模拟默认参数方式来补全参数,ES6 中简化为形参提供默认值的过程,如果没有参数传入值则为其提供一个初始值。调用 ES6 函数,只有当该形参不传人对应参数或传入参数为 undefined 时就会使用默认值。

arguments 对象

arguments表现行为分为两种:一种是命名参数变化与arguments对象相互影响,这种行为条件是:非严格模式下及函数不使用默认参数值;另一种是命名参数变化与arguments对象互不影响,这种行为条件是:严格模式下或函数使用默认参数值。

//  第一种行为:非严格模式且不使用默认参数值
function fun(a, b){
    console.log( a === arguments[0] );
    console.log( b === arguments[1] );
    a = 10;
    arguments[1] = 20;
    console.log( a === arguments[0]);
    console.log( b === arguments[1]);
}
fun(1,2);   // 对应打印:true true true true

//  第二种行为:严格模式或使用默认参数值
function fun(a, b){
    "use strict";
    console.log( a === arguments[0] );
    console.log( b === arguments[1] );
    a = 10;
    arguments[1] = 20;
    console.log( a === arguments[0]);
    console.log( b === arguments[1]);
}
/*  或者
    function fun(a, b = 1){
    console.log( a === arguments[0] );
    console.log( b === arguments[1] );
    a = 10;
    arguments[1] = 20;
    console.log( a === arguments[0]);
    console.log( b === arguments[1]);
}
*/
fun(1,2);   // 对应打印:true true false false

注意:arguments 对象是记录执行函数所传入的参数,如果对应形参传入值缺失,这部分命名参数与 arguments 对象也不会有关联。如下:

function fun(a, b){
    console.log( a === arguments[0] );
    console.log( b === arguments[1] );
    a = 10;
    arguments[1] = 10;
    console.log( a === arguments[0] );
    console.log( b === arguments[1] );
}
fun(1);     // 对应打印:true true true false

上述示例由于执行函数只传入一个实参,并没有传入第二个参数,所以 arguments[1] 并没有与命名参数 b 绑定起来,所以它们不会相互影响。

默认参数的临时死区

之前我也写过 let 和 const 临时死区的相关知识,其实默认参数也有临时死区。与let声明类似,函数定义参数时会为每个参数创建一个新的标识符来绑定,在绑定之前不能被引用。

//  正确使用
function fun (first, second = first) {
    console.log(first, second);
}
fun(1);

//  错误使用
function fun (first = second, second) {
    console.log(first, second);
}
fun(1,2);   //抛出错误

上述示例是默认参数值引用了函数命名的参数,规定只能引用该参数之前的命名参数。模拟JS引擎所做的事。

function fun(first = second, second){
    let first = second;
    let second;
}

因为 first 初始化需要 second,而 second 却尚未初始化。

不定参数

ES6 之前都是使用 arguments 对象来检查函数的所有参数,从而不必定义每个要用的参数。而 ES6 引入不定参数的特性来使我们更灵活使用不明确的参数。

function fun (fn, ...values) {
    for (let i = 0,len = values.length; i < len; i++) {
        fn(values[i]);
    }
}

var fn = function (value) {
    console.log(value);
}

fun(fn,1,2,3,4);    //  依次打印:1,2,3,4

在函数命名参数前添加 ... 表明这是个不定参数,该参数为一个数组,包含除第一个参数以外的参数 [1,2,3,4] 。
不定参数使用限制:每个函数最多声明一个不定参数,而且要放到所有参数末尾,如果不定参数后还有命名参数,抛出语法错误。

function fun (fn, ...values, len){ ... }    //  抛出语法错误

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

function fun (...args) {
    console.log(args.length);
    console.log(arguments.length);
}

fun(1,2,3);     //  打印两次 3

块级函数

简单记录下,块级函数在不同 ECMAScript 版本和执行模式下表现的不同。

//  在 ES5、ES6 非严格模式下
if (true) {
    function fun () {
        console.log(1111);
    }
    //  可执行 fun
}
//  可执行 fun


//  在 ES5 严格模式下
"use strict";
if (true) {
    function fun () {       //  抛出语法错误
        console.log(1111);
    }
}


//  在 ES6 严格模式下
"use strict";
if (true) {
    function fun () {
        console.log(1111);
    }
    //  可执行 fun
}
//  不可执行 fun

第一种情况:函数作用范围提升至外围函数或全局作用域,而不仅在代码块内;
第三种情况:函数提升至代码块的顶部,外围函数或全局作用域不能访问到块级函数。

元属性 new.target

JS 函数中有多重用途,一种是普通调用函数,另一种是通过new关键字来调用函数。在ES6 之前无法区分函数是否通过 new 来调用,对此 ES6 引入了 new.target 这个元属性。

function Fun () {
    console.log(new.target);
}

var fun = new Fun();    //  打印:function Fun
Fun();                  //  打印:undefined

这样我们就可以规定某些函数只能通过 new 来调用,普通调用抛出错误。

function Fun (name) {
    if( typeof new.target === "undefined" ){
        throw new Error ("必须使用 new 来调用 Fun");
    }
    this.name = name;
}
Fun("ES6");

箭头函数

ES6 中箭头函数是最有趣的特性,与传统 JS 函数不同,有以下几方面:

  1. 箭头函数不会重新绑定 this、arguments、new.target,也就是说箭头函数内部的这些属性都是沿用外围最近一层非箭头函数的属性;
  2. 不存在 prototype 属性,即没有原型;
  3. 不能通过 new 关键字调用;
  4. 无法通过 call、apply、bind 来改变箭头内部的 this;
  5. 不支持 arguments 对象,只能通过命名参数或不定参数来访问函数的参数;
  6. 无论在严格还是非严格模式下,都不支持重复命名参数。传统函数只有在严格模式下不支持重复命名参数。

下面通过示例来演示这几个方面:

//  示例1:
function fun () {
    this.name = "ES6";
    return () => {
        console.log(new.target);
        console.log(this.name);
        console.log(arguments[0]);
    };
}
var a = fun("hello");
a("arg1");    //打印:undefined、"ES6"、"hello"

var aa = new fun("hello");
aa("arg1");   //打印:function fun、"ES6"、"hello"


//  示例2:
var fun = () => ({ name:1 });
var object = new fun();     //  抛出语法错误


//  示例3:
var name = "aaa";
var fun = () => {
    console.log(this.name);
};
var object = { name: "bbb" };
fun.call(object);   //打印:"aaa"

第一个示例:通过两种方式返回箭头函数赋值给 a、aa。从new.target、this.name、arguments[0] 的值看出这些都是 fun 作用域下的值,而并不是箭头函数下的。
第二个示例:直接使用 new 调用箭头函数,抛出语法错误 "fun is not a constructor",fun 是没有构造 [constructor] 的属性,所以没办法通过 new 来调用。传统的函数是有 [constructor] 和 [call] 属性的,分别表示new方式调用和普通方式调用。
第三个示例:首先 fun 箭头函数在全局作用域下定义的,这里的 this 指的是 window 对象。我通过 call 的方式修改箭头函数的 this 指向 object 对象并执行。结果却依然是 window.name 的 "aaa" 值被打印,this 没有修改成功。

小结

ES6 函数最大改变的就是添加了箭头函数,而且箭头函数在项目上用得更多,根据代码任务来判断使用箭头函数还是普通函数。arguments 和 不定参数也要多多关注,这两个也挺重要的。