定义一个函数

所有函数都是 **Function** 构造出来的,包括 **Object****Array****Function** 也是

  1. /* 具名函数 */
  2. function 函数名(形式参数1 形式参数2){
  3. 语句
  4. return 返回值
  5. }
  6. /* 匿名函数 */
  7. let a = function(x,y){return x+y} // 等于号 左边是赋值,右边是函数表达式
  8. /* 箭头函数 */
  9. let f1 = x => x*x // 箭头函数 左边 x 是输入参数,右边 x*x 是输出参数
  10. let f2 = (x,y) => x+y // 圆括号不能省
  11. let f3 = (x,y) => {return x+y} // 加了花括号,return 不能省
  12. let f4 = (x,y) => ({name:x, age:y}) // 直接返回对象会出错,需要加个圆括号
  13. /* 构造函数 */
  14. let f = new Function('x','y','return x+y') // 基本没人用,但能知道函数是谁构造的
  • let a = function(x,y){return x+y}
    • 如果函数声明是在 = 右边,那么整个作用域就只能在函数右边,其他地方要用这个函数就用 a
  • function(x,y){return x+y}
    • 没有 = 等于号就是全局作用域

      函数自身 VS 函数调用 fn V.S. fn()

      函数自身 fn

      1. let fn = () console.log('hi')
      2. fn
      结果:不会有任何结果,因为 fn 没有执行

函数调用 fn()

  1. 代码 1
  2. let fn = () console.log('hi')
  3. fn()

结果:打印 hi ,有圆括号才是调用

  1. 代码 2
  2. let fn = () => console.log('hi')
  3. let fn2 = fn
  4. fn2()

结果:

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


函数的要素

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

调用时机

  • 时机不同,结果不同。示例: ```javascript 例 1 let a = 1 function fn(){ console.log(a) } // 没有调用函数

例 2 let a = 1 function fn(){ console.log(a) } fn() // 打印出 1

例 3 let a = 1 function fn(){ console.log(a) } a = 2 fn() // 打印出 2

例 4 let a = 1 function fn(){ console.log(a) } fn() a = 2 // 打印出 1

例 5 let a = 1 function fn(){ setTimeout(()=>{ console.log(a) },0) } fn() a = 2 // 打印出 2 例 6 let i = 0 for(i = 0; i<6; i++){ setTimeout(()=>{ // 延时函数 console.log(i) },0) } // 不是打印 0、1、2、3、4、5,而是 6 个 6 // 循环先执行完,再去打印

例 7 for(let i = 0; i<6; i++){ setTimeout(()=>{ console.log(i) },0) // 打印出 0、1、2、3、4、5 // 因为 JS 在 for 和 let 一起用的时候会加东西,每次循环会多创建一个 i (为了迎合新人而做出的处理)

  1. - `**setTimeout**` :用于在指定的毫秒数后调用函数或计算表达式。(没明确毫秒数时,尽快执行完当前代码,再打印)
  2. - **语法**有两种:
  3. - `setTimeout (要执行的代码, 等待的毫秒数)`
  4. - `setTimeout (JavaScript 函数, 等待的毫秒数)`
  5. <a name="iV6xA"></a>
  6. ### 作用域:就近原则 & 闭包
  7. - 每个函数都会默认创建一个作用域
  8. ```javascript
  9. function fn(){
  10. let a = 1
  11. }
  12. console.log(a) // a 不存在
  13. // 就算 fn 执行了,也访问不到作用域里面的 a
  14. // 如果在函数里面声明了一个 let 或者 const,那么这个变量就是局部变量,因为它只在函数块里能生效

全局变量 V.S. 局部变量

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

    函数可嵌套

  • 作用域也可嵌套。 示例:

    1. function f1(){
    2. let a = 1
    3. function f2(){
    4. let a = 2
    5. console.log(a)
    6. }
    7. console.log(a)
    8. a = 3
    9. f2()
    10. }
    11. f1()

    作用域规则

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

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

      闭包

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

  • 下面代码中的 af3 组成了闭包

image.png

形式参数

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

    1. function add(x, y){
    2. return x+y
    3. } // 其中 x 和 y 就是形参,因为并不是实际的参数
    4. add(1, 2) // 调用 add 时。1 和 2 是实际参数,会被赋值给 x y
  • 形参的本质是声明变量

    1. // 上面代码近似等价于下面的代码
    2. function add(){
    3. var x = arguments[0]
    4. var y = arguments[1]
    5. return x+y
    6. }
  • 形参可多可少,只是给参数取名字

示例

  1. /* 写法一 */
  2. function add(x){
  3. return x+ arguments[i] // arguments 意为所有参数
  4. }
  5. add(1,2) --> // 3
  6. /* 写法二 */
  7. function add(x,y,z){
  8. return x+y
  9. }
  10. add(3,4) --> // 7


返回值

  • 每个函数都有返回值
  • 返回值是在执行后才会返回的 ```javascript function hi(){ console,log(‘hi’)} hi() // 没写 return,所以返回值是 undefined

function hi(){ renturn console.log(‘hi’)} hi() // 返回值为 console.log(‘hi’) 的值,即 undefined

  1. - **只有函数有返回值**
  2. - 打印值和返回值不是一个东西,**返回值**是 return 后面的值
  3. - `~~1 + 2~~`~~ 返回值为 3~~
  4. - `1 + 2` 值为 3
  5. <a name="kZghT"></a>
  6. ### 调用栈
  7. <a name="jgYzI"></a>
  8. #### 什么是调用栈
  9. - JS 引擎在调用一个函数前,需要把函数所在的环境 push 到一个**数组**里,这个数组叫做**调用栈。**
  10. - 等函数执行完了,就会把环境**弹(pop)**出来
  11. - 然后 return 到之前的环境,继续执行后续代码
  12. <a name="nIVLl"></a>
  13. #### 调用栈的作用
  14. - 调用栈实际上是记录进入一个函数之后,回的话回去哪里?
  15. - 每次进入一个函数都得记下来等一会回到哪,所以要把回到的地址写到栈里。如果进入到函数后还要进入一个函数,就要把这个函数的地址再放到栈里面,函数执行完后就要弹栈,知道回到哪,此为**压栈**和**弹栈**。
  16. <a name="yv3th"></a>
  17. #### 递归函数
  18. > **先递进,再回归**
  19. > - 递进的过程就是**压栈**的过程
  20. > - 回归的过程就是**弹栈**的过程
  21. - **阶乘**
  22. ```javascript
  23. function f(n){
  24. return n !== 1 ? n* f(n-1) : 1
  25. }
  • 理解递归

    1. f(4)
    2. = 4 * f(3)
    3. = 4 * (3 * f(2))
    4. = 4 * (3 * (2 * f(1)))
    5. = 4 * (3 * (2 * (1)))
    6. = 4 * (3 * (2))
    7. = 4 * (6)
    8. = 24
  • 递归函数的调用栈

    • 如何测出浏览器调用栈最长有多长?

      1. function computeMaxCallStackSize() {
      2. try {
      3. return 1 + computeMaxCallStackSize();
      4. } catch (e) {
      5. // 报错说明 stack overflow 了
      6. return 1
      7. }
      8. }
    • Chrome 12578

    • Firefox 26773
    • Node 12536
  • 爆栈 :如果调用栈中压入的帧过多,程序就会崩溃

函数提升

什么是函数提升

说明 截图示例
function fn(){}
不管把具名函数声明在哪里,它都会跑到第一行
image.png
这就是函数提升

【注意】:
image.png

  • **let** 不允许在已经有一个函数 add 的情况下,再声明 add,因为函数会跑到前面去,变成这个顺序

image.png

  • **let** 有个特性

    • 如果这个变量已经存在就不许再声明了,不能有同名的函数和 **let** 变量,但是 **var** 变量没有这个问题。
    • **let** 必须先声明再使用,
    • 否则报错,示例:

      1. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/22104007/1638084486937-bb271439-f9a8-4384-bd95-818419f14e71.png#clientId=u09bc0ef2-31e8-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=75&id=v0EaI&margin=%5Bobject%20Object%5D&name=image.png&originHeight=140&originWidth=620&originalType=binary&ratio=1&rotation=0&showTitle=false&size=16336&status=done&style=none&taskId=ud9859c1a-3c9d-4112-a496-82348a993f8&title=&width=334)

什么不是函数提升

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

arguments 和 this

arguments 和 this 每个函数都有,除了箭头函数。

  1. function fn(){
  2. console.log(arguments)
  3. console.log(this)
  4. }

arguments

  • 是包含所有参数的伪数组

    如何把伪数组变成数组?

    • **Array.from** 可以把任何不是数组的东西变成数组。
  • 如何传 arguments

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

      this 的两个规则:

  • 如果不给任何的条件,那么 **this** 默认指向 **window**。(一般不用这个默认,属于 JS 糟粕)

  • 如果传的 this 不是对象,那么 JS 会自动封装成对象。( JS 的糟粕)
  • 如何传 this
    • 可以用 **fn.call**(xxx, 1,2,3) 传 this 和 arguments
    • 而且 xxx 会被自动封装成对象
    • 如何禁用 this 自动封装成对象的特性?
      • 声明函数时加一句 **'use strict'**:不要乱加东西 | 没加 **'use strict'** 的效果 | 加了 **'use strict'** 的效果 | **undefined** 的效果 | | —- | —- | —- | | 自动将 1 变成了对象 1
        image.png | 传什么就是什么
        image.png
        | 传了 undefined 就会变成全局变量 window
        image.png |
  • **this** 是隐藏参数
  • **arguments** 是普通参数
图示 说明
image.png **this** 是第一个参数
**arguments** 是第一个参数往后的所有参数
  • 在写函数时怎么得到未来要创建的对象的引用?

    • JS 在每个函数里加了 **this**,用 this 获取一个未知对象的引用

      (如果一个变量保存了一个对象的地址那就是引用

      1. let person = {
      2. name: 'tk',
      3. sayHi() { // 圆括号里有隐藏参数 this
      4. console.log(`你好,我叫` + this.name)
      5. }
      6. }
      7. person.sayHi()
  • person.sayHi() 会隐式地把 person(person 是个地址) 作为 this 传给 sayHi,方便 sayHi 获取 person 对应的对象

this 的两种调用

  • 小白调用法
    • **person.sayHi()** 会自动把 person 传到函数里,作为 this
    • 隐藏太多细节,只适合小白
  • [x] 大师调用法「推荐」

    • 不会自动传任何东西
    • **person.sayHi.call(person)** 需要自己手动把 person 传到函数里,作为 this

      1. function add(x,y){
      2. return x+y
      3. }
      4. // 如果没有用到 this ,可用 undefined 占位
      5. add.call(undefined,1,2) // 3
    • 为什么要多写一个 **undefined**

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

this 的两种使用方法

  • 隐式传递
    • **fn(1,2)** 等价于 **fn.call(undefined,1,2**) :把 **undefined** 作为 **this**
    • **obj.child.fn(1)** 等价于 **obj.child.fn.call(obj.child,1**) :把 **obj.call** 作为 **this****1** 为第二个参数
      • 如果是一个对象的属性里面的函数,那么就等价于把这个函数前面的这个部分作为 **this**
  • 显示传递
    • **fn.call(undefined,1,2)**
    • **fn.apply(undefined),[1,2]**
    • **call** **apply**区别:需要在 this 后面的其他参数上加中括号 **[]** ,因为后面的参数要用数组的形式来表示,只是形式不同,其他都一样。

绑定 this

如果不确定 **this** 可以强制绑定

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

    1. function f1(p1,p2){
    2. console.log(this, p1, p2)
    3. }
    4. let f2 = f1.bind({name:'tk'}) // f2 就是 f1 绑定了 this 之后的新函数 (也就是 f2 是 this 绑定之后的版本)
    5. // f2() 等价于 f1.call({name:'tk'})
  • **.bind** 还可以绑定其他参数

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

    箭头函数

    JS 在最新版把 this 给干掉了

    • this 功能复杂且隐晦,新版的 JS 函数放弃用 this,直接用箭头函数


箭头函数没有 argumentsthis

箭头函数里面的 **this** 就是外面的 **this**

  • 箭头函数的 **this** 就是一个普通的变量,外面是什么里面就是什么(外面的 this 默认为 window)
  • 怎么证明箭头函数没有 **this**
    • 不论对箭头函数做任何的 **call****.bind** 操作,它的 this 永远都是最开始获取到的定义 this(也就是 window
      1. /* 代码示例 */
      2. console.log(this) // 默认的 this 是 外面的 window
      3. let fn = ()=> console.log(this) // 声明一个箭头函数让它打印出 this
      4. fn() // 不管在任何时候打印出 fn,它的值都是 window
      5. // 就算加上 call 或 .bind 都没用,打印出的还是 window

总结:

每个函数都有这些东西

  • 调用时机:决定变量的值。
  • 作用域:每个函数都有作用域,若同时有多个作用域,那就采取就近原则。
  • 闭包:就是一个函数和一个变量合起来。
  • 形式参数:就是对参数取名字而已,相当于声明一个变量。
  • 返回值:每个函数都有返回值。
  • 调用栈:每个函数进去之前都要压一个栈,出来之后要弹栈
  • 函数提升:函数永远都会跑到最前面去。
  • arguments(除了箭头函数):是包含所有参数的一个伪数组
  • this(除了箭头函数):用来引用一个还不存在的东西,实际上 **this** 就是 **call** 的一个参数,只要坚持用 call,那么 this 就完全没用歧义,如果不用 callthis 就充满了歧义。

立即执行函数

  • 只有 JS 才有的变态玩意,现在用的少

原理

  • ES 5 时代,用 **var** 为了得到局部变量,必须引用一个函数,但是这个函数如果有名字,就得不偿失了
  • 后来发现只要在匿名函数前面加个运算符即可,!、~、()、+、- 都可以。
  • 但是有些运算符会往上走,所有推荐永远用 来解决,如果用了 () 就在圆括号前面加个分号 ; 来解决。
  • 新版 JS 用 **let** 就没有这个问题了