首頁 >web前端 >js教程 >跟我學習JScript的Bug與記憶體管理_javascript技巧

跟我學習JScript的Bug與記憶體管理_javascript技巧

WBOY
WBOY原創
2016-05-16 15:31:321285瀏覽

1、JScript的Bug

IE的ECMAScript實作JScript嚴重混淆了命名函數表達式,搞得現很多人都出來反對命名函數表達式,而且即便是現在還一直在用的一版(IE8中使用的5.8版)仍然存在下列問題。

下面我們就來看看IE在實現中究竟犯了那些錯誤,俗話說知已知彼,才能百戰不殆。我們來看看以下幾個例子:

例1:函數表達式的標示符洩漏到外部作用域

var f = function g(){};
typeof g; // "function"

前面我們說過,命名函數表達式的標示符在外部作用域是無效的,但JScript明顯是違反了這一規範,上面例子中的標示符g被解析成函數對象,這就亂了套了,很多難以發現的bug都是因為這個原因造成的。

註:IE9以後似乎已經修復了這個問題

例2:將命名函數表達式同時當作函數宣告和函數表達式

typeof g; // "function"
var f = function g(){};

特性環境下,函數宣告會優先於任何表達式被解析,上面的例子展示的是JScript其實是把命名函數表達式當成函數宣告了,因為它在實際宣告之前就解析了g。

這個例子引出了下一個例子。

例3:命名函數表達式會建立兩個截然不同的函數物件!

var f = function g(){};
f === g; // false
f.expando = 'foo';
g.expando; // undefined

看到這裡,大家會覺得問題嚴重了,因為修改任何一個對象,另外一個沒有什麼改變,這太惡了。透過這個例子可以發現,創建2個不同的對象,也就是說如果你想修改f的屬性中保存某個信息,然後想當然地通過引用相同對象的g的同名屬性來使用,那問題就大了,因為根本就不可能。

再來看一個稍微複雜的例子:

例4:僅順序解析函數宣告而忽略條件語句區塊

var f = function g() {
  return 1;
};
if (false) {
 f = function g(){
 return 2;
 };
}
g(); // 2

這個bug查找就難多了,但導致bug的原因很簡單。首先,g被當作函數宣告解析,由於JScript中的函數宣告不受條件程式碼區塊約束,所以在這個很惡的if分支中,g被當作另一個函數function g(){ return 2 },也就是又被聲明了一次。然後,所有「常規」的表達式被求值,而此時f被賦予了另一個新建立的物件的參考。由於在對表達式求值的時候,永遠不會進入「這個可惡if分支,因此f就會繼續引用第一個函數function g(){ return 1 }。分析到這裡,問題就很清楚了:假如你不夠細心,在f中呼叫了g,那麼將會呼叫一個毫不相干的g函數物件。

你可能會問,將不同的物件和arguments.callee相比較時,有什麼樣的差異呢?我們來看看:

var f = function g(){
  return [
  arguments.callee == f,
  arguments.callee == g
  ];
};
f(); // [true, false]
g(); // [false, true]
可以看到,arguments.callee的引用一直是被呼叫的函數,其實這也是好事,稍後會解釋。

還有一個有趣的例子,那就是在不包含宣告的賦值語句中使用命名函數表達式:

(function(){
 f = function f(){};
})();
按照程式碼的分析,我們原本是想創建一個全域屬性f(注意不要和一般的匿名函數混淆了,裡面用的是帶名字的聲明),JScript在這裡搗亂了一把,首先他把表達式當成函數宣告解析了,所以左邊的f被宣告為局部變數了(和一般的匿名函數裡的宣告一樣),然後在函數執行的時候,f已經是定義過的了,右邊的function f() {}則直接就賦值給局部變數f了,所以f根本就不是全域屬性。

了解了JScript這麼變態以後,我們就要及時預防這些問題了,首先防範標識符洩漏帶外部作用域,其次,應該永遠不引用被用作函數名稱的標識符;還記得前面例子中那個討人喜歡的標識符g嗎? ——如果我們能夠當g不存在,可以避免多少不必要的麻煩。因此,關鍵就在於始終要透過f或arguments.callee來引用函數。如果你使用了命名函數表達式,那麼應該只在調試的時候利用那個名字。最後,也要記住一點,一定要把命名函數表達式宣告期間錯誤建立的函數清理乾淨。

2、JScript的記憶體管理

知道了這些不符合規範的程式碼解析bug以後,我們如果用它的話,就會發現記憶體方面其實是有問題的,來看一個例子:

var f = (function(){
 if (true) {
 return function g(){};
 }
 return function g(){};
})();

我们知道,这个匿名函数调用返回的函数(带有标识符g的函数),然后赋值给了外部的f。我们也知道,命名函数表达式会导致产生多余的函数对象,而该对象与返回的函数对象不是一回事。所以这个多余的g函数就死在了返回函数的闭包中了,因此内存问题就出现了。这是因为if语句内部的函数与g是在同一个作用域中被声明的。这种情况下 ,除非我们显式断开对g函数的引用,否则它一直占着内存不放。

var f = (function(){
 var f, g;
 if (true) {
 f = function g(){};
 }
 else {
 f = function g(){};
 }
 // 设置g为null以后它就不会再占内存了
 g = null;
 return f;
})();

通过设置g为null,垃圾回收器就把g引用的那个隐式函数给回收掉了,为了验证我们的代码,我们来做一些测试,以确保我们的内存被回收了。

测试

测试很简单,就是命名函数表达式创建10000个函数,然后把它们保存在一个数组中。等一会儿以后再看这些函数到底占用了多少内存。然后,再断开这些引用并重复这一过程。下面是测试代码:

function createFn(){
 return (function(){
 var f;
 if (true) {
  f = function F(){
  return 'standard';
  };
 }
 else if (false) {
  f = function F(){
  return 'alternative';
  };
 }
 else {
  f = function F(){
  return 'fallback';
  };
 }
 // var F = null;
 return f;
 })();
}

var arr = [ ];
for (var i=0; i < 10000; i++) {
 arr[i] = createFn();
}

通过运行在Windows XP SP2中的任务管理器可以看到如下结果:

IE7:

 without `null`: 7.6K -> 20.3K
 with `null`:  7.6K -> 18K

IE8:

 without `null`: 14K -> 29.7K
 with `null`:  14K -> 27K

如我们所料,显示断开引用可以释放内存,但是释放的内存不是很多,10000个函数对象才释放大约3M的内存,这对一些小型脚本不算什么,但对于大型程序,或者长时间运行在低内存的设备里的时候,这是非常有必要的。

以上就是关于JScript的Bug与内存管理的全部介绍,希望对大家的学习有所帮助。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn