Table of Content

3. 函数

函数在任何语言中都是最重要的一部分,没有之一!js函数中.从这门语言诞生到现在...除了遇到了es6之后.发生了一次比较大的化.之前其实都是没有什么变化.但是并不是说完美的...还是有改进的空间.

3.1 在ES5中模拟默认的参数

function ajax(url,type,timeout,callback){
    timeout = timeout || 5000 ;
    type = type || 'get';
    callback = callback || function(){return 1}

    // 其它代码
}

以上这个例子.type和callback 参数是可选的,因为在没有参数的时候,会使用默认值,||或运算符在左侧为假的情况下.返回右侧的内容,因为我们的参数在没有传递的时候返回的是undefined.在逻辑或运算符中用来给缺失的参数提供默认值.但是有一个BUG,因为传进来的参数可能是 0 ,或运算符遇到0为假.所以timeout会被替换成5000

基于以上的情况.我们应该使用一种更加合理更加案例的方法

function ajax(url,type,timeout,callback){
    timeout = (typeof timeout !== 'undefined') ? timeout : 5000

    // 其它代码
}

这种方法,在今天也是比较常见的用法.虽然是安全的..但是代码量过多.而且代码了一个公共的模式.流行的js库中基本上这种写法到处都是..而在ES6当中有更方便的写法,

3.2 es6中的参数默认值

function ajax(url,type = 'get',timeout = 5000,callback = function(){alert(1);}){
    callback()
    // 其它代码
    return {
        url,
        timeout,
        type
    }
}
console.log(ajax('index.html')
console.log(ajax('index.html','post',1000))
console.log(ajax('index.html',undefined,1000))

在以上这个案例中,只有第一个参数需要被传参,其它的都有自己的默认值,这种写法明显比上面的更加的精巧。因为不需要再添加 更多的代码来检查参数值的缺失。

在这个默认参数值的调用中,null是有效的值。而 undefined 则被认为是没有传递参数;

3.3 参数默认值会影响到arguments对象

关于arguments 对象会在使用参数默认值的时候有一些不同的表现,的ES5非严格模式下,在argements 对象会反映出具名参数的变化

function ajax(url,type){
    console.log(url === arguments[0])
    console.log(type === arguments[1])
    url = 'q';
    type = 'w'
    // 其它代码
    console.log(url === arguments[0])
    console.log(type === arguments[1])
}

ajax('url','type')
//以上会得到四个
true
true
true
true

特别需要注意的是,在ES5的严格模式下,这种情况不存在、不会在反映出参数的变化

function ajax(url,type){
    "use strict"
    console.log(url === arguments[0])
    console.log(type === arguments[1])
    url = 'q';
    type = 'w'
    // 其它代码
    console.log(url === arguments[0])
    console.log(type === arguments[1])
}

ajax('url','type')
//以上会得到以下结果
true
true
false
false

在ES6参数默认的函数中。arguments对像的表现总是和ES5的严格模式保持一致。此时无论函数是否运行在严格模式下。参数默认值都会触发arguments对象与具名参数的分离。非常小的一个细节。但是很重要

function ajax(url,type = 'd'){
    console.log(arguments)
    console.log(url === arguments[0])
    console.log(type === arguments[1])
    url = 'q';
    type = 'w'
    // 其它代码
    console.log(url === arguments[0])
    console.log(type === arguments[1])
}

ajax('url')
//以上会得到以下结果
1   //length
true
false
false
false

以上例子中arguments.length的值为1,因为我们只传递了一个参数.也就说是arguments[1]的值是undefined,符合把单个参数传给函数的一个预期值.也就是说url和arguments[0]是相等的.如果去改变url和type 的值不会对arguments对象造成影响.无论是否在严格模式下.都可以始终依据arguments对象来反映初始值调用的状态.

3.4 参数默认值表达式

参数默认值并不要求一定是基本类型值.比如可以接受一个函数产生参数的默认值.比如:

function getValue(){
    return 5;
}
function addValue(number,int = getValue()){
    return number + int
}
console.log(addValue(4))
console.log(addValue(4,87))

以上如果未提供第二个参数...getValue()函数就会被凋用获取表达式的默认值.如果在调用addValue()函数时未提供第二参数时...getValue()才会调用,而在.getValue()的函数声明初次被解析时并不会进行调用.也就意味着.getValue()函数若被写为可变的.则它就有可能返回可变的值.

比如:

let value = 5;
function getValue(){
    return value++;
}

function addValue(number,int = getValue()){
    return number + int
}

console.log(addValue(4))  //9
console.log(addValue(4))  //10
console.log(addValue(4))  //11l

以上就是一个返回可变值的函数的一个体现.因为这样的一种行为.有一个比较有意思的现像,可以把前面参数作为后面参数的默认值

function addValue(number,int = number){
    return number + int
}

console.log(addValue(4,4)) 
console.log(addValue(4)) 

以上案例中number为int提供了默认值.意味只要传入一个参数就会让两个参数获得相同的值.如果是这样的话.哪我们的这个number,是不是可以作为第二个默认参数函数的形参.

function getValue(value){
    return value + 5;
}

function addValue(number,int = getValue(number)){
    return number + int
}

console.log(addValue(4,4))  //8
console.log(addValue(4))    //13

以上案例把number的值设为getValue()函数的返回值.因此addValue(4)会返回(4 + 9),而另一个依然是4+4.

引用其它参数 作为参数进行默认赋值时,只能引用前方的参数,因此前面的参数不能访问后面的参数.比如:

function addValue(number = int,int){
    return number + int
}

console.log(addValue(undefined,1))   //报错

因为int 在number 之后定义,不能把它作为后者的默认值..存在暂时性死区.

3.5 剩余参数

剩余参数 rest parameter由三个点( ... )与一个具名参数指定.会包含传递给函数的其余参数的一个数组.名称中的剩余由此而来.比如

function ajax(obj,...keys){
   for(let i=0,length = keys.length;i<length;i++){
       //其它代码
   }
}

keys是一个包含所有在obj之后的参数的剩余参数(这和包含的有的参数的 arguments 不同,因为arguments 会把第一个参数也包含在内).也就是说我可以去对keys进行从头到尾的遍历.

如果你用了剩余参数,就必须把剩余参数放在最后.一个函数中只能有一个剩余参数.

function ajax(obj,...keys,last){
   for(let i=0,length = keys.length;i<length;i++){
       //其它代码
   }
}

以上代码会发生语法错误,不能在剩余参数之合使用具名参数,同样的剩余参数也不能用于对象字面量中的set属性中使用,比如

let obj = {
    // 不能在setter中使用剩余参数
    set abc(...value){

    }
}

3.6 箭头函数

箭头函数算是一个比较明显改变, 使用(=>)来定交箭头函数.在行为上面和传统函数不同.

  1. 没有this,super,arguments,也没有所谓的new.target梆定
  2. 不能被new 调用.也就是如果你要写构造函数不能使用箭头函数.
  3. 没有原型,既然不能对箭头函数使用new 那么也就不需要原型.也就没有prototype属性
  4. 不能改变this.this的值在函数内部不能被修改/在 函数的整个生命周期内其值保持不变
  5. 没有arguments,箭头函数没有和arguments绑定.就必须依赖于具名参数或剩余参数来访问函数的参数
  6. 不允许重复的具名参数,无论是否在严格模式下都不能允许.而对于传统函数来说.只有在严格模式下才禁止这种重复.

 3.6.1 语法

箭头函数的语法有比较多的写法,怎么写取决于你想要完成什么样的目标.所有的写法都以函数参数为开头

后面跟着是箭头,接下来的是函数体.

var res = value => value;
//等价于以下
var res = function(value){
     return value
}

当箭头函数只有单个参数时..这个参数可以直接书写.不需要其它额外的语法.箭头右边的表达式,会被计算并返回结果.

如果要参入多个参数,就要放在一个括号内

var sum = (n,m) => n + m;
// 等价于以下
var sum = function(n,m){
  return n + m;
}

以上这个案例和上面的案例唯一的区别就是参数在括内.相互之间和传统函数一样.用逗号隔开.如果说函数没有任何参数.那必就必须使用一对空括号

var sum = () => '李虎';
// 等价于以下
var sum = function(){
  return '李虎'
}

如果说想要有传统的函数体,也可以包含多个语句的时候,用一对花括号包进来.然后也就以使用return,比如:

var sum = (n,m)  => {
    return n + m;
}
// 等价于以下

var sum = function(n,m){
  return n + m;
}

基本上坷以把花括号内的代码当成传统函数对待就可以,除了不能使用 arguments 如果说想要创建一个空函数;

var sum = ()  => {}
// 等价于以下
var sum = function(){}

花括号表示函数的主体,所有的正常的工作都必须放在花括号当中.如果箭头函数想从函数体内向外返回一个对象字面量.就必须把字面量包括在圆括内;

var getOBJ = id => ({id:id,name:"李虎"})

// 等价于以下

var getOBJ = id => {
    return {
        id:id,
        name:"李虎"
    }
}
var getOBJ = function(id){
    return {
        id:id,
        name:"李虎"
    }
} 

以上把对象字面量包括在括号内.表示了括号内是一个字面量而不是函数体.

3.6.2 创建立即调用函数表达式

我们在使用函数的一种流行的方式 创建立即调用函数表达式 (IIFE: Immediately Invoked Function Expression)

IIFE允许我们自定一个匿名函数并在未保存引用的情况下立即调用它.比如:

let obj = function(name,age){
    return {
        getName:function(){
            return {
                name,
                age
            }
        }
    }
}('李虎',22)

console.log(obj.getName())

在以上案例中IIFE 被用于创建一个包含 getName() 方法的对象.有两个参数作为返回值.可以有效的让为两个返回值(name,age)成为返回对象的一个私有成员. 我们可以使用箭头函数来完成同样的事 情.只要把它包括在括号内就可以.

let obj = ((name,age)=>{
    return {
        getName:function(){
            return {
                name,
                age
            }
        }
    } 
})('李虎',22)

console.log(obj.getName())

这里需要注意的是括号仅仅包含了箭头函数的定义.并没有包括('李虎',22),这区别于传统函数的方式.括号即可以连函数定义与参数一起包含.也可以只包含函数定义.

3.6.3 没有 this 绑定

JavaScript中最常见的错误就是在函数内的this绑定,因为在一个函数内部的this值可以被改变.这个改变的过程.取决于在调用这个函数时的上下文关系,就可能错误的影响一个对象,尽管本来是想要修改另一个对象的..但是结果可能并不是,比如:

var handler = {
    id:"53853",
    init:function(){
        document.addEventListener('click',function(ev){
             this.ido(ev.type) //报错了 this.ido is not a function
        },false) 
    },
    ido:function(type){
        console.log("handler" + type + ' - ' + this.id)
    }
}
handler.init()

handler 对象作用是用于设计处理页面上的交互,在调用init()方法,用于建立交互关系.用于和当前页面进行一次行为对话,并且还添加了一个事件处理函数this.ido().但是在实际调用过程中,this.ido() 发生了错误.告诉我们这不是一个方法.不是一个function,这是因为this是事件目标对象,也就是当前案例中的(docuemnt),而不是被绑定在handler对象上.所以在运行此代码时..会触发一个错误.因为this.ido()这个方法并不存在于document对象上.

在箭头函数之前解决此类问题.可以使用bind()方法把函数的this.绑定到handler对象上.以修正这段代码:示例如下

var handler = {
    id:"53853",
    init:function(){
        document.addEventListener('click',(function(ev){
            this.ido(ev.type) //已经是正确的
       }).bind(this),false) 
    },
    ido:function(type){
        console.log("handler" + type + ' - ' + this.id)
    }
}
handler.init()

以上案例.通过调用bind(this),实际上是创建了一个新函数,它的this绑定到当前的this也就是 handler 对象之上.如果想要避免创建一个函数.最好的方式.不是下面的在之前存一个对象.而是使用箭头函数

也可以在在事件对象之前保存this对象

var handler = {
    id:"53853",
    init:function(){
        var self = this;
        document.addEventListener('click',function(ev){
            self.ido(ev.type) //已经是正确的
       },false) 
    },
    ido:function(type){
        console.log("handler" + type + ' - ' + this.id)
    }
}
handler.init()

在使用箭头函数时.因为没有this绑定,也就意味着.箭头函数内部的this值.只能通过查找作用域链的方式来确定我的当前对象.如果箭头函数被包含在 一个非箭头函数中.哪么this就会和这个函数相等;否则this就会是全局对象也就是window.如果是在node.js当中的是global,下面我们用箭头函数再次修正此代码,示例如下:

//示例1
var handler = {
    id:"53853",
    init:function(){
        document.addEventListener('click',ev=>this.ido(ev.type),false) 
    },
    ido:function(type){
        console.log("handler" + type + ' - ' + this.id)
    }
}
handler.init()
//示例2
//也可以这样写
//效果同上,只是箭头函数的写法有所区别
var handler = {
    id:"53853",
    init:function(){
        document.addEventListener('click',(ev)=>{
            this.ido(ev.type)
        },false) 
    },
    ido:function(type){
        console.log("handler" + type + ' - ' + this.id)
    }
}
handler.init()

以上案例中事件处理函数是一个调用this.ido的箭头函数。。它的this和init()方法相同。所以这个版本代码工作方式类似于我们前面使用bind(this)的案例。尽管this.ido()方法并不返回何值。但依然是函数内体唯 一能被执行的语句。因此无须使用第二个案例用花括号来包含它。但是我依然推荐大家使用第二种,读性更高一点。

箭头函数不用被用于定义新的类型。没有prototype,所以不能使用 new 运算符。如果使用会报错。从这点上可以看出箭头函数。在设计之设就是属于抛弃型函数。

var Fn = ()=>{};
fn = new Fn()  //Uncaught TypeError: Fn is not a constructor

以上代码中报错了,因为是一个箭头函数,它不存在constructor方法,所以箭头函数不能被用于new

同样,由于箭头函数中的this由包含它的函数决定,因此也不能使用call(),apply()或bind()方法来改变其值。

3.6.4 箭头函数和数组

比如如果你想使用自定度的比较器来数组进行排序。我们可以这样写

传统的写法:

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

以上是传统的写法,如果使用箭头函数,可以有更简洁的书写体验

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

还可以使回调函数的数组方法,比如map() 或reduce().都可以把看着复杂的需求转换成更加简单明了的代码。

3.6.5 没有arguments绑定

箭头函数没有自己的arguments的绑定。但是依然可以访问包含它的函数的arguments对象。因此箭头函数无论在何处被执行。这个对象都是可以用的。比如:

function createFn(){
    return () => arguments[0];
}
var fn = createFn(5)
console.log(fn())  //5

以上案例在createFn()内部,arguments[0]被已经创建的箭头函数所引用。这个引用包含了传传递给createFn()函数的首个参数,因此当箭头函数在执行之后。可以反回5,这是因为传给createFn()的首个参数,尽管箭头不存在arguments。但是但是创建他的函数的作用域内是存在的。由于作用域链的关系。依然可以被访问。

识别箭头函数

尽管箭头函数和传统函数.语法不同.但是依然是函数.这个本质不能搞乱.也能被正常的识别

比如:

var fn = (a,b)=>a-b;
console.log(typeof fn)  //function
console.log(fn instanceof Function)  //true

以上两条console.log()的结果.可以明确的知道,typeof 和 instanceof 在箭头函数中的行为和作用与其它的函数完全一样.所以你依然可以对箭头函数使用call,apply和bind(),虽然函数的this绑定不会受影响.

比如:

var sum = (num1,num2) => num1 + num2;
console.log(sum(1,1))
console.log(sum.call(null,1,2))
console.log(sum.apply(null,[1,2]))
var bindSum = sum.bind(null,1,2);
console.log(bindSum())

以上案例sum()被使用call()和apply()方法.并传了参数,bind()方法用于创建一个新函数bindSum().

总结

可能对于很多有经验的开发者来说.并不会感受到箭头函数有什么革命性的变化.事实上变化也不是我们看到的这么大.只是增加了一些增量改进.让函数更加容易使用而已.

Leave a Reply

Your email address will not be published.