// 例1
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
console.log(n);
}
return f2;
}
f1()(); // 999
nAdd();
f1()(); // 999
//例2
function f1(){
var n=999;
nAdd=function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result=f1(); // 内存泄露
result(); // 999
nAdd();
result(); // 1000
为什么例1两次都是999
因为nAdd为全局变量,每次调用f1()都会重新生成一个全局变量nAdd,这个nAdd的作用域又在f1()内,所以读取的n为f1内的n。因为nAdd为全局变量,所以f1函数执行完不会被销毁。仍在内存中。只不过每次都执行了一遍f1()。内部的n�都被重新的赋值。
而在例2这段代码中,result实际上就是闭包f2函数。它一共运行了两次(f1只运行一次,所以result操作的是同一个作用域),第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
之前考虑一个函数里的局部变量是每次执行函数都会在内存中创建吗。var eg1 = f1() var eg2 = f1()
;那么eg1和eg2都会创建变量,并且这个份变量没有联系独立存在吗?是的。是独立存在的。但是如果没有内存泄露的情况。函数执行完成后局部变量就会被销毁。而不是你所想的一直都存在。
考虑一个问题
function cl() {
var ele = document.getElementsByClassName('main-header-box')[0]//获取页面上的一个dom元素
ele.onclick = function () {
console.log('1111')
};
}
cl()
cl函数执行完成后。点击页面上的dom元素还会打印1111吗?答案是会的。为什么呢?垃圾回收机制不是应该cl函数运行完后就被清除了吗。ele都清除了。为什么还能打印?垃圾回收机制:就是释放那些不再使用的变量,我的理解是垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存(现代浏览器一般使用标记清除法既从根部出发无法触及到的对象)。cl函数运行完后释放了变量ele(这就是垃圾回收机制,而不是函数内所有的代码都被收回而失效)。但是ele本身也就是个指针指向 document.getElementsByClassName('main-header-box')[0]这个dom对象。一个节点被变量ele和节点树同时引用,释放了变量中的引用肯定不会释放节点所占的内存啊,因为这时候还有节点树的引用没有释放呢。cl函数给dom对象绑定了click事件。所以这个绑定的事件并不会被清除。
搞定JavaScript内存泄漏
dom引起的内存泄露
js内存
黄色是指直接被 js变量所引用,在内存里
红色是指间接被 js变量所引用,如上图,refB 被 refA 间接引用,导致即使 refB 变量被清空,也是不会被回收的
子元素 refB 由于 parentNode 的间接引用,只要它不被删除,它所有的父元素(图中红色部分)都不会被删除
Object.defineProperty
用法:Object.defineProperty(obj, prop, descriptor)
它接受三个参数,而且都是必填的。。
第一个参数:目标对象
第二个参数:需要定义的属性或方法的名字。
第三个参数:目标属性所拥有的特性。(descriptor)
descriptor
他又以下取值,我们简单认识一下
value:属性的值(不用多说了)
writable:如果为false,属性的值就不能被重写,只能为只读了
configurable:总开关,一旦为false,就不能再设置他的(value,writable,configurable)
enumerable:是否能在for...in循环中遍历出来或在Object.keys中列举出来。
get/set
当使用了getter或setter方法,writable和value这两个属性就会无效。get或set不是必须成对出现,任写其一就可以。如果不设置方法,则get和set的默认值为undefined。这两个函数会在属性设置获取是自动调用。get函数必须return一个值被用作是定义属性的值。
MDN defineProperty
理解 JavaScript 的 Object.defineProperty() 函数
现在我们用getter和setter来简易的实现下vue里的双向数据绑定
如何监听一个对象的变化
// 观察者构造函数
function Observer(data) {
this.data = data; // vue里的data 这里需要对data进行双向数据绑定。也就是设置getter和setter
this.walk(data)
}
let p = Observer.prototype;
// 此函数用于深层次遍历对象的各个属性
// 采用的是递归的思路
// 因为我们要为对象的每一个属性绑定getter和setter
p.walk = function (obj) {
let val;
for (let key in obj) {
// 这里为什么要用hasOwnProperty进行过滤呢?
// 因为for...in 循环会把对象原型链上的所有可枚举属性都循环出来
// 而我们想要的仅仅是这个对象本身拥有的属性,所以要这么做。
if (obj.hasOwnProperty(key)) {
val = obj[key];
// 这里进行判断,如果还没有遍历到最底层,继续new Observer
if (typeof val === 'object') {
new Observer(val);
}
this.convert(key, val);// 调用函数进行设置
}
}
};
p.convert = function (key, val) {
Object.defineProperty(this.data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log('你访问了' + key);
return val
},
set: function (newVal) {
console.log('你设置了' + key);
console.log('新的' + key + ' = ' + newVal)
if (newVal === val) return;
val = newVal // 为什么需要这一步代码
}
})
};
let data = {
user: {
name: "liangshaofeng",
}
};
let app = new Observer(data);
上面的代码为什么需要 val = newVal
这一步操作呢??。因为get必须返回一个值。并且这个值是一个变量。代表现在属性的值为多少。如何监听一个对象的变化这里说是因为闭包。仔细看看。这里说的是get 和set函数是闭包。造成了内存泄露使得 val 始终保留在内存中。所以每一次执行 convert 函数,就会多一个 val 变量存储在内存中,且这些 val 的值各不相同。这里的形式参数val就相当于在convert函数内部定义一个局部变量val,是可以进行修改赋值的。怎么造成的内存泄露了呢?
《JavaScript 中的内存释放》
所以每一次执行 convert 函数。全局data中就会多一对set和get函数。导致convert函数没有被垃圾回收。作用域也一直存在。所以 val 变量一直存储在内存中。但每次执行convert函数。都会产生自己的内部作用域。和之前没有释放的作用域没有关联。所以这里把newVal赋值给val。每次get返回的是val。
var data1 = {
'count': 1,
}
Object.defineProperty(data1, 'count', {
enumerable: true,
configurable: true,
get: function () {
console.log('你访问了count', data1.count); // 运行到这句话就会报错。Maximum call stack size exceeded 的错误。
return 3
},
set: function (newVal) {
console.log('你设置了count', data1);
// if (newVal === val) return;
// val = newVal
}
})
为什么会报错呢。这个错误是栈溢出,一定是哪里循环造成了死循环。get函数就是获取值时候调用。而data1.count又是调用值。所以无线循环造成了死循环。