JavaScript高级程序设计 第7章 函数表达式与闭包


定义函数的方式:
1、函数声明,浏览器给函数定义了一个name属性
特征:函数声明提升。即在执行代码之前会先预读函数声明

sayHi()
function sayHi() {
    alert('hi')
}

2、函数表达式,使用前必须先声明(与其他表达式一样)
根据条件选择赋值函数定义:

var sayHi
if (condition) {
    sayHi = function() {
        alert('hi')
    }
} else {
    sayHi = function() {
        alert('Yo')
    }
}

7.1 递归

例:经典阶乘

function factorial(num) {
    if (num <= 1) 
        return 1;
    else
        return num * factorial(num-1)
}
var anotherFactorial = factorial
factorial = null
alert(anotherFactorial(4)) //Uncaught TypeError: factorial is not a function at factorial

将 factorial 变量设置为 null,接下来调用anotherFactorial()时,由于函数必须执行factorial(), 而factorial已不再是函数,会导致错误。
1、使用arguments.callee解决(代替递归函数名)

function factorial(num) {
    if (num <= 1) 
        return 1;
    else
        return num * arguments.callee(num-1)
}
var anotherFactorial = factorial
factorial = null
alert(anotherFactorial(4)) //24

但严格模式下报错:Uncaught TypeError: ‘caller’, ‘callee’, and ‘arguments’ properties may not be accessed on strict mode functions or the arguments objects for calls to them at factorial
2、使用命名函数表达式

'use strict'
var factorial = function f(num) {
	if (num <= 1)
		return 1;
	else
		return num * f(num-1)
}
var anotherFactorial = factorial
factorial = null
alert(anotherFactorial(4)) //24

7.2 闭包 Closure

闭包:有权访问另一个函数作用域中的变量的函数。
创建闭包:在一个函数内部创建另一个函数
例:

function Comparison(property) {
    return function(object1, object2) {
          return object1[property] > object2[property]
    }
}

返回的匿名函数中访问了外部函数中的变量 property。即使内部函数被返回了,再次调用仍可以访问变量property。
之所以还能访问,是因为内部函数的作用域中包含Comparison()的作用域。

先理解函数调用过程:
(1)第一次调用会创建一个执行环境(execution context)及相应的作用域链,并把作用域链赋值给特殊属性[[scope]]
(2)使用 arguments 和其他命名参数来初始化函数的活动对象(activation object)。而外部函数的活动对象始终处于第二位,以此类推直至作为作用域终点的全局执行环境。
(3)作用域链中查找变量

function compare(value1, value2) {
    return value1>value2 ? 1 : (value1==value2 ? 0 : -1)
}
var result = compare(5, 10) //-1

作用域链本质是一个指向变量对象的指针列表

一般而言,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域。但是闭包不同:
在一个函数内部定义的函数会将包含函数(外部函数)的活动对象添加到它的作用域链中。因此Comparison内部定义的匿名函数的作用域链中将包含外部函数Comparison()的活动对象。

function Comparison(property) {
    return function(object1, object2) {
          return object1[property] > object2[property]
    }
}
//创建函数
var compareName = Comparison('name')
//调用函数
var result = compareName({name: 'Nicholas'}, {name: 'Greg'}) //true
//解除对匿名函数的引用(以便垃圾回收例程清除)
compareName = null
compareName
ƒ (object1, object2) { return object1[property] > object2[property] }
compareName.prototype
{constructor: ƒ}
  constructor:ƒ (object1, object2)
    arguments:null
    caller:null
    length:2
    name:""
    prototype:{constructor: ƒ}
    __proto__:ƒ ()
    [[FunctionLocation]]:VM34651:2
    [[Scopes]]:Scopes[2]
      0:Closure (Comparison) {type: "closure", name: "Comparison", object: {}}
        property:"name"
      1:Global {type: "global", name: "", object: Window}
  __proto__:Object

【注】在解除内部函数的引用,匿名函数作用域链被销毁后,(除了全局)其它作用域链也安全销毁了。

7.2.1 闭包与变量

闭包只能取得包含函数中任何变量的最后一个值。

function Fun() {
	var result = new Array()
	for(var i=0; i<10; i++) {
		result[i] = function() {
			return i
		}
	}
	return result
}
Fun()[0]() //10

返回的函数数组每个位置的函数都返回10。每个函数的作用域链中都保存着Fun()函数的活动对象,所以它们引用的都是同一变量i。执行Fun()后,i=10。此时每个函数都引用保存变量 i 的同一变量对象。
通过创建匿名函数来使闭包行为符合预期:

function Fun() {
	var result = new Array()
	for(var i=0; i<10; i++) {
		result[i] = function(j) {
			return function() {
				return j
			}
		}(i); //立即执行
	}
	return result
}
Fun()[0]() //0
function Fun() {
	var result = new Array()
	for(var i=0; i<10; i++) {
		(function(j) {
			result[j] = function() {
				return j
			}
		})(i); //自执行
	}
	return result
}
Fun()[0]() //0

函数参数按值传递,会把变量 i 复制给匿名函数参数 j ,在匿名函数内部,创建并返回了一个访问 j 的闭包,result数组每个函数都有自己 j 变量的一个副本(返回各自不同值)

Fun()[0].prototype
{constructor: ƒ}
  constructor:ƒ ()
    arguments:null
    caller:null
    length:0
    name:""
    prototype:{constructor: ƒ}
    __proto__:ƒ ()
    [[FunctionLocation]]:VM36893:5
    [[Scopes]]:Scopes[2]
      0:Closure {type: "closure", name: "", object: {}}
        j: 0
      1:Global {type: "global", name: "", object: Window}
  __proto__:Object
7.2.2 关于this对象

匿名函数的执行环境具有全局性,其this对象指向window

function Fun() {
	var result = new Array()
	for(var i=0; i<10; i++) {
		result[i] = function(j) {
			console.log(this) //Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
			return function() {
				return j
			}
		}(i)
	}
	return result
}
Fun()

例:匿名函数没有取得包含作用域(外部作用域)的this对象

var name = 'Jack'
var object = {
	name: 'My Object',
	getName: function() {
	return function() { //函数返回匿名函数
		return this.name
	}
}
}
object.getName()() //"Jack"
object.getName().prototype
{constructor: ƒ}
  constructor:ƒ ()
    arguments:null
    caller:null
    length:0
    name:""
    prototype:{constructor: ƒ}
    __proto__:ƒ ()
    [[FunctionLocation]]:VM37900:5
    [[Scopes]]:Scopes[1]
      0:Global {type: "global", name: "", object: Window}
  __proto__:Object

因为函数在被调用时会自动取得this和arguments变量,但只会搜索到其活动对象为止。上述代码第二次调用即执行匿名函数,不能直接访问外部函数中的这两个变量。
解决:把外部作用域中的this对象保存在一个闭包能够访问到的变量里

var name = 'Jack'
var object = {
    name: 'My Object',
    getName: function() {
        var that = this //外部函数暴露this
        return function() {
            return that.name
        }
    }
}
object.getName()() //"My Object"
object.getName().prototype
{constructor: ƒ}
  constructor:ƒ ()
    arguments:null
    caller:null
    length:0
    name:""
    prototype:{constructor: ƒ}
    __proto__:ƒ ()
    [[FunctionLocation]]:VM37900:5
    [[Scopes]]:Scopes[2]
      0:Closure (getName) {type: "closure", name: "getName", object: {}}
        that:{name: "My Object", getName: ƒ}
      1:Global {type: "global", name: "", object: Window}
  __proto__:Object

特殊情况下this值改变:赋值表达式的值是函数本身(this值不能得到维持)

var name = 'Jack'
var object = {
	name: 'My Object',
	getName: function() {
		return this.name
	}
}
object.getName() //"My Object"
object.getName
ƒ () {
	return this.name
}
(object.getName = object.getName)() //"Jack"
7.2.3 内存泄露

如果闭包的作用域链中保存着一个HTML元素,就意味着该元素将无法被销毁

function assignHandler() {
	var elem = document.getElementById('someElem')
	elem.onclick = function() {
		console.log(elem.id)
	}
}

匿名函数保存了一个对assignHandler()的活动对象的引用,elem的引用数至少是1,其占用的内存无法被回收
解决:在匿名函数的外部作用域保存所需变量(闭包能访问到)

function assignHandler() {
	var elem = document.getElementById('someElem')
	var id = elem.id
	elem.onclick = function() {
		console.log(id)
	}
	elem = null
}

【注】仅仅将id保存在一个变量还不够,因为闭包会引用包含函数的整个活动对象,其中包含着elem,因此需要将elem=null(解除DOM对象的引用)

7.3 模仿块级作用域

JavaScript没有块级作用域,这意味着在块语句中定义的变量,实际上是在函数中而非语句中创建的。
JavaScript对一个变量后续的声明会视而不见,但会执行后续声明中的变量初始化语句

function outputNum(count) {
	for (var i=0; i<count; i++) {
		console.log(i)
	}
	var i //后续声明未初始化会被忽略
	console.log(i) 
}

匿名函数模仿块级作用域(私有作用域):

(function() {
    //块级作用域
})()

函数声明包含在圆括号中,实际上是一个函数表达式,即用函数的值取代函数名。
但若函数声明没有圆括号包含,会出错:

function() {
    //块级作用域
}() //Uncaught SyntaxError: Unexpected token (

因为函数声明之后不能跟圆括号,而函数表达式后面可以跟圆括号。
例:重写outputNum(), i 作为临时变量

function outputNum(count) {
	(function() {
		for (var i=0; i<count; i++) { 
			console.log(i)
		}
	})()
	console.log(i) //会出错
}
outputNum(3) //0 1 2 Uncaught ReferenceError: i is not defined at outputNum

匿名函数模仿块级作用域优点:限制向全局作用域添加方法函数,减少闭包占用的内存(没有指向匿名函数的引用),函数执行完毕就销毁其作用域。

7.4 私有变量

私有变量的概念:在函数中定义的变量都可认为是私有变量,因为不能在函数的外部访问这些变量。包括函数的参数、局部变量和在函数内部定义的其他函数。
访问私有变量的公有方法(特权方法):在函数内部创建一个闭包,闭包通过自己的作用域链可以访问这些变量

function MyObject() {
	//私有变量、私有函数
	var privateVariable = 10
	function privateFun() {
		return false
	}
	//特权方法
	this.publicMethod = function() {
		privateVariable++ //访问私有变量
		return privateFun() //访问私有函数
	}
}

利用私有和特权成员,隐藏那些不想被修改的数据:

function Person(name) {
	this.getName = function() {
		return name
	}
	this.setName = function(value) {
		name = value
	}
}
var person = new Person('Nicholas')
alert(person.getName()) //Nicholas

【注】构造函数模式的缺点:针对每个实例都会创建同一组新方法

7.4.1 静态私有变量

通过在私有定义域中定义私有变量和函数,并创建特权方法:

(function() {
	//私有变量、私有函数
	var privateVariable = 10
	function privateFun() {
		return false
	}
	//构造函数
	MyObject = function(){} //全局变量
	//公有/特权方法 - 原型模式
	MyObject.prototype.publicMethod = function() {
		privateVariable++
		return privateFun()
	}
})()
var obj = new MyObject()
obj.publicMethod() //false

【注】特权方法在原型上定义,私有变量、函数为类原型所共享,而原型对象为实例所共享

(function() {
	var name = ''
	Person = function(value) {
		name = value
	}
	Person.prototype.getName = function() {
		return name
	}
	Person.prototype.setName = function(value) {
		name = value
	}
})()
var person1 = new Person('Nicholas')
alert(person1.getName()) //Nicholas
var person2 = new Person('Greg')
alert(person1.getName()) //Greg
alert(person2.getName()) //Greg

【注】构造函数中设置静态变量,或者调用setName()方法都会影响所有实例。这种私有方式使每个实例没有自己的私有变量

7.4.2 模块模式 module pattern

前述模式是用于为自定义类型创建私有变量和特权方法。
模块模式是为单例对象创建私有变量和特权方法。单例(singleton):只有一个实例的对象。
单例(对象字面量):

var singleton = {
    name: value,
    method: function(){ ... }
}

模块模式增强单例(添加私有变量和特权方法):

var singleton = function() {
	//私有变量、私有方法
	var privateVariable = 10
	function privateFun() {
		return false
	}
	//特权/公有方法和属性 - 接口
	return {
		publicProperty: true,
		publicMethod: function() {
			privateVariable++
			return privateFun()
		}
	}
}
new singleton().publicProperty //true
new singleton().publicMethod() //false

例:

var app = function() {
	//私有变量、函数
	var components = new Array()
	//初始化
	components.push(new BaseComponent())
	//公共
	return {
		getComponentCount: function() {
			return components.length
		},
		registerComponent: function(component) {
			if (typeof component == 'object') {
				components.push(component)
			}
		}
	}
}()
7.4.3 增强的模块模式

单例是某种自定义类型的实例,在返回对象之前加入增强代码

var singleton = function() {
	//私有变量、私有方法
	var privateVariable = 10
	function privateFun() {
		return false
	}
	//创建对象
	var obj = new CustomType() //
	//特权/公有方法和属性
	obj.publicProperty: true,
	obj.publicMethod: function() {
		privateVariable++
		return privateFun()
	}
	//返回对象
	return obj
}

例:

var app = function() {
	//私有变量、函数
	var components = new Array()
	//初始化
	components.push(new BaseComponent())
	//创建app的一个局部副本
	var obj = new BaseComponent()
	//公共接口
	obj.getComponentCount: function() {
		return components.length
	},
	obj.registerComponent: function(component) {
		if (typeof component == 'object') {
			components.push(component)
		}
	}
	//返回副本
	return obj
}()

7.5 小结

函数表达式特点:

  • 没有名字的函数表达式也叫匿名函数
  • 递归函数应该使用arguments.callee来递归地调用自身(不要使用函数名)

闭包:在函数内部定义其他函数时机创建了闭包。闭包有权访问保护函数内部的所有变量。原理:

  • 闭包的作用域链包含着:自己的作用域、包含函数的作用域和全局作用域
  • 当函数返回闭包时,函数的作用域将会一直在内存中保存到闭包不存在为止

闭包可以模仿块级作用域

  • 创建一个函数表达式并立即调用执行 (function(){…})()
  • 函数内部的所有变量会立即销毁

闭包可以在对象中创建私有变量

  • 使用闭包来实现公有方法,通过公有方法可以访问在包含作用域中定义的变量
  • 有权访问私有变量的公有方法 - 特权方法
  • 实现自定义类型的特权方法:构造函数模式、原型模式;实现单例的特权方法:模块模式、增强的模块模式
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值