五、javascript函数


一、Javascript函数

1.1 带参数默认值的函数

JS函数的独特之处是可以接受任意数量的参数,而无视函数声明处的参数数量。这让你定义的函数可以命名用不同的参数数量来调用,调用时未提供的参数经常会使用默认值来代替。

// 在ES5中模拟参数默认值
function func01(url, timeout,callback){
    timeout = timeout || 2000
    callback = callback || function(){}
    // 函数的余下部分
}

// timeout与callback实际上都是可选参数,因为他们都会在参数未被提供的情况下使用
// 默认值。逻辑或运算符(||)在左侧的值为假的情况下总会返回右侧的操作数。由于函数
// 的具名参数在未被明确提供时会是undefined,逻辑或运算符就经常被用来给缺失的参数
// 提供默认值。不过此方法有个瑕疵,在此处的timeout的有效值实际上有可能是0,但是
// 因为0是假值,就会导致timeout的值在这种情况下会被替换为2000.

// 可使用更安全的替代方法实现
function func02(url, timeout, callback){
    timeout = (typeof timeout !== 'undefined') ? timeout : 2000
    callback = (typeof callback !== 'undefined') ? callback : function(){}
    // ......
}

// ES6中的参数默认值
// ES6能更容易地为参数提供默认值,它使用了初始化形式,以便在参数未被正确传递进来时使用
function func03(url, timeout=2000, callback=function(){}){
    //......
}
let value = 5
function getValue(){
    return value++
}
function add(first, second=getValue()){
    return first + second
}
console.log(add(1, 1)) //2
console.log(add(1))    //6
console.log(add(1))    //7

代码讲解
将函数调用作为参数的默认值时需要小心,如果你遗漏了括号,例如在上面盒子中使用
second=getValue,你就传递了对于该函数的一个引用,而没有传递调用函数的结果。

1.2 剩余参数

剩余参数( rest parameter )由三个点( … )与一个紧跟着的具名参数指定,它会是包含传递给函数的其余参数的一个数组,名称中的“剩余”也由此而来。
剩余参数的限制条件
剩余参数受到两点限制。一是函数只能有一个剩余参数,并且它必须被放在最后。

function func01(name, ...args){
    let result = ''
    for(let i=0; i<args.length; i++){
        result += ' '+args[i]
    }
    return name + ',' + result
}

console.log(func01('张三', '李四', '王五'));

1.3 函数构造器

Function 构造器允许你动态创建一个新函数,但在 JS 中并不常用。传给该构造器的参数都是字符串,它们就是目标函数的参数与函数体。

let add = new Function('first', 'second', 'return first+second')

console.log(add(1, 2))

ES6 增强了 Function 构造器的能力,允许使用默认参数以及剩余参数。对于默认参数来说,你只需为参数名称添加等于符号以及默认值

let add = new Function('first', 'second=first', 'return first+second')

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

1.4 扩展运算符

与剩余参数关联最密切的就是扩展运算符。剩余参数允许你把多个独立的参数合并到一个数组中;而扩展运算符则允许将一个数组分割,并将各个项作为分离的参数传给函数。

取出数组中的最大值

// ES5或更早版本JS
let values = [25, 50, 75, 100]
console.log(Math.max.apply(Math, values))

代码讲解:该解决方案是可行的,但如此使用 apply() 会让人有一点疑惑,它实际上使用了额外的语法混淆了代码的真实意图。

ES6 的扩展运算符令这种情况变得简单。无须调用 apply() ,你可以像使用剩余参数那样在该数组前添加 … ,并直接将其传递给 Math.max() 。 JS 引擎将会将该数组分割为独立参数并把它们传递进去,就像这样:

// 等价于console.log(Math.max(25, 50, 75, 100))
console.log(Math.max(...values)) //扩展运算符

1.5 ES6 的名称属性

定义函数有各种各样的方式,在 JS 中识别函数就变得很有挑战性。此外,匿名函数表达式的流行使得调试有点困难,经常导致堆栈跟踪难以被阅读与解释。正因为此, ES6 给所有函数添加了 name 属性。

1.6 明确函数的双重用途

在 ES5 以及更早版本中,函数根据是否使用 new 来调用而有双重用途。当使用 new 时,函数内部的 this 是一个新对象,并作为函数的返回值,如下例所示:

function Person(name){
    this.name = name
}

const person = new Person('kangyun')
const notAPerson = Person('kangyun')
console.log(person)       // Object
console.log(notAPerson)   // undefined

代码讲解:
当创建 notAPerson 时,未使用 new 来调用 Person() ,输出了 undefined (并且在非严格模式下给全局对象添加了 name 属性)。 Person 首字母大写是指示其应当使用 new 来调用的唯一标识,这在 JS 编程中十分普遍。函数双重角色的混乱情况在 ES6 中发生了一些改变。

JS 为函数提供了两个不同的内部方法: [[Call]] 与 [[Construct]] 。当函数未使用 new进行调用时, [[call]] 方法会被执行,运行的是代码中显示的函数体。而当函数使用 new 进行调用时, [[Construct]] 方法则会被执行,负责创建一个被称为新目标的新的对象,并且使用该新目标作为 this 去执行函数体。拥有 [[Construct]] 方法的函数被称为构造器。

记住并不是所有函数都拥有 [[Construct]] 方法,因此不是所有函数都可以用 new 来调用。在“箭头函数”小节中介绍的箭头函数就未拥有该方法。

  • 在 ES5 中判断函数如何被调用
    在 ES5 中判断函数是不是使用了 new 来调用(即作为构造器),最流行的方式是使用instanceof
function Person(name){
    if(this instanceof Person){
        this.name = name
    }else{
        throw new Error("You must use new with Person")
    }
}

const person = new Person('kangyun')
const notAPerson = Person('kangyun')   //抛出错误

此处对 this 值进行了检查,来判断其是否为构造器的一个实例:若是,正常继续执行;否则抛出错误。这能奏效是因为 [[Construct]] 方法创建了 Person 的一个新实例并将其赋值给 this 。可惜的是,该方法并不绝对可靠,因为在不使用 new 的情况下 this 仍然可能是 Person 的实例,正如下例:

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

const person = new Person('kangyun')
const notAPerson = Person.call(person, 'kangyun') // ok

调用 Person.call() 并将 person 变量作为第一个参数传入,这意味着将 Person 内部的
this 设置为了 person 。对于该函数来说,没有任何方法能将这种方式与使用 new 调用区分开来。

1.7 new.target 元属性

为了解决这个问题, ES6 引入了 new.target 元属性。元属性指的是“非对象”(例如 new )上的一个属性,并提供关联到它的目标的附加信息。当函数的 [[Construct]] 方法被调用时, new.target 会被填入 new 运算符的作用目标,该目标通常是新创建的对象实例的构造器,并且会成为函数体内部的 this 值。而若 [[Call]] 被执行, new.target 的值则会是 undefined 。

通过检查 new.target 是否被定义,这个新的元属性就让你能安全地判断函数是否被使用new 进行了调用

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

const person = new Person('kangyun')
const notAPerson = Person.call(person, 'kangyun') // 出错了

ES6 通过新增 new.target 而消除了函数调用方面的不确定性。

1.8 块级函数

块级函数与 let 函数表达式相似,在执行流跳出定义所在的代码块之后,函数定义就会被移除。关键区别在于:块级函数会被提升到所在代码块的顶部;而使用 let 的函数表达式则不会,正如以下范例所示:

if(true){
    console.log(typeof doSomething) // 抛出错误
    let doSomething = function(){
        //......
    }
    doSomething()
}

console.log(typeof doSomething)

此处代码在 typeof doSomething 被执行时中断了,因为 let 声明尚未被执行,将
doSomething() 放入了暂时性死区。知道这个区别之后,你就可以根据是否想要提升来选择应当使用块级函数还是 let 表达式。

ES6 在非严格模式下同样允许使用块级函数,但行为有细微不同。块级函数的作用域会被提升到所在函数或全局环境的顶部,而不是代码块的顶部。

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

console.log(typeof doSomething)    // function

1.9 箭头函数

ES6 最有意思的一个新部分就是箭头函数( arrow function )。箭头函数正如名称所示那样使用一个“箭头”( => )来定义,但它的行为在很多重要方面与传统的 JS 函数不同

  • 没有 this 、 super 、 arguments ,也没有 new.target 绑定: this 、 super 、arguments 、以及函数内部的 new.target 的值由所在的、最靠近的非箭头函数来决定 。
  • 不能被使用 new 调用: 箭头函数没有 [[Construct]] 方法,因此不能被用为构造函数,使用 new 调用箭头函数会抛出错误。
  • 没有原型: 既然不能对箭头函数使用 new ,那么它也不需要原型,也就是没有 prototype 属性。
  • 不能更改 this : this 的值在函数内部不能被修改,在函数的整个生命周期内其值会保持不变。
  • 没有 arguments 对象: 既然箭头函数没有 arguments 绑定,你必须依赖于具名参数或剩余参数来访问函数的参数。
  • 不允许重复的具名参数: 箭头函数不允许拥有重复的具名参数,无论是否在严格模式下;而相对来说,传统函数只有在严格模式下才禁止这种重复。

花括号被用于表示函数的主体,它在你至今看到的例子中都工作正常。但若箭头函数想要从函数体内向外返回一个对象字面量,就必须将该字面量包裹在圆括号内,例如:

const getObject01 = id => ({id: 10, name: 'kangyun'})
//等价于
const getObject02 = function(id){
    return {
        id: 10,
        name: 'kangyun'
    }
}

将对象字面量包裹在括号内,标示了括号内是一个字面量而不是函数体。

箭头函数没有 this 绑定,意味着箭头函数内部的 this 值只能通过查找作用域链来确定。如果箭头函数被包含在一个非箭头函数内,那么 this 值就会与该函数的相等;否则,this 值就会是全局对象(在浏览器中是 window ,在 nodejs 中是 global )

const PageHandler = {
    id: '123456',
    init: function(){
        //箭头函数在一个普通函数中,它的this和普通函数的this相同
        document.addEventListener("click", event=>this.doSomething(event.type), false)
    },
    doSomething: function(type){
        console.log('Handling' + type + ' for ' + this.id)
    }
}

1.10 箭头函数与数组

箭头函数的简洁语法也让它成为进行数组操作的理想选择。

const values = [2, 3, 1, 90, 70, 100]
let result = values.sort((a, b)=>a-b);
console.log(result)

1.11 没有 arguments 绑定

尽管箭头函数没有自己的 arguments 对象,但仍然能访问包含它的函数的 arguments 对象。无论此后箭头函数在何处执行,该对象都是可用的

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

let testFunc = func(5)
console.log(testFunc())

尽管箭头函数 testFunc 已不在创建它的函数的作用域内,但由于 arguments 标识符的作用域链解析, arguments 对象依然可被访问。

箭头函数能在任意位置替代你当前使用的匿名函数,例如回调函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

永恒的宁静

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值