JavaScript 函数

81 阅读5分钟

定义一个函数

具名函数

    function 函数名(形式参数1, 形式参数2){
        语句
        return 返回值
    }

匿名函数

  • 上面的具名函数,去掉函数名就是匿名函数
  • let a = function(x, y){return x+y}
  • 也叫函数表达式

箭头函数

  • let f1 = x => x*x
  • let f2 = (x,y) => x+y // 圆括号不能省
  • let f3 = (x,y) => {return x+y} // 花括号不能省
  • let f4 = (x,y) => ({name:x, age: y})
  • 直接返回对象会出错,需要加个圆括号

用构造函数

  • let f = new Function('x','y'.'return x+y')

fn 和 fn() 的区别

  • 函数本身
    • let fn = ()=>console.log('hi')
    • fn
    • 不会有任何结果
    • 因为fn没有执行
  • 函数调用
    • let fn = ()=>console.log('hi')
    • fn()
    • 打印出 hi
    • 有圆括号才是调用
  • 再进一步
    • let fn = ()=>console.log('hi')
    • let fn2 = fn
    • fn2()
    • fn 保存了匿名函数的地址
    • 这个地址被复制给了 fn2
    • fn2() 调用了匿名函数
    • fn 和 fn2 都是匿名函数的引用而已
    • 真正的函数既不是 fn 也不是 fn2

函数的要素

每个函数都有这些东西

  • 调用时机
  • 作用域
  • 闭包
  • 形式参数
  • 返回值
  • 调用栈
  • 函数提升
  • arguments(除了箭头函数)
  • this(除了箭头函数)

调用时机

两个例子

  • 例子一
    let i = 0
    for(i = 0; i<6; i++){
        setTimeout(()=>{
            console.log(i)
        },0)
    }  // 答案是6个6
  • 例子二
    for(let i = 0; i<6; i++){
        setTimeout(()=>{
            console.log(i)
        },0)
    }  // 答案是0、1、2、3、4、5

作用域

  • 每个函数都会默认创建一个作用域

全局变量 和 局部变量

  • 在顶级作用域声明的变量是全局变量
  • window 的属性是全局变量
  • 其他都是局部变量

函数可嵌套

  • 函数内可嵌套函数
    function f1(){
        let a = 1
        
        function f2(){
            let a = 2
            console.log(a)
        }
        
        console.log(a)
        a = 3
        f2()
    }
    f1()

如果多个作用域有同名变量a

  • 那么查找a的声明时,就向上取最近的作用域
  • 简称“就近原则”
  • 查找 a 的过程与函数执行无关
  • 但 a 的值与函数执行有关

闭包

  • 如果一个函数用到了外部的变量
  • 那么这个函数加这个变量
  • 就叫做闭包

形式参数

形式参数的意思就是非实际参数

    function add(x, y){
        return x+y
    }
    // 其中 x 和 y 就是形参,因为并不是实际的参数
    add(1, 2)
    // 调用 add 时,1 和 2 是实际参数,会被赋值给 x y

形参可认为是变量声明

    // 上面代码近似等价于下面代码
    function add(){
        var x = arguments[0]
        var y = arguments[1]
        return x+y
    }

形参可多可少

    // 少的情况
    function add(x){
        return x + argument[1]
    }
    add(1, 2)
    
    
    // 多的情况
    function add(x,y,z){
        return x+y
    }
    add(3,4)

返回值

每个函数都有返回值

  • 没写 return,所以返回值是 undefined
  • console.log('hi')的值是 undefined

函数执行完了才会返回

只有函数有返回值

调用栈

什么是调用栈

  • JS引擎在调用一个函数前
  • 需要把函数所在的环境 push 到一个数组里
  • 这个数组叫调用栈
  • 等函数执行完了,就会把环境弹(pop)出来
  • 然后 return 到之前的环境,继续执行后续代码

举例

  • console.log(1)
  • console.log('1+2的结果为' + add(1,2))
  • console.log(2)

image.png

图片来源:饥人谷

递归函数

阶乘

    function f(n){
        return n !== 1 ? n * f(n-1) : 1
    }

理解递归

  • f(4)
  • = 4 * (3 * f(2))
  • = 4 * (3 * (2 * f(1)))
  • = 4 * (3 * (2 * (1)))
  • = 4 * (3 * (2))
  • = 4 * (6)
  • = 24
  • 先递进,再回归

递归函数的调用栈

调用栈最长有多少

    function computeMaxCallStackSize(){
        try{
            return 1 + computeMaxCallStackSize();
        } catch(2) {
            // 报错说明 stack overflow 了
            return 1;
        }
    }
  • Chrome 12578
  • Firefox 26773
  • Node 12536
  • 超过就会爆栈

函数提升

什么是函数提升

  • function fn(){}
  • 不管你把具名函数声明在哪里,它都会跑到第一行

什么不是函数提升

  • let fn = function(){}
  • 这是赋值,右边的匿名函数声明不会提升

arguments

代码

    function fn(){
        console.log(arguments)
        console.log(this)
    }

如何传 arguments

  • 调用 fn 即可传 arguments
  • fn(1,2,3) 那么 arguments 就是[1,2,3]伪数组

this

如何传 this

  • 目前可以用 fn.call(xxx,1,2,3)传 this 和 arguments
  • 而且 xxx 会被 自动转化成对象(JS的糟粕)
  • 取消上述规则
    function fn(){
        'use strict'
        console.log(this)
    }
  • this 像是隐藏参数,arguments 是普通参数

需要一种办法拿到没有对象名的对象的引用

  • 获取对象的 name 属性

JS在每个函数里加了this

  • 用 this 获取那个对象
    let person = {
        name: 'frank',
        sayHi(){
            console.log(`你好,我叫` + this.name)
        }
    }
    
    person.sayHi()
    // 相当于
    person.sayHi(person)
  • 然后 person 被传给 this 了(person 是个地址)
  • this 就是引用的对象

两种调用

第一种

  • person.sayHi()
  • 会自动把 person 传到函数里,作为 this

第二种(推荐)

  • person.sayHi.call(person)
  • 需要自己手动把 person 传到函数里,作为 this

没有用到 this

  • add.call(undefined,1,2) // 3

为什么要多写一个 undefined

  • 因为第一个参数要作为 this
  • 但是代码里没有用 this
  • 所以只能用 undefined / null 占位

this 的两种使用方法

  • 隐式传递
    • fn(1,2) // 等价于 fn.call(undefined,1,2)
    • obj.child.fn(1) // 等价于 obj.child.fn.call(obj.child,1)
  • 显示传递
    • fn.call(undefined,1,2)
    • fn.apply(undefined,[1,2])

绑定 this

使用 .bind 可以让 this 不被改变

    function f1(p1, p2){
        console.log(this, p1, p2)
    }
    let f2 = f1.bind({name:'frank'})
    // 那么 f2 就是 f1 绑定了 this 之后的新函数
    f2() // 等价于 f1.call({name:'frank'})

.bind 还可以绑定其他参数

    let f3 = f1.bind({name:'frank'}, 'hi')
    f3()  // 等价于 f1.call({name:'frank'}, 'hi')

箭头函数

里面的 this 就是外面的 this

    console.log(this)  // window
    let fn = ()=> console.log(this)
    fn() // window

就算你加 call 都没有

  • fn.call({name:'frank'}) // window

立即执行函数(现在用不上)

原理

  • ES5时代,为了得到局部变量,必须引入一个函数
  • 但是这个函数如果有名字,就得不偿失
  • 于是这个函数必须是匿名函数
  • 声明匿名函数,然后立即加个()执行它
  • 但是JS标准认为这种语法不合法
  • 所以JS程序员需求各种方法
  • 只有在匿名函数前面加个运算符即可
  • !、~、()、+、- 这些符号都可以
  • 最好用 ! 来解决