一、执行环境和作用域链
执行环境定义了函数或变量有权访问的数据范围,包括全局执行环境与局部执行环境,每个函数都有一个自己的局部执行环境。当执行到一个函数时,函数的环境会被推入到一个环境栈中。当函数执行完后,栈将其环境弹出,该环境被销毁,保存在其中的变量和函数定义也随之销毁。
当代码在一个环境中执行时,会创建变量对象的一个作用域链,保证环境内所有变量和函数的有权访问。需要注意的是,当我们在找一个变量的时候,是从当前执行环境一层层向上寻找,内部环境可以通过作用域链访问外部环境,而外部环境不能访问内部环境中的任何变量和函数。
var name = 'pzx'
function changename(){
var anothername = 'pwj'
function swapname(){
var tempname = anothername
anothername = name
name = tempname
}
swapname()
}
changename()
上面的例子中,一共有三个执行环境,分别为全局执行环境、changename函数局部执行环境,swapname函数局部执行环境。swapname局部环境中可以访问到全局变量name和changename环境中的anothername变量。
二、垃圾回收机制
执行环境会负责管理代码执行过程中使用的内存,js中常用的垃圾回收机制包括标记清除和引用计数,当变量被标识为无用后就会自动清除,下面介绍一个这两种垃圾回收机制。
1、标记清除
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,去除掉环境中的变量以及被环境引用的变量的标记。然后剩下的仍有标记的变量就是准备删除的变量,销毁这些值并回收他们占用的内存空间。
2、引用计数
这个方法会跟踪记录每个值被引用的次数,当引用次数变为0时,就可以将其占用的内存空间回收。但这个方法存在一个问题,当两个变量互相引用时,他们的引用次数永远不会是0,导致这部分内存永远不会回收,示例如下:
function fn(){
var objA = new Object()
var objB = new Object()
objA.otherobj = objB
objB.otherobj = objA
}
三、闭包
闭包是指在一个函数内部有权访问另一个函数作用域中的变量
function createComparisonFunction(propertyName){
return function(object1,object2){
var value1 = object1[propertyName]
var value2 = object2[propertyName]
if(value1<value2){
return -1
} else if(value1 > value2){
return 1
} else {
return 0
}
}
}
var compare = createComparisonFunction('name')
var result = compare({name:'pjh'},{name:'pwj'})
上面的代码中匿名函数使用了外层函数中的propertyName,就形成了闭包。过程中形成的作用域链如图所示:
由此我们可以看出闭包存在的内存泄漏问题。当createComparisonFunction函数执行完后,其执行环境的作用域链会被销毁,但是,由于闭包的活动对象仍引用了这个函数的活动对象,它的活动对象就会保留在内存中。直到执行conpare=null解除对匿名函数的引用,匿名函数被销毁后,内存才被释放。
关于闭包的两个问题:
function createFunctions(){
var result = new Array()
for(var i=0;i<10;i++){
result[i] = function(){
return i
}
}
return result
}
console.log(createFunctions()) //10个函数调用都返回10
上面的代码10个函数调用都返回10的原因在于每个函数的作用域链中都包含createFunctions的活动对象,因此它们引用的都是用一个变量i。要想达到每个函数调用返回不同的值,要按如下所示:
function createFunctions(){
var result = new Array()
for(var i=0;i<10;i++){
result[i] = function(num){
return function(){
return num
}
}(i)
}
return result
}
console.log(createFunctions()[0]()) //第一个函数调用返回0
第二个问题是关于闭包函数的this指向问题。内部函数在搜索this和arguments这两个变量的时候有些不同,只会在自身的活动对象上搜索,不会访问到外部函数的这两个变量。因此,如果想访问外部作用域的this,就要先将外部作用域的this保存到一个闭包能访问的变量里。
var name = 'window'
var obj = {
name:'myobj',
getnamefunc: function(){
var that = this
return function(){
return that.name
}
}
}
console.log(obj.getnamefunc()()) //myobj
四、闭包的作用及应用场景
1、通过匿名函数的自动执行来模拟块级作用域
function outputnum(count){
(function (){
for(var i=0;i<count;i++){
console.log(i)
}
})()
console.log(i) //报错 匿名函数外访问不到i
}
outputnum(3)
2、实现方法和属性的私有化
一种方法是在构造函数内定义私有变量和函数,并在构造函数内利用闭包创建用于访问私有变量的公有方法。
function Person(name){
//name为私有属性
this.getName = function(){
return name
}
this.setName = function(value){
name = value
}
}
var p = new Person('pjh')
在Person这个构造函数内,name为私有属性,getName和setName为公有属性。当我们创建了Person的实例,想改变实例的name属性只能通过公有方法来改变。这种方法针对每个实例都会创建一组同样的方法。
第二种方法是使用静态私有变量
(function(){
//私有变量
var name = ""
//没有var声明 是全局构造函数
Person = function(value){
name = value
}
Person.prototype.getName = function(){
return name
}
Person.prototype.setName = function(value){
name = value
}
})()
var p1 = new Person('pjh')
console.log(p1.getName()) //pjh
var p2 = new Person('pwj')
console.log(p2.getName()) //pwj
console.log(p1.getName()) //pwj
这种方法与构造函数方法不同的是,私有变量name是有实例共享的,一个实例改变了那么,另一个实例的name也会改变。
还可以给单例创建私有变量
var application = function(){
//私有变量
var components = new Array()
components.push(new BaseComponent())
var app = new BaseComponent()
//公共接口
app.getComponentCount = function(){
return components.length
}
return app
}()