JS函数

229 阅读8分钟

定义一个函数:

具名函数

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

匿名函数

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

如果你的函数声明在等号右边,那么你的整个函数作用域就只在等号右边,其他地方你要用这个函数只能用a,但是没有这个等号就成为了全局作用域

箭头函数

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})
//直接返回对象会出错,需要加个圆括号,不加圆括号js会以为是个labal标签

用构造函数

let f = new Function('x','y','return x+y')
  • 基本没人用,但是能让你知道函数是谁构造的
  • 所有函数都是Function构造出来的
  • 包括ObjectArrayFunction也是
  1. 函数自身(fn)与函数调用(fn())的区别
let fn = () => console.log('hi')
let fn2 = fn
fn2()

结果:

  • fn保存了匿名函数的地址
  • 这个地址被复制给了fn2
  • fn2()调用了匿名函数
  • fnfn2都是匿名函数的引用而已
  • 真正的函数既不是fn也不是fn2

函数的要素

  1. 调用时机
let a = 1
function fn(){
    console.log(a)
}

问打印出多少? - 不知,因为没有调用代码

let a = 1
function fn(){
    console.log(a)
}
a = 2
fn()

问打印出几? - 2

let a = 1
function fn(){
    console.log(a)
}
fn()
a = 2

问打印出几? - 1

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

问打印出多少? - 2

let i = 0
for(i = 0 ; i < 6 ; i++){
    setTimeout(() => {
        console.log(i)
    },0)
}

问打印出多少?
不是0、1、2、3、4、5,而是6个6

for(let i = 0 ; i < 6 ; i++){
    setTimeout(() => {
        console.log(i)
    },0)
}

问打印出多少? 是0,1,2,3,4,5.因为JSforlet一起用的时候会加东西,每次循环会创建一个i

  1. 作用域 每个函数都会创建一个作用域
function fn(){
    let a = 1
}
console.log(a) //a不存在,只作用于函数内代码里面

问:是不是因为fn没法执行导致? - 就算fn执行了也访问不到作用域里面的a

function fn(){
    let a = 1
}
fn()
console.log(a) //a还是不存在,只作用于函数内代码里面
  • 全局变量与局部变量:在顶级作用域声明的变量是全局变量,window的属性是全局变量(写在哪里都是全局变量),其他都是局部变量
  • 函数可嵌套,作用域也嵌套
function f1(){
    let a = 1
    
    function f2(){
        let a = 2
        console.log(a) 
    }
    
    console.log(a) 
    a = 3
    f2() // 2
}
f1() // 1
  • 作用域规则:如果多个作用域有同名变量a,那么查找a的声明时,就向上取最近的作用域,简称「就近原则」;查找a的过程与函数执行无关,但a的值与函数执行有关
  • 跟函数执行没有关系的叫静态作用域(又名词法作用域);有关的叫动态作用域
function f1(){
    let a = 1
    function f2(){
        let a = 2
        function f3(){
            console.log(a)
        }
        a = 22
        f3() // 22
    }
    console.log(a)
    a = 100
    f2()
}
f1()
  1. 闭包
  • JS的函数会就近寻找最近的变量,就叫闭包
  • 如果一个函数用到了外部的变量,那么这个函数加这个变量,就叫做闭包;
  1. 形式参数
  • 形式参数的意思就是非实际参数
function add(x,y){
    return x+y
}          //其中x和y就是形参,因为并不是实际的参数
add(1,2)   // 调用add时,1和2是实际参数,会被复制一份,传给x  y
  • 形参可认为是变量声明
//上面的代码近似等价于下面的代码
function add(){
    var x = arguments[0] //arguments[0]表示提供的所有形参的第一个
    var y = arguments[1] // 提供的所有形参的第二个
    return x+y
}

  • 形参可多可少:形参只是给参数取名字
  1. 返回值
  • 返回值:每个参数都有返回值;
function hi(){ console.log('hi') }
hi() //没写return,所以返回值是undefined ,函数不执行不存在返回值,即函数执行了,才会有返回值

function hi(){ return console.log('hi')}
hi() //返回值为console.log('hi')的值,即undefined,打印值是打印值,返回值是返回值,不同,这个命令虽然打印了hi,但是返回值是undefined
  • 函数执行完了后才会返回
  • 只有函数有返回值
  1. 调用栈
  • 什么是调用栈?JS引擎在嗲用一个函数前,需要把函数所在的环境push到一个数组里,这个数组叫做调用栈,等函数执行完了,就会把环境弹(pop)出来,然后ruturn到之前的环境,继续执行后续代码
console.log(1)
console.log('1+2的结果为' + add(1,2))
console.log(2)
  • 但是递归很可能把栈压满。递归函数:
  • 阶乘
function f(n){
    return n !== 1? n*f(n-1) : 1
}

理解递归:

先递进再回归

  • 递归函数的调用栈很长,调用栈最长有多长
function computeMaxCallStackSize(){
    try {
        return 1 + computeMaxCallStackSize();
    } catch (e) {
        //报错说明 stack overflow 了
        return 1;
    }
}
// Chrome 12578
// Firefox 26773
// Node 12536
  • 爆栈:如果调用栈中压入的帧过多,程序就会崩溃
  1. 函数提升
  • 什么是函数提升:不管你把具名函数声明在哪里,它都会跑到第一行
function fn(){}
  • 什么不是函数提升?
let fn = function(){} //这是赋值,右边的匿名函数声明不会提升

  • let有一个特性,如果这个函数已经被声明了,就不允许再let;但是var可以

  • 先声明再使用

  1. arguments(每个函数都有,除了箭头函数)
  • 是个包含所有参数的伪数组,
  • 如何传arguments:调用fn即可传argumentsfn(1,2,3)那么arguments就是[1,2,3]伪数组
  1. this(每个函数都有,除了箭头函数)
  • 如果不给任何条件,this默认指向window
  • 如何传this:目前可以用fn.call(xxx,1,2,3)thisarguments,而且xxx会被自动转化成对象(糟粕),xxxthis[2,3,4]arguments
  • 如果传给this的不是一个对象,this会自动帮你封装成对象

1是对象:

而当前面传入一个'use strict'时,再传入什么就是什么了:

  • this是隐藏参数,arguments是普通参数

  • 假设没有this:

let person = {
    name: 'frank',
    sayHi(){
        console.log('你好,我叫' + person.name)
    }
}
  • 分析:我们可以用直接保存了对象地址的变量获取'name',把这种办法简称为引用
let sayHi = function(){
    console.log('你好,我叫' + person.name)
}
let person = {
    name:'frank'
    'sayHi': sayHi
}

分析:person如果改名,sayHi函数就挂,sayHi函数甚至有可能在另一个文件里面,所以我们不希望sayHi函数里出现person引用

class Person{
    constructor(name){
        this.name = name
        //这里的this是new强制指定的
    }
    sayHi(){
        console.log(???)
    } // 如果对象还没有生成,就没法引用这个对象
}

分析:这里只有类,还没创建对象,故不可能获取对象的引用,那么如何拿到对象的name?因此,需要一种办法拿到对象,这样才能获取对象的name属性

一种土方法:用参数

  • 对象
let person = {
    name: 'frank'
    sayHi(p){
        console.log('你好,我叫' + p.name)
    }
}
person.sayHi(person)
class Person{
    constructor(name){this.name = name}
    sayHi(p){
        console.log('你好,我叫' + p.name)
    }
}

JS在每个函数里加了this

  • 用this获取那个对象
let person = {
    name: 'frank'
    sayHi(this){
        console.log('你好,我叫' + this.name)
    }
}

person.sayHi()相当于person.sayHi(person) 然后person被传给this了(person是个地址)这样,每个函数都能用this获取一个为知对象的引用,person.sayHi()会隐式地把person作为this传给sayHi,方便sayHi获取person对应的对象

  • 两种调用
  • 小白调用法
  • person.sayHi(),会自动把person传到函数里,作为this
  • 大师调用法:
  • person.sayHi.call(person),需要自己手动把person传到函数里,作为this

function add(x,y){
    return x+y
}
  • 没有用到this
  • add.call(undefined,1,2) //3
  • 为什么要多写一个undefined?因为第一个参数要作为this,但是代码里没有用this,所以只能用undefined占位,其实用null也可以
Array.prototype.forEach2 = function(fn){
    for(let i = 0 ; i < this ; i++){
        fn(this[i], i, this)
    }
}
  • this是什么?由于大家使用forEach的时候总是会用arr.forEach2,所以arr就被自动传给forEach2
  • this一定是数组么?不一定,比如:Array.prototype.forEach2.call({0:'a',1:'b'})

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.qpply(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)

箭头函数

  • 没有arguments和this
  • 里面的this就是外面的this
console.log(this)//window
let fn = () => console.log(this)
fn()//window

就算你加加call都没有 fn.call({name:'frank'}) //window

立即执行函数

  • 原理:
  • ES5时代,为了得到局部变量,必须引入一个函数,但是这个函数如果有名字,就得不偿失,于是这个函数必须是匿名函数,声明匿名函数,然后立即加个()执行它,但是JS标准认为这种语法不合法,所以JS程序员寻求各种办法,最终发现,只要在匿名函数面前加个运算符即可,! ~ () + -都可以,但是这里面有些运算符会往上走,所以建议永远用!来解决,因为感叹号或者圆括号会往上看,前一个表达式的结果可能会影响立即执行函数,最好的方法是前面语句后面加个分号

新版JS创造局部变量

可能出现的bug: