目录
定义函数的方式:
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(){…})()
- 函数内部的所有变量会立即销毁
闭包可以在对象中创建私有变量
- 使用闭包来实现公有方法,通过公有方法可以访问在包含作用域中定义的变量
- 有权访问私有变量的公有方法 - 特权方法
- 实现自定义类型的特权方法:构造函数模式、原型模式;实现单例的特权方法:模块模式、增强的模块模式