JS基础--JS函数

310 阅读11分钟

函数是什么

函数本身就是一个比较特别的对象!!!

定义一个函数

具名函数

function 函数名(){
     语句
     return 返回值
}

匿名函数

let a = function () {
     语句
     return 返回值
}
  • 可以发现,具名函数和匿名函数的区别就是 function 后面是否直接跟了一个函数名。除了立即执行函数,匿名函数必须同时声明一个变量来存储这个函数的地址。
  • function () {语句return 返回值}称作“函数表达式”。
  • 匿名函数与具名函数的怪异结合声明函数:let a = function fn(){} 这中声明函数的方式最终结果还是一个匿名函数的声明,只能a()来调用函数,而不能fn()调用函数,因为fn这个变量的作用域只在let a = function fn(){}等号右边那一部分,其他地方无法调用这个fn。

箭头函数

  1. 箭头函数的完整结构。()内传入参数;{}中的内容,包括{}是函数体。

    let fn = (x,y)=>{
        console.log(x+y)
        return x+y
    }
    
  2. 箭头函数只有1个参数的时候可以省略 (),没有参数或2个及2个以上的参数()不可省略。

    let fn = x =>{
        console.log(x)
        return x
    }
    
  3. 箭头函数函数体中只有一句 return语句 ,则可以省略 {}及return

    let fn = (x,y)=>x+y // return x+y
    
  4. 在3.的基础上,如果函数体直接返回的是一个对象则会出现歧义。

    let fn = x => { name:x } // 本意是直接返回{ 'name': x }这个对象
    // 但是JS对{}符号首先会认为它是函数块,name:x这个结构会被认为是label
    // 正确写法,给对象添加一个()号
    let fn = x =>({ name:x })
    

构造函数

  1. 构造函数是最规范的写法,不过由于使用麻烦,所以基本没人使用。好处就是能一眼看出来,所有函数都是由 Function 构造函数构造而成的。

    let fn = new Function('x','y','return x+y')
    // 'x' 'y'是形式参数,'return x+y'是函数体。
    

函数本身与函数调用

let fn = ()=>{console.log('hello')}
fn2 = fn
fn()
fn2()
  • 直接一个 函数名 指的是函数本身,而 函数名() 则是函数调用
  • 上面 fn 是 fn函数本身,fn() 是调用函数fn。
  • fn其实只是一个保存了 ()=>{console.log('hello')} 这个函数对象的地址的变量。
  • fn2 = fn 只是把 fn 保存的地址复制给了 fn2
  • fn、fn2都只是()=>{console.log('hello')}这个匿名函数的引用,真正的函数其实只是一个存储在Heap区的一个对象。

函数的要素一:调用时机

  • 函数调用前,引用的变量是否已经声明?

  • 函数调用时,变量的值是多少?

  • setTimeout的调用:

    let a = 1
    function fn(){
        setTimeout(()=>{
            console.log(a)
        },0)
    }
    fn()
    a = 2
    

    setTimeout的执行涉及到JS执行队列的问题,因为setTimeout中的函数的执行顺序是在整个代码块执行之后,所以实际上console.log(a)是在 a = 2之后执行的,所以最后输出的是2。

  • 在for循环中的setTimeout的调用与变量的问题:

    let a = 1
    for (let i = 1 ; i < 6; i++ , a++) {
        setTimeout(() => {
            console.log('i:' + i)
            console.log('a:' + a)
        }, 0)
    }
    // i:1 2 3 4 5 
    // a:6 6 6 6 6 
    

    在上一节我们知道了setTimeout中的函数是在主程序执行后才执行。

    let声明的变量i,在for循环中会在内部不断地创建块级作用域,来存储每个i。所以每个setTimeout的回调函数在引用变量i的时候,会就近使用块级作用域的i。

    而当setTimeout的回调函数在引用变量a的时候,因为变量a是全局变量,而在就近的作用域中并没有另一个变量名为a的变量,所以直接在全局变量中引用a。

函数的要素二:作用域

什么是作用域?

  • 作用域:限制变量名、函数名等名字的的使用范围就是作用域。

  • 每个函数调用的时候都会默认创建一个作用域。作用域的范围就是函数的{}包裹的范围。

    function fn(){
        let a = 1
    }
    fn()
    console.log(a) // 报错,a未被定义
    

作用域、全局变量及局部变量

  • 全局变量:哪里都能访问的变量;
  • 局部变量:有访问范围限制的变量。
  • 顶级作用声明的变量是全局变量
  • window的属性是全局变量,为什么Object、parseInt可以随时使用,因为他们是window的一个属性。
  • 其他都是局部变量

函数的嵌套与作用域的嵌套

function f1() { // f1作用域开始
    let a = 1
    function f2() { // f2作用域开始
        let a = 2
        console.log(a) 
    } // f2作用域结束
    console.log(a)
    a = 3
    f2()
} // f1作用域结束
f1() 
  • 可以见到f2作用域是嵌套在f1作用域中的,和它们函数的嵌套关系相同。

作用域的规则

当多个作用域中同时存在同名变量a

  • 在查找a时,根据作用域的嵌套,从内而外向上取最近的作用域中的变量a。简称:就近原则
  • JS中的作用域是静态作用域,查找a的过程与函数执行无关。
  • 而 a 的值与函数执行有关。

根据这个规则,下面这个代码输出结果。

function f1() {
    let a = 1
    function f2() {
        let a = 2
        function f3() {
            console.log(a) // 22
        }
        a = 22
        f3() // 注意调用f3的时候a的值已经重新赋值22
    }
    console.log(a) // 1
    a = 100
    f2()
}
f1()
// 输出结果与顺序:
// 1
// 22 

函数的要素三:闭包

什么是闭包?

  • 如果一个函数用到了其外部的变量,那么这个函数加这个外部的变量就称作闭包

函数的要素四:形式参数

什么是形式参数?

  • 形式参数的意思就是非实际参数,可以理解为一个容器,当函数调用的时候实际参数会被赋值给这些形式参数。

    function add(x,y){
        return x+y
    }
    add(1,2)
    

    上面的函数,其中 x y 就是形式参数。当调用add(1,2)时,1 和 2是实际参数,这两个实际参数会分别赋值给 x y。

  • 形参的数量不代表实际参数的数量,形参可多可少,只是给参数取个名字。

函数的要素五:返回值

  • 每个函数都有返回值,如果没有指定返回的内容,JS函数默然返回一个undefined(return undefined

  • 函数执行完后才会返回

  • 函数 return 后,后面的函数内容不再继续执行

    function fn(){
        return 'hello'
        console.log('world')
    }
    fn() // 没有输出'world'
    
  • 返回值和值不是同一个东西。return 1+2 函数返回3;1+21+2的值是3。

函数的要素六:调用栈

什么是调用栈?

  • JS引擎在调用一个函数之前,会把函数执行完后应该返回的地址存储(push)在一个数组中(压栈)。这个数组就是调用栈。
  • 当函数执行完成后就会根据调用栈从后往前把地址pop出来(弹栈),当将调用栈所有地址都弹出后,即回到主程序,继续执行后续的代码。
  • 调用栈有最大限度,Chrome、Node:12500左右,Firefox:26500左右。当程序运行超出调用栈的最大限度,称作爆栈或栈溢出。
  • 调用栈最常关联的就是递归函数,一般也只有递归函数才可能出现爆栈的现象。所以在使用递归函数的时候,要确保函数都递归终点,否则必定爆栈。

调用栈的存储与弹出的过程

function fn(x){
    return x!==1 x*f(x):1
}
// 用调用栈分析fn(4)的实现过程
// 压栈:1.此时在主程序环境中。调用栈push当前 fn(3) 所处地址后,进入f(3)
// 弹栈:3.f(3)得到返回值 6 后,根据调用栈地址返回之前f(3)所处位置。此时已经返回到主程序的环境中,继续执行后续程序。
fn(3){
    return 3 * f(2)
    // 压栈:2.调用栈push f(2) 所处地址后,进入f(2)
    // 弹栈:2.f(2)得到返回值 2 后,根据调用栈地址返回之前f(2)所处位置
    f(2){
    	return 2 * f(1)
        // 压栈:3.调用栈push f(1)所处地址后,进入f(1)
        // 弹栈:1.f(1)得到返回值 1 后,根据调用栈地址返回之前f(1)所处位置 
        f(1){
       		return 1	
    	}
	}
}

// *上面的代码非正常代码,只是为了方便描述

函数的要素七:具名函数的提升

  • 具名函数会有函数提升的现象

    // fn()前,已经声明了 fn 但是还没给函数赋值
    fn()
    function fn(){
        console.log('hello')
    }
    // 输出'hello'
    

    正常逻辑来说 fn() 应该报错,但还是具名函数声明的函数会有函数提升的现象,即把声明函数这行代码提升到主程序的最前方。但是提升的只是声明,赋值并没有提升。

  • 匿名函数没有函数提升现象

    fn() // 报错
    let fn = function(){
        console.log('hello')
    }
    

函数的要素八:arguments和this

arguments

  • arguments是函数调用时所有参数的一个伪数组集合,也是一个对象。如:arguments[0]就是该函数第一个参数。

    function fn(){
        return arguments[0]+arguments[1] // 这里也可以用形参代替
    }
    fn('love','peace') // 返回 'lovepeace'
    // 即 arguments[0] = 'love' arguments[1] = 'peace'
    
  • 传arguments:在函数调用的时候直接通过传入实参即可以传入arguments。

    function fn(){
        console.log(arguments)
    }
    fn(1,2,3,4)
    
  • arguments是(除了箭头函数)函数的一个普通参数。arguments对象只能在函数中使用。

  • 一般我们在不能确定参数数量的时候使用arguments对象。

this

  • this的含义:当前执行代码的环境对象。this可以认为是隐藏在函数中一个属性。

  • 为什么我们需要this?因为我们需要在对象没声明之前拿到对象!!

  • 如果没有this,我们需要怎么拿到没声明的对象?

    class Person{
        constructor(name){
            this.name = name // 这个this的指向是new语句的效果
        }
        sayHi(self){
            // 没有this的时候,我们通过参数来拿到没有声明的对象
            console.log('我是'+self.name) 
        }
    }
    let p1 = new Person('jack')
    p1.sayHi(p1) // 这里我们把声明的p1当作实参传入函数
    
  • 一些语言如Python就通过上面的方法获取那个未声明的对象,而JS则在每个函数中添加了this来获取那个未声明的对象。

    let person = {
        name:'frank',
        sayHi(){console.log('我是'+this.name)} // this被赋值person的地址
    }
    
    person.sayHi() 
    // 相当于  
    person.sayHi(person){this = person} // 只是用于解释,这个写法不规范
    

涉及this的函数调用

  1. 小白调用法:

    • xxx.sayHi() 这个调用方法会自动把 xxx对象 传到函数中,作为this。
  2. 大师调用法:call

    • xxx.sayHi.call(xxx,其它参数),这个方法的第一个参数就是手动指定的this。可以是自己xxx,也可以是其他对象。
    • 如果函数用不到this咋办?一般传入 undefined 或 null占位。
  3. call apply bind的使用与区别

    • call 和 apply的作用类似,但是使用形式稍有不同。call 和 apply的第一个参数是this的指向,后面参数是调用函数的所传入的实参。
    • bind则可以绑定this指向的对象,以及绑定其他参数。bind的第一个参数是this的指向,后面参数是绑定的调用函数的实参,绑定实参后,调用函数时不需要传入已经绑定的实参。
    // call 与 apply
    function add(x,y,z){return x+y-z}
    add.call(undefined,1,2,3) // 因为用不上this,所以使用undefined占位
    add.apply(undefined,[1,2,3])
    
    // bind
    function fn(p1,p2){
        console.log(this,p1,p2)
    }
    // 给fn2赋值了绑定了 this指向为{'name':'jack'} ,第一个实参为10的 fn函数
    fn2 = fn.bind({'name':'jack'},10) 
    // fn2调用的时候只需要传入第3个参数就可以了
    fn2() // {name: "jack"} 10 undefined 因为没有传第三个参数所以undefined
    fn2(12) // {name: "jack"} 10 12 
    // fn2(12)相当于
    fn.call({'name':'jack'},10,12)
    
    

fn()中的this指向哪里?

  • 根据上面的内容:obj.sayHi()中的this指向的很显然是obj这个对象。
  • 那么一个最简单的函数 fn() 中的this指向哪里呢?
  • 答案:window。
  • 在顶级作用域声明的函数都挂载到了window这个对象上,所以fn() 即 window.fn() 那么,fn()中的this自然指向window。

箭头函数与this

  • 箭头函数的this永远指向的是其外面this的指向,因为箭头函数没有this。

  • 即使使用call调用箭头函数也没有用,因为call只是改变了this的指向,没有this就根本无法改变指向。

    let fn1 = {
        show(){    
        	console.log(this)
        }
    }
    let fn2 = {
        show:()=>{
            console.log(this)
        }
    }
    fn1.show() // fn1
    fn2.show() // window
    fn1.show.call({'name':'frank'}) // {'name':'frank'}
    fn2.show.call({'name':'frank'}) // window
    

立即执行函数

  • ES6以前的产物,为了生成一个局部变量,必须使用一个函数

  • 如果使用变量接收了这个函数就造成了资源的浪费

  • 所以我们声明一个没有名字的而且马上执行的函数来实现这个效果。

  • 在匿名函数前加个运算符都能实现立即执行函数,如:!、+、-、()、~

    // 立即执行函数
    (function(){var a = 1})()
    // ()实现的有个弊端
    console.log()
    (function(){var a = 1})()
    // 上面的代码会出现 undefined(function(){var a = 1})()的结果,然后就是报错
    // 如果非要用()则必须在立即执行函数前加 ; 
    
  • 推荐永远使用 ! 来实现立即执行函数

    !function(){var a = 1}()