Javascript与当前项目的思考

时间:2022-08-22 08:55:32

 

 

主体 分为以下三部分,能力、经验有限,欢迎拍砖。

1.低效的代码

2.面向对象的重构重复利用代码

3.调试的经验总结

 

第一部分 日常中低效的代码

 

  • 加载和运行
<html>

<head>
<title>Script Example</title>

</head> <body>

<p>

<script type="text/javascript">

document.write("The date is " + (new Date()).toDateString());

</script> </p>

</body> </html>

 

当浏览器遇到一个<script>标签时,正如上面 HTML 页面中那样,无法预知 JavaScript 是否在<p>标签中 添加内容。因此,浏览器停下来,运行此 JavaScript 代码,然后再继续解析、翻译页面。同样的事情发生 在使用 src 属性加载 JavaScript 的过程中。浏览器必须首先下载外部文件的代码,这要占用一些时间,然后 解析并运行此代码。此过程中,页面解析和用户交互是被完全阻塞的。

一个<script>标签可以放在 HTML 文档的<head><body>标签中,可以在其中多次 出现。传统上,<script>标签用于加载外部 JavaScript 文件

Javascript与当前项目的思考

第一个 JavaScript 文件开始下载,并阻塞了其他文件的下载过程。进 一步,在 file1.js 下载完之后和 file2.js 开始下载之前有一个延时,这是 file1.js 完全运行所需的时间。每个文件必须等待前一个文件下载完成并运行完之后,才能开始自己的下载过程。当这些文件下载时,用户面 对一个空白的屏幕。这就是几年前(现在当网速较慢时,仍可重现这个问题)大多数浏览器的行为模式。

因为脚本阻塞其他页面资源的下载过程,所以推荐的办法是:将所有<script>标签放在尽可能接近<body> 标签底部的位置,尽量减少对整个页面下载的影响。例如:

<html>

<head>
<title>Script Example</title>

<link rel="stylesheet" type="text/css" href="styles.css"> </head>
<body>

<p>Hello world!</p>
<-- Example of recommended script positioning --> 

<script type="text/javascript" src="file1.js"></script> 

<script type="text/javascript" src="file2.js"></script> 

<script type="text/javascript" src="file3.js"></script>

</body> 

</html>

 

  • 数据访问

 

 

数据存储在哪里, 关系到代码运行期间数据被检索到的速度。在 JavaScript 中,此问题相对简单,因为数据存储只有少量方 式可供选择。正如其他语言那样,数据存储位置关系到访问速度。在 JavaScript 中有四种基本的数据访问 位置:

直接量(Literal values)
 

直接量仅仅代表自己,而不存储于特定位置。

 JavaScript 的直接量包括:

字符串(string),数字(Number),布尔值(Boolean),对象(Object), 数组(Array),函数(Function),正则表达式(RegExp),具有特殊意义的空值(null),以及未定义(undefined)。

变量(Variables)
 

我们使用 var 关键字创建用于存储数据值。

数组项(Array items)

具有数字索引,存储一个 JavaScript 数组对象。

对象成员(Object members)

具有字符串索引,存储一个 JavaScript 对象。

每一种数据存储位置都具有特定的读写操作负担。大多数情况下,对一个直接量和一个局部变量数据访问的性能差异是微不足道的。 

 

管理作用域(Managing Scope)

 

作用域概念是理解 JavaScript 的关键,不仅从性能的角度,而且从功能的角度。作用域对 JavaScript 有 许多影响,从确定哪些变量可以被函数访问,到确定 this 的值,首先要理解作用域的工作原理。

作用域链和标识符解析(Scope Chains and Identifier Resolution)

 

每一个 JavaScript 函数都被表示为对象。进一步说,它是一个函数实例。函数对象正如其他对象那样, 拥有你可以编程访问的属性,和一系列不能被程序访问,仅供 JavaScript 引擎使用的内部属性。 

 内部[Scope]属性包含一个函数被创建的作用域中对象的集合。此集合被称为函数的作用域链,它决定哪些数据可由函数访问。此函数作用域链中的每个对象被称为一个可变对象,每个可变对象都以“键值对”

的形式存在。当一个函数创建后,它的作用域链被填充以对象,这些对象代表创建此函数的环境中可访问的数据。例如下面这个全局函数:

function add(num1, num2)

{ 

  var sum = num1 + num2; 

  return sum;

}

 

 

add()函数创建后,它的作用域链中填入一个单独的可变对象,此全局对象代表了所有全局范围定义的变量。此全局对象包含诸如窗口(window)、浏览器(browser)和文档(DOM)之类的访问接口。(注意: 此图中只画出全局变量中很少的一部分,其他部分还很多)。

 

 

Javascript与当前项目的思考 

 

add()函数的作用域链

 

add 函数的作用域链将会在运行时用到。假设运行下面的代码: var total = add(5, 10); 

运行此 add 函数时建立一个内部对象,称作运行期上下文。一个运行期上下文定义了一个函数运行时的环境。对函数的每次运行而言,每个运行期上下文都是独一的,所以多次调用同一个函数就会导致多次创建运行期上下文。当函数执行完毕,运行期上下文就被销毁。

 

一个运行期上下文有它自己的作用域链,用于标识符解析。当运行期上下文被创建时,它的作用域链被 初始化,连同运行函数的[[Scope]]属性中所包含的对象。这些值按照它们出现在函数中的顺序,被复制到 运行期上下文的作用域链中。这项工作一旦完成,一个被称作激活对象的新对象就为运行期上下文创建 好了。此激活对象作为函数执行期的一个可变对象,包含访问所有局部变量,命名参数,参数集合,和 this 的接口。然后,此对象被推入作用域链的前端。当作用域链被销毁时,激活对象也一同销毁。下图显示 了前面实例代码所对应的运行期上下文和它的作用域链。

 Javascript与当前项目的思考

 

在函数运行过程中,每遇到一个变量,标识符识别过程要决定从哪里获得或者存储数据。此过程搜索运 行期上下文的作用域链,查找同名的标识符。搜索工作从运行函数的激活目标之作用域链的前端开始。如 果找到了,那么就使用这个具有指定标识符的变量;如果没找到,搜索工作将进入作用域链的下一个对象。 此过程持续运行,直到标识符被找到,或者没有更多对象可用于搜索,这种情况下标识符将被认为是未定 义的。函数运行时每个标识符都要经过这样的搜索过程,例如前面的例子中,函数访问 sum,num1,num2 时都会产生这样的搜索过程。正是这种搜索过程影响了性能。

在运行期上下文的作用域链中, 一个标识符所处的位置越深,它的读写速度就越慢。所以,函数中局部变量的访问速度总是最快的,而全 局变量通常是最慢的(优化的 JavaScript 引擎在某些情况下可以改变这种状况)。请记住,全局变量总是 处于运行期上下文作用域链的最后一个位置,所以总是最远才能触及的。

 

最好尽可能使用局部变量。一个好的经验法则 是:用局部变量存储本地范围之外的变量值,如果它们在函数中的使用多于一次。考虑下面的例子:

function initUI(){
  var 
    bd = document.body,     links = document.getElementsByTagName_r("a"),
     i = 0,     len = links.length;   
  
   while(i < len){     update(links[i++]); }     document.getElementById("go-btn").onclick = function(){ start();    };    bd.className = "active"; }

 

 此函数包含三个对 document 的引用,document 是一个全局对象。搜索此变量,必须遍历整个作用域链, 直到最后在全局变量对象中找到它。你可以通过这种方法减轻重复的全局变量访问对性能的影响:首先将 全局变量的引用存储在一个局部变量中,然后使用这个局部变量代替全局变量。例如,上面的代码可以重 写如下:

function initUI(){

    var doc = document,
    bd = doc.body,
    links = doc.getElementsByTagName_r("a"),
    i = 0,
   len = links.length;
  
   while(i < len){      update(links[i++]);
   }    doc.getElementById(
"go-btn").onclick = function(){
    start();
   };   bd.className
= "active";
}

 

DOM 编程(DOM Scripting)

 

DOM 操作代价昂贵,在富网页应用中通常是一个性能瓶颈。 

ECMAScript 需要访 问 DOM 时,你需要过桥,交一次过桥费。你操作 DOM 次数越多,费用就越高。一般的建议是尽量减 少过桥次数,努力停留在 ECMAScript 岛上。本章将对此问题给出详细解答,告诉你应该关注什么地方, 以提高用户交互速度。

 

为了给你一个关于 DOM 操作问题的量化印象,考虑下面的例子:

function innerHTMLLoop() {
  for (var count = 0; count < 15000; count++) {

    document.getElementById('here').innerHTML += 'a';

  } 
}

 

此函数在循环中更新页面内容。这段代码的问题是,在每次循环单元中都对 DOM 元素访问两次:一次 读取 innerHTML 属性能容,另一次写入它。

 一个更有效率的版本将使用局部变量存储更新后的内容,在循环结束时一次性写入:

function innerHTMLLoop2() {
  var content = '';
  for (var count = 0; count < 15000; count++) {

    content += 'a'; 
  }   document.getElementById(
'here').innerHTML += content;
}

 

 

你访问 DOM 越多,代码的执行速度就越慢。 

 

事件托管(Event Delegation)

当页面中存在大量元素,而且每个元素有一个或多个事件句柄与之挂接(例如 onclick)时,可能会影 响性能。连接每个句柄都是有代价的,无论其形式是加重了页面负担(更多的页面标记和 JavaScript 代码) 还是表现在运行期的运行时间上。你需要访问和修改更多的 DOM 节点,程序就会更慢,特别是因为事件 挂接过程都发生在 onload(或 DOMContentReady)事件中,对任何一个富交互网页来说那都是一个繁忙的 时间段。挂接事件占用了处理时间,另外,浏览器需要保存每个句柄的记录,占用更多内存。当这些工作 结束时,这些事件句柄中的相当一部分根本不需要(因为并不是 100%的按钮或者链接都会被用户点到), 所以很多工作都是不必要的。

 

一个简单而优雅的处理 DOM 事件的技术是事件托管。它基于这样一个事实:事件逐层冒泡总能被父元 素捕获。采用事件托管技术之后,你只需要在一个包装元素上挂接一个句柄,用于处理子元素发生的所有 事件。

According to the DOM standard, each event has three phases: 根据 DOM 标准,每个事件有三个阶段:

  1. 捕获
  2. 到达目标
  3. 冒泡

Javascript与当前项目的思考

当用户点击了“menu #1”链接,点击事件首先被<a>元素收到。然后它沿着 DOM 树冒泡,被<li>元素收 到,然后是<ul>,接着是<div>,等等,一直到达文档的顶层,甚至 window。这使得你可以只在父元素上 挂接一个事件句柄,来接收所有子元素产生的事件通知。

假设你要为图中所显示的文档提供一个逐步增强的 Ajax 体验。如果用户关闭了 JavaScript,菜单中的链 接仍然可以正常地重载页面。但是如果 JavaScript 打开而且用户代理有足够能力,你希望截获所有点击, 阻止默认行为(转入链接),发送一个 Ajax 请求获取内容,然后不刷新页面就能够更新部分页面。使用 事件托管实现此功能,你可以在 UL"menu"单元挂接一个点击监听器,它封装所有链接并监听所有 click 事 件,看看他们是否发自一个链接。

document.getElementById('menu').onclick = function(e) {

  e = e || window.event;
  var target = e.target || e.srcElement; 
  var pageid, hrefparts;


  if (target.nodeName !== 'A') {

    return; 
  }   hrefparts
= target.href.split('/');   pageid = hrefparts[hrefparts.length - 1]; pageid = pageid.replace('.html', '');   ajaxRequest('xhr.php?page=' + id, updatePageContents);   if (typeof e.preventDefault === 'function') {
    e.preventDefault();     e.stopPropagation();   }
else {     e.returnValue = false;
    e.cancelBubble = true;   }
};

 

正如你所看到的那样,事件托管技术并不复杂;你只需要监听事件,看看他们是不是从你感兴趣的元素 中发出的。这里有一些冗余的跨浏览器代码,如果你将它们移入一个可重用的库中,代码就变得相当干净。 

 

  • 算法和流 程控制

 

要熟悉javascript的所有循环方法,不只是单纯for

 

在大多数编程语言中,代码执行时间多数在循环中度过。在一系列编程模式中,循环是最常用的模式之 一,因此也是提高性能必须关注的地区之一。理解 JavaScript 中循环对性能的影响至关重要,因为死循环 或者长时间运行的循环会严重影响用户体验。

for 循环,与类 C 语言使用同样的语法:

for (var i=0; i < 10; i++){

//loop body

}

 

for 循环大概是最常用的 JavaScript 循环结构。它由四部分组成:初始化体,前测条件,后执行体,循环 体。当遇到一个 for 循环时,初始化体首先执行,然后进入前测条件。如果前测条件的计算结果为 true, 则执行循环体。然后运行后执行体。for 循环封装上的直接性是开发者喜欢的原因。

 

第二种循环是 while 循环。while 循环是一个简单的预测试循环,由一个预测试条件和一个循环体构成:

var i = 0; 
while(i < 10){
//loop body i++;
}

 

 

在循环体执行之前,首先对前测条件进行计算。如果计算结果为 true,那么就执行循环体;否则循环体 将被跳过。任何 for 循环都可以写成 while 循环,反之亦然。

第三种循环类型是 do-while 循环。do-while 循环是 JavaScript 中唯一一种后测试的循环,它包括两部分: 循环体和后测试条件体:

var i = 0; 

do { //loop body } while (i++ < 10);

 

在一个 do-while 循环中,循环体至少运行一次,后测试条件决定循环体是否应再次执行。

 

第四种也是最后一种循环称为 for-in 循环。此循环有一个非常特殊的用途:它可以枚举任何对象的命名 属性。其基本格式如下:

for (var prop in object){

//loop body

}

 

每次循环执行,属性变量被填充以对象属性的名字(一个字符串),直到所有的对象属性遍历完成才返 回。返回的属性包括对象的实例属性和它从原型链继承而来的属性。

 一个典型的数组处理循环,可使用三种循环的任何一种。最常用的代码写法如下:

//original loops

for (var i=0; i < items.length; i++){
   process(items[i]); }
var j=0; while (j < items.length){   process(items[j++]]);

}
var k=0;
do {
  process(items[k
++]);
}
while (k < items.length);

 

 在每个循环中,每次运行循环体都要发生如下几个操作:

1. 在控制条件中读一次属性(items.length)
2. 在控制条件中执行一次比较(i < items.length)
3. 比较操作,察看条件控制体的运算结果是不是 true(i < items.length == true)

4. 一次自加操作(i++)
5.  一次数组查找(items[i])

6. 一次函数调用(process(items[i]))

在这些简单的循环中,即使没有太多的代码,每次迭代也要进行许多操作。代码运行速度很大程度上由 process()对每个项目的操作所决定,即使如此,减少每次迭代中操作的总数可以大幅度提高循环整体性能。

优化循环工作量的第一步是减少对象成员和数组项查找的次数。正如第 2 章讨论的,在大多数浏览器上, 这些操作比访问局部变量或直接量需要更长时间。前面的例子中每次循环都查找 items.length。这是一种浪 费,因为该值在循环体执行过程中不会改变,因此产生了不必要的性能损失。你可以简单地将此值存入一 个局部变量中,在控制条件中使用这个局部变量,从而提高了循环性能:

//minimizing property lookups

for (var i=0, len=items.length; i < len; i++){
   process(items[i]); }
var j=0, count = items.length;

while (j < count){   process(items[j++]]);
}
var k=0, num = items.length;

do {   process(items[k++]);

} while (k < num);

 

以下两个部分还没有整理好,争取在周末发出来。

2.面向对象的重构重复利用代码

 

3.调试的经验总结