当前位置: 首页 > article >正文

JavaScript基础 -- 函数

函数实际上是对象,每个函数都是 Function 类型的实例,而 Function 也有属性和方法,跟其他引用类型一样

因为函数是对象,所以函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定

箭头函数

箭头函数可以不使用大括号,但这会改变函数的行为。使用大括号就说明包含“函数体”,可以在一个函数中包含多条语句,跟常规的函数一样。如果不使用大括号,那箭头后面只能有一行代码,比如一个赋值操作,或者一个表达式;而且,省略大括号会隐式返回这行代码的值

在这里插入图片描述

箭头函数不能使用 arguments、super 和 new.target,也不能用作构造函数。此外,箭头函数也没有 prototype 属性

理解参数

ES 不关心传入的参数个数,也不关心这些参数的数据类型,ES 函数的参数在内部表现为一个数组。在使用 function 关键字定义(非箭头)函数时,可以在函数内部访问 arguments 对象,从中取得传进来的每个参数值

arguments 对象是一个类数组对象(但不是 Array 的实例),可以使用中括号语法访问其中的元素,访问 arguments.length 属性可确定传进来多少个参数

在 arguments 对象中,它的值始终会与对应的命名参数同步,通过下面这个例子进行理解:

function doAdd(num1, num2) {
    arguments[1] = 10
    console.log(arguments[0] + num2)
}

因为 arguments 对象的值会自动同步到对应的命名参数,所以修改 arguments[1] 也会修改 num2 的值,因此两者的值都是10。但这不意味着它们访问的是同一个内存地址,它们在内存中还是分开的,只不过会保持同步

PS:如果只传一个参数,然后把 arguments[1] 设置为某个值,那这个值并不会反映到第二个命名参数。这是因为 arguments 对象的长度是根据传入的参数个数,而非定义函数时给出的命名参数个数确定的

对于命名参数而言,如果调用函数时没有传这个参数,那么它的值是 underfunded,这类似于定义了变量而没有初始化

箭头函数中的参数

如果函数是使用箭头语法定义的,那不能使用 arguments 关键字访问传给函数的参数,只能通过定义的命名参数访问

在这里插入图片描述

虽然箭头函数中没有 arguments 对象,但可以在包装函数中把它提供给箭头函数

在这里插入图片描述

PS:ES 中的所有参数都按值传递的,不可能按引用传递参数。如果把对象作为参数传递,那么传递的值就是这个对象的引用

函数声明与函数表达式

JS引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义

console.log(sum(10, 30))
function sum(num1, num2) {
    return num1 + num2
}

以上代码可以正常运行,函数声明会在任何代码执行之前先被读取并添加到执行上下文,这个过程叫函数声明提升。如果把这段代码中的函数声明改成等价的函数表达式,那么执行就会出错:

console.log(sum(10, 30))
let sum = function sum(num1, num2) {
    return num1 + num2
}

因为这个函数定义包含在一个变量初始化的语句中,这意味着代码如果没有执行到加粗这一行,那么执行上下文中就没有函数的定义,所以上面的代码会出错。这不是因为使用 let 而导致的,使用 var 也会出现这样的问题

PS:除了函数什么时候真正有定义这个区别以外,这两种语法是等价的

函数内部

ES5中,函数内部存在两个特殊的对象:arguments 和 this,ES6又新增了 new.target 属性

arguments

arguments是一个类数组对象,包含调用函数时传入的所有参数,这个对象只有以 function 关键字定义函数时才会有。它还有一个 callee 属性,是一个指向 arguments 对象所在函数的指针

function factorial(num) {
    if (num <= 1) {
        return 1
    } else {
        return num * factorial(num - 1)
    }
}

在上面的递归代码中,确保正确执行函数名必须是factorial,而使用 arguments.callee 就可以让函数逻辑与函数名解耦,这样就避免了之前的硬编码:

function factorial(num) {
    if (num <= 1) {
        return 1
    } else {
        return num * arguments.callee(num - 1)
    }
}

在严格模式下是访问不到 arguments.callee 的,此时可以使用命名函数表达式达到目的

创建一个命名函数表达式 f(),将其赋值给 factorial,即使把函数赋值给另一个变量,函数表达式的名称 f 也不变,因此递归调用不会有问题,在严格和非严格模式下都可以使用

const factorial = (function f(num) {
    if(num <= 1) {
        return 1
    } else {
        return num * f(num - 1)
    }
})

this

this在标准函数和箭头函数中有不同的行为

在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称为 this 值(在网页的全局上下文中调用函数,this 指向 windows)

在这里插入图片描述

定于在全局上下文中的函数 sayColor() 引用了 this 对象,这个 this 到底引用哪个对象必须到函数被调用时才能确定,因此这个值在代码执行的过程中可能会变。在全局上下文中调用 sayColor(),this指向window,this.color 相当于 window.color;而把 sayColor() 赋值给 o 之后再调用,this 会指向 o

在箭头函数中,this 引用的是定义箭头函数的上下文,在下面这个例子中,因为这个箭头函数是在 window 上下文中定义的,this 引用的都是 window 对象

在这里插入图片描述

PS:函数名只是保存指针的变量,因此全局定义的 sayColor() 和 o.sayColor() 是同一个函数,只不过执行的上下文不同

在事件回调或定时回调中调用某个函数时,this 值指向的并非想要的对象,此时将回调函数写成箭头函数就可以解决问题,因为箭头函数中的 this 会保留定义该函数时的上下文

function King() {
    this.name = 'Henry'
    // this 引用 King 的实例
    setTimeout(() => console.log(this.name), 1000)
}
function Queen() {
    this.name = 'Jerry'
    // this 引用 Window 对象
    setTimeout(function() { console.log(this.name) }, 1000)
}
new King   // Henry
new Queen // undefined

new.target

ES 中的函数始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用,ES6 新增了检测函数是否使用 new 关键字调用的 new.target 属性。如果函数是正常调用的,则 new.target 的值是 undefined;如果是使用 new 关键字调用的,则 new.target 将引用被调用的构造函数

在这里插入图片描述

函数属性与方法

每个函数都有两个属性:length 和 prototype,length 属性保存函数定义的命名参数的个数,prototype 保存引用类型所有实例,这意味着 toString()、valueOf() 等方法都把保存在 prototype 上,进而由所有实例共享。

函数还有两个方法:apply() 和 call(),这两个方法都会以指定的 this 值来调用函数,即会设置调用函数时函数体内 this 对象的值

apply() 接受两个参数:函数内 this 的值和一个参数数组,第二个参数可以是 Array 的实例,也可以是 arguments 对象

在这里插入图片描述

PS:在严格模式下,调用函数时如果没有指定上下文对象,则 this 值不会指向 window。除非使用 apply() 或 call() 把函数指定给一个对象,否则 this 的值会变成 undefined

call() 与 apply() 的作用一样,只是传参的形式不同。第一个参数也是 this 值,第二个必须将参数一个一个列出来,逐个传递

在这里插入图片描述

使用 apply() 或 call() 可以将任意对象设置为任意函数的作用域,相当于可以改变 this 值。在 ES5 中,出于同样的目的定义了一个新方法:bind(),它会创建一个新的函数实例,其 this 值会被绑定到传给 bind() 的对象

在这里插入图片描述

闭包

了解闭包,先要知道 JS 中的几个特性:

1)词法作用域

又称为静态作用域,决定了变量和函数的可见性基于其在源代码中的位置。换句话说,函数的作用域在它被定义的时候就已经确定,而不是在运行时。这意味着,无论函数在哪里被调用,它都能访问到定义时所在的作用域中的变量

2)作用域链

它是 JS 引擎查找变量时的一套规则。当一段代码执行时,其所在的执行上下文就会被创建,这个上下文包含了当前作用域的所有变量和函数定义

闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的,这意味着,闭包可以让开发者从内部函数访问外部函数的作用域

function makeFunc() {
    var name = "Mozilla"
    function displayName() {
        alert(name)
    }
    return displayName
}

var myFunc = makeFunc()
myFunc()

displayName这个作用域下访问了另外一个函数makeFunc下的局部变量name闭包的实现实际上利用了 JS 中作用域链的概念,如果在某个作用域下访问某个变量的时候,如果不存在就一直向外层寻找,直到在全局作用域下找到对应的变量为止

闭包的特性

闭包的特性主要有以下:

1)闭包可以访问到父级函数的变量

2)访问到父级函数的变量不会销毁

对于变量不会被销毁的理解:函数当作值传递,即所谓的 first class 对象,可以把函数当作一个值来赋值,当作参数传给别的函数,也可以把函数当作一个值 return。一个函数被当作值返回时,也就相当于返回了一个通道,这个通道可以访问这个函数词法作用域中的变量,即函数所需要的数据结构保存了下来,数据结构中的值在外层函数执行时创建,外层函数执行完毕时理因销毁,但由于内部函数作为值返回出去,这些值得以保存下来。而且无法直接访问,必须通过返回的函数,这也就是私有性。

通过以下的例子来进行理解:

var age = 18
function person(){
    age++
    console.log(age)
}
    
person()  // 19
person() // 20

调用了 2 次函数,age 值增长到 20,但这样写会导致全局变量被污染,所以将 age 的定义移动到 person 函数内部

function person() {
  var age = 18
  age++
  console.log(age)
}

person() // 19
person() // 19

但这又导致另一个问题,变成局部变量的 age 不会自增了,这时就可以利用闭包的这个特性,将每次调用时的 age 保存起来这样就可以实现变量的自增了

function person() {
  var age = 18
  return function(){
    age++
    console.log(age)
  }
}

let getPersonAge = person()
getPersonAge() // 19
getPersonAge() // 20

可以这样理解,通过将 person 函数赋值给 getPersonAge 这个变量,可以看作如下代码:

let getPersonAge = function(){
  age++
  console.log(age)
}

每当调用 getPersonAge() 函数的时候,首先要获取 age 变量,因为 JS 中存在作用域链的关系,所以会从 person 函数下得到对应的 age,因为闭包存在着可以访问到父级函数的变量,且该变量不会销毁的特性,上次的变量会被保留下来,所以可以做到自增的实现

内存泄漏及解决方案

由于闭包维持对外部变量的引用,如果处理不当,可能会导致这些变量及相关的整个作用域链长时间驻留在内存中,从而引发内存泄漏。尤其是在大量使用闭包或循环中创建闭包的情况下,必须谨慎处理,确保不再使用的变量能够适时释放,避免不必要的内存占用

以下这个例子发生了内存泄漏,在 get 方法返回的匿名函数中,this 默认指向全局对象,而非 myObj 对象,因为函数独立调用时,其执行上下文默认绑定全局对象

this.name = 'WindowName'
let myObj = {
  name: 'myObj name',
  get: function() {
    return function() {
      console.log(this) // Window
      return this.name
    }
  }
}

let myObjname = myObj.get()()
console.log(myObjname) // WindowName

解决方案1:在 get 函数使用 that 保存此时的 this

get 的 this(即myObj)被赋值给变量 that,内部函数通过闭包访问 that。内部函数不再依赖动态绑定的 this,而是直接引用 myObj。当 myObj 不再需要时,只要没有其他引用,闭包释放后 that 也会被回收,避免内存泄漏

this.name = 'WindowName'
let myObj = {
  name: 'myObj name',
  get: function() {
    let that = this // 保存当前 myObj 的引用
    return function() {
      console.log(that) // myObj
      return that.name
    }
  }
}

let myObjname = myObj.get()()
console.log(myObjname) // myObj name

解决方案2:将 get 函数的返回值改回使用箭头函数的方式做返回

箭头函数没有自己的 this,继承自定义时的外层作用域,此处是 get 方法的 this,即myObj

this.name = 'WindowName'
let myObj = {
  name: 'myObj name',
  get: function() {
    return () => { // 箭头函数继承外层this
      console.log(this) // myObj
      return this.name 
    }
  }
}

let myObjname = myObj.get()()
console.log(myObjname) // myObj name

消除闭包

不用的时候解除引用,避免不必要的内存占用。取消 fn 对外部成员变量的引用,就可以回收相应的内存空间

function add() {
  var count = 0
  return function fn() {
    count++
    console.log(count)
  }
}

var a = add() // 产生了闭包
a() // 1
a() // 2
a = null // 取消 a 与 fn 的联系,这个时候浏览器回收机制就能回收闭包空间

私有变量

任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的变量。私有变量包括函数参数、局部变量,以及函数内部定义的其他函数

如果在一个函数中创建一个闭包,则这个闭包能访问其外部的变量,基于这一点,就可以创建出能够访问私有变量的公有方法

特权方法是能够访问函数私有变量(及私有函数)的公有方法,在对象上有两种方式创建特权方法。第一种是在构造函数中实现,比如:

在这里插入图片描述

这个模式是把所有私有变量和私有函数都定义在构造函数中,再创建一个能够访问这些私有成员的特权方法,其特权方法其实是一个闭包,因此可行

如下面例子所示,可以定义私有变量和特权方法,以隐藏不能被直接修改的数据:

在这里插入图片描述

这段代码中的构造函数定义了两个特权方法:getName() 和 setName(),每个方法都可以构造函数外部调用,并通过它们来读写私有的 name 变量

静态私有变量

特权方法也可以通过使用私有作用域定义私有变量和函数来实现,比如以下:

在这里插入图片描述

匿名函数表达式创建了一个包含构造函数及其方法的私有作用域,首先定义的是私有变量和私有函数,然后又定义了构造函数和公有方法。公有方法定义在构造函数的原型上,与典型的原型模式一样。

PS:这个模式定义的构造函数没有使用函数声明,使用的是函数表达式

使用闭包和私有变量会导致作用域链变长,作用域链越长,则查找变量所需的时间也越多


http://www.kler.cn/a/567760.html

相关文章:

  • Hue Load Balance配置
  • C++Primer学习(4.8位运算符)
  • FFT算法详解与STM32实战应用:从原理到代码实现
  • Electron一小时快速上手
  • 算法-数据结构(图)-弗洛伊德算法复现(Floyd)
  • Java数据结构第十五期:走进二叉树的奇妙世界(四)
  • 深入解析Java字符串:常量池、内存管理与StringBuilder、StringBuffer操作类指南
  • 安全传输,高效共享 —— 体验FileLink的跨网文件传输
  • ARM32汇编 -- align 指令说明及示例
  • 翻译: 深入分析LLMs like ChatGPT 二
  • An Efficient Anti-Interference Imaging Technology for Marine Radar 论文阅读
  • 泰康在线:以数字金融为基,跑赢互联网保险新时代
  • LeetCode 热题 100_寻找两个正序数组的中位数(68_4_困难_C++)(二分查找)(先合并再挑选中位数;划分数组(二分查找))
  • 【文献阅读】A Survey on Hardware Accelerators for Large Language Models
  • 广东专插本-政治毛泽东思想学习笔记
  • golang部分语法介绍(range关键字,函数定义+特性,结构体初始化+结构体指针/方法)
  • [STM32]从零开始的STM32 BSRR、BRR、ODR寄存器讲解
  • 【实战】使用PCA可视化神经网络提取后的特征空间【附源码】
  • 《深度剖析:生成对抗网络中生成器与判别器的高效协作之道》
  • 办公终端电脑文件资料防泄密系统