ES6-函数

198 阅读14分钟

ES6

函数

函数形参的默认值

我们看一下以前,处理函数默认参数的代码:

function query(url, timeout, callback){
    timeout = timeout || 2000;
    callback = callback || function(){}
}

在这个例子中,timeoutcallback 是非必传参数,代码中对非必传参数进行了处理,赋予默认值.但是这样的处理是不安全的,因为当 timeout0 时,条件为 false ,会赋予默认值,与预期不同.
优化之后的代码:

function query(url, timeout, callback){
    timeout = timeout!= 'undefined' ? timeout : 2000;
    callback = callback != 'undefined' ? callback :  function(){};
}

但是这样虽然安全,但是对于一个简单的参数默认值操作来说还是太冗余.于是在 ES6 中对默认参数的处理做了简化:

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

这个例子中,认为 url 是必传参数,其余的为非必传.当没有传参时,使用默认值.如果非必传参数也传参,则不使用默认值.

有一种情况是,我中间的参数不传,但是第一和第三个是传参的,那么当想使用默认参数时,因该:

makeRequest('/data', undefined, function(body){
    handleBody(body);
});

中间的参数要主动传 undefined .假设传的是 null 是什么效果呢?

对于默认参数, null 是一个合法值,会被判定为 true ,从而不使用默认参数,所以, null 是不会用默认参数的.

arguments对象

arguments 是一个对应函数入参的 类数组 对象,是所有(非箭头)函数中都可用的对象,可以使用该对象在函数中访问函数的参数.

类数组:类似于 Array ,但是除了 length 属性和索引元素之外没有任何 Array 属性.可以被转换为一个真正的 Array .
箭头函数:箭头函数表达式,相当于匿名函数,简化了函数定义,形如:

x => {
   if (x > 0) {
       return x * x;
   }
   else {
       return - x * x;
   }
}

arguments 对象在 ES5 中就存在,只是行为和 ES6 不一样.在 ES5非严格模式下,函数入参在函数中被修改后, arguments 对象中的参数值也会被修改,代码如下:

function changeArgs(pa1,pa2){
    console.log(pa1 === arguments[0]);
    console.log(pa2 === arguments[1]);
    pa1 = "3";
    pa2 = "4";
    console.log(pa1 === arguments[0]);
    console.log(pa2 === arguments[1]);
}
changeArgs(1,2);

通过打印的结果可以看出, arguments 中的值被同步更新了.

但是在严格模式下, arguments 中的值不会被同步修改. ES6arguments 的行为和 ES5严格模式表现一致,代码示例:

function changeArgs(pa1,pa2="2"){
    console.log(arguments.length);
    console.log(pa1 === arguments[0]);
    console.log(pa2 === arguments[1]);
    pa1 = "3";
    pa2 = "4";
    console.log(pa1 === arguments[0]);
    console.log(pa2 === arguments[1]);
}
changeArgs(1);

函数只有一个入参, arguments 对象的长度也为 1 ,默认参数的存在并不会影响 arguments 对象的行为,入参的改变也不会影响. arguments 对象的行为更加符合预期,同时也影响了你使用这个对象的方式.

默认参数表达式

默认参数不仅可以传原始值,也可以传表达式,例如:

function getValue(){
    return 5;
}
function add(pa1, pa2=getValue()){
    return pa1 + pa2;
}
console.log(add(1,2));
console.log(1);

这个例子比较简单,但是包含了两个注意点:

  1. 初次解析函数的时候,作为默认参数的函数不会被调用,只有当调用 add() 并且用到了默认参数的时候, getValue() 才会被调用.
  2. 当使用函数作为默认参数的时候,要加 () ,否则 pa2 的值是函数的引用,并不是函数执行的结果.

既然默认参数可以在函数被调用时求值,那么你也可以把第一个参数作为第二个参数的默认值.例如:

function add(pa1, pa2=pa1){
    return pa1 + pa2;
}

同理,也可将第一个参数作为第二个参数的默认值(函数)的参数,即:

function add(pa1, pa2=getValue(pa1)){
    return pa1 + pa2;
}

但是,反过来是不可以的,因为将第二个参数作为第一个参数的默认值,会出现 ++临时死区++ .

默认参数的临时死区

默认参数的临时死区更好理解一些:
++定义参数时会为每个参数创建一个新的标识符绑定,标识符在初始化之前不可以被引用,因为默认参数是在函数调用时初始化的,所以在此之前如果引用未被初始化的参数,会抛出错误++

例子:

function add(pa1=pa2,pa2){
    return pa1+pa2;
}
console.log(add(1,1));
console.log(add(undefined,1));

两次调用 add() 代码时对参数的声明相当于:

// 第一次调用add(1,1);
let pa1 = 1;
let pa2 = 1;
// 第二次调用add(undefined,1);
let pa1 = pa2;
let pa2 = 1;

这时候再根据变量声明时的临时死区来看默认参数的临时死区,这两者实际上还是一样的意思.

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

无名参数

JS的函数在传参时,无论函数本身定义的参数是多少个,都可以在调用时传入更多和更少的参数.

但是,如我们经常在ES5中使用的那样,函数不太容易被发现可以接受人一个参数,并且在我们传入任意个参数之后,想要使用未定义参数,需要循环遍历函数内部的arguments对象,并且遍历的索引从1开始.

ES6中,引入了 不定参数 来使函数参数的用法更加简单.不定参数形如...args . 熟悉java的同学肯定见过这种形式的入参写法.

...args在此处代表的是包括它自己和之后的参数在内的所有参数集合.与arguments相比最大的区别就是,后者代表所有入参,前者只包括具名参数之外的入参.这样做就消除了循环参数的限制(索引从1开始),更好管理你的无名参数.

不定参数的初衷是代替arguments对象,在ES6中两者共存.无论你是否使用不定参数,arguments对象总是包含函数在被调用时传入的所有参数.

Function构造函数

ES6增强了Function构造函数的能力,使其支持传参时传入默认参数和不定参数.

例子:

let add = new Function("pa1","pa2=pa1","return pa1+pa2");
let pickFirst = new Function("...args","return args[0]");

ES6增强的这两个特性使得Function构造函数具备了和声明式创建函数相同的能立.但是FunctionJS中是很少被使用的.

展开运算符

展开运算符形如...,与上面提到的不定参数看起来类似,但是两者作用相反.不定参数作用是将多个各自独立的参数整合数组被访问,展开运算符是将指定的数组打散成独立个体.

例子:

// Math.max不允许传入数组
let val1 = 1,val2 = 2;
console.log(Math.max(val1,val2);
// 如果出现从大量的值中选择最大的那一个,每个参数单独传入很麻烦,可以修改成
let vals = [1,2,3,4,5,6,7,8,9];
console.log(Math.max(...vals));

展开运算符还可以与其他正常参数混合使用.例如:

let vals = [-1,-2,-3,-4,-5];
console.log(...vals,0);

展开运算符可以简化参数的使用过程.

name属性

ES6为每个函数新增了一个name属性,目的是为了更好的辨别函数,同时为了兼容一些特殊情况,ES6做了很多改进来使所有的函数拥有更合适的名称.

例子:

function doSomething(){
    
};
console.log(doSomething.name);//打印 doSomething 

var doAnotherThing = function(){
    
};
console.log(doAnotherThing.name);//打印 doAnotherThing 

/**
这个例子中,函数表达式自己的名字比变量名字的优先级要高,因此函数的名字是函数表达式的名字
*/
var doAnotherThing = function doSomething(){
    
};
console.log(doAnotherThing.name);

/**
这个例子中,person.firstName是一个getter函数,因此函数名字前有一个get修饰
*/
let person = {
    get firstName(){
        return 'xlx';
    },
    sayName:function(){
        console.log(this.name);
    }
};
console.log(person.sayName.name);//打印sayName
console.log(person.firstName.name);//打印 get firstName

/**
绑定函数的name总是由函数的name和前缀 bound组成
*/
let doSomething = function(){
    
};
console.log(doSomething.bind().name);//打印 bound doSomething

/**
通过构造函数创建的函数,其名称将是anonymous
*/
console.log((new Function()).name);//打印anonymous

注意:
++name属性只是用来帮助调试,不能用来获取函数的引用++.

函数的双重身份

ES5及早期版本中,函数有两种调用方式,使用new调用或者直接调用.

JavaScript函数的内部有两个不同的方法,CallConstruct.当执行 Construct 方法时,会创建函数的新实例,并将函数内部的this绑定到这个实例上,具有 Construct 的函数被称为 构造函数 .如果是执行 Call ,则会直接执行函数体.

使用new和直接调用函数的区别就在于,new调用函数执行的是 Construct ,最终返回一个函数的新对象.而不使用这个关键字调用,就是执行 Call ,最终返回的是 undefined.

例子:

function Person(name){
    this.name = name;
}
console.log(new Person("xlx"));// 打印 Person {name: "xlx"} 
console.log(Person("xlx"));// 打印 undefined

++并不是所有函数都有Construct方法++.

ES5判断一个函数是否作为构造函数被调用,可以使用 instanceof 关键字(意思是"xxx的实例").当使用new调用的时候,会调用 Construct 创建一个新示例并绑定到this,因此this instanceof xxx可以用来判断函数是否是作为构造函数调用.但是这个方法并不安全.因为在下面这种情况下,虽然函数不是被作为构造函数调用的,instanceof也不能判断出来.

function Person(name){
    if(this instanceof Person){
        this.name = name;
    }else {
        throw new Error('非new调用');
    }
};
let person = new Person('xlx');
let anotherPerson = Person.call(person,'xlx');//不会抛异常

原因在于,其实上面的例子中,先创建了一个Person对象,然后调用call方法将Person对象当成第一个参数传给了Person函数.这相当于将函数体中的this赋值为person,即此时的this确实是一个Person实例.

为了解决函数的这个含糊不清的问题,ES6引入了一个新的属性--new.target元属性.

元属性:非对象属性,其可以以属性访问的方式提供非对象目标的补充信息.

在上面的例子中,当调用 Construct 方法,new.target会被赋值为函数的实例,如果调用的是 Call 方法,new.targetundefined.

例子:

function Person(name){
    if(tyoeof new.target != 'undefined'){
        this.name = name;
    }else {
        throw new Error('非new调用!');
    }
}
let person = new Person('xlx');//通过
let person2 = Person.call(person,'xlx');//抛异常

块级函数

ES5的严格模式中,在代码块中声明函数,会抛出错误.但是在ES6中,代码块中声明函数被视为块级声明,可以在函数所在的代码块内调用它.

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

块级函数let相似的地方在于,当代码块执行完毕,声明的对象会被立即移除.区别在于,代码块中的函数会被提升至代码块顶部,而let声明的函数不会.是否需要函数提升决定了你的函数声明方式.

++以上函数的行为都是在严格模式下++,在非严格模式中,也可以声明块级函数,但是函数提升的行为表现与严格模式不同,具体为:

++严格模式下的函数提升至代码块的顶级,但是非严格模式下函数提升至外围函数或者全局作用域的顶级++

ES6之前,每个浏览器对块级函数特性的支持都略有不同,在ES6中,这个行为被标准化,所有ES6运行时环境都执行相同的标准.

箭头函数

形如=>,是一种用箭头定义函数的新语法(在各种支持函数式编程的语言中还是很常见的).与传统的JS函数的区别如下:

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

语法

/**单参数,单表达式*/
let reflect = value => value;

/**多参数,单表达式*/
let reflect = (val1,val2) => val1+val2;

/**无参*/
let reflect = () =>"1";

/**多表达式,需要{}包裹函数体,并显式返回*/
let reflect = (val1,val2) =>{
    console.log(val1+val2);
    return val1+val2;
};

/**空函数*/
let reflect = ()=>{};

/**返回对象字面量,需要将返回的字面量包裹在小括号内,目的是为了将字面量的{}和函数体区分开*/
let reflect = id => ({id:id,name:'xlx'});

立即执行的函数表达式

立即调用函数表达式(IIFE)是一个在定义时就会执行的JavaScript函数.

(function(){
  statement
})();

这个表达式包含两个部分,第一个部分是包围在()内的匿名函数,第二部分再一次用()创建了一个立即执行函数表达式,JavaScript引擎到此将直接执行函数.

两个特点:

  • 立即执行表达式内部的变量不能从外部调用(作用域独立)
  • 将 IIFE 分配给一个变量,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果

箭头函数定义立即调用函数表达式如下:

let person = ((name) => {
    return {
        getName:function(){
            return name;
        }
    };
})('xlx');
console.log(person.getName());//打印 "xlx"

箭头函数的 this

函数内的 this 绑定容易造成不可预期的错误,因为函数内的 this 会根据函数调用的上下文发生改变.

例如:

let PageHandler = {
    id:11111,
    init:function(){
        document.addEventListener('click',function(event){
            this.doSomething(event.type);
        },false);
    },
    doSomething:function(type){
        console.log('Handling' + type + 'for' + this.id);
    }
}

这段代码并不会按照预期在 init 的时候调用 doSomething 方法,而是会抛出异常.因为 thisinit 中绑定的是事件目标对象,即 document ,而不是 PageHandler .

事件目标对象:触发事件的对象,在这里就是 document .

假设不使用箭头函数,我们可以使用 bind() 方法修正:

let PageHandler = {
    id:11111,
    init:function(){
        document.addEventListener('click',(function(event){
            this.doSomething(event.type);
        }).bind(this),false);
    },
    doSomething:function(type){
        console.log('Handling' + type + 'for' + this.id);
    }
}

这时候this绑定的就是PageHandler对象.

但是这样做还是不合适,因为bind()会创建一个新的函数,将函数的this绑定到当前的this.我们并不想额外创建一个函数解决这个问题

之前我们说过箭头函数的特点之一是没有this绑定,但是它是可以使用this的.箭头函数的this是通过作用域链来决定它的值的,具体规则是:

当箭头函数被非箭头函数包含时,this的值是最近一层非箭头函数的this,否则this的值就是全局对象.

刚才的例子可以修改为:

let PageHandler = {
    id:11111,
    init:function(){
        document.addEventListener(
            'click',
            event => this.doSomething(event.type),
            false
        );
    },
    doSomething:function(type){
        console.log('Handling' + type + 'for' + this.id);
    }
}

++箭头函数的设计初衷就是"即用即弃"++

++可以接受回调函数的方法,都可以通过箭头函数语法减少代码量++

ES6尾调用优化

什么是尾调用?

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

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

尾调用的实现和存在的问题

创建一个新得栈帧,将其推入调用栈表示函数调用,在事件循环中,尾调用会被作为未完成的调用栈帧被保存在内存中,当调用栈变的过大会造成程序问题.

优化方式

在严格模式下,ES6缩减了尾调用栈的大小(非严格模式不影响),尾调用不创建新的栈帧,而是清除并重用当前栈帧.

优化条件

  • 尾调用不访问当前栈帧的变量(函数非闭包)
  • 函数内部尾调用是最后一个语句
  • 尾调用的结果作为函数返回值

无法优化的情况:

"use static";

/**无返回*/
function doSomething(){
    doAnotherThing();
}

/**尾调用返回后执行其他操作*/
function doSomething(){
    return 1 + doAnotherThing();
}

/**尾调用结果保存在其他变量中再返回*/
function doSomething(){
    let res = doAnotherThing();
    return res;
}

/**闭包*/
function doSomething(){
    let num = 1;
    let func = () => num;
    return func();
}

以上情况下,引擎都不能对尾调进行优化.

如何利用尾调优化?

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

/**优化前,如果n是一个很大的数,则会每次调用都插入栈帧,可能导致栈溢出*/
function factorial(n){
    if(n<=1){
        return n;
    }else{
        return n * factorial(n-1);
    }
}
/**优化后*/
function factorial(n, p=1){
    if(n<=1){
        return n * p;
    }else{
        let res = n * p;
        return factorial(n-1, res);
    }
}