高性能JavaScript(数据存取)
数据存取分为4各部分
存取位置
作用域及改变作用域
原型以及原型链
缓存对象成员值
存取位置
JavaScript 有4中基本的数据存取位置
字面量:字面量代表自身,不存于特定的位置。比如这个的匿名函数 $btn.click(function(){... ...});
本地变量:本地变量使用var声明,一个字面量和一个局部变量中存取数据的性能差异是很小的。
数组成员:以数字为索引。
对象成员:以字符串作为索引。
每一种数据存储的位置都有不同的读写消耗,一般而言,从字面量和局部变量获取数据的速度要快于从对象或数组的属性中获取数据的速度,但在性能方面很大程度上取决于浏览器本身。
管理作用域
作用域简而言之:内层作用域可以访问外层作用域的变量,而反之,内层作用域的变量对外层是不可见的。
一切都要从Function构造器说起。在javascript中,万物皆对象,函数也是对象,由Function构造函数产生。在函数初始化时,Function构造函数既会为函数添加一些可以由程序员操作的属性,比如:prototype和name,还会为其添加一些程序员无法访问,仅供javascript引擎访问的属性,其中有一个属性叫做[[scope]],指向的就是该函数的作用域链
function add(a, b){ return a+b }
当函数add()创建时,他的作用域链中就会有一个全局对象,这个对象包括 window,document,navigator。
var Total= add(5,10)
执行函数add时会创建一个执行环境(执行上下文),并把函数add的[[scope]]属性复制一份作为执行环境自己的作用域链,之后会创建一个包含运行该函数的所有的局部变量,参数以及this的活动对象,并把它推送到自己作用域的顶端。
改变作用域(with)
一般而言,作用域一旦确定就无法改变,但js 提供了两种方式可以用来改变作用域:with和catch子语句。
ES5的严格模式下明文规定禁止使用with。为什么?看下面一段代码就明白了:
var obj = { nickname:'Kyle', age: 21 }; function foo() { var bar = 'bar'; var nickname = 'Agent'; with(obj){ console.log(nickname); // Kyle console.log(age); // 21 console.log(bar); // bar } } foo();
使用with 语句的本质是将括号中的对象,直接添加到函数的执行上下文,而且是最顶端,这就使得原本的局部变量跑到作用域链的第二个对象中了。增加了访问的代价,导致严重的性能损耗。
而且,访问nickname 时,根据作用域链由上而下的原则,obj的nickname 属性先被找到,立即返回结果,局部变量 nickname 就被遮蔽了。
这也是ES5 的严格模式中杜绝with 的原因。
改变作用域(catch子语句)
在try 语句中出现 error,执行会自动跳转到 catch 中,并把一个异常对象推到作用域的首位。
注意:一旦catch 子语句执行完毕,作用域链就会返回到之前的状态。
使用函数委托的方式能够把catch子句对性能的损耗降低到最小:
try{ // some error }catch(err){ handleError(err) };
这样做只执行了一条语句,并且没有访问局部变量,所以作用域链的临时改变就不会影响代码性能。
闭包
闭包的理解:闭包就是一个能访问函数内部变量的函数。
function test(){ var bar = 'hello'; return function(){ alert(bar); } } test()(); // hello
由于闭包的[[scope]]属性包含了与执行环境作用域链相同的对象的引用,因此会产生副作用,通常来说,函数的活动对象会随着执行环境一同销毁。但引入闭包时,引用仍然存在于闭包的[[scope]]的属性中,因此激活对象无法被销毁,这就意味着脚本中的闭包和非闭包函数相比,需要更多的内存开销。
function test(){ var bar = 'hello', foo = 'foo'; return function(){ alert(bar); } } test()();// 弹出hello
foo永远也不会被使用到,但是它仍然始终存在于活动对象中,这样就会导致内存泄露。
原型
对象可以有两种成员类型:实例成员和原型成员,实例成员直接存在与对象中的实例中,原型成员则是从对象原型继承而来
var book = { Tiitle: ‘hello’, Nums: 666 } alert(book.toString()) //[obj,obj]
在这段代码中,book 有两个实例成员 title和nums,这里面并没有定义toString方法,但是被顺利执行,没有抛出错误。原因就是 toString 是由对象book 继承来的原型成员。
原型链
原型链在我们试图从某个对象获取某个属性(或方法)时发挥作用。如果那个属性刚好像下面这样存在于这个对象之中,那无需多虑,直接返回即可。
var student = {name : 'Jack'}
student.name // =>Jack
但是,如果这个属性不直接存在于这个对象中,那么javascript会在这个对象的构造器的prototype属性,也就是这个对象的__proto__属性中进行查找。
如果找到就返回,否则继续往下面找。由于prototype属性一定是一个对象,因此原型链或者说查找中的最后一站是Object.prototype 若是还是查不到就会返回 undefined。
每深入一层原型链斗会增加性能的消耗,再加上遍历原型链的影响,这让性能问题更加严重。
缓存对象成员值
举个例子:
Function abc(ele,name1,name2) { Return ele.name == name1 || ele.name == name2; }
上面的例子中 ele.name 的值并没有被改变,但是却被读取了两次,可以将ele.name保存到局部变量中,这样读取一次就行了,也是因为局部变量的读取速度快很多。
Function abc(ele,name1,name2) { var name = ele.name; Return name == name1 || name == name2; }
小结
访问字面量和局部变量的速度最快,相反访问数组元素和对象成员相对较慢。
由于局部变量存在于作用域链的起始位置,因此访问局部变量比访问跨作用域变量更快,变量在在作用域链中的位置越深,访问的所需时间越长。
避免使用with 语句,因为改变执行环境作用于连, catch 也是 小心使用。
嵌套的对象成员会明显影响性能。
属性或方法在原型链中的位置越深,访问速度也就越慢。
通过局部变量减少对象成员的访问次数,以及提高速度,改善性能。