JavaScript函数

123 阅读9分钟

目录

函数介绍

调用时机

作用域

闭包

形式参数

返回值

调用栈

函数提升

arguments和this(除了箭头函数)

箭头函数

立即执行函数

一、函数介绍

1.定义一个函数

- 具名函数

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

- 匿名函数

let a = function(x,y){return x+y}
  • 具名函数去掉函数名就叫匿名函数

  • 也叫做函数表达式

  • 等于号右边又名字的函数,作用域仅在等于号的右边,不会作用到其他地方

  • 如果没有let 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})

  • 直接返回对象的话会出错,所以要加圆括号

- 用构造函数

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

  • 基本没有人用,但能知道函数是谁构造的

  • 所有函数都是Function构造出来的

  • 包括Object/Array/Function

2.函数自身和函数调用

  • fn与fn()

- 函数自身

  • 代码如下
let fn = () => console.log('hi')
fn
  • 不会产生任何结果,因为fn没有被调用

- 函数调用

  • fn加上()就执行了函数fn

  • 把代码改一下

let fn = () => console.log('hi')
let fn2 = fn
fn2()
  • 结果

    1. fn保存了匿名函数的地址

    2. 这个地址被复制给了fn2

    3. fn2() 调用了匿名函数

    1. fnfn2都是匿名函数的引用而已

    2. 真正的函数既不是fn也不是fn2

3.函数的要素

  • 每一个函数都有这些东西

    1. 调用时机

    2. 作用域

    3. 闭包

    4. 形式参数

    5. 返回值

    6. 调用栈

    7. 函数提升

    8. arguments和this(箭头函数没有)

二、调用时机

  • 调用时机不同,结果就不同
// 调用时机
// 例子1
let a = 1
function fn1() {
    console.log(a);
}
//问打印出多少
//答案:不知道,因为没有调用代码
// ---------------------------------------
//例子2
let b = 1
function fn2() {
    console.log(b);
}
fn2()
// 答案:打印出1
// ---------------------------------------
// 例子3
let c = 1
function fn3() {
    console.log(c);
}
c = 2
fn3()
// 答案:打印出2,因为在函数被调用前,变量c已经被改成2了
// ---------------------------------------
// 例子4
let d = 1
function fn4() {
    console.log(d);
}
fn()
d = 2
// 答案:打印出1,函数先调用,d才被改变
// ---------------------------------------
// 例子5
let e = 1
function fn5() {
    setTimeout(() => {
        console.log(a)
    }, 0)
}
fn5()
e = 2
// 答案:打印出2,因为setTimeout会让函数等一会儿才打印出e
// ---------------------------------------
// 例子6
let i = 0
for (i = 0; i < 6; i++) {
    setTimeout(() => {
        console.log(i)
    }, 0)
}
// 答案:打印出6个6,而不是0、1、2、3、4、5 ,i=6的时候才会退出循环,所以i
// ---------------------------------------
// 例子7
for (let i = 0; i < 6; i++) {
    setTimeout(() => {
        console.log(i);
    }, 0)
}
// 答案:0,1,2,3,4,5,因为JS在for和let一起用的时候会加东西
// 每次循环会多创建一个 i (蛋疼)
// ---------------------------------------

例子6和7有详细的解释JS 函数的执行时机 - 掘金 (juejin.cn)

三、作用域

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

  • let的作用域就在包住它的{}里面

1. 全局变量和局部变量

  • 在顶级作用域声明的变量是全局变量

  • window的属性是全局变量

  • 其他的变量都是局部变量

2. 函数可嵌套

  • 作用域也可以嵌套
// 作用域
// 例子1
function fn6() {
    let a = 1
}
console.log(a); // a不存在
// 就算fn执行了,也访问不到作用域里的a
// 例子2
function fn7() {
    let a = 1
}
fn7()
console.log(a); // a 还是不存在
// ---------------------------------------
// 例子3
function f1() {
    let a = 1
    function f2() {
        let a = 2
        console.log(a); // 这里打印的是2 作用域在f2内
    }
    console.log(a); // 这里打印的是1 作用域在f1内
    a = 3 // 这个没啥卵用
    f2()
}
f1()
// ---------------------------------------
// 例子4
function f3() {
    let a = 1
    function f4() {
        let a = 2
        function f5() {
            console.log(a); // a = 22,这个函数用到了外面的变量,所以它和外面的这个特变量叫做闭包
        }
        a = 22
        f5()
    }
    console.log(a); // a = 1
    a = 100
    f4()
}
f3()

3.作用域规则

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

    1. 那么查找a的声明时,就向上取最近的作用域里的a

    2. 简称(就近原则)

    3. 查找a的过程与函数无关

    4. 但a的值与函数的执行有关

    5. 这叫静态作用域,也叫词法作用域

四、闭包

function f3() {
    let a = 1
    function f4() {
        let a = 2
        function f5() {
            console.log(a); // a = 22,这个函数用到了外面的变量,所以它和外面的这个特变量叫做闭包
        }
        a = 22
        f5()
    }
    console.log(a); // a = 1
    a = 100
    f4()
}
f3()
  • 如果一个函数用到了外部的变量

  • 那么这个函数加上这个变量就叫做闭包

  • 以后会详细学

五、形式参数

  • 形式参数的意思就是不是实际的参数
function add(x,y){
  return x+y
}

add(1,2)
  • 代码中的xy就是形参,因为不是实际的参数

  • 调用add时,12就是实际参数,会被赋值给x,y

形参可以认为是变量声明(本质)

上面的代码等价于下面代码

function add(){
  var x = arguments[0]
  var y = arguments[1]
}
  • 形参可多可少,形参只是给参数取名字而已,尽量保持一致

六、返回值

1.每个函数都有返回值

function hi(){console.log('hi')}
hi()
  • 没有返回值,所以上面hi的返回值是undefined
function hi(){return console.log('hi')}
hi()
  • 返回值为console.log('hi')的值,还是undefined

2.函数执行完了才会返回,只有函数有返回值

  • 1 + 2 返回值为3(错的) 因为表达式没有返回值

  • 1 + 2 的值为3 (对的)

七、调用栈

1.什么是调用栈

  • JS引擎在调用一个函数前,需要把函数所在环境push到一个数组里,这个数组就叫调用栈

  • 等函数执行完了,就会把环境弹pop出来

  • 然后return到之前的环境,继续执行后续代码

ce1f3aae68b7dad0850fdb451e08cb0.png

八、递归函数

1.阶乘函数

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

2.理解递归

  • 上面函数的运行方式为
f(4)
= 4 * f(3)
= 4 * f(3 * f(2))
= 4 * f(3 * (2 * f(1)))
= 4 * f(3 * (2 * (1)))
= 4 * f(3 * (2))
= 4 * (6)
= 24
  • 先递进,再回归

3.调用栈过程

45cd62253ee299081176d0ed57512cf.png

  • 等于说f(4)就需要压4次栈,那么f(100)就要压100次栈

4.调用栈的上限是多少

funtion computeMaxCallStackSize(){
  try{
    return 1+ computeMaxCallStackSize()
  }(atch(e)){
  // 报错说明 stack over flow了 用完了
  return 1;
  }
}
  • 经过测试

    1. chrome: 12578

    2. Firefox: 26773

    3. Node: 12536

5.爆栈

  • 如果调用栈中压入的帧过多,程序就会崩溃

九、函数提升

1.什么是函数提升

fn()
function fn(){
    let a = 1
    console.log(a)
}
  • 不管把具名函数声明在哪里,它都会跑到第一行

  • 如果同时拥有var和function同名,那它就是var的值,而let会直接报错

2.什么不是函数提升

  • let fn = function(){}

  • 这是赋值,右边的匿名函数声明不会提升

十、argumentsthis

  • 每个函数都有argumentsthis,除了箭头函数

  • 代码

function fn(){
    console.log(arguments)
    console.log(this)
}
  • 可以打印出来

1.如何传arguments

  • 调用fn即可传arguments

  • fn(1,2,3)那么arguments就是[1,2,3]伪数组

2.如何传this

  • 目前可以用fn.call(xxx,1,2,3)传this和arguments

  • 而且xxx会被自动转化成对象(JS的不合理之处)

  • 如果xxx那里为空,this会默认指向window

  • 如果传的this不是对象,JS会自动帮你封装成对象

  • 声明函数时,加'use strict' 就不会变

3.结论

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

4.假如没有this

  • 代码如下
let person = {
  name:'jack',
  sayHi(){
    console.log('你好,我叫'+ person.name)
  }
}
  • 分析

    1. 我们可以直接用保存了对象地址的变量获取'name'

    2. 我们把这种方法简称引用

5.问题1

  • 代码如下
let sayHi = function(){
  console.log('你好,我叫'+ person.name)
}
let person = {
  name:'jack',
  'sayHi':sayHi
}
  • 分析

    1. person如果改名,sayHi函数就挂了

    2. sayHi函数甚至有可能出现在另一个文件里

    3. 所以我们不希望sayHi函数里出现person引用

6.问题2

  • 代码如下
class Person{
  constructor(name){
    this.name = name //这里的this是new强制指定的
  }
  sayHi(){
    console.log(???)
  }
}
  • 分析

    1. 这里只有类,还没创建对象,所以不可能获取到对象的引用(都没有咋获取)

    2. 那么如何拿到对象的name

7.解决方法

  • JS在每个函数里加了this

  • this获取那个对象

let person = {
  name:'jack',
  sayHi(){
    console.log('你好,我叫'+ this.name)
  }
}
  • person.sayHi()相当于person.sayHi(person)

  • 然后person被传给this了(person是个地址)

  • 这样,每个函数都能用this获取一个位置对象的引用了

8. 到底哪个对

  • person.sayHi(person)person.sayHi()都对,因为JS提供了两种调用模式解决这种不和谐

9.两种调用模式

  • 小白调用法

    1. person.sayHi()

    2. 会自动把person传到函数里,作为this

  • 大师调用法

    1. person.sayHi.call(person)

    2. 需要自己手动把person传到函数里,作为this

  • 学习那种

    1. 学习大师调用法,因为小白调用法会用了

    2. 以后培养好习惯,必须用大师调用法调用函数

    3. 如果函数里没有this,那用call代表this的参数必须要占用,用undefined或null都行

10.例子

Array.prototype.forEach2 = function(fn){
  for(let i = 0; i < this.length; i++){
    fn(this[i],i,this)
  }
}
  • 这里的this是什么

    • 大家使用forEach时总用arr.forEach()

    • arr自动被传给forEachthis

  • this可以是其他东西

    • 比如Array.prototype.forEach2,call({0:'a',1:'b'})

    • callthis传什么,this就是什么

11.this的两种用法

  • 隐式传递

    1.fn(1,2) 等价于fn.call(undefined,1,2)

    2.obj.child.fn(1) 等价于obj.child.fn.call(obj.child,1)

  • 显示传递

    1. fn.call(undefined,1,2)

    2. fn.apply(undefined,[1,2]) 中间arguments值用中括号括起来

12.绑定this

  • 在使用.bind时可以让this不被改变
function f1(p1,p2){
  console.log(this,p1,p2)
}

let f2 = f1.bind({name:'jack'})
//那么f2就是f1绑定了this之后的新函数
  • f2()等价于f1.call({name:'jack'})

13..bind还可以绑定其他参数

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

十一、箭头函数

  • 没有argumentsthis

  • 箭头函数里面的this就是外面全局的this

console.log(this) // window

let fn = () => console.log(this) 
fn() //window
//直接照外面的变量this,也就是window

十二、立即执行函数

  • 原理

    1. ES5,为了得到局部变量,必须引入一个函数

    2. 但是这个函数如果有名字,就变成了全局函数,一来一去等于啥都没做

    3. 于是这个函数被规定必须是匿名函数

    4. 声明匿名函数,然后立即加个括号执行它

    5. 但是JS标准认为这种语法不合法

    6. 最终发现,只要在匿名函数前加个运算符即可

    7. !~()+-都可以

    8. 推荐永远只用 !来解决,因为感叹号不会往上关联