如何在数据访问层上提高js的执行效率
本文讲到的是如何从数据访问层面上提高JS 代码的执行效率。总的来讲有以下几条原则:
- 函数中读写局部变量总是最快的,而全局变量的读取则是最慢的;
- 尽可能地少用with 语句,因为它会增加with 语句以外的数据的访问代价;
- 闭包尽管强大,但不可滥用,否则会影响到执行速度以及内存;
- 嵌套的对象成员会明显影响性能,尽量少用;
- 避免多次访问对象成员或函数中的全局变量,尽量将它们赋值给局部变量以缓存。
这么几句话看似简单,但要深刻理解其中的道理则需涉及到JS的 标识符解析、作用域链、运行期上下文(又称为执行环境)、原型链、闭包 等一系列概念,之前我有看过一篇网上翻译的 JavaScript 闭包,文中讲解了这些东东,但几遍下来还是似懂非懂。然而本书则是图文并茂,很好理解,不由的感慨一下,牛人就是牛~
作用域链和标识符解析
每一个JS 函数都表示为一个对象,该对象有一个内部属性[[Scope]],它包含了一个函数被创建的作用域中对象的集合,这个集合被称为函数的作用域链(Scope chain),它决定哪些数据能被函数访问。函数作用域中的每个对象被称为一个可变对象(variable object)。当一个函数创建后,它的作用域链会被创建此函数的作用域中可访问的数据对象所填充。
例如下面这个全局函数:
1 |
function add(num1, num2) { |
2 |
var sum = num1 + num2; |
3 |
return sum; |
4 |
} |
由于此函数是在全局作用域下创建的,所以函数add() 的作用域链中只填入了一个单独的可变对象——全局对象:
而执行此函数时则会创建一个称为"运行期上下文(execution context)"的内部对象,它定义了函数执行时的环境。函数每次执行时对应的运行期上下文都是独一无二的,所以多次调用同一个函数就会导致创建多个运行期上下文。当函数执行完毕,执行上下文就被销毁。
而我们刚刚讲的函数作用域链和这个运行期上下文有什么关系呢?是这样的,每个运行期上下文都有自己的作用域链,用于标识符解析,而它的作用域链引用的则是函数的内部对象[[Scope]] 所指向的作用域链。此外还会创建一个"活动对象(Activation object)",该对象包含了函数的所有局部变量、命名参数、参数集合以及this,当函数执行时它又被当作可变对象,和前面是一回事。然后此对象会被推入作用域链的前端,就这样运行期上下文也就创建好了,当运行期上下文被销毁,活动对象也随之销毁(闭包除外,后面会讲到)。
例如前面add() 函数运行时对应的运行期上下文和作用域链:
函数执行时,每遇到一个变量都会进行一次标识符解析的过程,该过程从头至尾搜索作用域链,从当前运行函数的活动对象到全局对象,直至查找到同名的标识符,如果没找到则被认为是undefined。
标识符解析的性能
标识符解析是有代价的,在运行期上下文的作用链中,一个标识符所在的位置越深,它的读写速度也就越慢,显然对局部变量的读写是最快的,因为它所在的对象处于作用链的最前端。一个好的经验法则是:如果某个跨作用域的值在函数中被引用一次以上,那么就把它存储到局部变量里。
此外代码执行时临时改变作用域链也会影响标识符解析的性能,有两个语句会造成这种情况——with 和catch,这两条语句被执行时都会将一个新的可变对象(with 的是语句中指定的对象、catch 的是异常对象)推入作用域链的头部,这样原有的可访问对象都被往后推了一个层次,这使得它们的访问代价更高了。因此对于with 语句最好避免使用,catch 语句要用的话可以定义一函数来进行错误的处理以减少catch 内的语句数量。
下图便是一个在with 语句中改变后的作用域链的例子:
闭包、作用域和内存
理解了作用域链之后,闭包也就好懂了。闭包是JavaScript 最强大的特性之一,它允许函数访问局部作用域之外的数据。然而,有一种性能问题与闭包有关,思考如下代码:
1 |
function assignEvents() { |
2 |
var id = 'xdi9592'; |
3 |
document.getElementById('save-btn').onclick = function(event) { |
4 |
saveDocument(id); |
5 |
} |
6 |
} |
函数内部的onclick 事件处理器就是一个闭包,为了能让该闭包访问函数assignEvents() 内的数据,闭包被创建时,它的[[Scope]] 属性被初始化为其外部函数运行时的作用域链中的对象,即闭包的[[Scope]] 属性包含了与运行期上下文作用域相同的对象的引用。
下图为函数assignEvents() 运行期上下文的作用域链和闭包:
通常来说,函数的活动对象会随同运行期上下文一同销毁。但引入闭包时,由于引用仍然存在于闭包的[[Scope]] 属性中,因此激活对象无法被销毁。这意味着脚本中的闭包与非闭包函数相比,需要更多的内存开销。
而当闭包被执行时,一个活动对象会为闭包自身所创建并被置于作用域链的最前端:
此时对于闭包外数据(如id、saveDocument 等) 的访问开销就更大了,因为它们在作用域链的位置均被推后了一个层次。这就是使用闭包最主要的性能关注点:你要经常访问大量跨作用域的标识符,每次访问都会导致性能损失。
对象成员解析、原型、原型链
对象成员指的是对象的属性或方法,当我们要访问TestObj.abc 这样一个对象成员时,首先在标识符解析的过程中找到了TestObj 对象,接下来要访问abc 属性(或是方法)则要进行对象成员解析的过程了。
JavaScript 中的对象是基于原型的,原型是其他对象的基础,它定义并实现一个新对象必须包含的成员列表。对象通过一个内部属性__proto__(这个属性在Firefox、Safari 和Chrome 中对开发者可见) 绑定到它的原型。
对象可以有两种成员类型:实例成员和原型成员。实例成员存在于对象实例中,原型成员则由对象原型继承而来。一旦创建了一个内置对象(如Object 和Array) 的实例,它们就会自动拥有一个Object 实例作为原型,如下面的代码:
1 |
var book = { |
2 |
title : 'High Performance JavaScript', |
3 |
publisher : 'Yahoo! Press' |
4 |
} |
5 |
alert(book.toString()); //"[object Object]" |
这个例子中book 并没有定义toString() 方法,然而却能被顺利执行,原因是方法toString() 是由对象book 继承而来的原型成员:
注意book 原型中也有__proto__ 属性,前面也说到了JavaScript 的对象是基于原型的,既然每个对象都具有原型,这自然便形成了一个"链",我们称之为原型链,原型链终止于原型为null 的那个对象上。而对象成员的解析实际上就是原型链的遍历过程,从实例成员开始查找到原型成员。
此外也可以定义并使用构造器来创建另一种类型的原型,这样则插入了新定义的原型对象至原型链中,考虑下面这个例子:
1 |
function Book(title, publisher) { |
2 |
this.title = title; |
3 |
this.publisher = publisher; |
4 |
} |
5 |
Book.prototype.sayTitle = function() { |
6 |
alert(this.title); |
7 |
} |
8 |
var book1 = new Book('High Performance JavaScript', 'Yahoo! Press'); |
9 |
var book2 = new Book('JavaScript: The Good Parts', 'Yahoo! Press'); |
这里为Book 对象手动创建了一个原型,并定义了一个方法sayTitle(),对于实例book1 来说原型链是这样的:book1 的原型 -> Book.prototype, Book.prototype 的原型 -> Object, Object 的原型 -> null。
当要访问一个book1 中的一个成员时,检索其原型链的过程则是:首先查找book1 的实例成员(title, publisher),若没找到则接着查找book1 的原型Book.prototype 中的原型成员(sayTitle),若还没到则继续查找Book.prototype 的原型Object 的原型成员(toString, valueOf ...),若仍然没找到,则继续查找Object 的原型,但Object 的原型为null,则查找终止,此时该成员判定为undefined,若该过程中有查找到的话则立即中止查找并返回。原型链的关系如图:
对象成员解析的性能
和标识符解析一样,对象成员的解析也是有开销的,原型链的遍历过程中,每深入一层都会增加性能的损失,于是对象在原型链中存在的位置越深,找到它就越慢。
另外由于对象成员可能包含其他成员,例如window.location.href,每次遇到点操作符,该嵌套成员都会导致JavaScript 引擎搜索所有对象成员,显然对象成员嵌套得越深,访问速度就会越慢,因此尽量少用,例如执行location.href 总是要比window.location.href 要快。
很显然,当要频繁地访问对象成员时,最好用变量将它们缓存起来。
如何在数据访问层上提高js的执行效率的更多相关文章
- ClownFish:比手写代码还快的通用数据访问层
http://www.cnblogs.com/fish-li/archive/2012/07/17/ClownFish.html 阅读目录 开始 ClownFish是什么? 比手写代码还快的执行速度 ...
- 数据访问层 (DAO)
数据持久化 持久化:将程序中的数据在瞬间状态下和持久状态间转换的机制(JDBC) 主要持久化操作:保存.删除.读取.和查找. 采用面向接口编程,可以降低代码间的耦合性,提高代码的可扩展性和可维护性. ...
- 1.1 DAL数据访问层
分布式(Distributed)数据访问层(Data Access Layer),简称DAL,是利用MySQL Proxy.Memcached.集群等技术优点而构建的一个架构系统.主要目的是解决高并发 ...
- 使用JDBC构建简单的数据访问层
本教程的目的是使用Java编写的分离的层去访问数据库中的表,这一层通常称为数据访问层(DAL) 使用DAL的最大好处是通过直接使用一些类似insert()和find()的方法简化了数据库的访问操作,而 ...
- 数据访问层DAL(数据库访问抽象类DataProvider)
晒晒数据访问层DAL,看看你的项目数据访问层使用的是什么形式,数据访问性能比较 采用什么样的数据访问形式是软件编码很重要的一个环节,良好的数据访问形式不仅能够提搞代码的执行效率,协作能力,更重要的是对 ...
- 企业级应用架构(三)三层架构之数据访问层的改进以及测试DOM的发布
在上一篇我们在宏观概要上对DAL层进行了封装与抽象.我们的目的主要有两个:第一,解除BLL层对DAL层的依赖,这一点我们通过定义接口做到了:第二,使我们的DAL层能够支持一切数据访问技术,如Ado.n ...
- 数据访问层的改进以及测试DOM的发布
数据访问层的改进以及测试DOM的发布 在上一篇我们在宏观概要上对DAL层进行了封装与抽象.我们的目的主要有两个:第一,解除BLL层对DAL层的依赖,这一点我们通过定义接口做到了:第二,使我们的DAL层 ...
- NHibernate:教你如何搭建数据访问层?
NHibernate:教你如何搭建数据访问层? 什么是NHibernate NHibernate 是一个基于.net 的针对关系型数据库的对象持久化类库.NHibernate 来源于非常优秀的基于Ja ...
- T4模板与数据访问层的分离
当在企业级应用中使用EF时,会发现实体类库与数据访问层是分离的. 来一张效果图. 具体步骤: 1.运用EF生成原始的实体类 在程序集中添加完ADO.NET实体数据模型后,生成相应的实体类,此时,T4模 ...
随机推荐
- 阻塞队列之LinkedBlockingQueue
概述 LinkedBlockingQueue内部由单链表实现,只能从head取元素,从tail添加元素.添加元素和获取元素都有独立的锁,也就是说LinkedBlockingQueue是读写分离的,读写 ...
- 2.3 进程控制之exec函数族
学习目标:学习使用exec函数族的重要的几个函数 一.引言 进程通过exec函数根据指定的文件名或目录名执行另一个可执行文件,当进程调用exec函数时,该进程的数据段.代码段和堆栈段完全被新程序替换 ...
- 初识python 面向对象
what the f**k!!这个知识点学不好的最大元凶就是,我还单身??? python基础(四): 面向对象的三个特点: 封装,继承,多态 类: 对象是面向对象编程的核心,在使用对象的过程中,为了 ...
- go学习笔记-结构体
结构体 结构体是由一系列具有相同类型或不同类型的数据构成的数据集合 定义 格式 type struct_variable_type struct { member definition; member ...
- 【Java】关于Spring框架的总结 (三)
前文对 Spring IoC 和 Spring AOP 的实现方法进行了整合.如果有不明白的或有质疑的地方可以评论出来,一起探讨问题,帮助别人也是帮助自己!本文探讨的中心主要放在 Spring 的注解 ...
- ionic打包apkFailed to execute shell command "input,keyevent,82"" on device: Error: adb: Command failed with exit code 137
错误代码如下 BUILD SUCCESSFUL in 12s 46 actionable tasks: 1 executed, 45 up-to-date Built the following ap ...
- 雷哥带你走进Javascript
javascript复习笔记--------------------------------------------1.概念2.面向对象思想3.作用认识4.引入方式5.执行顺序 变量 1)声明方式 x ...
- 利尔达NB-IOT的PSM和eDRX低功耗模式笔记
1. NB-IOT的技术优势,广覆盖,NB-IOT与GPRS和LTE相比较,最大链路预算提升了20dB,相当于提升了100倍,即使在地车车库.地下室.地下管道等普通无线网络信号难以到达的地方也容易覆盖 ...
- C#异步了解一下
如何让你的代码在“同一时间”干着两件件事呢?比如说,在初始化加载配置的同时,UI界面能够响应用户的各种点击事件.而不置于卡死,特别是出现如下面这种情况的时候,对于用户来说是很崩溃的.
- React Antd中样式的修改
如果需要对antd的样式进行修改, 进入你要修改的页面 注意:不能直接在自己的文件下面,加入一个css,修改这个class的样式,应该 加入global限定,global {} , 在{}里面写入 . ...