夜听城嚣 夜听城嚣
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏

dwfrost

前端界的小学生
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏
  • 第1章 什么是JavaScript
  • 第2章 HTML中的JavaScript
  • 第3章 语言基础
  • 第4章 变量、作用域与内存
  • 第5章 基本引用类型
  • 第6章 集合引用类型
  • 第7章 迭代器与生成器
  • 第8章 对象、类与面向对象编程
  • 第9章 代理与反射
  • 第10章 函数
    • 第11章 异步编程
    • 第12章 BOM
    • 第14章 DOM
    • 第15章 DOM扩展
    • 第16章 DOM2和DOM3
    • 第17章 事件
    • 第18章 动画与Canvas图形
    • 第19章 表单脚本
    • 第20章 JavaScript API
    • 第21章 错误处理与调试
    • 第23章 JSON
    • 第24章 网络请求与远程资源
    • 第25章 客户端存储
    • 第26章 模块
    • 第27章 工作者进程
    • 第28章 最佳实践
    • 《JavaScript高级程序设计》
    frost
    2021-12-26

    第10章 函数

    定义函数的几种方式:

    • 函数声明
    • 函数表达式
    • 箭头函数
    • Function构造函数(不推荐)

    # 箭头函数

    箭头函数由于其极简语法而受人喜欢,此外this穿透的特性也令人称道。

    不过有很多场合不适用,原因如下:

    • 不能使用arguments、super和new.target
    • 没有自己的this,不能用作构造函数
    • 没有prototype属性

    # 函数名

    所有函数对象都会暴露一个只读的name属性,它保存的是一个函数标识符,一般情况下,是函数的变量名。特殊情况有

    • Function构造函数创建
    • 箭头函数
    • foo.bind().name
    • getter,setter函数

    # 函数的参数

    函数不关心传入的参数个数和数据类型,解释器非常宽容。原因在于函数内部的参数是一个类数组,即arguments,函数调用时会访问arguments对象,从中取得传进来的每个参数值。

    注意,箭头函数内无法使用arguments关键字获取到参数。

    const foo = function () {
      console.log(arguments[0])
    }
    foo(1) // 1
    
    const bar = () => {
      console.log(arguments[0])
    }
    bar(1) // {}
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    # 没有重载

    在Java中,一个函数可以有两个定义,质押签名(接收参数的类型和数量)不同就行。ECMAScript中函数没有签名,自然也没有重载。

    如果定义了2个同名函数,则后定义的会覆盖先定义的。但不建议使用这样的编程方式,每个函数最好有其准确的命名,传参,返回,并说明用途。

    # 默认参数值

    ES6之后,可以显示定义默认参数。如

    function add(a, b = 1) {
      return a + b
    }
    console.log(add(1)) // 2
    console.log(add(1, 10)) // 11
    
    
    1
    2
    3
    4
    5
    6

    此外,默认参数值不限于原始值或对象类型,也可以使用调用函数返回的值。并且只有在函数被调用时才会求值,不会在函数定义时求值,

    let count = 1
    function countAdd() {
      return count++
    }
    function add(a, b = countAdd()) {
      return a + b
    }
    
    console.log('count', count) // 1
    console.log(add(1)) // 2
    console.log('count', count) // 2
    console.log(add(1)) // 3
    console.log('count', count) // 3
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    # 默认参数作用域与暂时性死区

    默认参数等同于在函数顶部使用let关键字声明了变量,因此也存在暂时性死区。

    暂时性死区:即前面定义的参数不能引用后面的定义的。

    function add1(a, b = 1, c = b) {
      return a + b + c
    }
    
    console.log(add1(1)) // 3
    
    // ---------------------------
    function add2(a, b = c, c = 1) {
      return a + b + c
    }
    
    console.log(add2(1)) // 报错
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    # 参数扩展与收集

    # 扩展参数

    es6之前,通过apply方法来扩展参数

    getSum.apply(null,[1,2,3])
    
    1

    有了扩展运算符之后,这个操作就非常简便了。

    getSum(...[1,2,3])
    getSum(...[1,2],...[3,4])
    
    1
    2

    # 收集参数

    函数内进行参数收集时,也可以使用扩展运算符。

    function getSum(...values){}
    
    1

    注意,收集参数的前面如果还有其他参数,则只会收集其余的参数。

    function getSum(...values,lastValue){} // 错误
    function getSum(firstValue,...values){} // 正确
    
    1
    2

    箭头函数不支持arguments对象,但支持收集参数。

    const getSum = (...values)=>{}
    
    1

    # 函数声明与函数表达式

    函数声明的特点在于,存在函数声明提升。

    console.log(sum(10,10))
    function sum(a,b){
      return a+b
    }
    
    1
    2
    3
    4

    注意eslint规则不推荐在函数定义之前调用。

    函数表达式则不能在定义之前调用。

    # 函数作为值

    也叫做回调函数。因为函数名就是变量,所以函数可以用在任何使用变量的地方。

    回调的最大用处在于可以处理异步任务或复杂流程。

    # 函数内部

    # arguments

    arguments.callee 表示arguments对象所在的函数指针,即函数名。通常在递归函数中,我们会写同名的函数调用,此时可以使用arguments.callee代替,达到解耦的目的。

    function dfs(num) {
      if (num <= 1) {
        return 1
      } else {
        return num + dfs(num - 1)
      }
    }
    
    console.log(dfs(4)) // 10
    
    let fn = dfs
    dfs = null
    
    console.log(fn(4)) // 报错
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 只需要改一行代码就不会报错了
    ...
    return num + arguments.callee(num - 1)
    ...
    
    1
    2
    3
    4

    # this

    this通常指调用函数的对象,即不是看函数在哪里定义,而是看调用时函数所属哪个对象。

    如果函数不属于任何对象,则该this指向window。

    可以通过call,apply,bind来改变this指向。

    # caller

    caller指向的是调用当前函数的函数,如果是全局作用域中调用,则是null。

    function outer() {
      inner()
    }
    function inner() {
      console.log(inner.caller) // [Function: outer]
    }
    
    outer()
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    # new.target

    用于检测函数是否使用new关键字调用。如果是,则new.target指向被调用的构造函数,如果否,则为undefined。

    function Target() {
      console.log(new.target)
    }
    new Target()
    
    1
    2
    3
    4

    # 函数属性与方法

    # length

    该属性保存函数定义的命名参数的个数。

    function add1(num1, num2) {}
    function add2(num1, num2, num3) {}
    console.log(add1.length) // 2
    console.log(add2.length) // 3
    
    1
    2
    3
    4

    # prototype

    该属性保存引用类型所有实例的方法,如toString()、valueOf()等。

    # apply(thisArg,values)

    第一个参数是函数内部的this指向thisArg,第二个参数是参数数组。

    # call(thisArg,value1,value2,...)

    用法同apply,区别在于参数是一个个传入的。

    # bind(thisArg)

    也可以改变this指向,但不会调用函数,而是返回一个新的函数实例。

    # 尾调用优化

    尾调用,即外部函数的返回值是一个内部函数的返回值。比如

    function outer(){
      return inner()
    }
    
    1
    2
    3

    在es6种,新增了内存管理优化机制,来进行尾调用优化,即无论调用多少次嵌套函数,都只有一个栈帧,这样就不会导致栈内存溢出。

    # 尾调用优化的条件

    下面的条件缺一不可

    • 代码在严格模式下进行。
    • 外部函数的返回值是对尾调用函数的调用。
    • 尾调用函数返回后不需要执行额外的逻辑。
    • 尾调用函数不是引用外部函数作用域中自由变量的闭包。

    下面是几个不符合尾调用优化的案例:

    // 无优化:尾调用没有返回
    function outerFunction() {
      innerFunction()
    }
    
    // 无优化:尾调用没有直接返回
    function outerFunction() {
      let inner = innerFunction()
      return inner
    }
    
    // 无优化:尾调用返回后转为字符串
    function outerFunction() {
      return innerFunction().toString()
    }
    
    // 无优化:尾调用是一个闭包
    function outerFunction() {
      let foo = 'bar'
      function innerFunction() {
        return foo
      }
      return innerFunction()
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    下面是几个符合尾调用优化的案例:

    'use strict'
    
    function outerFunction(a, b) {
      return innerFunction(a + b)
    }
    
    function outerFunction(a, b) {
      if (a < b) {
        return a
      }
      return innerFunction(a + b)
    }
    
    function outerFunction(condition) {
      return condition ? innerFunctionA() : innerFunctionB()
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    # 尾调用优化案例

    下面通过改写斐波那契数列来书写尾调用优化。

    普通递归实现:

    function fib(n) {
      if (n <= 2) return 1
    
      return fib(n - 2) + fib(n - 1)
    }
    
    // 1 1 2 3 5 8
    console.log(fib(6)) // 8
    
    1
    2
    3
    4
    5
    6
    7
    8

    然而传入60,就爆栈了。

    下面是尾调用优化实现。

    function fib(n) {
      return fibImpl(0, 1, n)
    }
    
    function fibImpl(a, b, n) {
      if (n === 0) {
        return a
      }
      return fibImpl(b, a + b, n - 1)
    }
    console.log(fib(60)) // 1548008755920
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    # 闭包

    闭包是指那些引用了另一个函数作用域中变量的函数。经典写法是

    function outer() {
      let foo = 'bar'
      let inner = function () {
        return 'this is ' + foo
      }
      return inner
    }
    
    1
    2
    3
    4
    5
    6
    7

    如何理解闭包呢?可以从作用域链的创建和使用进行理解。

    函数调用时会产生执行上下文和作用域链,同时会产生一个活动对象,包括该函数的arguments和实参。

    除了活动对象,还有一个全局的变量对象,它贯穿全局执行上下文,直到代码全部执行完毕。

    对比下普通函数和闭包的作用域链。

    // 普通函数
    function add(value1, value2) {
      return value1 + value2
    }
    let result = add(1, 2)
    
    1
    2
    3
    4
    5

    add()函数是在全局上下文中调用的,关系如下:

    • 函数活动对象:arguments、value1、value2。只在函数执行期间存在,函数执行完毕后,函数活动对象被销毁。
    • 全局变量对象:add、result、this。在代码执行期间始终存在。

    下面看看闭包的作用域链。

    // 闭包
    function createCompare(propertyName) {
      return function (obj1, obj2) {
        let value1 = obj1[propertyName]
        let value2 = obj2[propertyName]
        if (value1 < value2) {
          return -1
        } else if (value1 > value2) {
          return 1
        } else {
          return 0
        }
      }
    }
    let compare = createCompare('name')
    let result = compare({name:'nick'},{name:'matt'})
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    函数可以创建作用域,函数内部又定义了函数时,内部函数会把外部函数的活动对象添加到自己的作用域链中,即我们常说的函数内部可以访问外层的变量,但外部函数不能访问内层的变量,也即变量搜索原则。

    createCompare的作用域链关系如下:

    • 全局变量对象:closure、result
    • 函数活动对象:arguments、propertyName

    匿名函数的作用域链关系如下:

    • 全局变量对象:closure、result(同上)
    • 函数活动对象:arguments、propertyName(同上)
    • 闭包活动对象:arguments、obj1、obj2

    闭包的副作用是,createCompare函数执行完毕后,由于匿名函数保持对活动对象的引用,导致活动对象不能销毁。只有匿名函数被销毁时,createCompare活动对象才会销毁。

    // 解除对函数的引用,就可以释放内存了
    compare = null
    
    1
    2

    所以,回到闭包定义,必须是函数内的内部函数,访问或者使用了外层函数中的变量(参数或函数内定义的变量),然后该内部函数持续被某处引用,导致被访问的变量被困在闭包中,无法被垃圾回收机制回收。

    # 闭包中的this

    箭头函数中的this会穿透作用域,即this指向箭头函数所在作用域的this。

    普通函数调用时,有如下表现:

    • 如果在全局函数中调用,则this在非严格模式下等于window,严格模式下是undefined。
    • 如果作为某个对象的方法调用,则this等于这个对象。
    • 匿名函数没有绑定对象,this等于window。

    区别下面3个场景:

      window.identity = 'The Window'
    
      let object = {
        identity: 'My Object',
        getIdentityFunc() {
          return this.identity
        },
      }
    
      console.log(object.getIdentityFunc()) // My Object
    
    // =====================================
    
      window.identity = 'The Window'
    
      let object = {
        identity: 'My Object',
        getIdentityFunc() {
          return function(){
            return this.identity
          }
        },
      }
    
      console.log(object.getIdentityFunc()()) // The Window
    
    // =====================================
    
      window.identity = 'The Window'
    
      let object = {
        identity: 'My Object',
        getIdentityFunc() {
          let that = this
          return function(){
            return that.identity
          }
        },
      }
    
      console.log(object.getIdentityFunc()()) // My Object
    
    // =====================================
    
      window.identity = 'The Window'
    
      let object = {
        identity: 'My Object',
        getIdentityFunc() {
          return () => {
            return this.identity
          }
        },
      }
    
      console.log(object.getIdentityFunc()()) // My Object
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57

    注意第3种场景,是把this保存到闭包可以访问的变量中,从而可以访问到外层作用域的this。

    再来看看下面的例子。

      window.identity = 'The Window'
    
      let object = {
        identity: 'My Object',
        getIdentityFunc() {
          return this.identity
        },
      }
    
      console.log(object.getIdentityFunc()) // My Object
      console.log((object.getIdentityFunc)()) // My Object
    
      let getIdentity = object.getIdentityFunc
      console.log(getIdentity()) // The Window
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    第二个:规范认为object.getIdentityFunc和(object.getIdentityFunc)是相等的。

    第三个:真正执行调用的是getIdentity,它不属于对象方法的调用,所以this指向window。

    注意下面这个

      console.log((object.getIdentityFunc = object.getIdentityFunc)()) // The Window
    
    1

    执行了一次赋值,再调用赋值后的结果。赋值表达式的值是函数本身,也就是不属于对象的方法了,此时调用,this指向window。

    第二和第四个写法基本不会在代码中出现,但要注意js语法稍有不同,也可能影响this的值。

    # 内存泄漏

    由于闭包中的变量被困在内存中,如果不手动处理释放,是不能被垃圾回收机制回收的,这容易导致内存泄漏。

    常见的例子是,在旧版本IE中,把HTML元素保存在闭包中,就宣布了该元素不能被销毁,如下:

    function assignHandler(){
      let element = document.getElementById('hello')
      element.onclick = ()=>console.log(element.id)
    }
    
    1
    2
    3
    4

    这就是一个闭包。element元素的事件处理程序创建了一个循环引用。匿名函数引用着assignHandler()的活动对象,阻止了对element的引用计数归零。

    这里注意几点:

    • 什么是循环引用?我理解这里指的是,输出element.id时,程序需要找到element,发现它是一个DOM节点,这时候就是二次引用了element。所以解决方式是另外定义一个变量来保存element.id即可。
    • 由于闭包会引用包含函数的活动对象,其中就有element,所以完成onclick事件监听之后,需要设置element为null,就解除了对DOM对象的引用。
    function assignHandler(){
      let element = document.getElementById('hello')
      let id = element.id
      element.onclick = ()=>console.log(id)
      element = null
    }
    
    1
    2
    3
    4
    5
    6

    # 立即调用函数表达式

    简称IIFE,写法是

    ;(function () {
      // 块级作用域
    })()
    
    1
    2
    3

    在ES6之前,就是用这种方式来模拟块级作用域的。块级作用域内的变量不能被外界访问,很好的实现变量和模块隔离。

    下面是ES6的块级作用域。

    { // 大括号 + let/const生成块级作用域
      let i = 0
      i += 1
    
      console.log(i) // 1
    }
    
    console.log(i) // ReferenceError: i is not defined
    
    1
    2
    3
    4
    5
    6
    7
    8
    for (let i = 0; i < 3; i++) { // 循环 + let/const生成块级作用域
      console.log(i) // 0 1 2
    }
    console.log(i) // ReferenceError: i is not defined
    
    1
    2
    3
    4

    一个经典的例子是,如何锁住参数值。

    for (var i = 0; i < 2; i++) {
      setTimeout(() => {
        console.log(i) // 2 2
      }, 100)
    }
    
    for (let i = 0; i < 2; i++) {
      setTimeout(() => {
        console.log(i) // 0 1
      }, 100)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    # 私有变量

    严格地说,JS没有私有成员的概念,所有对象属性都是公开的。不过,私有变量是存在的。这是因为有作用域的存在,变量的访问和设置是有规则的。

    # 特权方法

    利用闭包可以创建特权方法,用来访问函数私有变量和私有函数。有2种方式来创建。

    • 构造函数实现。

      function Car(name) {
        this.getName = function () {
          return name
        }
        this.setName = function (value) {
          name = value
        }
      }
      
      const car = new Car('mini')
      console.log(car.name) // undefined
      console.log(car.getName()) // mini
      car.setName('jeep')
      console.log(car.getName()) // jeep
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
    • 私有作用域实现。

      ;(function () {
        let $ = {}
        let name = ''
      
        $.getName = function () {
          return name
        }
        $.setName = function (value) {
          name = value
        }
      
        window.$ = $
      })(window)
      
      window.$.setName('mini')
      console.log(window.$.getName()) // mini
      console.log(window.name) // undefined
      window.$.setName('jeep')
      console.log(window.$.getName()) // jeep
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19

    # 模块模式

    看看下面的代码结构

    let singleton = function () {
      // 私有变量和私有函数
      let privateVariable = 10
      let privateFunction = function () {
        return false
      }
    
      // 特权方法和属性
      return {
        publicProperty: true,
        publicMethods() {
          privateVariable++
          return privateFunction()
        },
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    这就是模块模式,它在单例对象的基础上加以扩展,使其通过作用域链来关联私有变量和特权方法。

    举个例子:

    let application = function () {
      // 私有变量
      let components = new Array()
    
      // 初始化
      components.push(new BaseComponent())
    
      // 公共接口
      return {
        getComponentCounts() {
          return components.length
        },
        registerComponent(component) {
          if (typeof component === 'object') {
            components.push(component)
          }
        },
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    对外暴露的是公共接口,私有变量则无法访问。

    此外,还可以在此基础上对其进行增强。

    let application = function () {
      let components = new Array()
      components.push(new BaseComponent())
    
      // 创建局部变量保存实例
      let app = new BaseComponent()
    
      app.getComponentCount = function () {
        return components.length
      }
      app.registerComponent = function (component) {
        if (typeof component === 'object') {
          components.push(component)
        }
      }
      return app
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #读书笔记#前端
    上次更新: 2022/01/14, 08:52:26
    第9章 代理与反射
    第11章 异步编程

    ← 第9章 代理与反射 第11章 异步编程→

    最近更新
    01
    提交代码时修改commit消息
    04-09
    02
    如何快速定位bug
    02-20
    03
    云端web项目开发踩坑
    08-25
    更多文章>
    Theme by Vdoing | Copyright © 2021-2025 dwfrost | 粤ICP备2021118995号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式
    ×