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:这个模式定义的构造函数没有使用函数声明,使用的是函数表达式
使用闭包和私有变量会导致作用域链变长,作用域链越长,则查找变量所需的时间也越多