垃圾回收机制以及Object.defineProperty

《JavaScript 内存管理》

// 例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又是调用值。所以无线循环造成了死循环。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,825评论 4 377
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,887评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,425评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,801评论 0 224
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,252评论 3 299
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,089评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,216评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 31,005评论 0 215
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,747评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,883评论 2 255
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,354评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,694评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,406评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,222评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,242评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,017评论 2 281

推荐阅读更多精彩内容