本文主要介绍,函数表达式特征、使用函数实现递归、使用闭包定义私有变量。
函数表达式特征
函数表达式是JavaScript中的一个即强大又容易令人困惑的特性。定义函数的方式有两种:函数声明、函数表达式
- 函数声明
// 函数声明
function fn() {
console.log('Hello world')
}
首先是 function 关键字,然后是函数的名字,这就是指定函数名的方式。这样会给函数指定一个非标准的name属性,通过这个属性可以访问到给函数指定的名字。这个属性的值永远等于 跟在 function 后面的标识符
关于函数声明,他有一个重要特征就是函数声明提升(function declaration hoisting),意思是在执行代码之前会先读取函数声明。这就意味着可以吧函数放在调用它的语句后面
fn() // Hello
function fn() {
console.log('fn')
}
- 函数表达式
// 函数表达式
var fn = function() {
// todo
}
这种方式类似于变量的复制,这种情况下创建的函数叫做匿名函数( anonymous function),因为function关键字后面没有标识符。(匿名函数也叫作拉姆达函数。)匿名函数的name属性时空字符串
sayHi()
var sayHi = function() { console.log('hh') }
理解函数提升的关键,就是理解函数声明与函数表达式之间的区别。var声明的变量也会提升,但初始值是undefined
。
递归
递归函数时在一个函数通过名字调用自身的情况下构成的,如下所示。
function factorial(num) {
if(num <= 1) {
return 1
} else {
return num * factorial(num - 1)
}
}
这是一个经典的递归阶乘函数。虽然整函数表面上看来没什么问题,但下面的代码可能导致他出错
var anotherFactorial = factorial
factorial = null
console.log(antherFactorial(10)) // throw error
上面的代码,将factorial重新赋值为null,调用 anotherFactorial后,通过递归会调用 factorial,而 factorial 已经不是函数,所以就会导致错误。这种清情况下可以使用 arguments.callee 来解决这个问题
function factorial(num) {
if (num <= 1) { // 递归出口
return 1
} else {
retun num * arguments.callee(num -1)
}
}
使用arguments.callee 来代替函数名,可以确保物流怎样调用函数都不会出现问题。所以在递归的时候,使用 arguments.callee 总比使用函数名 保险
var factorial = (function f(num) {
if (num <= 1) {
return 1
} else {
return num * f(num -1)
}
});
上面这个实例,也是一种很独特的方式,将函数赋值给另一个变量,函数的名字f 任然有效,所以递归调用照样能正确完成。这种方式在严格模式和 非严格模式下都行得通
闭包
闭包是一个比较抽象的概率。闭包是指有权访问另一个作用域中的变量和函数。创建比表的常见方式,就是在一个函数内部创建另一个函数
function createComparisonFunction( propertyName ) {
return function(obj1, obj2) {
const val1 = object1[propertyName] // 访问外部变量 propertyName
const val2 = object2[propertyName] // 访问外部变量 propertyName
if (val1 < val2) {
return -1
} else if (val1 > val2) {
return 1
} else {
return 0
}
}
}
上面实例中的内部函数被返回了,而且是在其他地方被调用,但它仍然可以访问变量 propertyName。之所以能够访问这个变量,是因为内部函数的作用域中包含 外部函数的 作用域。
有关 如何常见作用域链以及作用域链有什么作用的细节,对彻底理解闭包至关重要。当某个函数别调用时,会创建一个执行环境(execution context) 及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象( activeation object)。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位, ....直到作为作用域链终点的全局执行环境
在函数执行过程中,为读取和写入变量的值,就需要在作用域中查找变量。
function compare(val1, val2) {
if (val1 < val2) {
return -1
} else if (val1 > val2) {
return 1
} else {
retun 0
}
}
let res = compare(1, 2)
当调用 compare() 是,会创建一个包含 arguments、value1 和 value2 的活动对象。全局执行环境的变量对象(包含 result 和 compare) 在 compare() 执行环境的作用域链中处于第二位。
后台的每个执行环境都有一个表示变量的对象——变量对象。全局的变量对象始终存在,而像compare()函数这样的局部环境的变量对象,则只在函数执行的过程中存在。
在创建compare() 函数时,会创建一个预先包含全局变量对象的作用域链,而这个作用域链被保存在内部的 [[ Scope ]] 属性中。当调用compare() 函数时,会为函数创建一个执行环境,然后通过复制函数的[[ Scope ]]属性中的对象构建起执行环境的作用域链。此后,又有一个活动对(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端
对于这个例子中 compare() 函数的执行环境而言,其作用域链中包含两个变量对象;本地活动对象和全局变量对象。显然,作用域链本质上是一个执行变量对象的执政列表,他只引用但不实际包含变量对象
在匿名函数从 createComparisonFunction() 中被返回后,它的作用域链被初始化为包含 createComparisonFunction() 函数的活动对象和 全局对象。这样,匿名函数就可以访问在 createComprisonFunction() 中定义的所有变量。跟为重要的是,createComparisonFunction() 函数在执行完毕之后,其活动对象不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当createComparisonFunction() 函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;知道匿名函数被销毁后, createComparisonFunction() 的活动对象才会被销毁
// 创建函数
let compareNames = createComparisonFunction('name')
// 调用函数
let res = compareName(1, 3)
// 解除对匿名函数的引用(以便释放内存)
compareName = null
首先,将创建的比较函数保存在变量 compareNames中。最后将 compareNames 设置为 等于 null 解除该函数的引用,就等于通知垃圾回收例程将其清除
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。
闭包与变量
作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能包含函数中任何变量的最后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特使的变量。
function createFunction() {
let res = new Array()
for ( var i = 0; i < 10; i++){
res[i] = function() {
return i
}
}
return res
}
这个函数返回一个数组,表面上看。似乎每个函数都应该返回自己的索引值,即位置0的函数返回0,位置1的函数返回1,以此类推。但实际上,每个函数都返回10,。因为每个函数作用域中都保存在 createFunction() 函数的活动对象,所有它们引用的都是同一个变量 i。当 createFunction() 函数返回后,变量 i 的值是 10,此时每个函数都引用这变量 i 的 同一个变量对象,所有在每个函数内部 i 的值都是10。但是,我们可以创建另一个匿名函数强制让闭包的行为符合预期
function createFunction() {
let res = new Array()
for( var i = 0; i < 10; i++) {
res[i] = function(num) { // 自调用函数
// 返回一个匿名函数
return function() {
return num
}
}(i); // 传入参数
}
return res
}
上面重写了前面createFunction()函数后,每个函数就会返回各自不同的索引值。在这个版本中,我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组。这里的匿名函数有一个参数 num, 也就是最终的函数要返回的值。
由于函数参数是按值传递的,所以就会将变量 i 的当前值复制给参数 num。而在这个匿名函数内部,又创建并返回了一个访问 num的包。这样一来,res、 数组中的每个函数都有自己 num 变量的一个副本,因此就可以返回各自不同的数值了。
关于 this 对象
在闭包中 使用 this 对象也可能会导致一些问题。我们知道,this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this 等于 window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,
匿名函数的执行环境具有全局性,因此其this对象z通常指向window
。但有时由于编写闭包的方式不同,这一点可能不会那么明显.
var name= 'The Window'
var obj = {
name: 'My Object',
getNameFunc() {
return function() {
return this.name
}
}
}
console.log(obj.getNameFunc()()) // The Window
前面提到过,每个函数在被调用时都会自动取得两个特殊变量:this 和 arguments。内部函数在搜索这个变量时,只会搜索到其活动对象(arguments 和 其他命名参数)为止,因此永远不可能直接访问外部函数中的这两个变量。不过,把外部作用域的this对象保持在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。
var name = 'The Window'
const obj = {
name: 'My Object',
getName() {
const that = this
return function() {
return that.name
}
}
}
console.log(obj.getName()()) // My Object
我们把this 对象赋值给了一个名叫 that的变量。而在定义了闭包之后,闭包也可以访问这个变量,因为他是我们在包含函数中特意声明的一个变量。即使在函数返回之后,that 也任然引用 这 object
在几种特殊的情况下,this的值可能会以外地改变。
var name = 'The Window'
const obj = {
name: 'My Object',
getName: function() {
console.log(this.name)
}
}
obj.getName() // 'My Object'
;(obj.getName)() // 'My Object'
;(obj.getName = obj.getName)() // 'The Window' 在非严格模式下
第一个是普通的调用返回的是预期的结果
第二个在调用这个方法前给它加上了括号。虽然加上括号之后,好像只是在引用一个函数,但是this的值得到了维持,因此 obj.getName 和 (obj.getName) 的定义是相同的。
第三个,首先执行了一个赋值语句,然后在调用赋值后的结果。因为这个赋值表达式的值是函数本身(一个脱离上下文的匿名函数), 所有this的值不能得到维持,结果就返回了 The window
当然,后两种方式不大可能会使用。不过,这个例子有助于说明即使是语法的细微变化,都有可能改变this的值。
内存泄漏
由于IE9之前的版本对JScript对象和 COM对象使用不同的垃圾收集例程,因此闭包在IE的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素将无法销毁。
function assignHandler() {
var element = document.getElementById('someElement')
element.onclick = function() {
console.log(element.id)
}
}
由于匿名函数保存了一个队 assignHandler() 的活动对象的引用,因此就会导致无法减少element的引用次数。只要匿名函数存在,element的引用次数至少也是1,因此它所占用的内存就永远不会被回收。
function assignHandler() {
var element = document.getElementById('someElement')
var id = element.id
element/onclick = function() {
console.log(id)
}
element = null
}
上面的代码经过改写,将我们需要的属性保存在变量 id 中。但仅仅做到这一步,韩式不能解决内存泄漏的问题。必须要记住:闭包会引用包含函数的整个活动对象,而其中也包含着element。即使闭包不直接使用 element,包含函数的活动对象中也仍然会保存一个引用。因此,有必要吧element 变量设置为 null。这样就能够解除对DOM对象的引用,顺利的减少其引用次数,确保正常回收其占用的内存。
模仿块级作用域
JavaScript 中没有块级作用域的概念。这意味着在快语句中的变量,实际上是在包含函数中而非语句中创建的
function outputNumbers(count) {
for (var i = 0; i < count; i++){
console.log(i)
}
console.log(i)
}
在JavaScript中,变量i是定义在outputNumbers() 的活动对象中的,因此从它有定义开始,就可以在函数内部随处访问它。即使项下面这样错误地重新声明同一个变量,也不会改变它的值。
function outputNumbers(count) {
for (var i = 0; i < count; i++){
// console.log(i)
}
var i
console.log(i)
}
outputNumbers(10)
var 关键字可以多次申明同一个变量;遇到这种情况,他只会对后续的申明视而不见(不过,它会执行后续申明中的变量初始化。也有变量提升的原因)。匿名函数可以用来模仿块级作用域并避免这个问题。
// 自调用函数,形成块级作用域
(function() {
// todo
})()
无论在什么地方,只要临时需要一些变量,就可以使用私有作用域,例如:
function outputNumbers(count) {
(function() {
for(var i = 0; i < count; i++) {
// todo
}
})()
console.log(i) // throw error
}
我们在for循环外部插入一个私有作用域,在匿名函数中定义的任何变量,都会在执行结束时被销毁。因此,变量i只能在循环中使用,使用后即被销毁。此外,在私有作用域中能够访问变量 count, 是因为这个匿名函数是一个闭包,他能够访问闭包作用域中的所有变量。
这种技术经常在全局作用域中被用在函数外部。从而限制想全局作用域中添加过多的变量和函数。同时没有执行匿名函数的引用。只有函数执行完毕,就可以立即销毁其作用域链了。
私有变量
严格来讲,在JavaScript中没有私有成员的概念;所有对象属性都是共有的。不过,倒是有一个私有变量的改了。任何函数中定义的变量都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括
函数的参数
、全局变量
和函数内部定义的其他函数
。
我们把有权访问私有变量和私有函数的公有方法称为特权方法( privileged method )。有两种子啊对象上创建特权方法的方式。
第一中是在构造函数中定义特权方法
function MyObject() {
// 私有变量 和 私有函数
var privateVariable = 10
function privateFunction() {
return false
}
// 特权方法
this.publicMethod = function() {
provateVariable++
return privateFunction()
}
}
这个模式在构造函数内部定义了所有私有变量和函数。然后,有创建了能够访问这些私有成员的特权方法。因为特权方法作为闭包有权访问构造函数的活动对象。如上,创建实例过后除了调用 publicMethod 没有其它途径去访问 privateVariable 和 privateFunction
除此之外,还可以使用私有和特权成员,可以隐藏那些不应该被直接修改的数据
。
function Person(name) {
this.getName = function() {
return name
}
this.setName = function(val) {
name = val
}
}
const person = new Person('了凡')
person.setName('纤风')
console.log(person.getName()) // 纤风
以上实例,定义了量特权方法:getName() 和 setName()。这两个方法都可以在实例上使用,除此之外没有任何办法访问name。但构造函数存在的缺点是,针对每个实例都会创建一组新方法,而使用静态私有变量来实现特权方法就可以避免这个问题。
静态私有变量
通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法,其基本模式如下。
;(function() {
// 私有变量和私有函数
var privateVariable = 10
function privateFunction() {
return false
}
// 构造函数
MyObject = function() { // 隐式全局
}
// 公共 / 特权方法
MyObject.prototype.publicMethod = function() {
privateVariable++
return privateFunction()
}
})()
这个模式创建了一个私有作用域,使用原型继承的方式。构造函数使用函数表达式的方式,并且未使用var申明(全局变量),因此能够在私有作用域外访问。需要注意的是严格模式下会导致错误。
这个模式与构造函数中特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都是以同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。
;(function() {
let name = ''
Person = function(value) {
name = value
}
Person.prototype.getName = function() {
return name
}
Person.prototype.setName = function(val) {
name = val
}
})()
const person1 = new Person('云裳')
console.log(person1.getName()) // 云裳
person1.setName('皓霜')
console.log(person1.getName()) // 皓霜
const person2 = new Person('纤风')
console.log(person2.getName()) // 纤风
person2.setName('了凡')
console.log(person2.getName()) // 了凡
这种模式下,变量name就变成了一个静态的、由所有实例共享的属性。也就是说一个实例上调用了setName() 就会影响到所有实例。这种方式创建静态私有变量就会因为原型而增进代码复用,但每个实例都没有自己的私有变量。
多查找作用域链种的一个层次,就会在一定程度上影响查找速度。而这正式使用闭包和私有变量的一个明显不足之处。
模块模式
前面的模式是用于为自定义类型创建私有变量和特权方法的。而道格拉斯所说的模块模式( module pattern ) 则视为单例创建私有变量和特权方法。所谓单例( singleton ),指的就是只有一个实例的对象。按照惯例,JavaScript是以对象字面量的方式来创建单例对象的。
var singleton = {
name: value,
method: function() {
// todo
}
}
模块开发通常为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:
var singleton = function() {
// 私有变量 和 私有函数
var privateVariable = 10
function privateFunction() {
return false
}
// 特权 / 公有方法和属性
return {
publicProperty: true,
publicMethod: function() {
privateVariable++
return privateFunction()
}
}
}()
返回的对象字面量种包含可以公开的属性和方法。由于这个对象实在匿名函数内部定义的,因此它的共有方法有权访问私有变量和函数。从本质上来讲,这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时很有作用。
var application = function() {
// 私有变量和函数
var components = new Array()
// 初始化
components.push(new BaseComponent())
// 公共
return {
getComponentCount() {
return components.length
},
registerComponent(component) {
if (typeof component == 'object') {
components.push(component)
}
}
}
}()
这给例子创建了一个用于管理组件的 application 对象。首先申明了一个 私有的 components 数组,随后对他进行了初始化(这里不需要在意baseComponent)。而返回对象的getComponentCount() 和 registerComponent() 方法,都是有权访问数组的特权方法。
简言之,如果必须创建一个对象并以某些数据对齐进行初始化,同时还要公开一些能够反问这些私有数据的方法,那么就可能是以模式。以这种模式创建的每个实例都是Object的实例,因为最终要通过一个对象字面量来表示它。
增强的模块模式
有人进一步改进了模块模式,即在返回对象之前加入对齐增强的代码。这种增强的模块适合那些单例必须是某种类型的实例,同时还必须添加某些属性或方法对其加以增强的目的。
var singleton = function() {
var privateVariable = 10
function provateFunction() {
return false
}
// 创建对象
var obj = new CustomType()
// 添加特权/公共属性和方法
obj.publicMethod = function() {
privateVariable++
return privateFunction()
}
// 返回这个对象
return obj
}()
如果前面演示模块模式的例子中 application 对象必须是 BaseComponent 的实,那么就可以使用如下代码。
var application = function() {
// 私有变量和函数
var components = new Array()
// 初始化
components.push(new BaseComponent())
// 创建 application 的一个局部符本
var app = new BaseComponent()
// 公共接口
app.getComponentCount = function() {
return components.length
}
app.registerComponent = function(component) {
if (typeof component == 'object') {
components.push(compoent)
}
}
// 返回这个副本
return app
}()
小结:
在JavaScript中,函数表达式是一种非常有用的技术。使用函数表达式可以无须对函数命名,从而实现动态编程。
- 递归函数一个尽量使用
arguments.callee
来进行调用,而不是函数名——可能会发生变化 - 在后台执行环境中,闭包的作用域链包含着它自己的作用域、包含函数的作用域和全局作用域。
- 通常,函数的作用域及其所有变量都会在函数执行结束后销毁。
- 当函数返回一个闭包时,这个函数的作用域会一直在内存中保存到闭包不存在为止。使用闭包可以在JavaScript中模仿块级作用域。
- JavaScript中没有正式的私有对象属性的概念,但可以使用闭包来实现公有方法 ,而通过公有方法可以访问在包含作用域中的变量。
- 有权访问私有变量的公又方法叫特权方法。
- 可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法。