ZUUL路由服务遇到的坑

项目采用Spring cloud微服务框架,使用ZUUL作为路由服务,在使用过程中遇到了如下问题,记录下来供大家借鉴。

1、关于跨域

API需要提供给其他项目使用,由于服务通过zuul,所以zuul需要支持跨域访问。
解决办法:
增加跨域过滤器即可

1
2
3
4
5
6
7
8
9
10
11
12
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setMaxAge(3600L);
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}

2、文件上传中文名乱码

使用form上传文件,直接post到服务,文件名中文读取正常。但是通过zuul中转后,文件名变成乱码。
解决办法:
1、注意编码,全站api和前端全部要使用utf-8编码。zuul中强制编码为utf-8,参数配置如下:

1
2
3
4
5
6
spring:
http:
encoding:
charset: UTF-8
enabled: true
force: true

2、修改nginx路由设置,在原来的api地址前,统一增加zuul。因为默认上传文件是通过服务自己的controller来处理,增加zuul前缀后,通过zuul servlet来处理,避免了多次跳转,和引入编码错误。nginx配置举例:

1
2
3
4
5
6
7
8
9
10
11
12
location /api/ {
proxy_pass http://localhost:9999/zuul/api/;
proxy_redirect http://localhost:9999/zuul/api/ /api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 500m;
proxy_connect_timeout 60s;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}

3、增加zuul前缀后会导致header中出现Access-Control-Allow-Credentials等属性重复的异常,需要在zuul中设置header忽略。参数配置如下

1
2
3
zuul:
sensitiveHeaders: Authorization
ignored-headers: Access-Control-Allow-Credentials,Access-Control-Allow-Origin,Vary,X-Frame-Options

JConsole的远程连接

JConsole介绍

JConsole是JDK自带的Java性能分析器,用来监听Java应用程序性能和跟踪代码。默认安装在JDK的bin目录(例如:C:\Program Files\Java\jdk1.8.0_144\bin),直接双击运行即可。
JConsole可以监听本地的应用,也可以监听远程的应用。在新建连接界面上选择本地应用,或者输入远程连接地址,格式是ip:port,注意这个port是监听端口不是服务端口。
连接1.jpg

连接完成后进入监听界面,可以查看内存、线程、类、JVM等相关信息。
监听1.jpg

关于远程连接

测试环境部署在RedHat6.5服务器上,一般说明增加如下参数即可允许远程连接。

1
2
3
-Dcom.sun.management.jmxremote.port=8999
-Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

但是实际测试无法连接,经过查询资料,最后配置如下,实现了远程连接。

1
(java -jar -Dcom.sun.management.jmxremote  -Dcom.sun.management.jmxremote.port=8999 -Dcom.sun.management.jmxremote.rmi.port=9999 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false app-1.0.jar&)

同时还需要注意服务器的端口是否被屏蔽,hosts是否配置了实际IP。可以使用hostname -i命令来查询ip是否生效。例如实际ip是10.10.10.101,计算机名是mycomputer。hosts配置如下:

1
2
10.10.10.101   mycomputer
10.10.10.101 localhost localhost.localdomain localhost4 localhost4.localdomain4

linux上使用publickey访问gerrit异常

现象

redhat上,先使用A账号,能正常执行相关操作。切换成B账号,上传public key后,执行异常:

1
2
3
4
5
6
7
Agent admitted failure to sign using the key.
Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
`

解决办法

执行完ssh-keygen再执行一下ssh-add

1
2
ssh-keygen -t rsa -C "mymail@my.com"
ssh-add ~/.ssh/id_rsa

EffectiveJava--对象的通用方法

第1条,考虑用静态工厂方法代替构造器

优点:

  • 有名称,作用更清晰
  • 可以实现单例
  • 可以返回原类型的任何子类型
  • 创建参数化实例对象时,代码更简洁

缺点:

  • 类如果不含公有或者受保护的构造器,就不能被子类化
  • 与其他静态方法没区别

第2条,遇到多个构造器参数时考虑用建造者(builder)模式

构造器遇到多个参数组合的时候,需要定义不通组合的构造器,复杂而且顺序容易弄错。使用建造者模式就可以解决此类问题。
优点:灵活链式构建
缺点:需创建构建器,有开销

第3条,用私有构造器或者枚举类型强化Singleton属性

序列化,反射安全?
单元素的枚举类型已经成为实现Sibgleton的最佳方法。

第4条,通过私有构造器强化不可实例化的能力

不需要被实例化的类,添加私有构造函数,并且在构造函数中抛出异常,来避免被实例化。

1
2
3
4
5
   public class UtilityClass{
private UtilityClass(){
throw new AssertionError();
}
}

第5条,避免创建不必要的对象

如下情况可以不创建对象,示例如下:
1、””字符串本身就是一个String对象,再new会重复创建String对象。

1
2
String s = new String("this is a wrong");//bad  
String s = "good";//good

2、提供静态方法和构造函数的不可变类,静态方法优于创建对象

1
2
Boolean constructObj = new Boolean("true");//bad  
Boolean staticMethodObj = Boolean.valueOf("true");//good

3、作为常量使用的可变类。比如作为固定开始日期的Date,只需要实例化一次即可重复使用
4、优先使用基本类型而不是装箱基本类型,当心无意识的自动装箱

1
2
Long sum = 0L;//bad  
long sum = 0L;//good

不是不要创建对象,小对象的开销很小。
重量级的对象才需要维护资源池,例如数据库连接。

第6条,消除过期的对象引用

例如,Stack中pop的对象引用,需要主动释放内存,同时也能尽早暴漏错误调用

1
2
3
4
5
6
7
public Object pop(){
if(size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = bull; //释放过期的引用
return result;
}

清空对象引用应该是一种例外,而不是一种规范行为。通过再尽量小的作用域内,变量及时结束其生命周期来释放。
关于内存泄露的来源:

  • 类自己管理的内存,程序猿需要关注其释放
  • 缓存,可以使用WeakHashMap代替缓存,需要定期清理,或者类似LinkedHashMap的removeEldestEntry方法清理。
  • 监听器和其他回调。确保回调立即回收的方法保存他们的弱引用。例如保存成WeakHashMap的键。

第7条,避免使用终结方法(finalizer)

finalizer的缺点:

  • 不能保证会被及时的执行,间隔是任意的。
  • 在不同JVM平台表现不同。
  • finalizer线程的优先级比程序中其他线程低很多,会导致队列积压,内存溢出。
  • finalizer可能不会被执行。
    需要注意的地方:
  • 所以不应该依赖finalizer来更新重要的持久状态。
  • System.gc和System.runFinalization可以增加finalizer被执行的机会,单不能保证一定被执行。
  • finalizer中抛出的异常如果未捕获,该异常可能被忽略(警告也不会被打印),并且finalizer也会终止。继续使用此对象时会产生不确定的结果。
  • 使用finalizer会导致对象的创建和销毁时间大幅增加,甚至几百倍。
  • 建议定义一个显式的终止方法释放资源。例如InputStream/FileOutputStream/Connection的close等。本地对等体需要finalizer或者显式的终止方法才能释放,GC不会自动释放。对于需要及时释放资源的情况,应该用显式的终止方法来释放。
  • 显式的终止方法通常与try-finally结构结合起来使用,以确保及时终止
  • 子类的finalizer,需要在try-finally的finally中调用超类的finalizer。

    1
    2
    3
    4
    5
    6
    7
     @Override protected void finalize() throws Throwable {
    try{
    //Finalize subClass state
    } finally {
    super.finalize();
    }
    }
  • 可以考虑使用匿名类来充当总结方法守卫者,确保能够调用finalizer.因为成员变量会被主动释放,从而触发finalizer。

    1
    2
    3
    4
    5
      private final Object finalizerGuardian = new Object(){
    @Override protected void finalize() throws Throwable {
    super.finalize();
    }
    };

EffectiveJava--对象的通用方法

第8条,覆盖equals时请遵守通用约定

除非必要,不要重写equals方法。注意:

  • 类的每个实例本质上都是唯一的。这时候继承的Object的euqals方法是完全正确的,不需要重写
  • 不关心类是否提供“逻辑相等”功能。比如Random类。
  • 超类(父类)已经重写了quals,对于子类是适用的,无需重写。例如:AbstractSet——>Set,AbstractList——>List,AbstractMap——>Map
  • 类是私有的或者是包级私有的,并且确定它的equals方法永远不会被调用时。可以进行如下重写,防止意外调用:

    1
    2
    3
    @Override public boolean equals(Object o){
    throw new AssertionError();
    }
  • 如果类具有自己特有的“逻辑相等概念”,而且超类还没有覆盖equals实现期望的行为,此时就需要重写equals方法。重写后被用做map的key,或者set的元素时才能表现出预期的行为。

  • 对于“每个值至多只存在一个对象”的“值类”,Object的euqals方法等同于逻辑相等。例如:枚举类型
    Object的euqals方法的规范:
  • 自反性(reflexive)。对于任何非null的引用值x,x.equals(x)必须返回true。
  • 对称性(symmetric)。对于任何非null的引用值x和y,当且仅当y.equals(x)返回true时,x.equals(y)必须返回true。
  • 传递性(transitive)。对于任何非null的引用值x、y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)必须返回true。
  • 一致性(consistent)。对于任何非null的引用值x和y,只要x,y没有被修改过,多次调用x.equals(y),返回值一定是一致的。
  • 对于任何非null的引用值x,x.equals(null)必须返回false。
    如果不符合这些规范,程序会表现异常,甚至是崩溃,而且很难找到根源。
    在子类和超类,子类之间,以及同一个超类的子类之间,equals就很有可能出现问题。我们无法在扩展可实例化的类的同时,既增加新的值组件,同时又保留equals约定。推荐用复合的方式代替继承,每个属性分别equals。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
      public class ColorPoint {
    private Point point;
    private Color color;
    @Override public boolean equals(Object o){
    if(!(o instanceof ColorPoint)){
    return false;
    }
    ColorPoint cp = (ColorPoint)o;
    return cp.point.equals(point) && cp.color.equals(color);
    }
    }

equals实现时一般要使用instanceof进行类型检查,对null检查时必定返回false,所以没必要再检查是否为null。
实现高质量equals方法的诀窍:

  • 使用==操作符检查“参数是否为这个对象的引用”。(出于性能优化方面考虑)
  • 使用instanceof操作符检查“参数是否为正确的类型”。所谓“正确的类型”一般是指类,如果接口改进了equals约定,允许实现该接口的类之间进行比较,就可以使用接口。例如Set、List、Map和Map.Entry。
  • 把参数转换成正确的类型。即转换成当前对象的类型。
  • 对该类中的每个关键域(属性),分别进行比较。除float和double的基本值类型,直接使用==比较。float使用Float.compare,double使用Double.compare。如果要检查数组中的每个元素,可以使用Arrays.equals。对于引用域,可能存在null为合法值,需要进行null检查。多个域的情况下,要优先比较开销低的域。
  • 要确保equals符合对称性,传递性和一致性。
  • 不要企图让equals做过多的比较
  • 不要将equals方法的参数类型必须是Object,不能改成当前类。因为没有重写Object.equals,只是重载了。

第9条:重写equals时必须重写hashCode

如果没有重写hashCode方法,就无法作为散列集合(例如:HashMap,HashSet和HashTable)的key。因为在取值时会优先通过hashCode比较。

第10条,始终要重写toString

toString在打印日志时,可以明确当前实例的重要信息,便于理解。而且要注意格式一旦确定就不要修改,格式要便于字符串解析,去除对应域的值。

第11条,谨慎的重写clone

如果要支持clone需要实现Cloneable接口。
clone方法就是一个构造器,你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。

  • 对于子类型的域直接使用Object.clone即可。
  • 对于可变的引用类型,可变引用类型的集合,就需要递归的调用clone。以避免伤害到原始对象。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
      @Override public Stack clone() {
    try{
    Stack result = (Stack) super.clone();
    result.date = date.clone();
    result.elements = elements.clone();
    return result;
    } catch (CloneNotSupportedException e) {
    throw new AssertionError();
    }
    }
  • clone架构与引用可变对象的final域的正常用法是不相兼容的,除非在clone对象和原始对象间可以安全的共享此可变对象。

  • 对于散列桶数组,需要遍历每个数据元素,进行深度拷贝进行clone。
  • clone不应该在构造的过程中,调用新对象中任何非final的方法,防止新旧对象不一致。
  • 对于有线程安全要求的类,clone方法要处理好同步。Object.clone没有同步。
  • 对于ID或者时间戳一类的值域,注意clone后进行修正。
  • 可以考虑使用拷贝构造器或者拷贝工厂方法,更加灵活的实现拷贝操作,比如带参数或者类型转换(比如用数组生成List对象)。
  • 如果不能提供良好保护的clone方法,他的子类就不可能实现Cloneable接口。

编程通则:永远不要让客户去做任何类库能够替客户完成的事情。

第12条,考虑实现Compareable接口

compareTo的通用约定与equals类似。
注意:HashSet实例中添加new BigDecimal(“1.0”)和new BigDecimal(“1.00”),这个集合将含有两个元素。而如果把类型HashSet改成TreeSet,则只有一个元素。因为HashSet使用equals比较,TreeSet使用comapreTo比较。

mongodb的internalQueryExecMaxBlockingSortBytes异常修复

现象

node执行的服务出现异常,查看日志发现如下错误。

1
2
3
4
5
6
7
8
9
10
MongoError: QueryFailure flag set on getmore command
at Object.toError (e:\code\api\mobile\nodejs\node_modules\mongodb\lib\mongodb\utils.js:114:11)
at e:\code\api\mobile\nodejs\node_modules\mongodb\lib\mongodb\cursor.js:854:31
at e:\code\api\mobile\nodejs\node_modules\mongodb\lib\mongodb\db.js:1905:9
at Server.Base._callHandler (e:\code\api\mobile\nodejs\node_modules\mongodb\lib\mongodb\connection\base.js:453:41)
at e:\code\api\mobile\nodejs\node_modules\mongodb\lib\mongodb\connection\server.js:488:18
at MongoReply.parseBody (e:\code\api\mobile\nodejs\node_modules\mongodb\lib\mongodb\responses\mongo_reply.js:68:5)
at .<anonymous> (e:\code\api\mobile\nodejs\node_modules\mongodb\lib\mongodb\connection\server.js:446:20)
at emitOne (events.js:96:13)
at emit (events.js:188:7)

查询该错误,未找到具体原因。然后在mongodb\cursor.js 854行,增加打印result。得到具体错误信息如下:
Overflow sort stage buffered data usage exceeds in internal limit
mongo执行sort语句时,内存最大32M,如果数据量大,超过这个限制就出抛出异常。

解决办法

1、给sort语句中的字段建立索引。
比如: sort({ endDate: -1, createTime: -1})
建立索引如下:db.activity.createIndex({ endDate: -1, createTime: -1}) 。其中acitivity是集合名

2、增加内存限制
需要在admin数据库下role为root的账户下设置,例如设置成100M

1
2
3
use admin
db.auth("adminuser","passwd")
db.adminCommand({setParameter: 1, internalQueryExecMaxBlockingSortBytes: 104857600})

综合查询性能和服务器资源占用,推荐使用建立索引的方式。

其他需要注意的地方

除了sort, aggregate也存在内存限制,这是需要使用allowDiskUse参数,允许使用硬盘缓存中间数据。具体设置如下

1
2
3
4
5
6
7
8
db.activity.aggregate(
[{ $unwind: '$applyment' },
{ $match: { 'applyment.items.value': req.query.uid }},
{ $project : {id:1,title: 1, 'applyment.approve': 1,'applyment.createTime': 1, endDate: 1}},
{ $sort:{ 'applyment.createTime': -1 }},
{ $skip:(result.pageNumber - 1) * result.pageSize},
{ $limit:result.pageSize}],
{ allowDiskUse: true})

关于设置索引

1、mongoDB 3.0开始ensureIndex被废弃,今后都仅仅是db.collection.createIndex的一个别名。
2、子对象的属性设置索引db.activity.createIndex({ “applyment.createTime”: -1})
3、数组内置顶位置设置索引db.activity.createIndex({ “applyment.items.0.value”: 1})

非maven的jar包怎么引入maven工程

前几天因为业务需要,引入了一个其他部门的jar包。不是Maven工程构建的,也就没有pom文件。这里记录下引入的过程,以备今后参考。

  1. maven仓库中存在的jar包
    可以直接在pom文件中添加依赖。但是问题来了,只有jar包,怎么知道groupId和artifactId呢?下面我就用实例告诉大家,怎么引入。比如依赖一个json-lib-2.2.1.jar。只需要在 网站上查询这个jar包。然后在版本列表中,点击具体需要的版本。
    mvn1-1.jpg
    进入具体页面后,拷贝需要的依赖配置内容,复制到maven
    mvn1-2.jpg
    复制maven的配置描述,拷贝到pom文件里即可。
  2. maven仓库里不存在的jar包
    可以自己在本地手工添加的方式解决。例如,haha-1.0.jar。我们设置groupId为com.my.test,artifactId为haha。版本为1.0。
    a. maven添加本地jar包
    配置好maven环境,有些使用eclipse的童鞋,可能没有安装过maven,会导致无法执行,需要自己配置maven环境。
    执行如下命令:
    mvn install:install-file -Dfile=E:\work\haha-1.0.jar -DgroupId=com.my.test -DartifactId=haha -Dversion=1.0 -Dpackaging=jar
    b. 手工创建目录
    在pom文件里添加配置信息:
    <dependency>
        <groupId>commons-httpclient</groupId>
        <artifactId>commons-httpclient</artifactId>
        <version>3.1</version>
    </dependency>
    
    创建好目录.m2\repository\com\my\test\haha\1.0。把haha-1.0.jar文件复制过来,同时新建haha-1.0.jar.pom。参考maven从仓库下载的jar包里相同的文件,复制里面的内容,修改一下groupId,artifactId和version信息即可。执行完这些后,再更新和编辑就可以解决了。

也谈TDD

    最近在组织项目成员的能力提升,不可避免的就进入了TDD这个话题。对于TDD,已经成为很多公司员工入职的专业知识培训课程。网上关于TDD的各种文章和讨论也很多,但是感觉大家都是望文生义,都是先写测试用例,再写代码。
    可能有人会说TDD就是“Test Driven Development”,就是测试驱动开发。就是写测试用例,然后再写代码。我承认,书里是这么写的。大家也是这么理解,然后去实践的。实际效果如何呢,就我了解的情况,大家对这个都很纠结。很多都在说,我先写几行代码,然后再写用例为啥就不行了呢?难道真是缺少仪式感吗?
    那么我先说说,我最近的思考。TDD方式,实际上是关注点驱动开发,或者说规则驱动开发。我要实现一个关注点/规则,我就先一条用例来验证这个关注点/规则,然后去完成这个关注点/规则的代码。如果按照这个思路去思考,我每次实现一个关注点/规则,我先写用例后写代码,或者先写代码再写用例。每次都是围绕着一个关注点/规则,是否先后顺序就显得不那么重要了。关键是每次聚焦于一个关注点/规则,有测试用例保证这个关注点/规则的准确性。那么就可以放心的去实现下一个关注点/规则,并且保证整个功能的准确了。
    以上是我对于TDD的最新理解和思考。各位看过后是否也对TDD也有了新的理解了呢?

高性能JavaScript--原型链

对象成员

前面已经介绍过,访问对象成员的速度比字面量或变量要慢,某些浏览器比数组元素还要慢。这里所说的对象成员包括属性和方法。大部分的JS代码都是以面向对象风格编写的,这就导致了非常频繁的访问对象成员操作?

原型

JS的对象是基于原型的。原型是其他对象的基础,它定义并实现了一个新创建的对象所必须包含的成员列表。不同于其他编程语言的类,原型为所有对象实例所共享,因此这些实例也共享原型对象的成员。并且每个对象实例上的原型修改后,会影响原型的定义。
在Firefox,Safari,Chrome和IE11+浏览器里,对象实例可以通过proto读取原型对象。一旦创建一个内置对象(例如Object或Array)的实例,他们就会自动拥有一个Object对象作为原型。
对象可以有两种成员类型:实例成员(或own成员)和原型成员。实例成员直接存在于对象实例中,原型成员则从对象的原型继承而来。实例代码如下:

1
2
3
4
5
6
7
function Book(title, publisher){
this.title = title
this.publisher = publisher
}
var book1 = new Book("High Performance JavaScript","Yahoo! Press");
console.log(book1.title);//实例成员
console.log(book1.toString());//原型成员

如何判断对象的相关成员是否存在?是实例成员还是原型成员成员?方法如下:

1
2
3
4
console.log( book1.hasOwnProperty('title')) //true
console.log( book1.hasOwnProperty('toString')) //false
console.log('title' in book1) //true
console.log('toString' in book1) //true

原型链

一个对象实例的原型对象,如果不是Object对象,那么原型对象就还有其自己的原型对象,直到原型对象是Object对象才结束。这种原型对象的嵌套就是原型链。
Object实例的原型就是Object对象,而其他对象生成实例时,instance.proto.proto才是Object对象。
示例代码:

1
2
3
4
5
6
7
function Book(title, publisher){
this.title = title
this.publisher = publisher
}
Book.prototype.price = 1.2

var book1 = new Book("High Performance JavaScript","Yahoo! Press");

实例book1的原型(proto)是Book.prototype,而Book.prototype的原型是Object。
对象Book拥有proto和prototype两个属性,
注意prototype的使用,只有在实例中才能直接读取prototype定义的属性。

1
2
3
4
5
6
function seven() {
this.a = 7;
}
seven.prototype.a = -1;
console.log('seven.a', seven.a); //undefine
console.log('new seven().a', new seven().a); //7

缓存对象成员值

访问对象成员时,对象在原型链的位置越深,读取的速度也就越慢。只有在必要时才使用对象成员,特别是没有必要反复多次读取同一对象成员。最佳做法是将属性值保存在局部变量中,使用局部变量代替属性以避免多次查找带来的性能开销。
实例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function toggle(element){
if(YAHOO.util.Dom.hasClass(element,'selected')){
YAHOO.util.Dom.removeClass(element,'selected');
return false;
} else {
YAHOO.util.Dom.addClass(element,'selected');
return true;
}
}

//改进后
function toggle(element){
var dom = YAHOO.util.Dom;
var hasClass = dom.hasClass(element,'selected');
if(hasClass){
dom.removeClass(element,'selected');
} else {
dom.addClass(element,'selected');
}
return !hasClass;
}

注意:
这种优化技术,并不推荐用于对象的成员方法。因为许多对象方法使用this来判断执行环境,把一个对象方法保存在局部变量会导致this的改变,从而导致异常。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
function Book(title, publisher){
this.title = title
this.publisher = publisher
this.getTitle = function(){
return this.title
}
}

var book1 = new Book("High Performance JavaScript","Yahoo! Press")
console.log(book1.getTitle())
var mytitle = book1.getTitle
console.log(mytitle()) //throw exception

高性能JavaScript--作用域链

数据存取

JS中有如下四种基本数据的存取:

  • 字面量:字符串、数字、布尔值、对象、数组、函数、正则表达式、null和undefined。
  • 本地变量:var/let 定义的数据存储单元。
  • 数组元素
  • 对象成员
    通常情况下,访问速度排序:字面量 > 本地变量 > 数组元素 > 对象成员。个别浏览器的版本,可能有细微差别。

    作用域

    执行环境/运行期上下文(execution context): 是指当前变量或函数有权访问的其它数据。每个执行环境都有一个与之关联的变量对象(variable object, VO),VO是不能直接访问的,执行环境中定义的所有变量和函数都会保存在这个对象中,解析器在处理数据的时候就会访问这个内部对象。
    全局执行环境是最外层的一个执行环境,在web浏览器中全局执行环境是window对象,因此所有全局变量和函数都是作为window对象的属性和成员函数创建的。每个函数都有自己的执行环境,当执行流进入一个函数的时候,函数的环境会被推入一个函数栈中,而在函数执行完毕后执行环境出栈并被销毁,保存在其中的所有变量和函数定义随之销毁,控制权返回到之前的执行环境中,全局的执行环境在应用程序退出(浏览器关闭)才会被销毁。
    每一个JS函数可以看做是Function对象的一个实例,并且含有一个内部属性[[Scopes]],[[Scopes]]包含了一个函数被创建的作用域中对象的集合。这个集合被称为作用域链,它决定哪些数据能被函数访问。函数作用域中的每个对象被称为一个可变对象,每个可变对象都是以“key-value”形式存在。
    典型的作用域链:
  1. 函数创建时
    此时函数的作用域链会压入第一个作用域对象,即创建此函数的作用域中可访问的数据对象填充。如下图所示:

scope1.png

注意:这个作用域对象是可变的,可以理解为这个对象是引用的。
具体对象信息可以在chrome dev tool中查看,如下图:

scope1-tool.png

  1. 函数执行时
    每次执行函数时都会创建一个执行环境,每个执行环境都是独一无二的,多次调用函数就会导致创建多个执行环境。此时会将会将一个被称为“活动对象”(activation object,AO)的新对象作为第二个作用域对象压入作用域链。如下图所示:

scope2.png

在函数的执行过程中,每遇到一个变量或者函数,都会在作用域链中按照顺序进行查找,直到遍历所有的作用域,此过程会影响运行性能。

  1. 闭包时的作用域
    如下一段代码中,包含了闭包
    1
    2
    3
    4
    5
    6
    functionassignEvents(){
    var id= "xdi9592";
    document.getElementById("save-btn").onclick =function(event){
    saveDocument(id);
    };
    }

scope3.png

scope3-tool.png

  1. 其他改变作用域的情况
    一般作用域链的顺序是按照调用的顺序排列的,但是特殊情况下会改变。
  • with
    执行with语句时,会将with带入的对象压入作用域链,导致调用深度发生变化。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    functioninitUI(){
    with(document){
    var bd= body,
    links=getElementsByTagName_r("a"),
    i=0,
    len= links.length;
    while(i<len){
    update(links[i++]);
    }
    getElementById("go-btn").onclick=function(){
    start();
    };
    bd.className ="active";
    }
    }

with.png

  • try-catch
    类似with,当try块发生异常时,程序跳转到catch子句,并且把异常对象压入作用域首位。catch子句执行完作用域链恢复之前的状态。由于加深了调用深度,如果在catch子句执行操作会造成性能问题。可以采用错误处理函数的方式,改变作用域链的状态,从而减少调用深度。
    1
    2
    3
    4
    5
    try{
    methodThatMightCauseAnError();
    }catch (ex){
    handleError(ex);//delegate tohandlermethod
    }

标识符解析的性能

在执行环境的作用域链中,一个标识符的位置越深,他的读写速度就越慢,因此函数中读写局部变量是最快的,读写全局变量通常是最慢的。
改进办法,通过赋值给局部变量,改变标识符的深度,从而提高读写速度。

1
2
3
4
5
6
7
8
for(var i = 0; i < document.getElementsByTagName("a").length; i++){
document.getElementsByTagName("a")[i].class = 'active'
}
//改进后
var list = document.getElementsByTagName("a");
for(var i = 0; i < list.length; i++){
list[i].class = 'active'
}