How to Easily Create a JavaScript Framework, Part 1 | Admix Web

Currently, JavaScript is the one of the most utilized and popular programming language, because a large majority of the browsers are compatible with it and make use of it. Therefore, JavaScript has become very popular, very fast, because it is so uncomplicated, straightforward, and has a wide-range of capabilities. Nearly all programmers used to think that JavaScript was a toy language, but the emergence of AJAX into the market manifested something completely to the contrary, because it showed JavaScript diverse capabilities and functionalities. Since this discovery, programmers can create web applications that give the impression of desk top application, which is helpful because data can be changed more quickly.

However, the use of DOM in the browsers, such as Internet Explorer, makes it difficult to work with JavaScript at times. Consequently, that is why JavaScript frameworks are now exploding onto the market, in order to make cross browser scripting possible. Popular JavaScript frameworks like Prototype, JQuery, YUI, and Dojo have been used for the last five years by many programmers around the world to create amazing web applications. Now, I am going to take you step by step to create a JavaScript framework, walking through the DOM simple effects and AJAX utilities. Hopefully, you can take this information and learn to implement it yourself.

What is on topic for today?

  1. Avoiding Global Variables
  2. Creating a Main Object/Framework
  3. DOM Navigation Part 1
  4. Chaining Methods

1. Avoiding Global Variables

Before we start creating the VOZ framework, we should know that we have to avoid global variables in all pages because JavaScript uses the last variable declaration with its value without warning that we declared it in a sentence before. For example:

var x = 23; //Declare and use the x variable assigning 23 as value;
var x = 44; //Assign 44 to the variable without warning that our variable is already declared
x;  //The actual value is 44

A good method to avoid global variables is creating a anonymous function that keeps our variable inside the function scope. For example:

(function(){
 
//Variables and functions go here.
 
})();

Anonymous functions are very useful when we are working with JavaScript Frameworks, because we can pass them as value to our functions. We will see that later.

2. Creating a Main Object/Framework

It is well known that JavaScript has two ways to create objects: Constructor Functions and Literal Objects.

Constructor functions:

//Defining the Object/Class User with two parameters “name and lastname”.
function User(name, lastname){
	this.name = name;
	this.lastName = lastname;
}
//To use the User Object/Class with new
var myUser = new User('Carl','Anderson');
myUser.name // It returns “Carl”

Literal Object:

The second way and the most recommended by experts like Douglas Crockford is the Literal Object. This style is what we are going to use to create our Framework, because it is the most elegant and efficient way to create objects. For example:

myUser = {
	name: 'Carl',
   lastName: 'Anderson'
};
myUser.name //Returns “Carl”

Well, enough with theories, let’s put this into action and create our Framework. First, select a short, attractive name for your framework. For this example, I will use the name “VOZ.” For example:

// Wrapping my framework to avoid unpredictable errors with others frameworks and variables.
(function (){
//Creating our framework named VOZ 
var VOZ = {}
})();

Now we have our framework ready but we need to create some a property and some methods. For now we are going to add “getById,” “addClass,” and “ON” methods.

(function(){
 
var VOZ = {
//The elems array is going to contains all the elements in order to chain
elems :[],
//The method to get Elements By Id
getById:function(){},
//Method to add a CSS Class
addClass:function(){},
//Method to add an event to our Elements
on:function(){},
//Add text into an element
appendText:function(){}
//Show/Hide
toggleHide:function(){}
}
})() ;

Literal Object:

3. DOM Navigation Part 1

Walking trough the DOM is vital for a Javascript programmer, because that gives you the ability to modify, delete, or add new elements in an specific place of the web page.

The method for now is “getById;” however, this is not the normal “document.getElementById” method, because this takes more than one parameter to find the elements by their ID. For example:

              //Get all elements by Id
                //It could take more than one parameter
                getById:function(){
                            var tempElems = []; //temp Array to save the elements found
                            for(var i = 0; i < arguments.length;i++){
                                if(typeof arguments[i] === 'string'){ //Verify if the parameter is an string
                                    tempElems.push(document.getElementById(arguments[i])); //Add the element to tempElems
                                }
                            }
                            this.elems = tempElems; //All the elements are copied to the property named elems
                            return this; //Return this in order to chain
                        },

We have a method to find elements by ID, but let’s create the next method that adds a class name to our elements, called "addClass":

               //Add a new class to the elements
                //This does not delete the all class, it just add a new one
                addClass:function(name){
                    for(var i = 0;i < this.elems.length;i++){
                                this.elems[i].className += ' ' + name; //Here is where we add the new class
                            }
                            return this; //Return this in order to chain
                },

The last method for today is "ON," that is used to add events in our elements, e.g. ‘click’, ‘mouseover’,’mouseout’, etc.

//Add an Event to the elements found by the methods: getById and getByClass
                //--Action is the event type like 'click','mouseover','mouseout',etc
                //--Callback is the function to run when the event is triggered
                 on: function(action, callback){
                           if(this.elems[0].addEventListener){
                                for(var i = 0; i < this.elems.length;i++){
                                    this.elems[i].addEventListener(action,callback,false);//Adding the event by the W3C for Firefox, Safari, Opera...   
                                }
                            }
                           else if(this.elems[0].attachEvent){
                                    for(var i = 0; i < this.elems.length;i++){
                                        this.elems[i].attachEvent('on'+action,callback);//Adding the event to Internet Explorer :(
                                    }
                            }
                            return this;//Return this in order to chain
                        },

To attach text into our elements, we can use this method:

//Append Text into the elements
            //Text is the string to insert
            appendText:function(text){
                                    text = document.createTextNode(text); //Create a new Text Node with the string supplied
                                    for(var i = 0; i < this.elems.length;i++){
                                    this.elems[i].appendChild(text);//Append the text into the element
                                    }
                                    return this;//Return this in order to chain
                        },

To show/hide the elements found, we use this method:

 //Show and Hide the elements found
            toggleHide:function(){
                            for(var i = 0; i < this.elems.length;i++){
                            this.elems[i].style['display'] = (this.elems[i].style['display']==='none' || '') ?'block':'none'; //Check the status of the element to know if it could be displayed or hided
 
                            }
                            return this;//Return this in order to chain
                        }

Lastly, let’s add this at the end of the object:

if(!window.$$){window.$$=VOZ;}//We create a shortcut for our framework, we can call the methods by $$.method();

This is to use our framework with the $$ like shortcut. For example:

$$.getById('myElement')

Now our framework looks like this:

(function(){  
           var VOZ = {
            elems:[],//Array to save all the elements found by the functions getById, getByC
 
                //Get all elements by Id
                //It could take more than one parameter
                getById:function(){
                            var tempElems = []; //temp Array to save the elements found
                            for(var i = 0;i < arguments.length;i++){
                                if(typeof arguments[i] === 'string'){ //Verify if the parameter is an string
                                    tempElems.push(document.getElementById(arguments[i])); //Add the element to tempElems
                                }
                            }
                            this.elems = tempElems; //All the elements are copied to the property named elems
                            return this; //Return this in order to chain
                        },
 
 
                //Add a new class to the elements
                //This does not delete the all class, it just add a new one
                addClass:function(name){
                    for(var i = 0; i < this.elems.length;i++){
                                this.elems[i].className += ' ' + name; //Here is where we add the new class
                            }
                            return this; //Return this in order to chain
                },
 
 
                //Add an Event to the elements found by the methods: getById and getByClass
                //--Action is the event type like 'click','mouseover','mouseout',etc
                //--Callback is the function to run when the event is triggered
                 on: function(action, callback){
                           if(this.elems[0].addEventListener){
                                for(var i = 0; i < this.elems.length;i++){
                                    this.elems[i].addEventListener(action,callback,false);//Adding the event by the W3C for Firefox, Safari, Opera...   
                                }
                            }
                           else if(this.elems[0].attachEvent){
                                    for(var i = 0;i < this.elems.length;i++){
                                        this.elems[i].attachEvent('on'+action,callback);//Adding the event to Internet Explorer :(
                                    }
                            }
                            return this;//Return this in order to chain
                        },
 
            //Append Text into the elements
            //Text is the string to insert
            appendText:function(text){
                                    text = document.createTextNode(text); //Create a new Text Node with the string supplied
                                    for(var i = 0; i < this.elems.length;i++){
                                    this.elems[i].appendChild(text);//Append the text into the element
                                    }
                                    return this;//Return this in order to chain
                        },
 
            //Show and Hide the elements found
            toggleHide:function(){
                            for(var i = 0; i < this.elems.length;i++){
                            this.elems[i].style['display'] = (this.elems[i].style['display']==='none' || '') ?'block':'none'; //Check the status of the element to know if it could be displayed or hided
 
                            }
                            return this;//Return this in order to chain
                        }
 
    }
    if(!window.$$){window.$$=VOZ;}//We create a shortcut for our framework, we can call the methods by $$.method();
          })();

4. Chaining

Chaining is the ability to call methods together. This makes your code more understandable and organized.

Without Chaining

$('#element').method1();
$('#element').method2();
$('#element').method3();

With Chaining

$('#element').method1().method2().method3();

VOZ JavaScript Framework Example

Please visit the above link in order to view the first part of our VOZ framework put into action! Also, stay tuned for the second part of this post, which is in the works! I hope you enjoyed reading this tutorial and found it easy to follow! Please feel free to comment, as I love to hear from fellow developers!

Pass your 70-649 in first attempt using 000-106 dumps and other resources. We offer 100% success in real exam with up to date pmp dumps, 350-030 braindumps, and 70-640 dumps.

基于hive的日志数据统计实战 – 淡然一笑 的专栏 – 博客频道 – CSDN.NET

基于 hive 的日志数据统计实战

一、           hive 简介

        hive 是一个基于 hadoop 的开源数据仓库工具,用于存储和处理海量结构化数据。 它把海量数据存储于 hadoop 文件系统,而不是数据库,但提供了一套类数据库的数据存储和处理机制,并采用 HQL (类 SQL )语言对这些数据进行自动化管理和处理。我们可以把 hive 中海量结构化数据看成一个个的表,而实际上这些数据是分布式存储在 HDFS 中的。 Hive 经过对语句进行解析和转换,最终生成一系列基于 hadoop map/reduce 任务,通过执行这些任务完成数据处理

        Hive 诞生于 facebook 的日志分析需求,面对海量的结构化数据, hive 以较低的成本完成了以往需要大规模数据库才能完成的任务,并且学习门槛相对较低,应用开发灵活而高效。

        Hive 2009.4.29 发布第一个官方稳定版 0.3.0 至今,不过一年的时间,正在慢慢完善,网上能找到的相关资料相当少,尤其中文资料更少,本文结合业务对 hive 的应用做了一些探索,并把这些经验做一个总结,所谓前车之鉴,希望读者能少走一些弯路。

        Hive 的官方 wiki 请参考这里 :

        http://wiki.apache.org/hadoop/Hive

        官方主页在这里:

        http://hadoop.apache.org/hive/

        hive-0.5.0 源码包和二进制发布包的下载地址

        http://labs.renren.com/apache-mirror/hadoop/hive/hive-0.5.0/

二、           部署

        由于 Hive 是基于 hadoop 的工具,所以 hive 的部署需要一个正常运行的 hadoop 环境。以下介绍 hive 的简单部署和应用。

        部署环境:

        操作系统: Red Hat Enterprise Linux AS release 4 (Nahant Update 7)

        Hadoop hadoop-0.20.2 ,正常运行

        部署步骤如下:

1、   下载最新版本发布包 hive-0.5.0-dev.tar.gz ,传到 hadoop namenode 节点上,解压得到 hive 目录。假设路径为: /opt/hadoop/hive-0.5.0-bin

2、   设置环境变量 HIVE_HOME ,指向 hive 根目录 /opt/hadoop/hive-0.5.0-bin 。由于 hadoop 已运行,检查环境变量 JAVA_HOME HADOOP_HOME 是否正确有效。

3、   切换到 $HIVE_HOME 目录, hive 配置默认即可,运行 bin/hive 即可启动 hive ,如果正常启动,将会出现“ hive> ”提示符。

4、   在命令提示符中输入“ show tables; ”,如果正常运行,说明已部署成功,可供使用。

常见问题:

1、        执行“ show tables; ”命令提示“ FAILED: Error in metadata: java.lang.IllegalArgumentException: URI:  does not have a scheme ”,这是由于 hive 找不到存放元数据库的数据库而导致的,修改 conf/ hive-default.xml 配置文件中的 hive.metastore.local true 即可。由于 hive 把结构化数据的元数据信息放在第三方数据库,此处设置为 true hive 将在本地创建 derby 数据库用于存放元数据。当然如果有需要也可以采用 mysql 等第三方数据库存放元数据,不过这时 hive.metastore.local 的配置值应为 false

2、        如果你已有一套 nutch1.0 系统正在跑,而你不想单独再去部署一套 hadoop 环境,你可以直接使用 nutch1.0 自带的 hadoop 环境,但这样的部署会导致 hive 不能正常运行,提示找不到某些方法。这是由于 nutch1.0 使用了 commons-lang-2.1.jar 这个包,而 hive 需要的是 commons-lang-2.4.jar ,下载一个 2.4 版本的包替换掉 2.1 即可, nutch hive 都能正常运行。

三、           应用场景

        本文主要讲述使用 hive 的实践,业务不是关键,简要介绍业务场景,本次的任务是对搜索日志数据进行统计分析。

        集团搜索刚上线不久,日志量并不大 。这些日志分布在 5 台前端机,按小时保存,并以小时为周期定时将上一小时产生的数据同步到日志分析机,统计数据要求按小时更新。这些统计项,包括关键词搜索量 pv ,类别访问量,每秒访问量 tps 等等。

基于 hive ,我们将这些数据按天为单位建表,每天一个表,后台脚本根据时间戳将每小时同步过来的 5 台前端机的日志数据合并成一个日志文件,导入 hive 系统,每小时同步的日志数据被追加到当天数据表中,导入完成后,当天各项统计项将被重新计算并输出统计结果。

        以上需求若直接基于 hadoop 开发,需要自行管理数据,针对多个统计需求开发不同的 map/reduce 运算任务,对合并、排序等多项操作进行定制,并检测任务运行状态,工作量并不小。但使用 hive ,从导入到分析、排序、去重、结果输出,这些操作都可以运用 hql 语句来解决,一条语句经过处理被解析成几个任务来运行,即使是关键词访问量增量这种需要同时访问多天数据的较为复杂的需求也能通过表关联这样的语句自动完成,节省了大量工作量。

四、           Hive 实战

        初次使用 hive ,应该说上手还是挺快的。 Hive 提供的类 SQL 语句与 mysql 语句极为相似,语法上有大量相同的地方,这给我们上手带来了很大的方便,但是要得心应手地写好这些语句,还需要对 hive 有较好的了解,才能结合 hive 特色写出精妙的语句。

        关于 hive 语言的详细语法可参考官方 wiki 的语言手册 :

        http://wiki.apache.org/hadoop/Hive/LanguageManual

        虽然语法风格为我们提供了便利,但初次使用遇到的问题还是不少的,下面针对业务场景谈谈我们遇到的问题,和对 hive 功能的定制。

1、 分隔符问题

                首先遇到的是日志数据的分隔符问题,我们的日志数据的大致格式如下:

2010-05-24 00:00:02@$_$@QQ2010@$_$@all@$_$@NOKIA_1681C@$_$@1@$_$@10@$_$@@$_$@-1@$_$@10@$_$@application@$_$@1

        从格式可见其分隔符是“ @$_$@ ”,这是为了尽可能防止日志正文出现与分隔符相同的字符而导致数据混淆。本来 hive 支持在建表的时候指定自定义分隔符的,但经过多次测试发现只支持单个字符的自定义分隔符,像“ @$_$@ ”这样的分隔符是不能被支持的,但是我们可以通过对分隔符的定制解决这个问题, hive 的内部分隔符是“ /001 ”,只要把分隔符替换成“ /001 ”即可。

经过探索我们发现有两条途径解决这个问题。

a)          自定义 outputformat inputformat

        Hive outputformat/inputformat hadoop outputformat/inputformat 相当类似, inputformat 负责把输入数据进行格式化,然后提供给 hive outputformat 负责把 hive 输出的数据重新格式化成目标格式再输出到文件,这种对格式进行定制的方式较为底层,对其进行定制也相对简单,重写 InputFormat RecordReader 类中的 next 方法即可,示例代码如下:

    public boolean next(LongWritable key, BytesWritable value)

        throws IOException {

        while ( reader .next(key, text ) ) {

        String strReplace = text .toString().toLowerCase().replace( "@$_$@" , "/001" );

        Text txtReplace = new Text();

        txtReplace.set(strReplace );

        value.set(txtReplace.getBytes(), 0, txtReplace.getLength());

        return true ;

      }

         return false ;

}

        重写 HiveIgnoreKeyTextOutputFormat RecordWriter 中的 write 方法,示例代码如下:

    public void write (Writable w) throws IOException {

      String strReplace = ((Text)w).toString().replace( "/001" , "@$_$@" );

      Text txtReplace = new Text();

      txtReplace.set(strReplace);

      byte [] output = txtReplace.getBytes();

      bytesWritable .set(output, 0, output. length );

      writer .write( bytesWritable );

}

        自定义 outputformat/inputformat 后,在建表时需要指定 outputformat/inputformat ,如下示例:

stored as INPUTFORMAT ‘com.aspire.search.loganalysis.hive.SearchLogInputFormat’ OUTPUTFORMAT ‘com.aspire.search.loganalysis.hive.SearchLogOutputFormat’

b)          通过 SerDe(serialize/deserialize) ,在数据序列化和反序列化时格式化数据。

这种方式稍微复杂一点,对数据的控制能力也要弱一些,它使用正则表达式来匹配和处理数据,性能也会有所影响。但它的优点是可以自定义表属性信息 SERDEPROPERTIES ,在 SerDe 中通过这些属性信息可以有更多的定制行为。

2、 数据导入导出

a)          多版本日志格式的兼容

        由于 hive 的应用场景主要是处理冷数据(只读不写),因此它只支持批量导入和导出数据,并不支持单条数据的写入或更新,所以如果要导入的数据存在某些不太规范的行,则需要我们定制一些扩展功能对其进行处理。

        我们需要处理的日志数据存在多个版本,各个版本每个字段的数据内容存在一些差异,可能版本 A 日志数据的第二个列是搜索关键字,但版本 B 的第二列却是搜索的终端类型,如果这两个版本的日志直接导入 hive 中,很明显数据将会混乱,统计结果也不会正确。我们的任务是要使多个版本的日志数据能在 hive 数据仓库中共存,且表的 input/output 操作能够最终映射到正确的日志版本的正确字段。

        这里我们不关心这部分繁琐的工作,只关心技术实现的关键点,这个功能该在哪里实现才能让 hive 认得这些不同格式的数据呢?经过多方尝试,在中间任何环节做这个版本适配都将导致复杂化,最终这个工作还是在 inputformat/outputformat 中完成最为优雅,毕竟 inputformat 是源头, outputformat 是最终归宿。具体来说,是在前面提到的 inputformat next 方法中和在 outputformat 的 write 方法中完成这个适配工作。

b)          Hive 操作本地数据

        一开始,总是把本地数据先传到 HDFS ,再由 hive 操作 hdfs 上的数据,然后再把数据从 HDFS 上传回本地数据。后来发现大可不必如此, hive 语句都提供了“ local ”关键字,支持直接从本地导入数据到 hive ,也能从 hive 直接导出数据到本地,不过其内部计算时当然是用 HDFS 上的数据,只是自动为我们完成导入导出而已。

3、 数据处理

日志数据的统计处理在这里反倒没有什么特别之处,就是一些 SQL 语句而已,也没有什么高深的技巧,不过还是列举一些语句示例,以示 hive 处理数据的方便之处,并展示 hive 的一些用法。

a)          hive 添加用户定制功能,自定义功能都位于 hive_contrib.jar 包中

add jar /opt/hadoop/hive-0.5.0-bin/lib/hive_contrib.jar;

b)          统计每个关键词的搜索量,并按搜索量降序排列,然后把结果存入表 keyword_20100603

create table keyword_20100603 as select keyword,count(keyword) as count from searchlog_20100603 group by keyword order by count desc;

c)          统计每类用户终端的搜索量,并按搜索量降序排列,然后把结果存入表 device_20100603

create table device_20100603 as select device,count(device) as count from searchlog_20100603 group by device order by count desc;

d)          创建表 time_20100603 ,使用自定义的 INPUTFORMAT OUTPUTFORMAT ,并指定表数据的真实存放位置在 ‘/LogAnalysis/results/time_20100603’ HDFS 路径),而不是放在 hive 自己的数据目录中

create external table if not exists time_20100603(time string, count int) stored as INPUTFORMAT ‘com.aspire.search.loganalysis.hive.XmlResultInputFormat’ OUTPUTFORMAT ‘com.aspire.search.loganalysis.hive.XmlResultOutputFormat’ LOCATION ‘/LogAnalysis/results/time_20100603’;

e)          统计每秒访问量 TPS ,按访问量降序排列,并把结果输出到表 time_20100603 中,这个表我们在上面刚刚定义过,其真实位置在 ‘/LogAnalysis/results/time_20100603’ ,并且由于 XmlResultOutputFormat 的格式化,文件内容是 XML 格式。

insert overwrite table time_20100603 select time,count(time) as count from searchlog_20100603 group by time order by count desc;

f)           计算每个搜索请求响应时间的最大值,最小值和平均值

insert overwrite table response_20100603 select max(responsetime) as max,min(responsetime) as min,avg(responsetime) as avg from searchlog_20100603;

g)          创建一个表用于存放今天与昨天的关键词搜索量和增量及其增量比率,表数据位于 ‘/LogAnalysis/results/keyword_20100604_20100603’ ,内容将是 XML 格式。

create external table if not exists keyword_20100604_20100603(keyword string, count int, increment int, incrementrate double) stored as INPUTFORMAT ‘com.aspire.search.loganalysis.hive.XmlResultInputFormat’ OUTPUTFORMAT ‘com.aspire.search.loganalysis.hive.XmlResultOutputFormat’ LOCATION ‘/LogAnalysis/results/keyword_20100604_20100603’;

h)          设置表的属性,以便 XmlResultInputFormat XmlResultOutputFormat 能根据 output.resulttype 的不同内容输出不同格式的 XML 文件。

alter table keyword_20100604_20100603 set tblproperties (‘output.resulttype’=’keyword’);

i)            关联今天关键词统计结果表( keyword_20100604 )与昨天关键词统计结果表( keyword_20100603 ),统计今天与昨天同时出现的关键词的搜索次数,今天相对昨天的增量和增量比率,并按增量比率降序排列,结果输出到刚刚定义的 keyword_20100604_20100603 表中,其数据文件内容将为 XML 格式。

insert overwrite table keyword_20100604_20100603 select cur.keyword, cur.count, cur.count-yes.count as increment, (cur.count-yes.count)/yes.count as incrementrate from keyword_20100604 cur join keyword_20100603 yes on (cur.keyword = yes.keyword) order by incrementrate desc;

j)           

4、 用户自定义函数 UDF

部分统计结果需要以 CSV 的格式输出,对于这类文件体全是有效内容的文件,不需要像 XML 一样包含 version encoding 等信息的文件头,最适合用 UDF(user define function) 了。

UDF 函数可直接应用于 select 语句,对查询结构做格式化处理之后,再输出内容。自定义 UDF 需要继承 org.apache.hadoop.hive.ql.exec.UDF ,并实现 evaluate 函数, Evaluate 函数支持重载,还支持可变参数。我们实现了一个支持可变字符串参数的 UDF ,支持把 select 得出的任意个数的不同类型数据转换为字符串后,按 CSV 格式输出,由于代码较简单,这里给出源码示例:

    public String evaluate(String… strs) {

       StringBuilder sb = new StringBuilder();

       for ( int i = 0; i < strs. length ; i++) {

           sb.append(ConvertCSVField(strs[i])).append( ‘,’ );

       }

       sb.deleteCharAt(sb.length()-1);

       return sb.toString();

}

需要注意的是,要使用 UDF 功能,除了实现自定义 UDF 外,还需要加入包含 UDF 的包,示例:

add jar /opt/hadoop/hive-0.5.0-bin/lib/hive_contrib.jar;

然后创建临时方法,示例:

CREATE TEMPORARY FUNCTION Result2CSv AS ‘com.aspire.search.loganalysis.hive. Result2CSv’;

使用完毕还要 drop 方法,示例:

DROP TEMPORARY FUNCTION Result2CSv;

5、   输出 XML 格式的统计结果

前面看到部分日志统计结果输出到一个表中,借助 XmlResultInputFormat XmlResultOutputFormat 格式化成 XML 文件,考虑到创建这个表只是为了得到 XML 格式的输出数据,我们只需实现 XmlResultOutputFormat 即可,如果还要支持 select 查询,则我们还需要实现 XmlResultInputFormat ,这里我们只介绍 XmlResultOutputFormat

前面介绍过,定制 XmlResultOutputFormat 我们只需重写 write 即可,这个方法将会把 hive 的以 ’/001’ 分隔的多字段数据格式化为我们需要的 XML 格式,被简化的示例代码如下:

    public void write(Writable w) throws IOException {

           String[] strFields = ((Text) w).toString().split( "/001" );

           StringBuffer sbXml = new StringBuffer();

           if ( strResultType .equals( "keyword" )) {

    sbXml.append( "<record><keyword>" ).append(strFields[0]).append(

    "</keyword><count>" ).append(strFields[1]).append(           "</count><increment>" ).append(strFields[2]).append(

    "</increment><rate>" ).append(strFields[3]).append(

"</rate></result>" );

           }

           Text txtXml = new Text();

           byte [] strBytes = sbXml.toString().getBytes( "utf-8" );

           txtXml.set(strBytes, 0, strBytes. length );

           byte [] output = txtXml.getBytes();

           bytesWritable .set(output, 0, output. length );

           writer .write( bytesWritable );

    }

        其中的 strResultType .equals( "keyword" ) 指定关键词统计结果,这个属性来自以下语句对结果类型的指定,通过这个属性我们还可以用同一个 outputformat 输出多种类型的结果。

        alter table keyword_20100604_20100603 set tblproperties (‘output.resulttype’=’keyword’);

        仔细看看 write 函数的实现便可发现,其实这里只输出了 XML 文件的正文,而 XML 的文件头和结束标签在哪里输出呢?所幸我们采用的是基于 outputformat 的实现,我们可以在构造函数输出 version encoding 等文件头信息,在 close() 方法中输出结束标签。

        这也是我们为什么不使用 UDF 来输出结果的原因,自定义 UDF 函数不能输出文件头和文件尾,对于 XML 格式的数据无法输出完整格式,只能输出 CSV 这类所有行都是有效数据的文件。

五、           总结

        Hive 是一个可扩展性极强的数据仓库工具,借助于 hadoop 分布式存储计算平台和 hive SQL 语句的理解能力,我们所要做的大部分工作就是输入和输出数据的适配,恰恰这两部分 IO 格式是千变万化的,我们只需要定制我们自己的输入输出适配器, hive 将为我们透明化存储和处理这些数据,大大简化我们的工作。本文的重心也正在于此,这部分工作相信每一个做数据分析的朋友都会面对的,希望对您有益。

        本文介绍了一次相当简单的基于 hive 的日志统计实战,对 hive 的运用还处于一个相对较浅的层面,目前尚能满足需求。对于一些较复杂的数据分析任务,以上所介绍的经验很可能是不够用的,甚至是 hive 做不到的, hive 还有很多进阶功能,限于篇幅本文未能涉及,待日后结合具体任务再详细阐述。

        如您对本文有任何建议或指教,请评论,谢谢。

另,转载烦请注明出处。

MongoDB数据文件内部结构

NoSQLFan

关注NoSQL相关的新闻与技术

MongoDB数据文件内部结构

作者:nosqlfan on 星期一, 十二月 5, 2011 · 2条评论 【阅读:1,730 次】 

有人在Quora上提问:MongoDB数据文件内部的组织结构是什么样的。随后10gen的工程师Jared Rosoff出来做了简短的回答。

每一个数据库都有自己独立的文件。如果你开启了directoryperdb选项,那你每个库的文件会单独放在一个文件夹里。

数据库文件在内部会被切分成单个的块,每个块只保存一个名字空间的数据。在MongoDB中,名字空间用于区分不同的存储类别。比如每个collection有一个独立的名字空间,每个索引也有自己的名字空间。

在一个块中,会保存多条记录,每条记录是BSON格式的,记录与记录之间通过双向链表进行连接。

索引数据也存在数据文件中,不过索引是被组织成B-Tree结构,而不是双向链表。

对每个数据库,有一个命名空间文件,用于保存每个名字空间对应的元数据。我们通过查询这些元数据来找到对应的名字空间的存储块位置。

如果你开启了jorunaling日志,那么还会有一些文件存储着你所有的操作记录。

下面图片摘自10gen工程师Mathias Stearn在MongoSV2011大会上的发言稿,手绘的数据文件结构。

1.每个数据库有相应的数据文件和命名空间文件

2.数据文件从16MB开始,新的数据文件比上一个文件大一倍,最大为2GB

3.文件使用MMAP进行内存映射,会将所有数据文件映射到内存中,但是只是虚拟内存,只有访问到这块数据时才会交换到物理内存。

4.MongoDB的数据文件映射到内存表中的位置

5.使用32位机器的话,内存地址最大可以标识4GB内存

6.但是在32位机器上,4GB内存会有1GB被内核战胜,大约0.5GB会用于mongod进程的stack空间,只剩下大约2.5GB可用于映射数据文件。

7.在64位机器上则最多可以表示128TB的空间

8.每个数据文件会被分成一个一个的数据块,块与块之间用双向链表连接

9.在名字空间文件中,保存的是一个hash table,保存了每个名字空间的存储信息元数据,包括其大小,块数,第一块位置,最后一块位置,被删除的块的链表以及索引信息

10.这些位置通过DiskLoc数据结构进行存储,存储了数据文件编号和块在文件中的位置

11.对每一个块来说,其头部包含了一些块的元数据,比如自己的位置,上一个和下一个块的位置以及块中第一条和最后一条记录的位置指针。剩下的部分用于存储具体的数据,具体数据之间也是通过双向链接来进行连接。

12.下面是B-Tree的存储结构和工作原理

anyShare赠人玫瑰,手有余香,分享知识,德艺双馨!
          

分类 MongoDB · tag , ,

  • http://zoomquiet.org Zoom.Quiet

    手绘V5!

  • peter

    这手绘看着有感觉啊

© 2011 NoSQLFan · NoSQLFan theme by @ninayan

从Hadoop框架与MapReduce模式中谈海量数据处理(淘宝技术架构) – 结构之法 算法之道 – 博客频道 – CSDN.NET

            从hadoop框架与MapReduce模式中谈海量数据处理

    几周前,当我最初听到,以致后来初次接触Hadoop与MapReduce这两个东西,我便稍显兴奋,觉得它们很是神秘,而神秘的东西常能勾起我的兴趣,在看过介绍它们的文章或论文之后,觉得Hadoop是一项富有趣味和挑战性的技术,且它还牵扯到了一个我更加感兴趣的话题:海量数据处理。

    由此,最近凡是空闲时,便在看“Hadoop”,“MapReduce”“海量数据处理”这方面的论文。但在看论文的过程中,总觉得那些论文都是浅尝辄止,常常看的很不过瘾,总是一个东西刚要讲到紧要处,它便结束了,让我好生“愤懑”。

    尽管我对这个Hadoop与MapReduce知之甚浅,但我还是想记录自己的学习过程,说不定,关于这个东西的学习能督促我最终写成和“经典算法研究系列”一般的一系列文章。

    Ok,闲话少说。本文从最基本的mapreduce模式,Hadoop框架开始谈起,然后由各自的架构引申开来,谈到海量数据处理,最后谈谈淘宝的海量数据产品技术架构,以为了兼备浅出与深入之效,最终,希望得到读者的喜欢与支持。谢谢。

    由于本人是初次接触这两项技术,文章有任何问题,欢迎不吝指正。再谢一次。Ok,咱们开始吧。

第一部分、mapreduce模式与hadoop框架深入浅出

         想读懂此文,读者必须先要明确以下几点,以作为阅读后续内容的基础知识储备:

  1. Mapreduce是一种模式。
  2. Hadoop是一种框架。
  3. Hadoop是一个实现了mapreduce模式的开源的分布式并行编程框架。

    所以,你现在,知道了什么是mapreduce,什么是hadoop,以及这两者之间最简单的联系,而本文的主旨即是,一句话概括:在hadoop的框架上采取mapreduce的模式处理海量数据。下面,咱们可以依次深入学习和了解mapreduce和hadoop这两个东西了。

Mapreduce模式

    前面说了,mapreduce是一种模式,一种什么模式呢?一种云计算的核心计算模式,一种分布式运算技术,也是简化的分布式编程模式,它主要用于解决问题的程序开发模型,也是开发人员拆解问题的方法。

    Ok,光说不上图,没用。如下图所示,mapreduce模式的主要思想是将自动分割要执行的问题(例如程序)拆解成map(映射)和reduce(化简)的方式,流程图如下图1所示:

    在数据被分割后通过Map 函数的程序将数据映射成不同的区块,分配给计算机机群处理达到分布式运算的效果,在通过Reduce 函数的程序将结果汇整,从而输出开发者需要的结果。

    MapReduce 借鉴了函数式程序设计语言的设计思想,其软件实现是指定一个Map 函数,把键值对(key/value)映射成新的键值对(key/value),形成一系列中间结果形式的key/value 对,然后把它们传给Reduce(规约)函数,把具有相同中间形式key 的value 合并在一起。Map 和Reduce 函数具有一定的关联性。函数描述如表1 所示:

    MapReduce致力于解决大规模数据处理的问题,因此在设计之初就考虑了数据的局部性原理,利用局部性原理将整个问题分而治之。MapReduce集群由普通PC机构成,为无共享式架构。在处理之前,将数据集分布至各个节点。处理时,每个节点就近读取本地存储的数据处理(map),将处理后的数据进行合并(combine)、排序(shuffle and sort)后再分发(至reduce节点),避免了大量数据的传输,提高了处理效率。无共享式架构的另一个好处是配合复制(replication)策略,集群可以具有良好的容错性,一部分节点的down机对集群的正常工作不会造成影响。

    ok,你可以再简单看看下副图,整幅图是有关hadoop的作业调优参数及原理,图的左边是MapTask运行示意图,右边是ReduceTask运行示意图:

    如上图所示,其中map阶段,当map task开始运算,并产生中间数据后并非直接而简单的写入磁盘,它首先利用内存buffer来对已经产生的buffer进行缓存,并在内存buffer中进行一些预排序来优化整个map的性能。而上图右边的reduce阶段则经历了三个阶段,分别Copy->Sort->reduce。我们能明显的看出,其中的Sort是采用的归并排序,即merge sort。

    了解了什么是mapreduce,接下来,咱们可以来了解实现了mapreduce模式的开源框架—hadoop。

Hadoop框架

    前面说了,hadoop是一个框架,一个什么样的框架呢?Hadoop 是一个实现了MapReduce 计算模型的开源分布式并行编程框架,程序员可以借助Hadoop 编写程序,将所编写的程序运行于计算机机群上,从而实现对海量数据的处理。

    此外,Hadoop 还提供一个分布式文件系统(HDFS)及分布式数据库(HBase)用来将数据存储或部署到各个计算节点上。所以,你可以大致认为:Hadoop=HDFS(文件系统,数据存储技术相关)+HBase(数据库)+MapReduce(数据处理)。Hadoop 框架如图2 所示:

    借助Hadoop 框架及云计算核心技术MapReduce 来实现数据的计算和存储,并且将HDFS 分布式文件系统和HBase 分布式数据库很好的融入到云计算框架中,从而实现云计算的分布式、并行计算和存储,并且得以实现很好的处理大规模数据的能力。

Hadoop的组成部分

    我们已经知道,Hadoop是Google的MapReduce一个Java实现。MapReduce是一种简化的分布式编程模式,让程序自动分布到一个由普通机器组成的超大集群上并发执行。Hadoop主要由HDFS、MapReduce和HBase等组成。具体的hadoop的组成如下图:

    由上图,我们可以看到:

    1、             Hadoop HDFS是Google GFS存储系统的开源实现,主要应用场景是作为并行计算环境(MapReduce)的基础组件,同时也是BigTable(如HBase、HyperTable)的底层分布式文件系统。HDFS采用master/slave架构。一个HDFS集群是有由一个Namenode和一定数目的Datanode组成。Namenode是一个中心服务器,负责管理文件系统的namespace和客户端对文件的访问。Datanode在集群中一般是一个节点一个,负责管理节点上它们附带的存储。在内部,一个文件其实分成一个或多个block,这些block存储在Datanode集合里。如下图所示(HDFS体系结构图):

    2、             Hadoop MapReduce是一个使用简易的软件框架,基于它写出来的应用程序能够运行在由上千个商用机器组成的大型集群上,并以一种可靠容错的方式并行处理上TB级别的数据集。

    一个MapReduce作业(job)通常会把输入的数据集切分为若干独立的数据块,由 Map任务(task)以完全并行的方式处理它们。框架会对Map的输出先进行排序,然后把结果输入给Reduce任务。通常作业的输入和输出都会被存储在文件系统中。整个框架负责任务的调度和监控,以及重新执行已经失败的任务。如下图所示(Hadoop MapReduce处理流程图):

    3、             Hive是基于Hadoop的一个数据仓库工具,处理能力强而且成本低廉。

主要特点

存储方式是将结构化的数据文件映射为一张数据库表。提供类SQL语言,实现完整的SQL查询功能。可以将SQL语句转换为MapReduce任务运行,十分适合数据仓库的统计分析。

不足之处:

采用行存储的方式(SequenceFile)来存储和读取数据。效率低:当要读取数据表某一列数据时需要先取出所有数据然后再提取出某一列的数据,效率很低。同时,它还占用较多的磁盘空间。

由于以上的不足,有人(查礼博士)介绍了一种将分布式数据处理系统中以记录为单位的存储结构变为以列为单位的存储结构,进而减少磁盘访问数量,提高查询处理性能。这样,由于相同属性值具有相同数据类型和相近的数据特性,以属性值为单位进行压缩存储的压缩比更高,能节省更多的存储空间。如下图所示(行列存储的比较图):

4、             HBase

    HBase是一个分布式的、面向列的开源数据库,它不同于一般的关系数据库,是一个适合于非结构化数据存储的数据库。另一个不同的是HBase基于列的而不是基于行的模式。HBase使用和 BigTable非常相同的数据模型。用户存储数据行在一个表里。一个数据行拥有一个可选择的键和任意数量的列,一个或多个列组成一个ColumnFamily,一个Fmaily下的列位于一个HFile中,易于缓存数据。表是疏松的存储的,因此用户可以给行定义各种不同的列。在HBase中数据按主键排序,同时表按主键划分为多个HRegion,如下图所示(HBase数据表结构图):

    Ok,行文至此,看似洋洋洒洒近千里,但若给读者造成阅读上的负担,则不是我本意。接下来的内容,我不会再引用诸多繁杂的专业术语,以给读者心里上造成不良影响。

    我再给出一副图,算是对上文所说的hadoop框架及其组成部分做个总结,如下图所示,便是hadoop的内部结构,我们可以看到,海量的数据交给hadoop处理后,在hadoop的内部中,正如上文所述:hadoop提供一个分布式文件系统(HDFS)及分布式数据库(Hbase)用来存储或部署到各个计算点上,最终在内部采取mapreduce的模式对其数据进行处理,然后输出处理结果:

第二部分、淘宝海量数据产品技术架构解读—学习海量数据处理经验

    在上面的本文的第一部分中,我们已经对mapreduce模式及hadoop框架有了一个深入而全面的了解。不过,如果一个东西,或者一个概念不放到实际应用中去,那么你对这个理念永远只是停留在理论之内,无法向实践迈进。

    Ok,接下来,本文的第二部分,咱们以淘宝的数据魔方技术架构为依托,通过介绍淘宝的海量数据产品技术架构,来进一步学习和了解海量数据处理的经验。

淘宝海量数据产品技术架构

    如下图2-1所示,即是淘宝的海量数据产品技术架构,咱们下面要针对这个架构来一一剖析与解读。

    相信,看过本博客内其它文章的细心读者,定会发现,图2-1最初见于本博客内的此篇文章:从几幅架构图中偷得半点海量数据处理经验之上,同时,此图2-1最初发表于《程序员》8月刊,作者:朋春。

    在此之前,有一点必须说明的是:本文下面的内容大都是参考自朋春先生的这篇文章:淘宝数据魔方技术架构解析所写,我个人所作的工作是对这篇文章的一种解读与关键技术和内容的抽取,以为读者更好的理解淘宝的海量数据产品技术架构。与此同时,还能展示我自己读此篇的思路与感悟,顺带学习,何乐而不为呢?。

    Ok,不过,与本博客内之前的那篇文章(几幅架构图中偷得半点海量数据处理经验)不同,本文接下来,要详细阐述这个架构。我也做了不少准备工作(如把这图2-1打印了下来,经常琢磨):

 

                                              图2-1 淘宝海量数据产品技术架构

    好的,如上图所示,我们可以看到,淘宝的海量数据产品技术架构,分为以下五个层次,从上至下来看,它们分别是:数据源,计算层,存储层,查询层和产品层。我们来一一了解这五层:

  1. 数据来源层。存放着淘宝各店的交易数据。在数据源层产生的数据,通过DataX,DbSync和Timetunel准实时的传输到下面第2点所述的“云梯”。
  2. 计算层。在这个计算层内,淘宝采用的是hadoop集群,这个集群,我们暂且称之为云梯,是计算层的主要组成部分。在云梯上,系统每天会对数据产品进行不同的mapreduce计算。
  3. 存储层。在这一层,淘宝采用了两个东西,一个使MyFox,一个是Prom。MyFox是基于MySQL的分布式关系型数据库的集群,Prom是基于hadoop Hbase技术 的(读者可别忘了,在上文第一部分中,咱们介绍到了这个hadoop的组成部分之一,Hbase—在hadoop之内的一个分布式的开源数据库)的一个NoSQL的存储集群。
  4. 查询层。在这一层中,有一个叫做glider的东西,这个glider是以HTTP协议对外提供restful方式的接口。数据产品通过一个唯一的URL来获取到它想要的数据。同时,数据查询即是通过MyFox来查询的。下文将具体介绍MyFox的数据查询过程。
  5.  产品层。简单理解,不作过多介绍。

    接下来,咱们重点来了解第三层-存储层中的MyFox与Prom,然后会稍带分析下glide的技术架构,最后,再了解下缓存。文章即宣告结束。

    我们知道,关系型数据库在我们现在的工业生产中有着广泛的引用,它包括Oracle,MySQL、DB2、Sybase和SQL Server等等。

MyFOX

    淘宝选择了MySQL的MyISAM引擎作为底层的数据存储引擎。且为了应对海量数据,他们设计了分布式MySQL集群的查询代理层-MyFOX。

如下图所示,是MySQL的数据查询过程:

                                                            图2-2 MyFOX的数据查询过程

    在MyFOX的每一个节点中,存放着热节点和冷节点两种节点数据。顾名思义,热节点存放着最新的,被访问频率较高的数据;冷节点,存放着相对而来比较旧的,访问频率比较低的数据。而为了存储这两种节点数据,出于硬件条件和存储成本的考虑,你当然会考虑选择两种不同的硬盘,来存储这两种访问频率不同的节点数据。如下图所示:

                                                           图2-3 MyFOX节点结构

     “热节点”,选择每分钟15000转的SAS硬盘,按照一个节点两台机器来计算,单位数据的存储成本约为4.5W/TB。相对应地,“冷数据”我们选择了每分钟7500转的SATA硬盘,单碟上能够存放更多的数据,存储成本约为1.6W/TB。

Prom

出于文章篇幅的考虑,本文接下来不再过多阐述这个Prom了。如下面两幅图所示,他们分别表示的是Prom的存储结构以及Prom查询过程:

                                              图2-4 Prom的存储结构

 

                                                          图2-5 Prom查询过程

glide的技术架构

   

                                               图2-6 glider的技术架构

    在这一层-查询层中,淘宝主要是基于用中间层隔离前后端的理念而考虑。Glider这个中间层负责各个异构表之间的数据JOIN和UNION等计算,并且负责隔离前端产品和后端存储,提供统一的数据查询服务。

缓存

    除了起到隔离前后端以及异构“表”之间的数据整合的作用之外,glider的另外一个不容忽视的作用便是缓存管理。我们有一点须了解,在特定的时间段内,我们认为数据产品中的数据是只读的,这是利用缓存来提高性能的理论基础。

在上文图2-6中我们看到,glider中存在两层缓存,分别是基于各个异构“表”(datasource)的二级缓存和整合之后基于独立请求的一级缓存。除此之外,各个异构“表”内部可能还存在自己的缓存机制。

                                                           图2-7 缓存控制体系

    图2-7向我们展示了数据魔方在缓存控制方面的设计思路。用户的请求中一定是带了缓存控制的“命令”的,这包括URL中的query string,和HTTP头中的“If-None-Match”信息。并且,这个缓存控制“命令”一定会经过层层传递,最终传递到底层存储的异构“表”模块。

    缓存系统往往有两个问题需要面对和考虑:缓存穿透与失效时的雪崩效应。

  1. 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。至于如何有效地解决缓存穿透问题,最常见的则是采用布隆过滤器(这个东西,在我的此篇文章中有介绍:),将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

    而在数据魔方里,淘宝采用了一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

      2、缓存失效时的雪崩效应尽管对底层系统的冲击非常可怕。但遗憾的是,这个问题目前并没有很完美的解决方案。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。

    在数据魔方中,淘宝设计的缓存过期机制理论上能够将各个客户端的数据失效时间均匀地分布在时间轴上,一定程度上能够避免缓存同时失效带来的雪崩效应。

本文参考:

  1. 基于云计算的海量数据存储模型,侯建等。

  2. 基于hadoop的海量日志数据处理,王小森

  3. 基于hadoop的大规模数据处理系统,王丽兵。

  4. 淘宝数据魔方技术架构解析,朋春。

  5. Hadoop作业调优参数整理及原理,guili。

读者点评@xdylxdyl:

  1. We want to count all the books in the library. You count up shelf #1, I count up shelf #2. That’s map. The more people we get, the faster it goes. Now we get together and add our individual counts. That’s reduce。

  2. 数据魔方里的缓存穿透,架构,空数据缓存这些和Hadoop一点关系都么有,如果是想讲一个Hadoop的具体应用的话,数据魔方这部分其实没讲清楚的。

  3. 感觉你是把两个东西混在一起了。不过这两个都是挺有价值的东西,或者说数据魔方的架构比Hadoop可能更重要一些,基本上大的互联网公司都会选择这么做。Null对象的缓存保留五分钟未必会有好的结果吧,如果Null对象不是特别大,数据的更新和插入不多也可以考虑实时维护。

  4. Hadoop本身很笨重,不知道在数据魔方里是否是在扮演着实时数据处理的角色?还是只是在做线下的数据分析的?

结语:写文章是一种学习的过程。尊重他人劳动成果,转载请注明出处。谢谢。July、2011/8/20。完。

百度统计与Google Analytics相同之处及优势与不足_SEO/推广技巧_三好技巧网–好新好全好实用的IT技巧!

百度统计与Google Analytics相同之处及优势与不足

时间:2010-06-19 09:04来源:未知 作者:admin 点击:154次  

  百度统计与Google Analytics相同的地方:

  1、工具定位和层次基本相同

  均定位于流量分析工具,而不是流量统计。流量分析差别于流量统计的显著特点是,流量分析会关注与流量的质量,其分析基本单元是“访问次数(Visit)”,相当于Session,而流量统计分析的基本单元是“浏览量(PV)”。

  Btw:由这个区分来看,目前市场上的CNZZ,51la,量子统计均只是流量统计的工具,而且由于基本的分析方法迥异,要升级为流量分析几乎不可能。随着站长的成熟,停留在统计层面的工具,迟早要消失的。

  2、都致力于帮助站长提升网站质量

  搜索引擎处于互联网生态链的顶端,到了一定的阶段,互联网上是否有高质量的内容和网站,将成为搜索引擎下一个发展瓶颈,这一点百度和Google是一致的。

  无论是Google还是百度,都希望能够把整个蛋糕做到更大,这样才可以分到更大的一块。搜索引擎推流量分析工具都是这样的目的。

  3、都独家支持自家推广平台的统计

  与Google Analytics独家无缝整合Google Adwords一样,百度统计对百度推广也实现了完美的整合。百度推广的关键词、创意、计划、单元带来的流量与点击消费信息被整合在平台下,能很好地帮助百度推广用户做各类SEM优化。

  独家支持这一点,本身就可以吸引到大量用户来用,毕竟依赖于付费推广的用户还是很多很多的。

  百度统计相对于Google Analytics的几点优势:

  1、更加符合中国站长的需求,比如:

  a)提供独立IP数

  不知道为什么Google Anlytics一直不提供独立IP数,但是IP数在国内站长圈内还是一个用个最多、广为接受的指标,甚至远超过访客数这样的概念。

  b)提供实时数据

  Google Analytics一直有1-2个小时的数据延时,这对于运营成熟的站点来说,对数据分析没有什么影响。但是国内更多的站点来说,还处于初级阶段,互联网上瞬息万变的环境中,1个小时的延迟就可能错过很多机会。

  百度统计在这一点上,做到所有报表最多延迟5分钟(当前在线用户数报告延迟2分钟左右),而且完成了大量一般流量统计工具不提供的流量分析功能,其中必须花了很多精力。

  c)提供访客明细记录

  Google Analytics的定位是一个彻底的分析工具,因此,对微观的用户信息是完全不提供的。但实际上,经过加工之后的数据固然有其作用,但是细节数据也不可或缺,对于网站管理者来说,通过微观信息,揣摩用户行为和心理,对于网站优化时大有裨益的。

  百度统计有两处访客明细记录很有用,一个是最新访客中的最近500条访客记录,均按照访问次数(visit)给出了用户的详细访问轨迹,还一个是搜索词报告里的“链”字链接,提供了搜索词的详细搜索URL.

  2、部分功能推成出新,比如:

  a)事件目标分析功能

  Google Analytics提供的是JS函数接口,功能非常强大,但是需要相当的编程能力,部署非常复杂。

  百度统计提供的事件目标分析功能,只需要在后台对要监控的事件进行设置就可以了,用起来很方便,不懂编程的人也可以很快用起来。

  b)子目录分析

  Google Analytics提供的过滤器功能强大,几乎能想到的过滤都能够提供,但是如果想在一个网站向看多个子目录的各类数据,也是做不到的。

  百度统计子目录设置功能提供的是一个页面包含和排除的简单规则设置,能够把要分析的页面单列出来作为一个子目录,提供这个子目录各个维度的数据,对于子目录很多的站点来说,很好用。

  c)升降榜、历史趋势分析

  升降榜这类功能本是51la等统计工具做的比较好的地方,百度统计也吸纳了,作为流量原因分析的重要工具。

  流量的分析最重要就是看:细分、趋势、并做比较,百度统计在各个报告中都提供了快速趋势查询功能,方便观察和比较。

  3、使用体验,更加本土,更加清晰好用

  a)菜单清晰简单

  Google Analytics服务于全球用户,在功能的理解上,对于初学者来说是需要一定时间。百度统计菜单上相对就好理解多了。

  b)统计图标、查看密码功能

  Google Analytics不提供统计图标,开放数据需要单开账号,百度统计这方面做的更接近于CNZZ,要给广告主看数据,很简单,给个查看密码,到网站下面去点击统计图标就可以看了。

  c)系统环境

  在地域分省上,百度统计具有国内最准确的IP库,保证统计最准确,在浏览器、网络服务商这些参数的统计上,也更加本地化,统计全面而准确。

  4、开放和整合力度更大,包括:

  a)不限制PV规模

  Google Analytics的 PV限制是500万/月,相当于一天30万pv不到,这对于很多大站点都是远远不够的。如果超过了这个限制必须做Adwords才可以。

  百度统计是完全不限制流量,可以支撑每天上千万PV的流量。

  b)巧妙整合百度指数、热门搜索词功能

  Google也有类似的功能,但是分布在不同的产品中。

  百度统计在搜索词报告中,通过搜索词,把相关的信息串在一起,对于流量原因分析很有帮助。

  百度统计相对于Google Analytics不足的方面:

  1、交叉数据分析方面

  Google在数据存储方面的能力毫无疑问不是盖的。用过的Google Analytics人都会被其强大的维度交叉分析功能所降服。

  百度统计虽然也在大多数报告中提供了地域和来源的交叉过滤,但与Google Analytics相比,还有差距。

  2、自定义报告和功能方面

  Google Analytics提供了自定义维度、指标,能够自由组合出各种需要的报告。过滤器就更不用说了,可以自由设置,过滤出各类数据,只有想不到,没有做不到。

  百度统计虽然也提供了IP排除、来源过滤、跨域统计等功能,但是在自定义方面显然不如Google Analytics.

  3、转化分析方面

  Google Analytics在转化路径分析方面做得更加完善,功能更加强大。

  百度统计也提供了转化路径分析功能,但还非常简单,提供了上下游报告也还有不少要完善的地方。

  4、网站分析规则制定和普及方面

  Google Analytics不仅是一个工具,而且提供了一套方法和规则,帮助网站分析从业者、网站主来做网站分析,成为了业界默认的规范。

  这方面,百度统计还刚刚起步。

  来源:读者投稿,作者:百度商业产品部喻友平

原创文章如转载,请注明:转载自月光博客 [ http://www.williamlong.info/ ] 本文链接地址:http://www.williamlong.info/archives/2214.html

分享网站分析工具使用的十条原则 | 威海seo_威海网站建设_网站推广_网站优化_★最好的网络营销顾问公司★ – 威海网站建设

分享网站分析工具使用的十条原则 | 威海seo_威海网站建设_网站推广_网站优化_★最好的网络营销顾问公司★ – 威海网站建设

分享网站分析工具使用的十条原则

34 views
 

11 条讨论

 

发布日期:
八 10, 2011
 

文章位于:
威海seo知识
 

  

今天看了一篇文章,很好很强大,我把文章的内容整理了一下,分享给大家。

网站分析工具是做网站分析的必备武器。工具利用水平的高低,很大程度上决定了网站分析水平的高低。我们的误区是认为越好的工具越能帮助做出更好的网站分析,但是工具的先进和网站分析的先进完全是两回事。记住,是你使用工具,而不是工具使用你。

1、不要采用超出你的能力的工具

网站分析工具的功能强大程度和其复杂程度成正比。功能强大的背后,意味着更为灵活的定制化能力,而定制化只能由人完成的。好的定制化取决于你对自己业务的清晰掌握你对这个工具的清晰掌握;你对你的业务与网站表现之间关系的清晰掌握。这三点,对于我们大多数朋友来说都是极高的要求。

中国现在有很多企业使用Omniture(详见:名词解释)的旗舰分析工具SiteCatalyst(详见:名词解释),但实际上并没有发挥超于Google Analytics 的能力,甚至还不如GA的能力。如果你只是使用简单的功能,那么确实GA在用户体验上可能更佳,甚至CNZZ或者百度统计已经足够。如果你做了定制化功能,你就必须要了解SiteCatalyst的流量流和业务流之间的逻辑,以及有这两种流所引发的traffic、event、prop和evar。

2、免费工具没用好,付费工具会更困难

商用的付费的WA工具(详见:名词解释)能有多么强大啊!我们认为,免费工具不好使是工具的功能毕竟有限,我们用付费工具肯定会完全不同!免费工具没有用好,付费工具不仅不会帮到你,反而你会更加烦恼。类似于Google Analytics或是百度统计这样的免费工具汇集了网站分析所需的一些最核心功能,并且体现了网站分析的很多核心思想。你认为的工具不好使,可能真的只是因为你没有最大化的挖掘网站分析的潜力。

GA有如此变通的使用方法。这些方法都仅仅只是基于GA的一个方法,即_trackPageview()方法,我们默认的使用是不加参数的,可是如果你把各种参数——包括页面的Title,包括面包屑,包括页面URL中截取后翻译的信息加入进去,GA就增加了许多我们从前根本不认为它能够实现的功能。利用面包屑,GA甚至可以有一点点路径功能了!

同样,对于_gaq.push([‘_trackTrans’])这个命令也可以挖掘的很深,你可以把更多商品的分类信息、价格以及按照你的要求总结的信息通过动态变量的方式传递给GA。GA对于商品的监测也能变得更强大。

免费工具的能力是有限的,如果你愿意花时间在免费工具的压榨上,那么当你开始使用付费工具的时候,你会发现一切并不那么困难。但是,如果你在使用免费工具的时候不断埋怨,那么我可以想象你在付费工具使用时会有更多的埋怨。

3、不要试图用WA工具准确监测交易

我们总是相信付费的工具比免费工具更加准确。由于监测机制本身的原理,工具的准确和你理解的准确是不同的。常见付费工具和免费工具在流量监测的实现方法上几乎一样,所以二者之间不存在谁更准确的问题。而在监测交易数据上,毕竟不同于电子商务网站内部的ERP系统,网站分析工具是通过捕捉页面上的交易信息实现交易数据获取的,因此常常只是订单确认,或者支付确认的数据,但并不是最终成交的数据(COD交易的成功与否,以及用户退货撤单等情况是很难被网站分析工具监测的)。

能够准确监测交易的工具是你公司的ERP系统(或进销存系统),但WA工具很困难。WA工具无法满足你的所有。它的最大优点是能够帮助我们通过数据发现没有察觉的问题,也能够帮我们证明我们的改进是否能够带来更好的数据。但很多时候通过WA工具是做不到的,必须要的AB测试工具、调研工具、Pannel、眼动仪等这些分析工具和调研、可用性测试、德尔菲、用户追踪等方法。我们常用的GA、SiteCatalyst、CNZZ或者百度统计,只不过是网站分析的点击流分析工具,网站分析工具应该有更大的包容范围。

商用网站分析工具必须保留并提供Raw Data(例如Omniture SiteCatalyst提供Data Warehouse功能)。WA工具是指Google Analytics、Omniture的SiteCatalyst这样的网站用户点击流工具(clickstream tool),这类工具能够告诉你现象,但一般很难告诉你现象后面的原因。你通过Google Analytics的Top Content报告看到某一个页面的bounce rate(蹦失率)很高,你就能分析出你网站的粘性怎么样。

怎么解决这种情况:1、查看页面是否是无链接page,或者是少链接的end page。2、如果不是,查看这个页面的流量来源是哪里,如果主要是搜索引擎,那么恭喜你,你的问题比较容易解决;如果不是搜索引擎,而是直接流量为主,那么很遗憾,你的问题很难通过WA工具刨根问底了。

4、选用一个工具之前,首先必须了解它的原理

从GA上,我们能够轻易的看到真实的流量,而不是机器访问的流量;而日志文件(log file),则能够捕获大部分机器人(蜘蛛爬虫)遍历网站的流量。因此,如果GA的数据暴涨,服务器压力大,应该添加服务器了。可是,如果GA没有暴涨,而log file的数据暴涨,那么可能是某些恶意的网站数据采集软件在作恶,就要注意网络安全领域的问题。

日志文件的分析工具(例如逆火软件,老版本的WebTrends,或是Piwik),跟页面标记监测分析工具(例如Google Analytics和Omniture SiteCatalyst)在监测原理上有根本性的差异。

5、不同原理的工具,收集数据的范围是不同的

GA的监测数据利用的是cookie,典型的GA的cookie是utma,utmb,utmc,utmz什么的。而Omniture SiteCatalyst则是使用cookie和虚拟cookie的共同作用监测数据。SiteCatalyst的cookie比GA的cookie简单,只用来记录访问者的唯一识别编号,而这个编号对应的数据,则全部存放在数据库的虚拟cookie中。虚拟cookie实际上是数据库中的一张大表,记录了这个cookie编号用户访问网站的全部行为。由于利用了虚拟cookie,因此,在用户的客户端不支持cookie的情况下,SiteCatalyst还是可以利用IP地址或client agent记录一个visitor,但GA在没有cookie支持时,就什么也不能做了。

监测原理的不同,工具的作用也就有很大不同。做SEO,了解机器人扒取数据的原理,用日志工具;想要看鼠标轨迹和停留,用鼠标捕捉工具,例如ClickTale;要看人的行为,还是用页面标记工具。

6、再智能,也要重视手工

网站分析工具的趋势更加智能化,不管多么精细的监测实施,都不可能尽善尽美。比如热图,热图无法智能化的原因是,页面上有很多同样URL的链接入口,或者页面上的链接经常被更新。处理的方式是对页面上所有重复URL链接入口和动态变化的链接,加上标识参数。然后手工统计。需要强大的执行力,不简单,但很准确,但是这个方法的弊端是影响SEO。

不过这个方法不是很适用于GA,原因在于GA没有next page报告,因为GA没有路径功能。V5版本的GA也没有这个功能,但有了Omniture SiteCatalyst或者Yahoo! Web Analytics,热图就能通过上面说的手工的方法非常准确地做出。

那么在网站分析中使用的最多的工具是什么,我的答案不是Google Analytics,也不是Omniture的一众工具,而是Excel。如何才能很好的评估SEO的长尾效果?这个工作直接读取Google Analytics的报表是很难完成的,你只能想办法把所有的长尾organic keyword全部导出,然后利用自己的经验和智慧结合数据分析它们的规律。

7、不要利用不同工具做一件事

用Google Analytics和Omniture SiteCatalyst监测网站每天visit,你永远说不清楚到底谁的数据更准确。如果GA记录的昨天的visit比前天的大,那么SC的visit也应该是昨天大于前天。为什么GA的数据和SC的数据不一样,你永远也不能找到一个圆满的解释,除了骗自己花钱买的SC更加可靠之外。所以,为了监测某个相同的度量,如果你认准了一个,那么就一直认准它,重在分析,而不是数据本身。

8、利用多个工具的长处

用Google Analytics和Omniture SiteCatalyst比较visit,但的确会用SC弥补GA没有路径监测的缺憾。当你花了钱用了ClickTale之后,你也会知道GA的In-Page Analysis不是不好,但功能确实还不够强大。你应该用多种工具,因为没有任何工具能八面玲珑,面面俱到。

9、善用复制和过滤

如果你用过GA,你不会对它的account和profile陌生。account由不同的GA记录号区分,例如UA-123456和UA-123457就会被放入不同account中。而一个account下可以容下很多个profile,每个profile对应一个报告。例如,我想在我的这个网站www.weihai-seo.com上做一些实验,但我又不想影响正常的数据监测,可以用两个方法。

第一个方法是在页面上再新建一个GATC(Google Analytics Tracking Code),也就是在GA上生成的监测代码。这个代码一定跟最初的代码有不同的记录号,即UA-XXXXXXX不同。第二个方法是在你原来的account中再复制一个profile,即把你已有的报告再复制一个一模一样的。
如果你不了解如何进行这种操作,请看谷歌官方的帮助讲解:http://services.google.com/analytics/breeze/en/accounts_profiles/index.html

其实,很多工具存在类似Google的这种报告复制的机制。例如Omniture SiteCatalyst拥有report suite的定义,你可以定义多个report suite为你的一个网站服务,当然,收费也就会更贵一些。一旦复制了一个一模一样的报告,你就可以对这个报告进行过滤操作。过滤实际上是一个不恰当的说法,应该说,是对这个报告的监测方式进行新的配置。

例如,我可以利用这个新的profile(report),加上一个地域的过滤,用于只监测从北京访问网站的客户。也可以加上一个过滤,只监测从搜索引擎导入的,且只以某一个页面为landing page的流量。复制和过滤给你更多的尝试机会,和更灵活的细分功能,使用任何一个工具,你都需要了解这个工具的复制和过滤功能,它能让你的工作事半功倍。

10、让需求指引你,而不是让工具指引你

不要为工具所累,是使用工具的最重要原则。

ps:名词解释:

Omniture:Omniture 是全球最大的基于ASP的网站收入在线分析供应商,而 Omniture 的SiteCatalyst™ 是市场上最成熟和最全面的技术,具有业界领先的可伸缩性、灵活性和直观的用户界面。Omniture是业界仅有的能够提供全面的公司网站活动,包括历史的(数据仓库)和实时的分析和报告分析的公司。

SiteCatalyst:Omniture的旗舰产品-SiteCatalyst™ 功能强大,拥有业界领先的直观用户界面,使用户能够快速获取可用信息。SiteCatalyst™ 是唯一的能够在同一个界面访问流量、路径、促销活动、商务、区段和数据仓库的解决方案。简单易用的使用方法允许不同技术能力的Omniture客户轻松访问可执行的实时报表和功能板。SiteCatalyst™ 含有一个综合的即用型报表组,能根据顾客的特殊需求自定义。SiteCatalyst™ 的独特之处在于,它能够以互动的方式在报表之间切换,使用户的分析活动始终连贯。报表集中在3个主要区域:流量、路径和商务。

Google Analytics:Google Analytics(Google分析)是Google的一款免费的网站分析服务工具,可以对目标网站进行访问数据统计和分析,并提供多种参数供网站拥有者使用。它的功能非常强大,只要在网站的页面上加入一段代码,就可以提供的丰富详尽的图表式报告。它会向您显示人们如何找到和浏览您的网站以及您能如何改善访问者的体验。提高您的网站投资回报率、增加转换,在网上获取更多收益,可对您整个网站的访问者进行跟踪,并能持续跟踪您的营销广告系列的效果:不论是 AdWords广告系列、电子邮件广告系列,还是任何其他广告计划。利用此信息,您将了解哪些关键字真正起作用、哪些广告词最有效,访问者在转换过程中从何处退出。请勿因此功能免费提供而小看它,Google Analytics(分析)是一种功能全面而强大的分析软件包。

WA工具:全称Web Analytics网站分析,是种网站访客行为的研究。于商务应用背景来说,网站分析特别指的是来自某网站搜集来的资料之使用,以决定网站布局是否符合商业目标;例如,哪个登陆页面(landing page)比较容易刺激顾客购买欲。这些搜集来的资料几乎总是包括网站流量报告,也可能包括电子邮件回应率、直接邮件活动资料、销售与客户资料、使用者效能资料如点击热点地图、或者其他自订需求资讯。这些资料通常与关键绩效指标比较以得效能资讯,并且可用来改善网站或者行销活动里观众的反映情况。

破解 Android Market 的区域限制-运营商伪装/Market Faker v1.1.2 | Null’s Notebook

2011/11/02 V1.1.2
1,界面微调

2011/11/01 V1.1.0
1,增加自定义运营商选项
2,增加自动伪装功能,先设置一个预设运营商,然后点击菜单键选择“启用自动伪装”,即可启用。
3,增加英语

Android Market 电子市场 会根据运营商的不同,而呈现不同的应用列表,身在天朝,有着深刻的体会:
1,搜索”Google”,谷歌地图,Gmail 并未出现在搜索结果中。
2,查看不到没有任何手收费应用。

之前在网上找到个免费的 Market Enabler ,但这个东西有个问题:
1,系统启动后,必须要在电子市场运行之前操作,不能即时切换
2,有广告

后来更具他的源码及帮助,找到了相关的原理 Android market switch简单的说,原理如下:
注:以下操作都需要 root 权限,所以执行前要运行 su
1,设置系统环境变量

1 setprop gsm.sim.operator.numeric MCC_MNC
2 #//setprop gsm.operator.numeric MCC_MNC 不需要设置这个值,看更新

这个 MCC_MNC 可以从这里查询:Mobile Network Code
直接把把两个值拼接起来就行了,不能省略 MNC 的0,比如天朝移不动的值是:46000,天朝连不通的是:46001,貌似电不信的有错误,这个有待求证,没用过。
其他还有比较多的相关环境变量,经过测试,其他的可以不设置,就设置这两个就可以了。

2,杀掉 Android Market 电子市场的进程
杀掉进程才能重新初始化

1 busybox killall com.android.vending

这里要说明下,必须要有 busybox ,否则这个命令无法执行

3,清理掉缓存文件
如果缓存没清理,会有些诡异的问题,比如推荐列表显示不出来。
原来的代码是将 电子市场 的全部缓存一起清理,这个对于使用天朝运营商的GPRS用户非常不人道的,简直是把人往破产的边缘推。
后来研究了下,只要删除其中几个文件就可以了

1 busybox rm -rf /data/data/com.android.vending/cache/AVMC_UGCR_P_
2 busybox rm -rf /data/data/com.android.vending/cache/AVMC_UGCIR_
3 busybox rm -rf /data/data/com.android.vending/cache/AVMC_UAR{*
4 busybox rm -rf /data/data/com.android.vending/cache/AVMC_PUAR{*

到这里,整个过程就结束了。

2011/04/30 更新
1,取消掉设置 setprop gsm.operator.numeric MCC_MNC 的值,设置该值后会导致 Google Maps 停止纠编,而导致显示位置偏移巨大

 

 

 

下载:MarketFaker_V1.1.2_Signed.apk (914)

MarketFaker_V1.0.2_src (226)

4

无觅猜您也喜欢:

IP Client for Android 2.2

[出售]谷歌地图偏移数据,谷歌GPS定位偏移修正数据

WordPress 代码高亮

VPN Client For M8 beta

无觅

Did you like this? Share it:

Tweet

理解 JavaScript 闭包 » 为之漫笔

理解 JavaScript 闭包 » 为之漫笔

要成为高级 JavaScript 程序员,就必须理解闭包。

本文结合 ECMA 262 规范详解了闭包的内部工作机制,让 JavaScript 编程人员对闭包的理解从“嵌套的函数”深入到“标识符解析、执行环境和作用域链”等等 JavaScript 对象背后的运行机制当中,真正领会到闭包的实质。

原文链接:JavaScript Closures

可打印版:JavaScript 闭包

返回目录

Closure所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。 “闭包”是一个表达式(一般是函数),它具有自由变量以及绑定这些变量的环境(该环境“封闭了”这个表达式)。(闭包,就是封闭了外部函数作用域中变量的内部函数。但是,如果外部函数不返回这个内部函数,闭包的特性无法显现。如果外部函数返回这个内部函数,那么返回的内部函数就成了名副其实的闭包。此时,闭包封闭的外部变量就是自由变量,而由于该自由变量存在,外部函数即便返回,其占用的内存也得不到释放。——译者注,2010年4月3日)

闭包是 ECMAScript (JavaScript)最强大的特性之一,但用好闭包的前提是必须理解闭包。闭包的创建相对容易,人们甚至会在不经意间创建闭包,但这些无意创建的闭包却存在潜在的危害,尤其是在比较常见的浏览器环境下。如果想要扬长避短地使用闭包这一特性,则必须了解它们的工作机制。而闭包工作机制的实现很大程度上有赖于标识符(或者说对象属性)解析过程中作用域的角色。

关于闭包,最简单的描述就是 ECMAScript 允许使用内部函数--即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。也就是说,内部函数会在外部函数返回后被执行。而当这个内部函数执行时,它仍然必需访问其外部函数的局部变量、参数以及其他内部函数。这些局部变量、参数和函数声明(最初时)的值是外部函数返回时的值,但也会受到内部函数的影响。

遗憾的是,要适当地理解闭包就必须理解闭包背后运行的机制,以及许多相关的技术细节。虽然本文的前半部分并没有涉及 ECMA 262 规范指定的某些算法,但仍然有许多无法回避或简化的内容。对于个别熟悉对象属性名解析的人来说,可以跳过相关的内容,但是除非你对闭包也非常熟悉,否则最好是不要跳下面几节。

对象属性名解析

返回目录

ECMAScript 认可两类对象:原生(Native)对象和宿主(Host)对象,其中宿主对象包含一个被称为内置对象的原生对象的子类(ECMA 262 3rd Ed Section 4.3)。原生对象属于语言,而宿主对象由环境提供,比如说可能是文档对象、DOM 等类似的对象。

原生对象具有松散和动态的命名属性(对于某些实现的内置对象子类别而言,动态性是受限的--但这不是太大的问题)。对象的命名属性用于保存值,该值可以是指向另一个对象(Objects)的引用(在这个意义上说,函数也是对象),也可以是一些基本的数据类型,比如:String、Number、Boolean、Null 或 Undefined。其中比较特殊的是 Undefined 类型,因为可以给对象的属性指定一个 Undefined 类型的值,而不会删除对象的相应属性。而且,该属性只是保存着 undefined 值。 

下面简要介绍一下如何设置和读取对象的属性值,并最大程度地体现相应的内部细节。

返回目录

对象的命名属性可以通过为该命名属性赋值来创建,或重新赋值。即,对于:

var objectRef = new Object(); //创建一个普通的 JavaScript 对象。

可以通过下面语句来创建名为 “testNumber” 的属性:

objectRef.testNumber = 5;
/* – 或- */
objectRef["testNumber"] = 5;

在赋值之前,对象中没有“testNumber” 属性,但在赋值后,则创建一个属性。之后的任何赋值语句都不需要再创建这个属性,而只会重新设置它的值:

objectRef.testNumber = 8;
/* – or:- */
objectRef["testNumber"] = 8;

稍后我们会介绍,Javascript 对象都有原型(prototypes)属性,而这些原型本身也是对象,因而也可以带有命名的属性。但是,原型对象命名属性的作用并不体现在赋值阶段。同样,在将值赋给其命名属性时,如果对象没有该属性则会创建该命名属性,否则会重设该属性的值。

返回目录

当读取对象的属性值时,原型对象的作用便体现出来。如果对象的原型中包含属性访问器(property accessor)所使用的属性名,那么该属性的值就会返回:

/* 为命名属性赋值。如果在赋值前对象没有相应的属性,那么赋值后就会得到一个:*/
objectRef.testNumber = 8;

/* 从属性中读取值 */
var val = objectRef.testNumber;

/* 现在, – val – 中保存着刚赋给对象命名属性的值 8*/

而且,由于所有对象都有原型,而原型本身也是对象,所以原型也可能有原型,这样就构成了所谓的原型链。原型链终止于链中原型为 null 的对象。Object 构造函数的默认原型就有一个 null 原型,因此:

var objectRef = new Object(); //创建一个普通的 JavaScript 对象。

创建了一个原型为 Object.prototype 的对象,而该原型自身则拥有一个值为 null 的原型。也就是说, objectRef 的原型链中只包含一个对象-- Object.prototype。但对于下面的代码而言:

/* 创建 – MyObject1 – 类型对象的函数*/
function MyObject1(formalParameter){
/* 给创建的对象添加一个名为 – testNumber – 的属性
并将传递给构造函数的第一个参数指定为该属性的值:*/
this.testNumber = formalParameter;
}
/* 创建 – MyObject2 – 类型对象的函数*/
function MyObject2(formalParameter){
/* 给创建的对象添加一个名为 – testString – 的属性
并将传递给构造函数的第一个参数指定为该属性的值:*/
this.testString = formalParameter;
}

/* 接下来的操作用 MyObject1 类的实例替换了所有与 MyObject2 类的实例相关联的原型。而且,为 MyObject1 构造函数传递了参数 – 8 – ,因而其 – testNumber – 属性被赋予该值:*/
MyObject2.prototype = new MyObject1( 8 );

/* 最后,将一个字符串作为构造函数的第一个参数,创建一个 – MyObject2 – 的实例,并将指向该对象的引用赋给变量 – objectRef – :*/
var objectRef = new MyObject2( “String_Value” );

被变量 objectRef 所引用的 MyObject2 的实例拥有一个原型链。该链中的第一个对象是在创建后被指定给 MyObject2 构造函数的 prototype 属性的 MyObject1 的一个实例。MyObject1 的实例也有一个原型,即与 Object.prototype 所引用的对象对应的默认的 Object 对象的原型。最后, Object.prototype 有一个值为 null 的原型,因此这条原型链到此结束。

当某个属性访问器尝试读取由 objectRef 所引用的对象的属性值时,整个原型链都会被搜索。在下面这种简单的情况下:

var val = objectRef.testString;

因为 objectRef 所引用的 MyObject2 的实例有一个名为“testString”的属性,因此被设置为“String_Value”的该属性的值被赋给了变量 val。但是:

var val = objectRef.testNumber;

则不能从 MyObject2 实例自身中读取到相应的命名属性值,因为该实例没有这个属性。然而,变量 val 的值仍然被设置为 8,而不是未定义--这是因为在该实例中查找相应的命名属性失败后,解释程序会继续检查其原型对象。而该实例的原型对象是 MyObject1 的实例,这个实例有一个名为“testNumber”的属性并且值为 8,所以这个属性访问器最后会取得值 8。而且,虽然 MyObject1MyObject2 都没有定义 toString 方法,但是当属性访问器通过 objectRef 读取 toString 属性的值时:

var val = objectRef.toString;

变量 val 也会被赋予一个函数的引用。这个函数就是在 Object.prototypetoString 属性中所保存的函数。之所以会返回这个函数,是因为发生了搜索 objectRef 原型链的过程。当在作为对象的 objectRef 中发现没有“toString”属性存在时,会搜索其原型对象,而当原型对象中不存在该属性时,则会继续搜索原型的原型。而原型链中最终的原型是 Object.prototype,这个对象确实有一个 toString 方法,因此该方法的引用被返回。

最后:

var val = objectRef.madeUpProperty;

返回 undefined,因为在搜索原型链的过程中,直至 Object.prototype 的原型--null,都没有找到任何对象有名为“madeUpPeoperty”的属性,因此最终返回 undefined

不论是在对象或对象的原型中,读取命名属性值的时候只返回首先找到的属性值。而当为对象的命名属性赋值时,如果对象自身不存在该属性则创建相应的属性。

这意味着,如果执行像 objectRef.testNumber = 3 这样一条赋值语句,那么这个 MyObject2 的实例自身也会创建一个名为“testNumber”的属性,而之后任何读取该命名属性的尝试都将获得相同的新值。这时候,属性访问器不会再进一步搜索原型链,但 MyObject1 实例值为 8 的“testNumber”属性并没有被修改。给 objectRef 对象的赋值只是遮挡了其原型链中相应的属性。

注意:ECMAScript 为 Object 类型定义了一个内部 [[prototype]] 属性。这个属性不能通过脚本直接访问,但在属性访问器解析过程中,则需要用到这个内部 [[prototype]] 属性所引用的对象链--即原型链。可以通过一个公共的 prototype 属性,来对与内部的 [[prototype]] 属性对应的原型对象进行赋值或定义。这两者之间的关系在 ECMA 262(3rd edition)中有详细描述,但超出了本文要讨论的范畴。

标识符解析、执行环境和作用域链

返回目录

执行环境是 ECMAScript 规范(ECMA 262 第 3 版)用于定义 ECMAScript 实现必要行为的一个抽象的概念。对如何实现执行环境,规范没有作规定。但由于执行环境中包含引用规范所定义结构的相关属性,因此执行环境中应该保有(甚至实现)带有属性的对象--即使属性不是公共属性。

所有 JavaScript 代码都是在一个执行环境中被执行的。全局代码(作为内置的JS 文件执行的代码,或者 HTML 页面加载的代码)是在我称之为“全局执行环境”的执行环境中执行的,而对函数的每次调用(
有可能是作为构造函数)同样有关联的执行环境。通过 eval 函数执行的代码也有截然不同的执行环境,但因为 JavaScript 程序员在正常情况下一般不会使用 eval,所以这里不作讨论。有关执行环境的详细说明请参阅 ECMA 262(3rd edition)第 10.2 节。

当调用一个 JavaScript 函数时,该函数就会进入相应的执行环境。如果又调用了另外一个函数(或者递归地调用同一个函数),则又会创建一个新的执行环境,并且在函数调用期间执行过程都处于该环境中。当调用的函数返回后,执行过程会返回原始执行环境。因而,运行中的 JavaScript 代码就构成了一个执行环境栈。

在创建执行环境的过程中,会按照定义的先后顺序完成一系列操作。首先,在一个函数的执行环境中,会创建一个“活动”对象。活动对象是规范中规定的另外一种机制。之所以称之为对象,是因为它拥有可访问的命名属性,但是它又不像正常对象那样具有原型(至少没有预定义的原型),而且不能通过 JavaScript 代码直接引用活动对象。

为函数调用创建执行环境的下一步是创建一个 arguments 对象,这是一个类似数组的对象,它以整数索引的数组成员一一对应地保存着调用函数时所传递的参数。这个对象也有 lengthcallee 属性(这两个属性与我们讨论的内容无关,详见规范)。然后,会为活动对象创建一个名为“arguments”的属性,该属性引用前面创建的 arguments对象。

接着,为执行环境分配作用域。作用域由对象列表(链)组成。每个函数对象都有一个内部的 [[scope]] 属性(该属性我们稍后会详细介绍),这个属性也由对象列表(链)组成。指定给一个函数调用执行环境的作用域,由该函数对象的 [[scope]] 属性所引用的对象列表(链)组成,同时,活动对象被添加到该对象列表的顶部(链的前端)。

之后会发生由 ECMA 262 中所谓“可变”对象完成的“变量实例化”的过程。只不过此时使用活动对象作为可变对象(这里很重要,请注意:它们是同一个对象)。此时会将函数的形式参数创建为可变对象的命名属性,如果调用函数时传递的参数与形式参数一致,则将相应参数的值赋给这些命名属性(否则,会给命名属性赋 undefined 值)。对于定义的内部函数,会以其声明时所用名称为可变对象创建同名属性,而相应的内部函数则被创建为函数对象并指定给该属性。变量实例化的最后一步是将在函数内部声明的所有局部变量创建为可变对象的命名属性。

根据声明的局部变量创建的可变对象的属性在变量实例化过程中会被赋予 undefined 值。在执行函数体内的代码、并计算相应的赋值表达式之前不会对局部变量执行真正的实例化。

事实上,拥有 arguments 属性的活动对象和拥有与函数局部变量对应的命名属性的可变对象是同一个对象。因此,可以将标识符 arguments 作为函数的局部变量来看待。

最后,要为使用 this 关键字而赋值。如果所赋的值引用一个对象,那么前缀以 this 关键字的属性访问器就是引用该对象的属性。如果所赋(内部)值是 null,那么 this 关键字则引用全局对象。

创建全局执行环境的过程会稍有不同,因为它没有参数,所以不需要通过定义的活动对象来引用这些参数。但全局执行环境也需要一个作用域,而它的作用域链实际上只由一个对象--全局对象--组成。全局执行环境也会有变量实例化的过程,它的内部函数就是涉及大部分 JavaScript 代码的、常规的顶级函数声明。而且,在变量实例化过程中全局对象就是可变对象,这就是为什么全局性声明的函数是全局对象属性的原因。全局性声明的变量同样如此。

全局执行环境也会使用 this 对象来引用全局对象。

作用域链与 [[scope]]

返回目录

调用函数时创建的执行环境会包含一个作用域链,这个作用域链是通过将该执行环境的活动(可变)对象添加到保存于所调用函数对象的 [[scope]] 属性中的作用域链前端而构成的。所以,理解函数对象内部的 [[scope]] 属性的定义过程至关重要。

在 ECMAScript 中,函数也是对象。函数对象在变量实例化过程中会根据函数声明来创建,或者是在计算函数表达式或调用 Function 构造函数时创建。

通过调用 Function 构造函数创建的函数对象,其内部的 [[scope]] 属性引用的作用域链中始终只包含全局对象。

通过函数声明或函数表达式创建的函数对象,其内部的 [[scope]] 属性引用的则是创建它们的执行环境的作用域链。

在最简单的情况下,比如声明如下全局函数:-

function exampleFunction(formalParameter){
… // 函数体内的代码
}

– 当为创建全局执行环境而进行变量实例化时,会根据上面的函数声明创建相应的函数对象。因为全局执行环境的作用域链中只包含全局对象,所以它就给自己创建的、并以名为“exampleFunction”的属性引用的这个函数对象的内部 [[scope]] 属性,赋予了只包含全局对象的作用域链。

当在全局环境中计算函数表达式时,也会发生类似的指定作用域链的过程:-

var exampleFuncRef = function(){
… // 函数体代码
}

在这种情况下,不同的是在全局执行环境的变量实例化过程中,会先为全局对象创建一个命名属性。而在计算赋值语句之前,暂时不会创建函数对象,也不会将该函数对象的引用指定给全局对象的命名属性。但是,最终还是会在全局执行环境中创建这个函数对象(当计算函数表达式时。译者注),而为这个创建的函数对象的 [[scope]] 属性指定的作用域链中仍然只包含全局对象。内部的函数声明或表达式会导致在包含它们的外部函数的执行环境中创建相应的函数对象,因此这些函数对象的作用域链会稍微复杂一些。在下面的代码中,先定义了一个带有内部函数声明的外部函数,然后调用外部函数:

 /* 创建全局变量 - y - 它引用一个对象:- */
var y = {x:5}; // 带有一个属性 - x - 的对象直接量
function exampleFuncWith(){
  var z;
  /* 将全局对象 - y - 引用的对象添加到作用域链的前端:- */
  with(y){
  /* 对函数表达式求值,以创建函数对象并将该函数对象的引用指定给局部变量 - z - :- */
  z = function(){
  ... // 内部函数表达式中的代码;
  }
}
...
}
/* 执行 - exampleFuncWith - 函数:- */

exampleFuncWith();在调用 exampleFuncWith 函数创建的执行环境中包含一个由其活动对象后跟全局对象构成的作用域链。而在执行 with 语句时,又会把全局变量 y 引用的对象添加到这个作用域链的前端。在对其中的函数表达式求值的过程中,所创建函数对象的 [[scope]] 属性与创建它的执行环境的作用域保持一致--即,该属性会引用一个由对象 y 后跟调用外部函数时所创建执行环境的活动对象,后跟全局对象的作用域链。

当与 with 语句相关的语句块执行结束时,执行环境的作用域得以恢复(y 会被移除),但是已经创建的函数对象(z。译者注)的 [[scope]] 属性所引用的作用域链中位于最前面的仍然是对象 y

例 3:包装相关的功能

返回目录

闭包可以用于创建额外的作用域,通过该作用域可以将相关的和具有依赖性的代码组织起来,以便将意外交互的风险降到最低。假设有一个用于构建字符串的函数,为了避免重复性的连接操作(和创建众多的中间字符串),我们的愿望是使用一个数组按顺序来存储字符串的各个部分,然后再使用 Array.prototype.join 方法(以空字符串作为其参数)输出结果。这个数组将作为输出的缓冲器,但是将数组作为函数的局部变量又会导致在每次调用函数时都重新创建一个新数组,这在每次调用函数时只重新指定数组中的可变内容的情况下并不是必要的。

一种解决方案是将这个数组声明为全局变量,这样就可以重用这个数组,而不必每次都建立新数组。但这个方案的结果是,除了引用函数的全局变量会使用这个缓冲数组外,还会多出一个全局属性引用数组自身。如此不仅使代码变得不容易管理,而且,如果要在其他地方使用这个数组时,开发者必须要再次定义函数和数组。这样一来,也使得代码不容易与其他代码整合,因为此时不仅要保证所使用的函数名在全局命名空间中是唯一的,而且还要保证函数所依赖的数组在全局命名空间中也必须是唯一的。

而通过闭包可以使作为缓冲器的数组与依赖它的函数关联起来(优雅地打包),同时也能够维持在全局命名空间外指定的缓冲数组的属性名,免除了名称冲突和意外交互的危险。

其中的关键技巧在于通过执行一个单行(in-line)函数表达式创建一个额外的执行环境,而将该函数表达式返回的内部函数作为在外部代码中使用的函数。此时,缓冲数组被定义为函数表达式的一个局部变量。这个函数表达式只需执行一次,而数组也只需创建一次,就可以供依赖它的函数重复使用。

下面的代码定义了一个函数,这个函数用于返回一个 HTML 字符串,其中大部分内容都是常量,但这些常量字符序列中需要穿插一些可变的信息,而可变的信息由调用函数时传递的参数提供。

通过执行单行函数表达式返回一个内部函数,并将返回的函数赋给一个全局变量,因此这个函数也可以称为全局函数。而缓冲数组被定义为外部函数表达式的一个局部变量。它不会暴露在全局命名空间中,而且无论什么时候调用依赖它的函数都不需要重新创建这个数组。

/* 声明一个全局变量 - getImgInPositionedDivHtml -
并将一次调用一个外部函数表达式返回的内部函数赋给它。      

   这个内部函数会返回一个用于表示绝对定位的 DIV 元素
   包围着一个 IMG 元素 的 HTML 字符串,这样一来,
   所有可变的属性值都由调用该函数时的参数提供:
*/
var getImgInPositionedDivHtml = (function(){
    /* 外部函数表达式的局部变量 - buffAr - 保存着缓冲数组。
     这个数组只会被创建一次,生成的数组实例对内部函数而言永远是可用的
     因此,可供每次调用这个内部函数时使用。      

    其中的空字符串用作数据占位符,相应的数据
    将由内部函数插入到这个数组中:
    */
    var buffAr = [
        '<div id="',
        '',   //index 1, DIV ID 属性
        '" style="position:absolute;top:',
        '',   //index 3, DIV 顶部位置
        'px;left:',
        '',   //index 5, DIV 左端位置
        'px;width:',
        '',   //index 7, DIV 宽度
        'px;height:',
        '',   //index 9, DIV 高度
        'px;overflow:hidden;\"><img src=\"',
        '',   //index 11, IMG URL
        '\" width=\"',
        '',   //index 13, IMG 宽度
        '\" height=\"',
        '',   //index 15, IMG 高度
        '\" alt=\"',
        '',   //index 17, IMG alt 文本内容
        '\"></div>'
    ];
    /* 返回作为对函数表达式求值后结果的内部函数对象。
     这个内部函数就是每次调用执行的函数
	- getImgInPositionedDivHtml( ... ) -
    */
    return (function(url, id, width, height, top, left, altText){
        /* 将不同的参数插入到缓冲数组相应的位置:*/
        buffAr[1] = id;
        buffAr[3] = top;
        buffAr[5] = left;
        buffAr[13] = (buffAr[7] = width);
        buffAr[15] = (buffAr[9] = height);
        buffAr[11] = url;
        buffAr[17] = altText;
        /* 返回通过使用空字符串(相当于将数组元素连接起来)
	连接数组每个元素后形成的字符串:
        */
        return buffAr.join('');
    }); //:内部函数表达式结束。
})();
/*^^- :单行外部函数表达式。*/

如果一个函数依赖于另一(或多)个其他函数,而其他函数又没有必要被其他代码直接调用,那么可以运用相同的技术来包装这些函数,而通过一个公开暴露的函数来调用它们。这样,就将一个复杂的多函数处理过程封装成了一个具有移植性的代码单元。

有关闭包的一个可能是最广为人知的应用是 Douglas Crockford’s technique for the emulation of private instance variables in ECMAScript objects 。这种应用方式可以扩展到各种嵌套包含的可访问性(或可见性)的作用域结构,包括 the emulation of private static members for ECMAScript objects

闭包可能的用途是无限的,可能理解其工作原理才是把握如何使用它的最好指南。

意外的闭包

返回目录

在创建可访问的内部函数的函数体之外解析该内部函数就会构成闭包。这表明闭包很容易创建,但这样一来可能会导致一种结果,即没有认识到闭包是一种语言特性的 JavaScript 作者,会按照内部函数能完成多种任务的想法来使用内部函数。但他们对使用内部函数的结果并不明了,而且根本意识不到创建了闭包,或者那样做意味着什么。

正如下一节谈到 IE 中内存泄漏问题时所提及的,意外创建的闭包可能导致严重的负面效应,而且也会影响到代码的性能。问题不在于闭包本身,如果能够真正做到谨慎地使用它们,反而会有助于创建高效的代码。换句话说,使用内部函数会影响到效率。

使用内部函数最常见的一种情况就是将其作为 DOM 元素的事件处理器。例如,下面的代码用于向一个链接元素添加 onclick 事件处理器:

/* 定义一个全局变量,通过下面的函数将它的值
   作为查询字符串的一部分添加到链接的 - href - 中:
*/
var quantaty = 5;
/* 当给这个函数传递一个链接(作为函数中的参数 - linkRef -)时,
   会将一个 onclick 事件处理器指定给该链接,该事件处理器
   将全局变量 - quantaty - 的值作为字符串添加到链接的 - href -
   属性中,然后返回 true 使该链接在单击后定位到由  - href -
   属性包含的查询字符串指定的资源:
*/
function addGlobalQueryOnClick(linkRef){
    /* 如果可以将参数 - linkRef - 通过类型转换为 ture
      (说明它引用了一个对象):
    */
    if(linkRef){
        /* 对一个函数表达式求值,并将对该函数对象的引用
           指定给这个链接元素的 onclick 事件处理器:
        */
        linkRef.onclick = function(){
            /* 这个内部函数表达式将查询字符串
               添加到附加事件处理器的元素的 - href - 属性中:
            */
            this.href += ('?quantaty='+escape(quantaty));
            return true;
        };
    }
}

无论什么时候调用 addGlobalQueryOnClick 函数,都会创建一个新的内部函数(通过赋值构成了闭包)。从效率的角度上看,如果只是调用一两次 addGlobalQueryOnClick 函数并没有什么大的妨碍,但如果频繁使用该函数,就会导致创建许多截然不同的函数对象(每对内部函数表达式求一次值,就会产生一个新的函数对象)。

上面例子中的代码没有关注内部函数在创建它的函数外部可以访问(或者说构成了闭包)这一事实。实际上,同样的效果可以通过另一种方式来完成。即单独地定义一个用于事件处理器的函数,然后将该函数的引用指定给元素的事件处理属性。这样,只需创建一个函数对象,而所有使用相同事件处理器的元素都可以共享对这个函数的引用:

/* 定义一个全局变量,通过下面的函数将它的值
   作为查询字符串的一部分添加到链接的 - href - 中:
*/
var quantaty = 5;
/* 当把一个链接(作为函数中的参数 - linkRef -)传递给这个函数时,
   会给这个链接添加一个 onclick 事件处理器,该事件处理器会
   将全局变量  - quantaty - 的值作为查询字符串的一部分添加到
   链接的 - href -  中,然后返回 true,以便单击链接时定位到由
   作为 - href - 属性值的查询字符串所指定的资源:
*/
function addGlobalQueryOnClick(linkRef){
    /* 如果 - linkRef - 参数能够通过类型转换为 true
    (说明它引用了一个对象):
    */
    if(linkRef){
        /* 将一个对全局函数的引用指定给这个链接
           的事件处理属性,使函数成为链接元素的事件处理器:
        */
        linkRef.onclick = forAddQueryOnClick;
    }
}
/* 声明一个全局函数,作为链接元素的事件处理器,
   这个函数将一个全局变量的值作为要添加事件处理器的
   链接元素的  - href - 值的一部分:
*/
function forAddQueryOnClick(){
    this.href += ('?quantaty='+escape(quantaty));
    return true;
}

在上面例子的第一个版本中,内部函数并没有作为闭包发挥应有的作用。在那种情况下,反而是不使用闭包更有效率,因为不用重复创建许多本质上相同的函数对象。

类似地考量同样适用于对象的构造函数。与下面代码中的构造函数框架类似的代码并不罕见:

function ExampleConst(param){
    /* 通过对函数表达式求值创建对象的方法,
      并将求值所得的函数对象的引用赋给要创建对象的属性:
    */
    this.method1 = function(){
        ... // 方法体。
    };
    this.method2 = function(){
        ... // 方法体。
    };
    this.method3 = function(){
        ... // 方法体。
    };
    /* 把构造函数的参数赋给对象的一个属性:*/
    this.publicProp = param;
}

每当通过 new ExampleConst(n) 使用这个构造函数创建一个对象时,都会创建一组新的、作为对象方法的函数对象。因此,创建的对象实例越多,相应的函数对象也就越多。

Douglas Crockford 提出的模仿 JavaScript 对象私有成员的技术,就利用了将对内部函数的引用指定给在构造函数中构造对象的公共属性而形成的闭包。如果对象的方法没有利用在构造函数中形成的闭包,那么在实例化每个对象时创建的多个函数对象,会使实例化过程变慢,而且将有更多的资源被占用,以满足创建更多函数对象的需要。

这那种情况下,只创建一次函数对象,并把它们指定给构造函数 prototype 的相应属性显然更有效率。这样一来,它们就能被构造函数创建的所有对象共享了:

function ExampleConst(param){
    /* 将构造函数的参数赋给对象的一个属性:*/
    this.publicProp = param;
}
/* 通过对函数表达式求值,并将结果函数对象的引用
      指定给构造函数原型的相应属性来创建对象的方法:
*/
ExampleConst.prototype.method1 = function(){
    ... // 方法体。
};
ExampleConst.prototype.method2 = function(){
    ... // 方法体。
};
ExampleConst.prototype.method3 = function(){
    ... // 方法体。
};

Internet Explorer 的内存泄漏问题

返回目录

Internet Explorer Web 浏览器(在 IE 4 到 IE 6 中核实)的垃圾收集系统中存在一个问题,即如果 ECMAScript 和某些宿主对象构成了 “循环引用”,那么这些对象将不会被当作垃圾收集。此时所谓的宿主对象指的是任何 DOM 节点(包括 document 对象及其后代元素)和 ActiveX 对象。如果在一个循环引用中包含了一或多个这样的对象,那么这些对象直到浏览器关闭都不会被释放,而它们所占用的内存同样在浏览器关闭之前都不会交回系统重用。

当两个或多个对象以首尾相连的方式相互引用时,就构成了循环引用。比如对象 1 的一个属性引用了对象 2 ,对象 2 的一个属性引用了对象 3,而对象 3 的一个属性又引用了对象 1。对于纯粹的 ECMAScript 对象而言,只要没有其他对象引用对象 1、2、3,也就是说它们只是相互之间的引用,那么仍然会被垃圾收集系统识别并处理。但是,在 Internet Explorer 中,如果循环引用中的任何对象是 DOM 节点或者 ActiveX 对象,垃圾收集系统则不会发现它们之间的循环关系与系统中的其他对象是隔离的并释放它们。最终它们将被保留在内存中,直到浏览器关闭。

闭包非常容易构成循环引用。如果一个构成闭包的函数对象被指定给,比如一个 DOM 节点的事件处理器,而对该节点的引用又被指定给函数对象作用域中的一个活动(或可变)对象,那么就存在一个循环引用。DOM_Node.onevent ->function_object.[[scope]] ->scope_chain ->Activation_object.nodeRef ->DOM_Node。形成这样一个循环引用是轻而易举的,而且稍微浏览一下包含类似循环引用代码的网站(通常会出现在网站的每个页面中),就会消耗大量(甚至全部)系统内存。

多加注意可以避免形成循环引用,而在无法避免时,也可以使用补偿的方法,比如使用 IE 的 onunload 事件来来清空(null)事件处理函数的引用。时刻意识到这个问题并理解闭包的工作机制是在 IE 中避免此类问题的关键。

comp.lang.javascript FAQ notes T.O.C.

  • 撰稿 Richard Cornford,2004 年 3 月
  • 修改建议来自:
    • Martin Honnen.
    • Yann-Erwan Perio (Yep).
    • Lasse Reichstein Nielsen. (definition of closure)
    • Mike Scirocco.
    • Dr John Stockton.

理解 JavaScript 闭包 » 为之漫笔

理解 JavaScript 闭包 » 为之漫笔

要成为高级 JavaScript 程序员,就必须理解闭包。

本文结合 ECMA 262 规范详解了闭包的内部工作机制,让 JavaScript 编程人员对闭包的理解从“嵌套的函数”深入到“标识符解析、执行环境和作用域链”等等 JavaScript 对象背后的运行机制当中,真正领会到闭包的实质。

原文链接:JavaScript Closures

可打印版:JavaScript 闭包

返回目录

Closure所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。 “闭包”是一个表达式(一般是函数),它具有自由变量以及绑定这些变量的环境(该环境“封闭了”这个表达式)。(闭包,就是封闭了外部函数作用域中变量的内部函数。但是,如果外部函数不返回这个内部函数,闭包的特性无法显现。如果外部函数返回这个内部函数,那么返回的内部函数就成了名副其实的闭包。此时,闭包封闭的外部变量就是自由变量,而由于该自由变量存在,外部函数即便返回,其占用的内存也得不到释放。——译者注,2010年4月3日)

闭包是 ECMAScript (JavaScript)最强大的特性之一,但用好闭包的前提是必须理解闭包。闭包的创建相对容易,人们甚至会在不经意间创建闭包,但这些无意创建的闭包却存在潜在的危害,尤其是在比较常见的浏览器环境下。如果想要扬长避短地使用闭包这一特性,则必须了解它们的工作机制。而闭包工作机制的实现很大程度上有赖于标识符(或者说对象属性)解析过程中作用域的角色。

关于闭包,最简单的描述就是 ECMAScript 允许使用内部函数--即函数定义和函数表达式位于另一个函数的函数体内。而且,这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。也就是说,内部函数会在外部函数返回后被执行。而当这个内部函数执行时,它仍然必需访问其外部函数的局部变量、参数以及其他内部函数。这些局部变量、参数和函数声明(最初时)的值是外部函数返回时的值,但也会受到内部函数的影响。

遗憾的是,要适当地理解闭包就必须理解闭包背后运行的机制,以及许多相关的技术细节。虽然本文的前半部分并没有涉及 ECMA 262 规范指定的某些算法,但仍然有许多无法回避或简化的内容。对于个别熟悉对象属性名解析的人来说,可以跳过相关的内容,但是除非你对闭包也非常熟悉,否则最好是不要跳下面几节。

对象属性名解析

返回目录

ECMAScript 认可两类对象:原生(Native)对象和宿主(Host)对象,其中宿主对象包含一个被称为内置对象的原生对象的子类(ECMA 262 3rd Ed Section 4.3)。原生对象属于语言,而宿主对象由环境提供,比如说可能是文档对象、DOM 等类似的对象。

原生对象具有松散和动态的命名属性(对于某些实现的内置对象子类别而言,动态性是受限的--但这不是太大的问题)。对象的命名属性用于保存值,该值可以是指向另一个对象(Objects)的引用(在这个意义上说,函数也是对象),也可以是一些基本的数据类型,比如:String、Number、Boolean、Null 或 Undefined。其中比较特殊的是 Undefined 类型,因为可以给对象的属性指定一个 Undefined 类型的值,而不会删除对象的相应属性。而且,该属性只是保存着 undefined 值。 

下面简要介绍一下如何设置和读取对象的属性值,并最大程度地体现相应的内部细节。

返回目录

对象的命名属性可以通过为该命名属性赋值来创建,或重新赋值。即,对于:

var objectRef = new Object(); //创建一个普通的 JavaScript 对象。

可以通过下面语句来创建名为 “testNumber” 的属性:

objectRef.testNumber = 5;
/* – 或- */
objectRef["testNumber"] = 5;

在赋值之前,对象中没有“testNumber” 属性,但在赋值后,则创建一个属性。之后的任何赋值语句都不需要再创建这个属性,而只会重新设置它的值:

objectRef.testNumber = 8;
/* – or:- */
objectRef["testNumber"] = 8;

稍后我们会介绍,Javascript 对象都有原型(prototypes)属性,而这些原型本身也是对象,因而也可以带有命名的属性。但是,原型对象命名属性的作用并不体现在赋值阶段。同样,在将值赋给其命名属性时,如果对象没有该属性则会创建该命名属性,否则会重设该属性的值。

返回目录

当读取对象的属性值时,原型对象的作用便体现出来。如果对象的原型中包含属性访问器(property accessor)所使用的属性名,那么该属性的值就会返回:

/* 为命名属性赋值。如果在赋值前对象没有相应的属性,那么赋值后就会得到一个:*/
objectRef.testNumber = 8;

/* 从属性中读取值 */
var val = objectRef.testNumber;

/* 现在, – val – 中保存着刚赋给对象命名属性的值 8*/

而且,由于所有对象都有原型,而原型本身也是对象,所以原型也可能有原型,这样就构成了所谓的原型链。原型链终止于链中原型为 null 的对象。Object 构造函数的默认原型就有一个 null 原型,因此:

var objectRef = new Object(); //创建一个普通的 JavaScript 对象。

创建了一个原型为 Object.prototype 的对象,而该原型自身则拥有一个值为 null 的原型。也就是说, objectRef 的原型链中只包含一个对象-- Object.prototype。但对于下面的代码而言:

/* 创建 – MyObject1 – 类型对象的函数*/
function MyObject1(formalParameter){
/* 给创建的对象添加一个名为 – testNumber – 的属性
并将传递给构造函数的第一个参数指定为该属性的值:*/
this.testNumber = formalParameter;
}
/* 创建 – MyObject2 – 类型对象的函数*/
function MyObject2(formalParameter){
/* 给创建的对象添加一个名为 – testString – 的属性
并将传递给构造函数的第一个参数指定为该属性的值:*/
this.testString = formalParameter;
}

/* 接下来的操作用 MyObject1 类的实例替换了所有与 MyObject2 类的实例相关联的原型。而且,为 MyObject1 构造函数传递了参数 – 8 – ,因而其 – testNumber – 属性被赋予该值:*/
MyObject2.prototype = new MyObject1( 8 );

/* 最后,将一个字符串作为构造函数的第一个参数,创建一个 – MyObject2 – 的实例,并将指向该对象的引用赋给变量 – objectRef – :*/
var objectRef = new MyObject2( “String_Value” );

被变量 objectRef 所引用的 MyObject2 的实例拥有一个原型链。该链中的第一个对象是在创建后被指定给 MyObject2 构造函数的 prototype 属性的 MyObject1 的一个实例。MyObject1 的实例也有一个原型,即与 Object.prototype 所引用的对象对应的默认的 Object 对象的原型。最后, Object.prototype 有一个值为 null 的原型,因此这条原型链到此结束。

当某个属性访问器尝试读取由 objectRef 所引用的对象的属性值时,整个原型链都会被搜索。在下面这种简单的情况下:

var val = objectRef.testString;

因为 objectRef 所引用的 MyObject2 的实例有一个名为“testString”的属性,因此被设置为“String_Value”的该属性的值被赋给了变量 val。但是:

var val = objectRef.testNumber;

则不能从 MyObject2 实例自身中读取到相应的命名属性值,因为该实例没有这个属性。然而,变量 val 的值仍然被设置为 8,而不是未定义--这是因为在该实例中查找相应的命名属性失败后,解释程序会继续检查其原型对象。而该实例的原型对象是 MyObject1 的实例,这个实例有一个名为“testNumber”的属性并且值为 8,所以这个属性访问器最后会取得值 8。而且,虽然 MyObject1MyObject2 都没有定义 toString 方法,但是当属性访问器通过 objectRef 读取 toString 属性的值时:

var val = objectRef.toString;

变量 val 也会被赋予一个函数的引用。这个函数就是在 Object.prototypetoString 属性中所保存的函数。之所以会返回这个函数,是因为发生了搜索 objectRef 原型链的过程。当在作为对象的 objectRef 中发现没有“toString”属性存在时,会搜索其原型对象,而当原型对象中不存在该属性时,则会继续搜索原型的原型。而原型链中最终的原型是 Object.prototype,这个对象确实有一个 toString 方法,因此该方法的引用被返回。

最后:

var val = objectRef.madeUpProperty;

返回 undefined,因为在搜索原型链的过程中,直至 Object.prototype 的原型--null,都没有找到任何对象有名为“madeUpPeoperty”的属性,因此最终返回 undefined

不论是在对象或对象的原型中,读取命名属性值的时候只返回首先找到的属性值。而当为对象的命名属性赋值时,如果对象自身不存在该属性则创建相应的属性。

这意味着,如果执行像 objectRef.testNumber = 3 这样一条赋值语句,那么这个 MyObject2 的实例自身也会创建一个名为“testNumber”的属性,而之后任何读取该命名属性的尝试都将获得相同的新值。这时候,属性访问器不会再进一步搜索原型链,但 MyObject1 实例值为 8 的“testNumber”属性并没有被修改。给 objectRef 对象的赋值只是遮挡了其原型链中相应的属性。

注意:ECMAScript 为 Object 类型定义了一个内部 [[prototype]] 属性。这个属性不能通过脚本直接访问,但在属性访问器解析过程中,则需要用到这个内部 [[prototype]] 属性所引用的对象链--即原型链。可以通过一个公共的 prototype 属性,来对与内部的 [[prototype]] 属性对应的原型对象进行赋值或定义。这两者之间的关系在 ECMA 262(3rd edition)中有详细描述,但超出了本文要讨论的范畴。

标识符解析、执行环境和作用域链

返回目录

执行环境是 ECMAScript 规范(ECMA 262 第 3 版)用于定义 ECMAScript 实现必要行为的一个抽象的概念。对如何实现执行环境,规范没有作规定。但由于执行环境中包含引用规范所定义结构的相关属性,因此执行环境中应该保有(甚至实现)带有属性的对象--即使属性不是公共属性。

所有 JavaScript 代码都是在一个执行环境中被执行的。全局代码(作为内置的JS 文件执行的代码,或者 HTML 页面加载的代码)是在我称之为“全局执行环境”的执行环境中执行的,而对函数的每次调用(
有可能是作为构造函数)同样有关联的执行环境。通过 eval 函数执行的代码也有截然不同的执行环境,但因为 JavaScript 程序员在正常情况下一般不会使用 eval,所以这里不作讨论。有关执行环境的详细说明请参阅 ECMA 262(3rd edition)第 10.2 节。

当调用一个 JavaScript 函数时,该函数就会进入相应的执行环境。如果又调用了另外一个函数(或者递归地调用同一个函数),则又会创建一个新的执行环境,并且在函数调用期间执行过程都处于该环境中。当调用的函数返回后,执行过程会返回原始执行环境。因而,运行中的 JavaScript 代码就构成了一个执行环境栈。

在创建执行环境的过程中,会按照定义的先后顺序完成一系列操作。首先,在一个函数的执行环境中,会创建一个“活动”对象。活动对象是规范中规定的另外一种机制。之所以称之为对象,是因为它拥有可访问的命名属性,但是它又不像正常对象那样具有原型(至少没有预定义的原型),而且不能通过 JavaScript 代码直接引用活动对象。

为函数调用创建执行环境的下一步是创建一个 arguments 对象,这是一个类似数组的对象,它以整数索引的数组成员一一对应地保存着调用函数时所传递的参数。这个对象也有 lengthcallee 属性(这两个属性与我们讨论的内容无关,详见规范)。然后,会为活动对象创建一个名为“arguments”的属性,该属性引用前面创建的 arguments对象。

接着,为执行环境分配作用域。作用域由对象列表(链)组成。每个函数对象都有一个内部的 [[scope]] 属性(该属性我们稍后会详细介绍),这个属性也由对象列表(链)组成。指定给一个函数调用执行环境的作用域,由该函数对象的 [[scope]] 属性所引用的对象列表(链)组成,同时,活动对象被添加到该对象列表的顶部(链的前端)。

之后会发生由 ECMA 262 中所谓“可变”对象完成的“变量实例化”的过程。只不过此时使用活动对象作为可变对象(这里很重要,请注意:它们是同一个对象)。此时会将函数的形式参数创建为可变对象的命名属性,如果调用函数时传递的参数与形式参数一致,则将相应参数的值赋给这些命名属性(否则,会给命名属性赋 undefined 值)。对于定义的内部函数,会以其声明时所用名称为可变对象创建同名属性,而相应的内部函数则被创建为函数对象并指定给该属性。变量实例化的最后一步是将在函数内部声明的所有局部变量创建为可变对象的命名属性。

根据声明的局部变量创建的可变对象的属性在变量实例化过程中会被赋予 undefined 值。在执行函数体内的代码、并计算相应的赋值表达式之前不会对局部变量执行真正的实例化。

事实上,拥有 arguments 属性的活动对象和拥有与函数局部变量对应的命名属性的可变对象是同一个对象。因此,可以将标识符 arguments 作为函数的局部变量来看待。

最后,要为使用 this 关键字而赋值。如果所赋的值引用一个对象,那么前缀以 this 关键字的属性访问器就是引用该对象的属性。如果所赋(内部)值是 null,那么 this 关键字则引用全局对象。

创建全局执行环境的过程会稍有不同,因为它没有参数,所以不需要通过定义的活动对象来引用这些参数。但全局执行环境也需要一个作用域,而它的作用域链实际上只由一个对象--全局对象--组成。全局执行环境也会有变量实例化的过程,它的内部函数就是涉及大部分 JavaScript 代码的、常规的顶级函数声明。而且,在变量实例化过程中全局对象就是可变对象,这就是为什么全局性声明的函数是全局对象属性的原因。全局性声明的变量同样如此。

全局执行环境也会使用 this 对象来引用全局对象。

作用域链与 [[scope]]

返回目录

调用函数时创建的执行环境会包含一个作用域链,这个作用域链是通过将该执行环境的活动(可变)对象添加到保存于所调用函数对象的 [[scope]] 属性中的作用域链前端而构成的。所以,理解函数对象内部的 [[scope]] 属性的定义过程至关重要。

在 ECMAScript 中,函数也是对象。函数对象在变量实例化过程中会根据函数声明来创建,或者是在计算函数表达式或调用 Function 构造函数时创建。

通过调用 Function 构造函数创建的函数对象,其内部的 [[scope]] 属性引用的作用域链中始终只包含全局对象。

通过函数声明或函数表达式创建的函数对象,其内部的 [[scope]] 属性引用的则是创建它们的执行环境的作用域链。

在最简单的情况下,比如声明如下全局函数:-

function exampleFunction(formalParameter){
… // 函数体内的代码
}

– 当为创建全局执行环境而进行变量实例化时,会根据上面的函数声明创建相应的函数对象。因为全局执行环境的作用域链中只包含全局对象,所以它就给自己创建的、并以名为“exampleFunction”的属性引用的这个函数对象的内部 [[scope]] 属性,赋予了只包含全局对象的作用域链。

当在全局环境中计算函数表达式时,也会发生类似的指定作用域链的过程:-

var exampleFuncRef = function(){
… // 函数体代码
}

在这种情况下,不同的是在全局执行环境的变量实例化过程中,会先为全局对象创建一个命名属性。而在计算赋值语句之前,暂时不会创建函数对象,也不会将该函数对象的引用指定给全局对象的命名属性。但是,最终还是会在全局执行环境中创建这个函数对象(当计算函数表达式时。译者注),而为这个创建的函数对象的 [[scope]] 属性指定的作用域链中仍然只包含全局对象。内部的函数声明或表达式会导致在包含它们的外部函数的执行环境中创建相应的函数对象,因此这些函数对象的作用域链会稍微复杂一些。在下面的代码中,先定义了一个带有内部函数声明的外部函数,然后调用外部函数:

 /* 创建全局变量 - y - 它引用一个对象:- */
var y = {x:5}; // 带有一个属性 - x - 的对象直接量
function exampleFuncWith(){
  var z;
  /* 将全局对象 - y - 引用的对象添加到作用域链的前端:- */
  with(y){
  /* 对函数表达式求值,以创建函数对象并将该函数对象的引用指定给局部变量 - z - :- */
  z = function(){
  ... // 内部函数表达式中的代码;
  }
}
...
}
/* 执行 - exampleFuncWith - 函数:- */

exampleFuncWith();在调用 exampleFuncWith 函数创建的执行环境中包含一个由其活动对象后跟全局对象构成的作用域链。而在执行 with 语句时,又会把全局变量 y 引用的对象添加到这个作用域链的前端。在对其中的函数表达式求值的过程中,所创建函数对象的 [[scope]] 属性与创建它的执行环境的作用域保持一致--即,该属性会引用一个由对象 y 后跟调用外部函数时所创建执行环境的活动对象,后跟全局对象的作用域链。

当与 with 语句相关的语句块执行结束时,执行环境的作用域得以恢复(y 会被移除),但是已经创建的函数对象(z。译者注)的 [[scope]] 属性所引用的作用域链中位于最前面的仍然是对象 y

例 3:包装相关的功能

返回目录

闭包可以用于创建额外的作用域,通过该作用域可以将相关的和具有依赖性的代码组织起来,以便将意外交互的风险降到最低。假设有一个用于构建字符串的函数,为了避免重复性的连接操作(和创建众多的中间字符串),我们的愿望是使用一个数组按顺序来存储字符串的各个部分,然后再使用 Array.prototype.join 方法(以空字符串作为其参数)输出结果。这个数组将作为输出的缓冲器,但是将数组作为函数的局部变量又会导致在每次调用函数时都重新创建一个新数组,这在每次调用函数时只重新指定数组中的可变内容的情况下并不是必要的。

一种解决方案是将这个数组声明为全局变量,这样就可以重用这个数组,而不必每次都建立新数组。但这个方案的结果是,除了引用函数的全局变量会使用这个缓冲数组外,还会多出一个全局属性引用数组自身。如此不仅使代码变得不容易管理,而且,如果要在其他地方使用这个数组时,开发者必须要再次定义函数和数组。这样一来,也使得代码不容易与其他代码整合,因为此时不仅要保证所使用的函数名在全局命名空间中是唯一的,而且还要保证函数所依赖的数组在全局命名空间中也必须是唯一的。

而通过闭包可以使作为缓冲器的数组与依赖它的函数关联起来(优雅地打包),同时也能够维持在全局命名空间外指定的缓冲数组的属性名,免除了名称冲突和意外交互的危险。

其中的关键技巧在于通过执行一个单行(in-line)函数表达式创建一个额外的执行环境,而将该函数表达式返回的内部函数作为在外部代码中使用的函数。此时,缓冲数组被定义为函数表达式的一个局部变量。这个函数表达式只需执行一次,而数组也只需创建一次,就可以供依赖它的函数重复使用。

下面的代码定义了一个函数,这个函数用于返回一个 HTML 字符串,其中大部分内容都是常量,但这些常量字符序列中需要穿插一些可变的信息,而可变的信息由调用函数时传递的参数提供。

通过执行单行函数表达式返回一个内部函数,并将返回的函数赋给一个全局变量,因此这个函数也可以称为全局函数。而缓冲数组被定义为外部函数表达式的一个局部变量。它不会暴露在全局命名空间中,而且无论什么时候调用依赖它的函数都不需要重新创建这个数组。

/* 声明一个全局变量 - getImgInPositionedDivHtml -
并将一次调用一个外部函数表达式返回的内部函数赋给它。      

   这个内部函数会返回一个用于表示绝对定位的 DIV 元素
   包围着一个 IMG 元素 的 HTML 字符串,这样一来,
   所有可变的属性值都由调用该函数时的参数提供:
*/
var getImgInPositionedDivHtml = (function(){
    /* 外部函数表达式的局部变量 - buffAr - 保存着缓冲数组。
     这个数组只会被创建一次,生成的数组实例对内部函数而言永远是可用的
     因此,可供每次调用这个内部函数时使用。      

    其中的空字符串用作数据占位符,相应的数据
    将由内部函数插入到这个数组中:
    */
    var buffAr = [
        '<div id="',
        '',   //index 1, DIV ID 属性
        '" style="position:absolute;top:',
        '',   //index 3, DIV 顶部位置
        'px;left:',
        '',   //index 5, DIV 左端位置
        'px;width:',
        '',   //index 7, DIV 宽度
        'px;height:',
        '',   //index 9, DIV 高度
        'px;overflow:hidden;\"><img src=\"',
        '',   //index 11, IMG URL
        '\" width=\"',
        '',   //index 13, IMG 宽度
        '\" height=\"',
        '',   //index 15, IMG 高度
        '\" alt=\"',
        '',   //index 17, IMG alt 文本内容
        '\"></div>'
    ];
    /* 返回作为对函数表达式求值后结果的内部函数对象。
     这个内部函数就是每次调用执行的函数
	- getImgInPositionedDivHtml( ... ) -
    */
    return (function(url, id, width, height, top, left, altText){
        /* 将不同的参数插入到缓冲数组相应的位置:*/
        buffAr[1] = id;
        buffAr[3] = top;
        buffAr[5] = left;
        buffAr[13] = (buffAr[7] = width);
        buffAr[15] = (buffAr[9] = height);
        buffAr[11] = url;
        buffAr[17] = altText;
        /* 返回通过使用空字符串(相当于将数组元素连接起来)
	连接数组每个元素后形成的字符串:
        */
        return buffAr.join('');
    }); //:内部函数表达式结束。
})();
/*^^- :单行外部函数表达式。*/

如果一个函数依赖于另一(或多)个其他函数,而其他函数又没有必要被其他代码直接调用,那么可以运用相同的技术来包装这些函数,而通过一个公开暴露的函数来调用它们。这样,就将一个复杂的多函数处理过程封装成了一个具有移植性的代码单元。

有关闭包的一个可能是最广为人知的应用是 Douglas Crockford’s technique for the emulation of private instance variables in ECMAScript objects 。这种应用方式可以扩展到各种嵌套包含的可访问性(或可见性)的作用域结构,包括 the emulation of private static members for ECMAScript objects

闭包可能的用途是无限的,可能理解其工作原理才是把握如何使用它的最好指南。

意外的闭包

返回目录

在创建可访问的内部函数的函数体之外解析该内部函数就会构成闭包。这表明闭包很容易创建,但这样一来可能会导致一种结果,即没有认识到闭包是一种语言特性的 JavaScript 作者,会按照内部函数能完成多种任务的想法来使用内部函数。但他们对使用内部函数的结果并不明了,而且根本意识不到创建了闭包,或者那样做意味着什么。

正如下一节谈到 IE 中内存泄漏问题时所提及的,意外创建的闭包可能导致严重的负面效应,而且也会影响到代码的性能。问题不在于闭包本身,如果能够真正做到谨慎地使用它们,反而会有助于创建高效的代码。换句话说,使用内部函数会影响到效率。

使用内部函数最常见的一种情况就是将其作为 DOM 元素的事件处理器。例如,下面的代码用于向一个链接元素添加 onclick 事件处理器:

/* 定义一个全局变量,通过下面的函数将它的值
   作为查询字符串的一部分添加到链接的 - href - 中:
*/
var quantaty = 5;
/* 当给这个函数传递一个链接(作为函数中的参数 - linkRef -)时,
   会将一个 onclick 事件处理器指定给该链接,该事件处理器
   将全局变量 - quantaty - 的值作为字符串添加到链接的 - href -
   属性中,然后返回 true 使该链接在单击后定位到由  - href -
   属性包含的查询字符串指定的资源:
*/
function addGlobalQueryOnClick(linkRef){
    /* 如果可以将参数 - linkRef - 通过类型转换为 ture
      (说明它引用了一个对象):
    */
    if(linkRef){
        /* 对一个函数表达式求值,并将对该函数对象的引用
           指定给这个链接元素的 onclick 事件处理器:
        */
        linkRef.onclick = function(){
            /* 这个内部函数表达式将查询字符串
               添加到附加事件处理器的元素的 - href - 属性中:
            */
            this.href += ('?quantaty='+escape(quantaty));
            return true;
        };
    }
}

无论什么时候调用 addGlobalQueryOnClick 函数,都会创建一个新的内部函数(通过赋值构成了闭包)。从效率的角度上看,如果只是调用一两次 addGlobalQueryOnClick 函数并没有什么大的妨碍,但如果频繁使用该函数,就会导致创建许多截然不同的函数对象(每对内部函数表达式求一次值,就会产生一个新的函数对象)。

上面例子中的代码没有关注内部函数在创建它的函数外部可以访问(或者说构成了闭包)这一事实。实际上,同样的效果可以通过另一种方式来完成。即单独地定义一个用于事件处理器的函数,然后将该函数的引用指定给元素的事件处理属性。这样,只需创建一个函数对象,而所有使用相同事件处理器的元素都可以共享对这个函数的引用:

/* 定义一个全局变量,通过下面的函数将它的值
   作为查询字符串的一部分添加到链接的 - href - 中:
*/
var quantaty = 5;
/* 当把一个链接(作为函数中的参数 - linkRef -)传递给这个函数时,
   会给这个链接添加一个 onclick 事件处理器,该事件处理器会
   将全局变量  - quantaty - 的值作为查询字符串的一部分添加到
   链接的 - href -  中,然后返回 true,以便单击链接时定位到由
   作为 - href - 属性值的查询字符串所指定的资源:
*/
function addGlobalQueryOnClick(linkRef){
    /* 如果 - linkRef - 参数能够通过类型转换为 true
    (说明它引用了一个对象):
    */
    if(linkRef){
        /* 将一个对全局函数的引用指定给这个链接
           的事件处理属性,使函数成为链接元素的事件处理器:
        */
        linkRef.onclick = forAddQueryOnClick;
    }
}
/* 声明一个全局函数,作为链接元素的事件处理器,
   这个函数将一个全局变量的值作为要添加事件处理器的
   链接元素的  - href - 值的一部分:
*/
function forAddQueryOnClick(){
    this.href += ('?quantaty='+escape(quantaty));
    return true;
}

在上面例子的第一个版本中,内部函数并没有作为闭包发挥应有的作用。在那种情况下,反而是不使用闭包更有效率,因为不用重复创建许多本质上相同的函数对象。

类似地考量同样适用于对象的构造函数。与下面代码中的构造函数框架类似的代码并不罕见:

function ExampleConst(param){
    /* 通过对函数表达式求值创建对象的方法,
      并将求值所得的函数对象的引用赋给要创建对象的属性:
    */
    this.method1 = function(){
        ... // 方法体。
    };
    this.method2 = function(){
        ... // 方法体。
    };
    this.method3 = function(){
        ... // 方法体。
    };
    /* 把构造函数的参数赋给对象的一个属性:*/
    this.publicProp = param;
}

每当通过 new ExampleConst(n) 使用这个构造函数创建一个对象时,都会创建一组新的、作为对象方法的函数对象。因此,创建的对象实例越多,相应的函数对象也就越多。

Douglas Crockford 提出的模仿 JavaScript 对象私有成员的技术,就利用了将对内部函数的引用指定给在构造函数中构造对象的公共属性而形成的闭包。如果对象的方法没有利用在构造函数中形成的闭包,那么在实例化每个对象时创建的多个函数对象,会使实例化过程变慢,而且将有更多的资源被占用,以满足创建更多函数对象的需要。

这那种情况下,只创建一次函数对象,并把它们指定给构造函数 prototype 的相应属性显然更有效率。这样一来,它们就能被构造函数创建的所有对象共享了:

function ExampleConst(param){
    /* 将构造函数的参数赋给对象的一个属性:*/
    this.publicProp = param;
}
/* 通过对函数表达式求值,并将结果函数对象的引用
      指定给构造函数原型的相应属性来创建对象的方法:
*/
ExampleConst.prototype.method1 = function(){
    ... // 方法体。
};
ExampleConst.prototype.method2 = function(){
    ... // 方法体。
};
ExampleConst.prototype.method3 = function(){
    ... // 方法体。
};

Internet Explorer 的内存泄漏问题

返回目录

Internet Explorer Web 浏览器(在 IE 4 到 IE 6 中核实)的垃圾收集系统中存在一个问题,即如果 ECMAScript 和某些宿主对象构成了 “循环引用”,那么这些对象将不会被当作垃圾收集。此时所谓的宿主对象指的是任何 DOM 节点(包括 document 对象及其后代元素)和 ActiveX 对象。如果在一个循环引用中包含了一或多个这样的对象,那么这些对象直到浏览器关闭都不会被释放,而它们所占用的内存同样在浏览器关闭之前都不会交回系统重用。

当两个或多个对象以首尾相连的方式相互引用时,就构成了循环引用。比如对象 1 的一个属性引用了对象 2 ,对象 2 的一个属性引用了对象 3,而对象 3 的一个属性又引用了对象 1。对于纯粹的 ECMAScript 对象而言,只要没有其他对象引用对象 1、2、3,也就是说它们只是相互之间的引用,那么仍然会被垃圾收集系统识别并处理。但是,在 Internet Explorer 中,如果循环引用中的任何对象是 DOM 节点或者 ActiveX 对象,垃圾收集系统则不会发现它们之间的循环关系与系统中的其他对象是隔离的并释放它们。最终它们将被保留在内存中,直到浏览器关闭。

闭包非常容易构成循环引用。如果一个构成闭包的函数对象被指定给,比如一个 DOM 节点的事件处理器,而对该节点的引用又被指定给函数对象作用域中的一个活动(或可变)对象,那么就存在一个循环引用。DOM_Node.onevent ->function_object.[[scope]] ->scope_chain ->Activation_object.nodeRef ->DOM_Node。形成这样一个循环引用是轻而易举的,而且稍微浏览一下包含类似循环引用代码的网站(通常会出现在网站的每个页面中),就会消耗大量(甚至全部)系统内存。

多加注意可以避免形成循环引用,而在无法避免时,也可以使用补偿的方法,比如使用 IE 的 onunload 事件来来清空(null)事件处理函数的引用。时刻意识到这个问题并理解闭包的工作机制是在 IE 中避免此类问题的关键。

comp.lang.javascript FAQ notes T.O.C.

  • 撰稿 Richard Cornford,2004 年 3 月
  • 修改建议来自:
    • Martin Honnen.
    • Yann-Erwan Perio (Yep).
    • Lasse Reichstein Nielsen. (definition of closure)
    • Mike Scirocco.
    • Dr John Stockton.

HTTP状态码 – 维基百科,自由的百科全书

跳过字词转换说明

HTTP状态码(HTTP Status Code)是用以表示网页服务器 HTTP 响应状态的3位数字代码。它由RFC 2616 规范定义的,并得到RFC 2518 RFC 2817 RFC 2295 RFC 2774 RFC 4918 等规范扩展。

所有状态码的第一个数字代表了响应的五种状态之一。

[编辑 ] 1xx消息

这一类型的状态码,代表请求已被接受,需要继续处理。这类响应是临时响应,只包含状态行和某些可选的响应头信息,并以空行结束。由于HTTP/1.0协议中没有定义任何1xx状态码,所以除非在某些试验条件下,服务器禁止向此类客户端发送1xx响应。 这些状态码代表的响应都是信息性的,标示客户应该采取的其他行动。

100 Continue客户端应当继续发送请求。这个临时响应是用来通知客户端它的部分请求已经被服务器接收,且仍未被拒绝。客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应。服务器必须在请求完成后向客户端发送一个最终响应。101 Switching Protocols服务器已经理解了客户端的请求,并将通过Upgrade消息头通知客户端采用不同的协议来完成这个请求。在发送完这个响应最后的空行后,服务器将会切换到在Upgrade消息头中定义的那些协议。: 只有在切换新的协议更有好处的时候才应该采取类似措施。例如,切换到新的HTTP版本比旧版本更有优势,或者切换到一个实时且同步的协议以传送利用此类特性的资源。102 Processing由WebDAV RFC 2518 )扩展的状态码,代表处理将被继续执行。

[编辑 ] 2xx成功

这一类型的状态码,代表请求已成功被服务器接收、理解、并接受。

200 OK请求已成功,请求所希望的响应头或数据体将随此响应返回。201 Created请求已经被实现,而且有一个新的资源已经依据请求的需要而创建,且其URI 已经随Location头信息返回。假如需要的资源无法及时创建的话,应当返回’202 Accepted‘。202 Accepted服务器已接受请求,但尚未处理。正如它可能被拒绝一样,最终该请求可能会也可能不会被执行。在异步操作的场合下,没有比发送这个状态码更方便的做法了。:返回202状态码的响应的目的是允许服务器接受其他过程的请求(例如某个每天只执行一次的基于批处理 的操作),而不必让客户端一直保持与服务器的连接直到批处理操作全部完成。在接受请求处理并返回202状态码的响应应当在返回的实体中包含一些指示处理当前状态的信息,以及指向处理状态监视器或状态预测的指针,以便用户能够估计操作是否已经完成。203 Non-Authoritative Information服务器已成功处理了请求,但返回的实体头部元信息不是在原始服务器上有效的确定集合,而是来自本地或者第三方的拷贝。当前的信息可能是原始版本的子集或者超集。例如,包含资源的元数据 可能导致原始服务器知道元信息的超级。使用此状态码不是必须的,而且只有在响应不使用此状态码便会返回200 OK的情况下才是合适的。204 No Content服务器成功处理了请求,但不需要返回任何实体内容,并且希望返回更新了的元信息。响应可能通过实体头部的形式,返回新的或更新后的元信息。如果存在这些头部信息,则应当与所请求的变量相呼应。如果客户端是浏览器 的话,那么用户浏览器应保留发送了该请求的页面,而不产生任何文档视图上的变化,即使按照规范新的或更新后的元信息应当被应用到用户浏览器活动视图中的文档。由于204响应被禁止包含任何消息体,因此它始终以消息头后的第一个空行结尾。205 Reset Content服务器成功处理了请求,且没有返回任何内容。但是与204响应不同,返回此状态码的响应要求请求者重置文档视图。该响应主要是被用于接受用户输入后,立即重置表单,以便用户能够轻松地开始另一次输入。与204响应一样,该响应也被禁止包含任何消息体,且以消息头后的第一个空行结束。206 Partial Content服务器已经成功处理了部分GET请求。类似于FlashGet 或者迅雷 这类的HTTP 下载工具 都是使用此类响应实现断点续传或者将一个大文档分解为多个下载段同时下载。该请求必须包含Range头信息来指示客户端希望得到的内容范围,并且可能包含If-Range来作为请求条件。响应必须包含如下的头部域:

  • Content-Range用以指示本次响应中返回的内容的范围;如果是Content-Type为multipart/byteranges的多段下载,则每一multipart段中都应包含Content-Range域用以指示本段的内容范围。假如响应中包含Content-Length,那么它的数值必须匹配它返回的内容范围的真实字节数。
  • ETag和/或Content-Location,假如同样的请求本应该返回200响应
  • Expires, Cache-Control,和/或Vary,假如其值可能与之前相同变量的其他响应对应的值不同的话。

假如本响应请求使用了If-Range强缓存验证,那么本次响应不应该包含其他实体头;假如本响应的请求使用了If-Range弱缓存验证,那么本次响应禁止包含其他实体头;这避免了缓存的实体内容和更新了的实体头信息之间的不一致。否则,本响应就应当包含所有本应该返回200响应中应当返回的所有实体头部域。假如ETag或Last-Modified头部不能精确匹配的话,则客户端缓存 应禁止将206响应返回的内容与之前任何缓存过的内容组合在一起。任何不支持Range以及Content-Range头的缓存都禁止缓存206响应返回的内容。207 Multi-Status由WebDAV(RFC 2518 )扩展的状态码,代表之后的消息体将是一个XML 消息,并且可能依照之前子请求数量的不同,包含一系列独立的响应代码。

[编辑 ] 3xx重定向

这类状态码代表需要客户端采取进一步的操作才能完成请求。通常,这些状态码用来重定向,后续的请求地址(重定向目标)在本次响应的Location域中指明。

当且仅当后续的请求所使用的方法是GET或者HEAD时,用户浏览器 才可以在没有用户介入的情况下自动提交所需要的后续请求。客户端应当自动监测无限循环 重定向(例如:A->A,或者A->B->C->A),因为这会导致服务器和客户端大量不必要的资源消耗。按照HTTP/1.0版规范的建议,浏览器不应自动访问超过5次的重定向。

300 Multiple Choices被请求的资源有一系列可供选择的回馈信息,每个都有自己特定的地址和浏览器驱动的商议信息。用户或浏览器能够自行选择一个首选的地址进行重定向。除非这是一个HEAD请求,否则该响应应当包括一个资源特性及地址的列表的实体,以便用户或浏览器从中选择最合适的重定向地址。这个实体的格式由Content-Type定义的格式所决定。浏览器可能根据响应的格式以及浏览器自身能力,自动作出最合适的选择。当然,RFC 2616 规范并没有规定这样的自动选择该如何进行。如果服务器本身已经有了首选的回馈选择,那么在Location中应当指明这个回馈的URI ;浏览器可能会将这个Location值作为自动重定向的地址。此外,除非额外指定,否则这个响应也是可缓存的。301 Moved Permanently被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干个URI之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。除非额外指定,否则这个响应也是可缓存的。新的永久性的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接 及简短说明。如果这不是一个GET或者HEAD请求,因此浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。注意:对于某些使用HTTP/1.0协议的浏览器,当它们发送的POST请求得到了一个301响应的话,接下来的重定向请求将会变成GET方式。302 Found请求的资源现在临时从不同的URI响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。新的临时性的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。如果这不是一个GET或者HEAD请求,那么浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。注意:虽然RFC 1945 RFC 2068 规范不允许客户端在重定向时改变请求的方法,但是很多现存的浏览器将302响应视作为303响应,并且使用GET方式访问在Location中规定的URI,而无视原先请求的方法。状态码303和307被添加了进来,用以明确服务器期待客户端进行何种反应。303 See Other对应当前请求的响应可以在另一个URI上被找到,而且客户端应当采用GET的方式访问那个资源。这个方法的存在主要是为了允许由脚本激活的POST请求输出重定向到一个新的资源。这个新的URI不是原始资源的替代引用。同时,303响应禁止被缓存。当然,第二个请求(重定向)可能被缓存。新的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。注意:许多HTTP/1.1版以前的浏览器不能正确理解303状态。如果需要考虑与这些浏览器之间的互动,302状态码应该可以胜任,因为大多数的浏览器处理302响应时的方式恰恰就是上述规范要求客户端处理303响应时应当做的。304 Not Modified如果客户端发送了一个带条件的GET请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变,则服务器应当返回这个状态码。304响应禁止包含消息体,因此始终以消息头后的第一个空行结尾。该响应必须包含以下的头信息:

  • Date,除非这个服务器没有时钟。假如没有时钟的服务器也遵守这些规则,那么代理服务器 以及客户端可以自行将Date字段添加到接收到的响应头中去(正如RFC 2068 中规定的一样),缓存机制将会正常工作。
  • ETag和/或Content-Location,假如同样的请求本应返回200响应
  • Expires, Cache-Control,和/或Vary,假如其值可能与之前相同变量的其他响应对应的值不同的话。

假如本响应请求使用了强缓存验证,那么本次响应不应该包含其他实体头;否则(例如,某个带条件的GET请求使用了弱缓存验证),本次响应禁止包含其他实体头;这避免了缓存了的实体内容和更新了的实体头信息之间的不一致。假如某个304响应指明了当前某个实体没有缓存,那么缓存系统必须忽视这个响应,并且重复发送不包含限制条件的请求。假如接收到一个要求更新某个缓存条目的304响应,那么缓存系统必须更新整个条目以反映所有在响应中被更新的字段的值。305 Use Proxy被请求的资源必须通过指定的代理才能被访问。Location域中将给出指定的代理所在的URI信息,接收者需要重复发送一个单独的请求,通过这个代理才能访问相应资源。只有原始服务器才能创建305响应。注意:RFC 2068 中没有明确305响应是为了重定向一个单独的请求,而且只能被原始服务器创建。忽视这些限制可能导致严重的安全后果。306 Switch Proxy在最新版的规范中,306状态码已经不再被使用。307 Temporary Redirect请求的资源现在临时从不同的URI响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在Cache-Control或Expires中进行了指定的情况下,这个响应才是可缓存的。新的临时性的URI应当在响应的Location域中返回。除非这是一个HEAD请求,否则响应的实体中应当包含指向新的URI的超链接及简短说明。因为部分浏览器不能识别307响应,因此需要添加上述必要信息以便用户能够理解并向新的URI发出访问请求。如果这不是一个GET或者HEAD请求,那么浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。

[编辑 ] 4xx请求错误

这类的状态码代表了客户端看起来可能发生了错误,妨碍了服务器的处理。除非响应的是一个HEAD请求,否则服务器就应该返回一个解释当前错误状况的实体,以及这是临时的还是永久性的状况。这些状态码适用于任何请求方法。浏览器应当向用户显示任何包含在此类错误响应中的实体内容。

如果错误发生时客户端正在传送数据,那么使用TCP 的服务器实现应当仔细确保在关闭客户端与服务器之间的连接之前,客户端已经收到了包含错误信息的数据包 。如果客户端在收到错误信息后继续向服务器发送数据,服务器的TCP栈将向客户端发送一个重置数据包,以清除该客户端所有还未识别的输入缓冲 ,以免这些数据被服务器上的应用程序 读取并干扰后者。

400 Bad Request由于包含语法 错误,当前请求无法被服务器理解。除非进行修改,否则客户端不应该重复提交这个请求。401 Unauthorized当前请求需要用户验证。该响应必须包含一个适用于被请求资源的WWW-Authenticate信息头用以询问用户信息。客户端可以重复提交一个包含恰当的Authorization头信息的请求。如果当前请求已经包含了Authorization证书,那么401响应代表着服务器验证已经拒绝了那些证书。如果401响应包含了与前一个响应相同的身份验证询问,且浏览器已经至少尝试了一次验证,那么浏览器应当向用户展示响应中包含的实体信息,因为这个实体信息中可能包含了相关诊断信息。参见RFC 2617 。402 Payment Required该状态码是为了将来可能的需求而预留的。403 Forbidden服务器已经理解请求,但是拒绝执行它。与401响应不同的是,身份验证并不能提供任何帮助,而且这个请求也不应该被重复提交。如果这不是一个HEAD请求,而且服务器希望能够讲清楚为何请求不能被执行,那么就应该在实体内描述拒绝的原因。当然服务器也可以返回一个404响应,假如它不希望让客户端获得任何信息。404 Not Found 请求失败,请求所希望得到的资源未被在服务器上发现。没有信息能够告诉用户这个状况到底是暂时的还是永久的。假如服务器知道情况的话,应当使用410状态码来告知旧资源因为某些内部的配置机制问题,已经永久的不可用,而且没有任何可以跳转的地址。404这个状态码被广泛应用于当服务器不想揭示到底为何请求被拒绝或者没有其他适合的响应可用的情况下。405 Method Not Allowed请求行中指定的请求方法不能被用于请求相应的资源。该响应必须返回一个Allow头信息用以表示出当前资源能够接受的请求方法的列表。鉴于PUT,DELETE方法会对服务器上的资源进行写操作,因而绝大部分的网页服务器 都不支持或者在默认配置下不允许上述请求方法,对于此类请求均会返回405错误。406 Not Acceptable请求的资源的内容特性无法满足请求头中的条件,因而无法生成响应实体。除非这是一个HEAD请求,否则该响应就应当返回一个包含可以让用户或者浏览器从中选择最合适的实体特性以及地址列表的实体。实体的格式由Content-Type头中定义的媒体类型决定。浏览器可以根据格式及自身能力自行作出最佳选择。但是,规范中并没有定义任何作出此类自动选择的标准。407 Proxy Authentication Required与401响应类似,只不过客户端必须在代理服务器上进行身份验证。代理服务器必须返回一个Proxy-Authenticate用以进行身份询问。客户端可以返回一个Proxy-Authorization信息头用以验证。参见RFC 2617 。408 Request Timeout请求超时。客户端没有在服务器预备等待的时间内完成一个请求的发送。客户端可以随时再次提交这一请求而无需进行任何更改。409 Conflict由于和被请求的资源的当前状态之间存在冲突,请求无法完成。这个代码只允许用在这样的情况下才能被使用:用户被认为能够解决冲突,并且会重新提交新的请求。该响应应当包含足够的信息以便用户发现冲突的源头。冲突通常发生于对PUT请求的处理中。例如,在采用版本检查的环境下,某次PUT提交的对特定资源的修改请求所附带的版本信息与之前的某个(第三方)请求向冲突,那么此时服务器就应该返回一个409错误,告知用户请求无法完成。此时,响应实体中很可能会包含两个冲突版本 之间的差异比较,以便用户重新提交归并 以后的新版本。410 Gone被请求的资源在服务器上已经不再可用,而且没有任何已知的转发地址。这样的状况应当被认为是永久性的。如果可能,拥有链接编辑功能的客户端应当在获得用户许可后删除所有指向这个地址的引用。如果服务器不知道或者无法确定这个状况是否是永久的,那么就应该使用404状态码。除非额外说明,否则这个响应是可缓存的。410响应的目的主要是帮助网站管理员维护网站,通知用户该资源已经不再可用,并且服务器拥有者希望所有指向这个资源的远端连接也被删除。这类事件在限时、增值服务中很普遍。同样,410响应也被用于通知客户端在当前服务器站点上,原本属于某个个人的资源已经不再可用。当然,是否需要把所有永久不可用的资源标记为’410 Gone’,以及是否需要保持此标记多长时间,完全取决于服务器拥有者。411 Length Required服务器拒绝在没有定义Content-Length头的情况下接受请求。在添加了表明请求消息体长度的有效Content-Length头之后,客户端可以再次提交该请求。412 Precondition Failed服务器在验证在请求的头字段中给出先决条件时,没能满足其中的一个或多个。这个状态码允许客户端在获取资源时在请求的元信息(请求头字段数据)中设置先决条件,以此避免该请求方法被应用到其希望的内容以外的资源上。413 Request Entity Too Large服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。此种情况下,服务器可以关闭连接以免客户端继续发送此请求。如果这个状况是临时的,服务器应当返回一个Retry-After的响应头,以告知客户端可以在多少时间以后重新尝试。414 Request-URI Too Long请求的URI长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务。这比较少见,通常的情况包括:

  • 本应使用POST方法的表单提交变成了GET方法,导致查询字符串 Query String )过长。
  • 重定向URI“黑洞”,例如每次重定向把旧的URI作为新的URI的一部分,导致在若干次重定向后URI超长。
  • 客户端正在尝试利用某些服务器中存在的安全漏洞 攻击服务器。这类服务器使用固定长度的缓冲读取或操作请求的URI,当GET后的参数超过某个数值后,可能会产生缓冲区溢出,导致任意代码被执行[1]。没有此类漏洞的服务器,应当返回414状态码。

415 Unsupported Media Type对于当前请求的方法和所请求的资源,请求中提交的实体并不是服务器中所支持的格式,因此请求被拒绝。416 Requested Range Not Satisfiable如果请求中包含了Range请求头,并且Range中指定的任何数据范围都与当前资源的可用范围不重合,同时请求中又没有定义If-Range请求头,那么服务器就应当返回416状态码。假如Range使用的是字节范围,那么这种情况就是指请求指定的所有数据范围的首字节位置都超过了当前资源的长度。服务器也应当在返回416状态码的同时,包含一个Content-Range实体头,用以指明当前资源的长度。这个响应也被禁止使用multipart/byteranges作为其Content-Type。417 Expectation Failed在请求头Expect中指定的预期内容无法被服务器满足,或者这个服务器是一个代理服务器,它有明显的证据证明在当前路由 的下一个节点上,Expect的内容无法被满足。418 I’m a teapot本操作码是在1998年作为IETF 的传统愚人节笑话 , 在RFC 2324 超文本咖啡壶控制协议 中定义的,并不需要在真实的HTTP服务器中定义。421 There are too many connections from your internet address从当前客户端所在的IP地址 到服务器的连接数超过了服务器许可的最大范围。通常,这里的IP地址指的是从服务器上看到的客户端地址(比如用户的网关 或者代理服务器 地址)。在这种情况下,连接数的计算可能涉及到不止一个终端 用户。422 Unprocessable Entity请求格式正确,但是由于含有语义 错误,无法响应。(RFC 4918 WebDAV )423 Locked当前资源被锁定。(RFC 4918 WebDAV )424 Failed Dependency由于之前的某个请求发生的错误,导致当前请求失败,例如PROPPATCH。(RFC 4918 WebDAV )425 Unordered Collection在WebDav Advanced Collections草案中定义,但是未出现在《WebDAV顺序集协议》(RFC 3658 )中。426 Upgrade Required客户端应当切换到TLS/1.0 。(RFC 2817 )449 Retry With由微软 扩展,代表请求应当在执行完适当的操作后进行重试。

[编辑 ] 5xx服务器错误

这类状态码代表了服务器在处理请求的过程中有错误或者异常状态发生,也有可能是服务器意识到以当前的软硬件资源无法完成对请求的处理。除非这是一个HEAD请求,否则服务器应当包含一个解释当前错误状态以及这个状况是临时的还是永久的解释信息实体。浏览器应当向用户展示任何在当前响应中被包含的实体。

这些状态码适用于任何响应方法。

500 Internal Server Error服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。一般来说,这个问题都会在服务器的程序码出错时出现。501 Not Implemented服务器不支持当前请求所需要的某个功能。当服务器无法识别请求的方法,并且无法支持其对任何资源的请求。502 Bad Gateway作为网关 或者代理 工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。503 Service Unavailable由于临时的服务器维护或者过载 ,服务器当前无法处理请求。这个状况是临时的,并且将在一段时间以后恢复。如果能够预计延迟时间,那么响应中可以包含一个Retry-After头用以标明这个延迟时间。如果没有给出这个Retry-After信息,那么客户端应当以处理500响应 的方式处理它。504 Gateway Timeout作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器(URI标识出的服务器,例如HTTP FTP LDAP )或者辅助服务器(例如DNS )收到响应。注意:某些代理服务器在DNS查询超时 时会返回400或者500错误505 HTTP Version Not Supported服务器不支持,或者拒绝支持在请求中使用的HTTP版本。这暗示着服务器不能或不愿使用与客户端相同的版本。响应中应当包含一个描述了为何版本不被支持以及服务器支持哪些协议的实体。506 Variant Also Negotiates由《透明内容协商协议》(RFC 2295 )扩展,代表服务器存在内部配置错误:被请求的协商变元资源被配置为在透明内容协商中使用自己,因此在一个协商处理中不是一个合适的重点。507 Insufficient Storage服务器无法存储完成请求所必须的内容。这个状况被认为是临时的。WebDAV(RFC 4918 )509 Bandwidth Limit Exceeded服务器达到带宽 限制。这不是一个官方的状态码,但是仍被广泛使用。510 Not Extended获取资源所需要的策略并没有没满足。(RFC 2774

[编辑 ] 参考

[编辑 ] 引用文献

[编辑 ] 外部链接