动态修改script标签中的src属性存在的问题
今天某个同事遇到一个诡异的问题,问题描述如下:
需求:通过脚本动态修改script标签的src来载入一段外部脚本并执行
实现方式(#1):
<script type="text/javascript" id="external-script">
</script>
<script type="text/javascript">
document.getElementById('external-script').src='url2';
</script>
url2的内容如下:
alert('I am dynamic');
结果:
- Chrome: 什么事都没发生(没有请求url2)
- Firefox: 什么事都没发生(没有请求url2)
- IE9:什么事都没发生(请求url2但不执行url2的脚本)
- IE(6,7,8): I am dynamic(请求并执行了url2的脚本)
注意实现方式中,第一段的script标签中间是有内容的(空格、换行符以及回车符)。
如何来解释这个问题呢?要解释这个问题,我们来看两个变种的例子,第一个例子(明确内联内容),如下所示(#2):
<script type="text/javascript" id="external-script">
alert('I am inline');
</script>
<script type="text/javascript">
document.getElementById('external-script').src='url2';
</script>
结果如下:
- Chrome: I am inline(没有请求url2)
- Firefox: I am inline(没有请求url2)
- IE9:I am inline(请求url2但不执行url2的脚本)
- IE(6,7,8): I am inline I am dynamic(请求并执行了url2的脚本)
再来看看第二个变种的例子(#3):
<script type="text/javascript" id="external-script" src="url1">
alert('I am inline script');
</script>
<script type="text/javascript">
document.getElementById('external-script').src='url2';
</script>
其中url1的内容如下:
alert('I am url1');
结果如下:
- Chrome: I am url1(没有请求url2)
- Firefox: I am url1(没有请求url2)
- IE9:I am url1(请求url2但不执行url2的脚本)
- IE(6,7,8): I am url1 I am dynamic(请求并执行了url2的脚本)
首先这里肯定的是src属性是修改成功的,可以通过看dom的变化看到src已经设置进去了。这个时候我们比对这三个例子,思考几十秒。分析下这三个例子,其实#2和#1是一样的,这里用#2是为了说明#1中的空格、换行符以及回车符会被浏览器认为是内联的脚本。通过比对#2和#3,是不是会让你想到什么?没错,我们第一个会想到的就是:当script标签既有src属性又有内联脚本的时候浏览器该如何处理? , 先来解释这个问题。
一谈到浏览器应该怎样处理,就不得不翻出各种宝典,这次不再是葵花宝典了,而是九阴真经(W3C的HTML4标准),标准中关于script标签的src部分有如下一段话:
If the src attribute is not set, user agents must interpret the contents of the element as the script. If the src has a URI value, user agents must ignore the element’s contents and retrieve the script via the URI
上面这段话的意思就是说:如果src没有设置,那么就执行内联脚本,如果src设置了浏览器就必须忽略内敛脚本而要去请求src指定的url的内容
这解释了为什么#3中标准浏览器(甚至IE6,7,8)都没有执行内联脚本(因为src设置了url1)。
搞清楚了这个基础问题之后,接下来问题就定位到了动态修改script的src属性的时候浏览器如何处理? ,从结果来看,标准的浏览器都没有去请求url2(更改src无效),这回IE6,7,8终于犯傻了。当然了,咱们也不能随随便便说人家犯傻,要拿出证据,这个时候继续拿出九阴真经W3C的HTML5标准,其中有这样一句话:
Changing the src, type, charset, async, and defer attributes dynamically has no direct effect; these attribute are only used at specific times described below.
意思就是说:修改src是没用的,对src的处理只会在特定的时候进行(个人猜测就是第一次看到这个属性的时候浏览器会去做相应处理,之后就无视它了)。
好了,这下真相大白了:这解释了为啥#3和#1中除了IE6,7,8之外其他浏览器都没有去请求url2(IE9请求了,但没执行),而且实验发现IE6,7,8对动态修改src都会做请求执行处理。
最后,这个故事至少告诉我们:写script标签的时候千万别手贱打回车。
参考文档:
通过什么途径能够深入了解JavaScript引擎是如何工作的?
昨天收到一封来自深圳的一位前端童鞋的邮件,邮件内容如下(很抱歉,未经过他的允许,公开邮件内容,不过我相信其他人肯定也有同样的问题,所以,直接把问题原文抛出来):
“读了你的几篇关于JS(变量对象、作用域、上下文、执行代码)的文章,我个人觉得有点抽象,难以深刻理解。我想请教下通过什么途径能够深入点的了解javascript解析引擎在执行代码前后是怎么工作的,ecma英文版实在看不下去呵呵。”
其实这个问题个人觉得太笼统了,直接回答很难回答,所以,我打算先把他的问题拆解成如下几个子问题,并对其表达个人的观点,希望对有同样困惑的童鞋能够有所帮助。
1. 什么是JavaScript解析引擎?
简单地说,JavaScript解析引擎就是能够“读懂”JavaScript代码,并准确地给出代码运行结果的一段程序。比方说,当你写了 var a = 1 + 1; 这样一段代码,JavaScript引擎做的事情就是看懂(解析)你这段代码,并且将a的值变为2。
学过编译原理的人都知道,对于静态语言来说(如Java、C++、C),处理上述这些事情的叫编译器(Compiler),相应地对于JavaScript这样的动态语言则叫解释器(Interpreter)。这两者的区别用一句话来概括就是:编译器是将源代码编译为另外一种代码(比如机器码,或者字节码),而解释器是直接解析并将代码运行结果输出。 比方说,firebug的console就是一个JavaScript的解释器。
但是,现在很难去界定说,JavaScript引擎它到底算是个解释器还是个编译器,因为,比如像V8(Chrome的JS引擎),它其实为了提高JS的运行性能,在运行之前会先将JS编译为本地的机器码(native machine code),然后再去执行机器码(这样速度就快很多),相信大家对JIT(Just In Time Compilation)一定不陌生吧。
我个人认为,不需要过分去强调JavaScript解析引擎到底是什么,了解它究竟做了什么事情我个人认为就可以了。对于编译器或者解释器究竟是如何看懂代码的,翻出大学编译课的教材就可以了。
这里还要强调的就是,JavaScript引擎本身也是程序,代码编写而成。比如V8就是用C/C++写的。
2. JavaScript解析引擎与ECMAScript是什么关系?
JavaScript引擎是一段程序,我们写的JavaScript代码也是程序,如何让程序去读懂程序呢?这就需要定义规则。比如,之前提到的var a = 1 + 1;,它表示:
- 左边var代表了这是申明(declaration),它申明了a这个变量
- 右边的+表示要将1和1做加法
- 中间的等号表示了这是个赋值语句
- 最后的分号表示这句语句结束了
上述这些就是规则,有了它就等于有了衡量的标准,JavaScript引擎就可以根据这个标准去解析JavaScript代码了。那么这里的ECMAScript就是定义了这些规则。其中ECMAScript 262这份文档,就是对JavaScript这门语言定义了一整套完整的标准。其中包括:
- var,if,else,break,continue等是JavaScript的关键词
- abstract,int,long等是JavaScript保留词
- 怎么样算是数字、怎么样算是字符串等等
- 定义了操作符(+,-,>,<等)
- 定义了JavaScript的语法
- 定义了对表达式,语句等标准的处理算法,比如遇到==该如何处理
- ⋯⋯
标准的JavaScript引擎就会根据这套文档去实现,注意这里强调了标准,因为也有不按照标准来实现的,比如IE的JS引擎。这也是为什么JavaScript会有兼容性的问题。至于为什么IE的JS引擎不按照标准来实现,就要说到浏览器大战了,这里就不赘述了,自行Google之。
所以,简单的说,ECMAScript定义了语言的标准,JavaScript引擎根据它来实现,这就是两者的关系。
3. JavaScript解析引擎与浏览器又是什么关系?
简单地说,JavaScript引擎是浏览器的组成部分之一。因为浏览器还要做很多别的事情,比如解析页面、渲染页面、Cookie管理、历史记录等等。那么,既然是组成部分,因此一般情况下JavaScript引擎都是浏览器开发商自行开发的。比如:IE9的Chakra、Firefox的TraceMonkey、Chrome的V8等等。
从而也看出,不同浏览器都采用了不同的JavaScript引擎。因此,我们只能说要深入了解哪个JavaScript引擎。
4. 深入了解其内部原理的途径有哪些?
搞清楚了前面三个问题,那这个问题就好回答了。个人认为,主要途径有如下几种(依次由浅入深):
- 看讲JavaScript引擎工作原理的书
这种方式最方便,不过我个人了解到的这样的书几乎没有,但是Dmitry A.Soshnikov博客上的文章真的是非常的赞,建议直接看英文,实在英文看起来吃力的,可以看我翻译的译本 - 看ECMAScript的标准文档
这种方式相对直接,原汁原味,因为引擎就是根据标准来实现的。目前来说,可以看第五版和第三版,不过要看懂也是不容易的。 - 看JS引擎源代码
这种方式最直接,当然也最难了。因为还牵涉到了如何实现词法分析器,语法分析器等等更加底层的东西了,而且并非所有的引擎代码都是开源的。
5. 以上几种方式中第一种都很难看明白怎么办?
其实第一种方式中的文章,作者已经将文档中内容提炼出来,用通俗易懂的方式阐述出来了。如果,看起来还觉得吃力,那说明还缺少两块的东西:
- 对JavaScript本身还理解的不够深入
如果你刚刚接触JavaScript,或者说以前甚至都没有接触过。那一下子就想要去理解内部工作原理,的确是很吃力的。首先应该多看看书,多实践实践,从知识和实践的方式来了解JavaScript预言特性。这种情况下,你只需要了解现象。比方说,(function(){})() 这样可以直接调用该匿名函数、用闭包可以解决循环中的延迟操作的变量值获取问题等等。要了解这些,都是需要多汲取和实践的。实践这里就不多说了,而知识汲取方面可以多看看书和博客。这个层面的书就相对比较多了,《Professional JavaScript for Web Developers》就是本很好的书(中文版请自行寻找)。 - 缺乏相应的领域知识
当JavaScript也达到一定深度了,但是,还是看不大明白,或者没法很深入到内部去一探究竟。那就意味着缺少对应的领域知识。这里明显的就是编译原理相关的知识。不过,其实对这块了解个大概基本看起来就没问题了。要再继续深入,那需要对编译原理了解的很深入,比如说词法分析采用什么算法,一般怎么处理。会有什么问题,如何解决,AST生成算法一般有哪几种等等。那要看编译原理方面的书,也有基本经典的书,比如《Compilers: Principles, Techniques, and Tools》这本也是传说中的龙书,还有非常著名的《SICP》和《PLAI》。不过其实根据个人经验,对于Dmitry的文章,要看懂它,只要你对JavaScript有一定深度的了解,同时你大学计算机的课程都能大致掌握了(尤其是操作系统),也就是说基础不错,理解起来应该没问题。因为这些文章基本没有涉及底层编译相关的,只是在解释文档的内容,并且其中很多东西都是相通的,比如:context的切换与CPU的进程切换、函数相关的的局部变量的栈存储、函数退出的操作等等都是一致的。
以上就是个人对这个问题的看法,除此之外,我觉得,学习任何技术都不能操之过急,要把基础打扎实了,这样学什么都会很快。
说说为什么 [] == ![] 为true
此前在微博上无意中看到有人问“为什么alert([] == ![])会是true?”,
刚看到这个问题我也说不上来究竟是什么原因,只知道这个肯定又是和==操作相关的类型转换问题。
于是,就翻开了“葵花宝典(ECMA-262-5th)”,你懂的。
在宝典的帮助下,我尝试着来解释下该问题的原因:
- 首先看看==这个操作内部是如何工作的
宝典中的关于==操作的工作描述如下(11.9.1):
The production EqualityExpression: EqualityExpression == RelationalExpression is evaluated as follows:
1. Let lref be the result of evaluating EqualityExpression
2. Let lval be GetValue(lref)
3. Let rref be the result of evaluating RelationalExpression
4. Let rval be GetValue(rref)
5. Return the result of performing abstract equality comparison rval==lval
- 根据上面的步骤,我们来对问题作如下解析:
- 先求GetValue([])
- 再求GetValue(![])
- 最后求 GetValue([]) == GetValue(![])
先要搞清楚GetValue方法是干嘛的,继续看宝典关于GetValue的描述(8.7.1):
- If Type(V) is not Reference, return V
- ….
对于解释我们的问题,看到这里就足够了,因为[]和![]都不属于Reference,所以,GetValue([])和GetValue(![])都返回自身。
这里关于什么是Reference不想再赘述了,要详细了解的可以看宝典(8.7)。
那么,上述问题进一步转化成了如下问题:
- GetValue([])为 []
- GetValue(![])为 false, (这里!会使得[]强制转化为Boolean类型)
- 这里就成了求 [] == false的问题
也就是说: [] == ![] 现在转化为了 [] == false。
- 根据”abstract equality comparison”算法来求结果:
宝典中第五步就提到了根据”abstract equality comparison”来求最后的结果。
现在先来看看[]和false的类型,两者类型显而易见,前者是Object,后者Boolean。
然后,我们进一步来看看这个算法是如何的(11.9.3),以下只列出了和我们这个问题相关的算法步骤,其中有这么一条:
The comparison x == y, where x and y are values, produces true or false. Such a comparison is performed as follows:
7. If Type(y) is Boolean, return the result of the comparison x == ToNumber(y)
这句话很容易理解,就是要把y转化类型为数值,也就是说false变为0。
这样以来,问题有变成了求: [] == 0
继续看宝典中这个算法(11.9.3),其中有这么一条:
The comparison x == y, where x and y are values, produces true or false. Such a comparison is performed as follows:
9. If Type(x) is Object and Type(y) is either String or Number, return the result of the comparison ToPrimitive(x) == y.
于是,问题有变成了求: ToPrimitive([]) == 0。
- 查看ToPrimitive的工作机制(9.1)
其中对于Object有这种转换描述:
Return a default value for the Object. The default value of an object is retrieved by calling the [[DefaultValue]] internal method of the object,
passing the optional hint PreferredType. The behaviour of the
[[DefaultValue]] internal method is defined by this specification for all native ECMAScript objects in 8.12.8.
继续顺藤摸瓜,看[[DefaultValue]](hint),我们的例子中hint是Number,因为它是和0去做比较。
根据宝典(8.12.8)描述:
When the [[DefaultValue]] internal method of O is called with hint Number, the following steps are taken:
1. Let valueOf be the result of calling the [[Get]] internal method of object O with argument “valueOf”.
2. If IsCallable(valueOf) is true then
a. Let val be the result of calling the [[Call]] internal method of valueOf, with O as the this value and an empty argument list.
b. If val is a primitive value, return val.
3. Let toString be the result of calling the [[Get]] internal method of object O with argument “toString”.
a. Let str be the result of calling the [[Call]] internal method of toString, with O as the this value and an empty argument list.
b. If str is a primitive value, return str.
…
这里不对valueOf再去做赘述了,MDN上面有很简短的说明,大致意思如下:
默认,每个对象都有从Object继承下来的valueOf方法。其中每个内置的核心对象都会重载该方法来返回正确的值,对于没有基础类型值的对象,则返回对象自身。
那么,对于我们的情况来说,进入了算法中的2,但是,因为val是对象不是基础类型,所以继续进入第3步,这个时候关键来了:
开始调用[]的toString方法,这个时候会返回"",一个空的字符串,因此ToPrimitive([])为""
因此,问题又转化成了:
"" == 0
现在答案就很明显了,根据宝典的==工作原理如下描述(11.9.3):
The comparison x == y, where x and y are values, produces true or false. Such a comparison is performed as follows:
5. If Type(x) is String and Type(y) is Number, return the result of the comparison ToNumber(x) == y.
好了,根据算法,会将""转化为数值类型,那么自然就变成了0,于是 0 == 0 是很自然而然的。
总结:
最终问题就从: [] == ![] 变成了 0 == 0。答案自然是true了。
其实遇到这种语言层面的问题,直接看宝典即可。
说明:
以上诸如 11.9.3 这样的数字均表示葵花宝典中的章节。
参考资料
JavaScript内部原理实践——真的懂JavaScript吗?
通过翻译了Dmitry A.Soshnikov的关于ECMAScript-262-3 JavaScript内部原理的文章,
从理论角度对JavaScript中部分特性的内部工作机制有了一定的了解。
但是,邓爷爷说过:“实践才是检验真理的唯一标准”。
所以,我打算通过从内部原理来解释一些经常在笔试或者面试中遇到的关于JavaScript语言层面的题目来进一步学习和掌握JavaScript内部工作原理。
那么,首先就是要去找那些题目,google了一圈终于找到了来自Dmitry Baranovskiy的非常著名的5个问题, 这5个问题,NCZ给出了非常清楚的解释。 不过,我还是想尝试下从low-level——JavaScript内部工作机制的角度去解释下这些问题。
好吧,我承认我废话很多,那就开始吧。
问题 #1
if (!("a" in window)) {
var a = 1;
}
alert(a);
- 正确答案: undefined
解释:
这个问题,初一看感觉答案很自然是1。因为从上到下执行下去,if语句中的条件应该为 true,因为”a”的确是没有定义啊。
随后,顺理成章地进入 var a = 1;,最后,alert出来就应该是1。
而事实上,从JavaScript内部工作原理去看,在变量对象中讲过, JavaScript处理上下文分为两个阶段:
- 进入执行上下文
- 执行代码
可以理解为,第一个阶段是静态处理阶段,第二个阶段为动态处理阶段。
而在静态处理阶段,就会创建 变量对象(variable object),并且将变量申明作为属性进行填充。
到了执行阶段,才会根据执行情况,来对变量对象中属性(就是申明的变量)的值进行更新。
针对这个问题,其实际过程如下:
- 进入执行上下文: 创建VO,并填充变量申明 a,VO如下所示:
VO(global) = {
a: undefined
}
所以,这个时候,a其实已经存在了。
- 执行代码: 进入 if语句,发现条件判断 “a” in window 为true。于是就不会进入if代码块,直接执行alert语句,因此,最终为undefined。
问题 #2
var a = 1,
b = function a(x) {
x && a(--x);
};
alert(a);
- 正确答案: 1
解释:
这个问题,第一反应可能会是将 function a打印出来。因为明明就看到了function a了。看似,也顺其自然。
但是,事实并非如此。还是和此前一个问题一样。从两个阶段来分析:
- 进入执行上下文: 这个时候要注意了, b = function a(){},这里的 function a并非函数申明,因为整个这个句话属于赋值语句(assignment statement),所以,这里的 function a会被看作是函数表达式。 函数表达式是不会对VO造成影响的。所以,这个时候VO中其实只有 a和x(函数形参):
VO(global) = {
a: undefined,
b: undefined
}
- 执行代码: 这个时候a的值修改为1:
VO(global) = {
x: undefined,
a: 1
}
所以,最后alert(a)的结果是1。
问题 #3
function a(x) {
return x * 2;
}
var a;
alert(a);
- 正确答案: 函数a
解释:
这个问题,很多人可能会以为是: undefined。理由可能是,明明看到了 var a定义在了function a的后面,感觉应该会覆盖之前a的申明。
事实又是怎样的呢? 老套路,从两个阶段来分析:
- 进入执行上下文: 这里出现了名字一样的情况,一个是函数申明,一个是变量申明。那么,根据变量对象
介绍的,填充VO的顺序是: 函数的形参 -> 函数申明 -> 变量申明。
上述例子中,变量a在函数a后面,那么,变量a遇到函数a怎么办呢?还是根据 变量对象中介绍的,当变量申明遇到VO中已经有同名的时候,不会影响已经存在的属性。因此,VO如下所示:
VO(global) = {
a: 引用了函数申明“x”
}
- 执行代码:啥也没变化
所以,最终的结果是:函数a。
问题 #4
function b(x, y, a) {
arguments[2] = 10;
alert(a);
}
b(1, 2, 3);
- 正确答案: 10
解释:
个人感觉这个问题其实不是很复杂。这里也不需要从两个阶段去分析了。根据 变量对象中介绍的,arguments对象的properties-indexes和实际传递的参数是共享的
也就是说,通过arguments[2]修改的参数,也会影响到a,所以,这里的值是10。但是,要注意的是和实际传递的值,所以,如果把上述问题改成如下形式:
function b(x, y, a) {
arguments[2] = 10;
alert(a);
}
b(1, 2);
结果就会是: undefined。因为,并没有传递a的值。
问题 #5
function a() {
alert(this);
}
a.call(null);
- 正确答案: 全局对象(window)
解释:
这个问题,可能会比较困惑。因为懂call的童鞋都会觉得,call的时候把null传递为了当前的上下文了。里面的this应该是null才对啊。
事实却是: 前面都没错,this会是null。但是,this中介绍过,null是没有任何意义的,因此,最终会变成全局对象。
所以,这里结果就变成了全局对象。 这里还有ECMAScript-262-3标准文档中的一句话作为证据:
“If thisArg is null or undefined, the called function is passed the global object as the this value. Otherwise, the called function is passed ToObject(thisArg) as the this value.”
总结
上面这5个问题其实也只是牵涉到了JavaScript内部原理中的部分知识点,要想了解更多,还是建议读完JavaScript内部原理系列 以及去看Dmitry A.Soshnikov的文章。
函数(Functions)
说明
此文译自Dmitry A.Soshnikov 的文章Functions
概要
本文将给大家介绍ECMAScript中的一般对象之一——函数。我们将着重介绍不同类型的函数以及不同类型的函数是如何影响上下文的变量对象以及函数的作用域链的。 我们还会解释经常会问到的问题,诸如:“不同方式创建出来的函数会不一样吗?(如果会,那么到底有什么不一样呢?)”:
var foo = function () {
...
};
上述方式创建的函数和如下方式创建的有什么不同?
function foo() {
...
}
如下代码中,为啥一个函数要用括号包起来呢?
(function () {
...
})();
由于本文和此前几篇文章都是有关联的,因此,要想完全搞懂这部分内容,建议先去阅读第二章-变量对象 以及第四章-作用域链。
下面,来我们先来介绍下函数类型。
函数类型
ECMAScript中包含三类函数,每一类都有各自的特性。
函数声明(Function Declaration)
函数声明(简称FD)是指这样的函数
* 有函数名
* 代码位置在:要么在程序级别或者直接在另外一个函数的函数体(FunctionBody)中
* 在进入上下文时创建出来的
* 会影响变量对象
* 是以如下形式声明的
function exampleFunc() {
...
}
这类函数的主要特性是:只有它们可以影响变量对象(存储在上下文的VO中)。此特性同时也引出了非常重要的一点(变量对象的天生特性导致的) —— 它们在执行代码阶段就已经存在了(因为FD在进入上下文阶段就收集到了VO中)。
下面是例子(从代码位置上来看,函数调用在声明之前):
foo();
function foo() {
alert('foo');
}
从定义中还提到了非常重要的一点 —— 函数声明在代码中的位置:
// 函数声明可以直接在程序级别的全局上下文中
function globalFD() {
// 或者直接在另外一个函数的函数体中
function innerFD() {}
}
除了上述提到了两个位置,其他位置均不能出现函数声明 —— 比方说,在表达式的位置或者是代码块中进行函数声明都是不可以的。
介绍完了函数声明,接下来介绍函数表达式(function expression)。
函数表达式
函数表达式(简称:FE)是指这样的函数:
* 代码位置必须要在表达式的位置
* 名字可有可无
* 不会影响变量对象
* 在执行代码阶段创建出来
这类函数的主要特性是:它们的代码总是在表达式的位置。最简单的表达式的例子就是赋值表达式:
var foo = function () {
...
};
上述例子中将一个匿名FE赋值给了变量“foo”,之后该函数就可以通过“foo”来访问了—— foo()。
正如定义中提到的,FE也可以有名字:
var foo = function _foo() {
...
};
这里要注意的是,在FE的外部可以通过变量“foo”——foo()来访问,而在函数内部(比如递归调用),还可以用“_foo”(译者注:但在外部是无法使用“_foo”的)。
当FE有名字的时候,它很难和FD作区分。不过,如果仔细看这两者的定义的话,要区分它们还是很容易的: FE总是在表达式的位置。 如下例子展示的各类ECMAScript表达式都属于FE:
// 在括号中(grouping operator)只可能是表达式
(function foo() {});
// 在数组初始化中 —— 同样也只能是表达式
[function bar() {}];
// 逗号操作符也只能跟表达式
1, function baz() {};
定义中还提到FE是在执行代码阶段创建的,并且不是存储在变量对象上的。如下所示:
// 不论是在定义前还是定义后,FE都是无法访问的
// (因为它是在代码执行阶段创建出来的),
alert(foo); // "foo" is not defined
(function foo() {});
// 后面也没用,因为它根本就不在VO中
alert(foo); // "foo" is not defined
问题来了,FE要来干嘛?其实答案是很明显的 —— 在表达式中使用,从而避免对变量对象造成“污染”。最简单的例子就是将函数作为参数传递给另外一个函数:
function foo(callback) {
callback();
}
foo(function bar() {
alert('foo.bar');
});
foo(function baz() {
alert('foo.baz');
});
上述例子中,部分变量存储了对FE的引用,这样函数就会保留在内存中并在之后,可以通过变量来访问(因为变量是可以影响VO的):
var foo = function () {
alert('foo');
};
foo();
如下例子是通过创建一个封装的作用域来对外部上下文隐藏辅助数据(例子中我们使用FE使得函数创建后就立马执行):
var foo = {};
(function initialize() {
var x = 10;
foo.bar = function () {
alert(x);
};
})();
foo.bar(); // 10;
alert(x); // "x" is not defined
我们看到函数“foo.bar”(通过其[[Scope]]属性)获得了对函数“initialize”内部变量“x”的访问。 而同样的“x”在外部就无法访问到。很多库都使用这种策略来创建“私有”数据以及隐藏辅助数据。通常,这样的情况下FE的名字都会省略掉:
(function () {
// 初始化作用域
})();
还有一个FE的例子是:在执行代码阶段在条件语句中创建FE,这种方式也不会影响VO:
var foo = 10;
var bar = (foo % 2 == 0
? function () { alert(0); }
: function () { alert(1); }
);
bar(); // 0
“有关括号”的问题
让我们回到本文之初,来回答下此前提到的问题 —— “为什么在函数创建之后立即进行函数调用时,需要用括号将其包起来?”。 要回答此问题,需要先介绍下关于表达式语句的限制。
标准中提到,表达式语句(ExpressionStatement)不能以左大括号{开始 —— 因为这样一来就和代码块冲突了, 也不能以function关键字开始,因为这样一来又和函数声明冲突了。比方说,以如下所示的方式来定义一个立马要执行的函数:
function () {
...
}();
// or with a name
function foo() {
...
}();
对于这两种情况,解释器都会抛出错误,只是原因不同。
如果我们是在全局代码(程序级别)中这样定义函数,解释器会以函数声明来处理,因为它看到了是以function开始的。 在第一个例子中,会抛出语法错误,原因是既然是个函数声明,则缺少函数名了(一个函数声明其名字是必须的)。
而在第二个例子中,看上去已经有了名字了(foo),应该会正确执行。然而,这里还是会抛出语法错误 —— 组操作符内部缺少表达式。 这里要注意的是,这个例子中,函数声明后面的()会被当组操作符来处理,而非函数调用的()。因此,如果我们有如下代码:
// "foo" 是函数声明
// 并且是在进入上下文的时候创建的
alert(foo); // function
function foo(x) {
alert(x);
}(1); // 这里只是组操作符,并非调用!
foo(10); // 这里就是调用了, 10
上述代码其实就是如下代码:
// function declaration
function foo(x) {
alert(x);
}
// 含表达式的组操作符
(1);
// 另外一个组操作符
// 包含一个函数表达式
(function () {});
// 这里面也是表达式
("foo");
// etc
当这样的定义出现在语句位置时,也会发生冲突并产生语法错误:
if (true) function foo() {alert(1)}
上述结构根据标准规定是不合法的。(表达式是不能以function关键字开始的),然而,正如我们在后面要看到的,没有一种实现对其抛出错误, 它们各自按照自己的方式在处理。
讲了这么多,那究竟要怎么写才能达到创建一个函数后立马就进行调用的目的呢? 答案是很明显的。它必须要是个函数表达式,而不能是函数声明。而创建表达式最简单的方式就是使用上述提到的组操作符。因为在组操作符中只可能是表达式。 这样一来解释器也不会纠结了,会果断将其以FE的方式来处理。这样的函数将在执行阶段创建出来,然后立马执行,随后被移除(如果有没有对其的引用的话):
(function foo(x) {
alert(x);
})(1); // 好了,这样就是函数调用了,而不再是组操作符了,1
要注意的是,在下面的例子中,函数调用,其括号就不再是必须的了,因为函数本来就在表达式的位置了,解释器自然会以FE来处理,并且会在执行代码阶段创建该函数:
var foo = {
bar: function (x) {
return x % 2 != 0 ? 'yes' : 'no';
}(1)
};
alert(foo.bar); // 'yes'
因此,对“括号有关”问题的完整的回答则如下所示:
如果要在函数创建后立马进行函数调用,并且函数不在表达式的位置时,括号就是必须的 —— 这样情况下,其实是手动的将其转换成了FE。 而当解释器直接将其以FE的方式处理的时候,说明FE本身就在函数表达式的位置 —— 这个时候括号就不是必须的了。
另外,除了使用括号的方式将函数转换成为FE之外,还有其他的方式,如下所示:
1, function () {
alert('anonymous function is called');
}();
// 或者这样
!function () {
alert('ECMAScript');
}();
// 当然,还有其他很多方式
...
不过,括号是最通用也是最优雅的方式。
顺便提下,组操作符既可以包含没有调用括号的函数,又可以包含有调用括号的函数,这两者都是合法的FE:
(function () {})();
(function () {}());
实现扩展: 函数语句
看如下代码,符合标准的解释器都无法解释这样的代码:
if (true) {
function foo() {
alert(0);
}
} else {
function foo() {
alert(1);
}
}
foo(); // 1 还是 0 ? 在不同引擎中测试
这里有必要提下:根据标准,上述代码结构是不合法的,因为,此前我们就介绍过,函数声明是不能出现在代码块中的(这里if和else就包含代码块)。 此前提到的,函数声明只能出现在两个位置: 程序级别或者另外一个函数的函数体中。
为什么这种结构是错误的呢?因为在代码块中只允许语句。函数要想在这个位置出现的唯一可能就是要成为表达式语句。 但是,根据定义表达式语句又不能以左大括号开始(这样会与代码块冲突)也不能以function关键字开始(这样又会和FD冲突)。
然而,在错误处理部分,标准允许实现对程序语法进行扩展。而上述例子就是其中一种扩展。目前,所有的实现中都不会对上述情况抛出错误,都会以各自的方式进行处理。
因此根据标准,上述if-else中应当需要FE。然而,绝大多数实现中都在进入上下文的时候在这里简单地创建了FD,并且使用了最后一次的声明。 最后“foo”函数显示了1,尽管理论上else中的代码根本不会被执行到。
而SpiderMonkey(TraceMonkey也是)实现中,会将上述情况以两种方式来处理: 一方面它不会将这样的函数以函数声明来处理(也就意味着函数会在执行代码阶段才会创建出来), 然而,另外一方面,它们又不属于真正的函数表达式,因为在没有括号的情况是不能作函数调用的(同样会有解析错误——和FD冲突),它们还是存储在变量对象中。
我认为SpiderMonkey单独引入了自己的中间函数类型——(FE+FD),这样的做法是正确的。这样的函数会根据时间和对应的条件正确创建出来,不像FE。 和FD有点类似,可以在外部对其进行访问。SpiderMonkey将这种语法扩展命名为函数语句(Function Statement)(简称FS);这部分理论在MDC中有具体的介绍。 JavaScript的发明者 Brendan Eich也提到过这类函数类型。
有名字的函数表达式的特性(NFE)
当FE有名字之后(named function expression,简称:NFE),就产生了一个重要的特性。 正如在定义中提到的,函数表达式是不会影响上下文的变量对象的(这就意味着不论是在定义前还是在定义后,都是不可能通过名字来进行调用的)。 然而,FE可以通过自己的名字进行递归调用:
(function foo(bar) {
if (bar) {
return;
}
foo(true); // "foo" name is available
})();
// but from the outside, correctly, is not
foo(); // "foo" is not defined
这里“foo”这个名字究竟保存在哪里呢?在foo的活跃对象中吗?非也,因为在foo函数中根本就没有定义任何“foo”。 那么是在上层上下文的变量对象中吗?也不是,因为根据定义——FE是不会影响VO的——正如我们在外层对其调用的结果所看到的那样。 那么,它究竟保存在哪里了呢?
不卖关子了,马上来揭晓。当解释器在执行代码阶段看到了有名字的FE之后,它会在创建FE之前,创建一个辅助型的特殊对象,并把它添加到当前的作用域链中。 然后,再创建FE,在这个时候(根据第四章-作用域链描述的),函数拥有了[[Scope]]属性 —— 创建函数所在上下文的作用域链(这个时候,在[[Scope]]就有了那个特殊对象)。 之后,特殊对象中唯一的属性 —— FE的名字添加到了该对象中;其值就是对FE的引用。在最后,当前上下文退出的时候,就会把该特殊对象移除。 用伪代码来描述此算法就如下所示:
specialObject = {};
Scope = specialObject + Scope;
foo = FunctionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}
delete Scope[0]; // 从作用域链的最前面移除specialObject
这就是为什么在函数外是无法通过名字访问到该函数的(因为它并不在上层作用域中存在),而在函数内部却可以访问到。
而这里要注意的一点是: 在某些实现中,比如Rhino,FE的名字并不是保存在特殊对象中的,而是保存在FE的活跃对象中。 再比如微软的实现 —— JScript,则完全破坏了FE的规则,直接将该名字保存在上层作用域的变量对象中了,这样在外部也可以访问到。
NFE和SpiderMonkey
说到实现,部分版本的SpiderMonkey有一个与上述提到的特殊对象相关的特性,这个特性也可以看作是个bug(既然所有的实现都是严格遵循标准的,那么这个就是标准的问题了)。 此特性和标识符处理相关: 作用域链的分析是二维的,在标识符查询的时候,还要考虑作用域链中每个对象的原型链。
当在Object.prototype对象上定义一个属性,并将该属性值指向一个“根本不存在”的变量时,就能够体现该特性。 比如,如下例子中的变量“x”,在查询过程中,通过作用域链,一直到全局对象也是找不到“x”的。 然而,在SpiderMonkey中,全局对象继承自Object.prototype,于是,对应的值就在该对象中找到了:
Object.prototype.x = 10;
(function () {
alert(x); // 10
})();
活跃对象是没有原型一说的。可以通过内部函数还证明。 如果在定义一个局部变量“x”并声明一个内部函数(FD或者匿名的FE),然后,在内部函数中引用变量“x”,这个时候该变量会在上层函数上下文中查询到(理应如此),而不是在Object.prototype中:
Object.prototype.x = 10;
function foo() {
var x = 20;
// function declaration
function bar() {
alert(x);
}
bar(); // 20, from AO(foo)
// the same with anonymous FE
(function () {
alert(x); // 20, also from AO(foo)
})();
}
foo();
在有些实现中,存在这样的异常:它们会在活跃对象设置原型。比方说,在Blackberry的实现中,上述例子中变量“x”值就会变成10。 因为,“x”从Object.prototype中就找到了:
AO(bar FD or anonymous FE) -> no ->
AO(bar FD or anonymous FE).[[Prototype]] -> yes - 10
当出现有名字的FE的特殊对象的时候,在SpiderMonkey中也是有同样的异常。该特殊对象是常见对象 —— “和通过new Object()表达式产生的一样”。 相应地,它也应当继承自Object.prototype,上述描述只针对SpiderMonkey(1.7版本)。其他的实现(包括新的TraceMonkey)是不会给这个特殊对象设置原型的:
function foo() {
var x = 10;
(function bar() {
alert(x); // 20, but not 10, as don't reach AO(foo)
// "x" is resolved by the chain:
// AO(bar) - no -> __specialObject(bar) -> no
// __specialObject(bar).[[Prototype]] - yes: 20
})();
}
Object.prototype.x = 20;
foo();
NFE和JScript
微软的实现——JScript,是IE的JS引擎(截至本文撰写时最新是JScript5.8——IE8),该引擎与NFE相关的bug有很多。每个bug基本上都和ECMA-262-3rd标准是完全违背的。 有些甚至会引发严重的错误。
第一,针对上述这样的情况,JScript完全破坏了FE的规则:不应当将函数名字保存在变量对象中的。 另外,FE的名字应当保存在特殊对象中,并且只有在函数自身内部才可以访问(其他地方均不可以)。而JScript却将其直接保存在上层上下文的变量对象中。 并且,JScript居然还将FE以FD的方式处理,在进入上下文的时候就将其创建出来,并在定义之前就可以访问到:
// FE 保存在变量对象中
// 和FD一样,在定义前就可以通过名字访问到
testNFE();
(function testNFE() {
alert('testNFE');
});
// 同样的,在定义之后也可以通过名字访问到
testNFE();
正如大家所见,完全破坏了FE的规则。
第二,在声明同时,将NFE赋值给一个变量的时候,JScript会创建两个不同的函数对象。 这种行为感觉完全不符合逻辑(特别是考虑到在NFE外层,其名字根本是无法访问到的):
var foo = function bar() {
alert('foo');
};
alert(typeof bar); // "function", NFE 有在VO中了 – 这里就错了
// 然后,还有更有趣的
alert(foo === bar); // false!
foo.x = 10;
alert(bar.x); // undefined
// 然而,两个函数完全做的是同样的事情
foo(); // "foo"
bar(); // "foo"
然而,要注意的是: 当将NFE和赋值给变量这两件事情分开的话(比如,通过组操作符),在定义好后,再进行变量赋值,这样,两个对象就相同了,返回true:
(function bar() {});
var foo = bar;
alert(foo === bar); // true
foo.x = 10;
alert(bar.x); // 10
这个时候就好解释了。实施上,一开始的确创建了两个对象,不过之后就只剩下一个了。这里将NFE以FD的方式来处理,然后,当进入上下文的时候,FD bar就创建出来了。 在这之后,到了执行代码阶段,又创建出了第二个对象 —— FE bar,该对象不会进行保存。相应的,由于没有变量对其进行引用,随后FE bar对象就被移除了。 因此,这里就只剩下一个对象——FD bar对象,对该对象的引用就赋值给了foo变量。
第三,通过arguments.callee对一个函数进行间接引用,它引用的是和激活函数名一致的对象(事实上是——函数,因为有两个对象):
var foo = function bar() {
alert([
arguments.callee === foo,
arguments.callee === bar
]);
};
foo(); // [true, false]
bar(); // [false, true]
第四,JScript会将NFE以FD来处理,但当遇到条件语句又不遵循此规则了。比如说,和FD那样,NFE会在进入上下文的时候就创建出来,这样最后一次定义的就会被使用:
var foo = function bar() {
alert(1);
};
if (false) {
foo = function bar() {
alert(2);
};
}
bar(); // 2
foo(); // 1
上述行为从逻辑上也是可以解释通的: 当进入上下文的时候,最后一次定义的FD bar被创建出来(有alert(2)的函数), 之后到了执行代码阶段又一个新的函数 —— FE bar被创建出来,对其引用赋值给了变量foo。因此(if代码块中由于判断条件是false,因此其代码块中的代码永远不会被执行到)foo函数的调用会打印出1。 尽管“逻辑上”是对的,但是这个仍然算是IE的bug。因为它明显就破坏了实现的规则,所以我这里用了引号“逻辑上”。
第五个JScript中NFE的bug和通过给一个未受限的标识符赋值(也就是说,没有var关键字)来创建全局对象的属性相关。 由于这里NFE会以FD的方式来处理,并相应地会保存在变量对象上,赋值给未受限的标识符(不是给变量而是给全局对象的一般属性), 当函数名和标识符名字相同的时候,该属性就不会是全局的了。
(function () {
// 没有var,就不是局部变量,而是全局对象的属性
foo = function foo() {};
})();
// 然而,在匿名函数的外层,foo又是不可访问的
alert(typeof foo); // undefined
这里从“逻辑上”又是可以解释通的: 进入上下文时,函数声明在匿名函数本地上下文的活跃对象中。 当进入执行代码阶段的时候,因为foo这个名字已经在AO中存在了(本地),相应地,赋值操作也只是简单的对AO中的foo进行更新而已。 并没有在全局对象上创建新的属性。
通过Function构造器创建的函数
这类函数有别于FD和FE,有自己的专属特性: 它们的[[Scope]]属性中只包含全局对象:
var x = 10;
function foo() {
var x = 20;
var y = 30;
var bar = new Function('alert(x); alert(y);');
bar(); // 10, "y" is not defined
}
我们看到bar函数的[[Scope]]属性并未包含foo上下文的AO —— 变量“y”是无法访问的,并且变量“x”是来自全局上下文。 顺便提下,这里要注意的是,Function构造器可以通过new关键字和省略new关键字两种用法。上述例子中,这两种用法都是一样的。
此类函数其他特性则和同类语法产生式以及联合对象有关。 该机制在标准中建议在作优化的时候采用(当然,具体的实现者也完全有权利不使用这类优化)。比方说,有100元素的数组,在循环数组过程中会给数组每个元素赋值(函数), 这个时候,实现的时候就可以采用联合对象的机制了。这样,最终所有的数组元素都会引用同一个函数(只有一个函数):
var a = [];
for (var k = 0; k < 100; k++) {
a[k] = function () {}; // 这里就可以使用联合对象
}
但是,通过Function构造器创建的函数就无法使用联合对象了:
var a = [];
for (var k = 0; k $lt; 100; k++) {
a[k] = Function(''); // 只能是100个不同的函数
}
下面是另外一个和联合对象相关的例子:
function foo() {
function bar(z) {
return z * z;
}
return bar;
}
var x = foo();
var y = foo();
上述例子,在实现过程中同样可以使用联合对象。来使得x和y引用同一个对象,因为函数(包括它们内部的[[Scope]]属性)物理上是不可分辨的。 因此,通过Function构造器创建的函数总是会占用更多内存资源。
函数创建的算法
如下所示使用伪代码表示的函数创建的算法(不包含联合对象的步骤)。有助于理解ECMAScript中的函数对象。此算法对所有函数类型都是一样的。
F = new NativeObject();
// 属性 [[Class]] is "Function"
F.[[Class]] = "Function"
// 函数对象的原型
F.[[Prototype]] = Function.prototype
// 对函数自身引用
// [[Call]] 在函数调用时F()激活
// 同时创建一个新的执行上下文
F.[[Call]] = <reference to function>
// 内置的构造器
// [[Construct]] 会在使用“new”关键字的时候激活
// 事实上,它会为新对象申请内存
// 然后调用 F.[[Call]]来初始化创建的对象,将this值设置为新创建的对象
F.[[Construct]] = internalConstructor
// 当前上下文(创建函数F的上下文)的作用域名链
F.[[Scope]] = activeContext.Scope
// 如果是通过new Function(...)来创建的,则
F.[[Scope]] = globalContext.Scope
// 形参的个数
F.length = countParameters
// 通过F创建出来的对象的原型
__objectPrototype = new Object();
__objectPrototype.constructor = F // {DontEnum}, 在遍历中不能枚举
F.prototype = __objectPrototype
return F
要注意的是,F.[[Prototype]]是函数(构造器)的原型,而F.prototype是通过该函数创建出来的对象的原型(因为通常对这两个概念都会混淆,在有些文章中会将F.prototype叫做“构造器的原型”,这是错误的)。
总结
本文介绍了很多关于函数的内容;不过在后面的关于对象和原型的文章中,还会提到函数作为构造器是如何工作的。
扩展阅读
ECMAScript标准:
另外一篇文章:
this
说明
此文译自Dmitry A.Soshnikov 的文章this
概要
本文将进一步讨论与执行上下文密切相关的概念——this关键字。
事实证明,this这块的内容非常的复杂,它在不同执行上下文的情况下其值都会不同,并且会相应的引发一些问题。
很多程序员一看到this关键字,就会把它和面向对象的编程方式联系在一起,它指向利用构造器新创建出来的对象。在ECMAScript中,也支持this,然而, 正如大家所熟知的,this不仅仅只用来表示创建出来的对象。
接下来给大家揭开在ECMAScript中this神秘的面纱。
定义
This是执行上下文的一个属性:
activeExecutionContext = {
VO: {...},
this: thisValue
};
这里的VO就是前一章介绍的变量对象。
This与上下文的可执行代码类型有关,其值在进入上下文阶段就确定了,并且在执行代码阶段是不能改变的。
下面就来详细对其作个介绍。
全局代码中This的值
这种情况下,一切都变得非常简单,this的值总是全局对象本身;因此,可以间接地获取引用:
// 显式定义全局对象的属性
this.a = 10; // global.a = 10
alert(a); // 10
// 通过赋值给不受限的标识符来进行隐式定义
b = 20;
alert(this.b); // 20
// 通过变量声明来进行隐式定义
// 因为全局上下文中的变量对象就是全局对象本身
var c = 30;
alert(this.c); // 30
函数代码中This的值
当this在函数代码中的时候,事情就变得有趣多了。这种情况下是最复杂的,并且会引发很多的问题。
函数代码中this值的第一个特性(同时也是最主要的特性)就是:它并非静态的绑定在函数上。
正如此前提到的,this的值是在进入上下文的阶段确定的,并且在函数代码中的话,其值每次都会大不相同。
然而,一旦进入执行代码阶段,其值就不能改变了。比方说,要想给this赋一个新的值是不可能的,因为this根本就不是变量(相反的,在Python语言中,它显示定义的self对象是可以在运行时随意更改的):
var foo = {x: 10};
var bar = {
x: 20,
test: function () {
alert(this === bar); // true
alert(this.x); // 20
this = foo; // error, 不能更改this的值
alert(this.x); // 如果没有错误,则其值为10而不是20
}
};
// 在进入上下文的时候,this的值就确定了是“bar”对象
// 至于为什么,会在后面作详细介绍
bar.test(); // true, 20
foo.test = bar.test;
// 但是,这个时候,this的值又会变成“foo”
// 纵然我们调用的是同一个函数
foo.test(); // false, 10
因此,在函数代码中影响this值的因素是有很多的。
首先,在一般的函数调用中,this的值是由激活上下文代码的调用者决定的,比如说,调用函数的外层上下文。this的值是由调用表达式的形式决定的。
理解并谨记这一点是非常必要的,有利于在任何上下文中都能准确的确定this的值。
影响调用上下文中的this的值的只有可能是调用表达式的形式,也就是调用函数的方式。 (一些关于JavaScript的文章和书籍中指出的“this的值取决于函数的定义方式,如果是全局函数,则this的值就会设置为全局对象,如果是某个对象的方法,则this的值就会设置为该对象”——这纯属扯淡,根本就是在误人子弟)。 正如此前大家看到的,纵然是全局函数,this的值也会随着函数调用方式的不同而不同:
function foo() {
alert(this);
}
foo(); // global
alert(foo === foo.prototype.constructor); // true
// 然而,同样的函数,以另外一种调用方式的话,this的值就不同了
foo.prototype.constructor(); // foo.prototype
调用一个对象的某个方法的时候,this的值也有可能不是该对象的:
var foo = {
bar: function () {
alert(this);
alert(this === foo);
}
};
foo.bar(); // foo, true
var exampleFunc = foo.bar;
alert(exampleFunc === foo.bar); // true
// 同样地,相同的函数以不同的调用方式,this的值也就不同了
exampleFunc(); // global, false
那么,究竟调用表达式的方式是如何影响this的值的呢?为了完全搞明白这其中的奥妙,首先,这里有必要先介绍一种内部类型——引用类型(the Reference type)。
引用类型
引用类型的值可以用伪代码表示为一个拥有两个属性的对象——base属性(属性所属的对象)以及该base对象中的propertyName属性:
var valueOfReferenceType = {
base: ,
propertyName:
};
引用类型的值只有可能是以下两种情况:
- 当处理一个标识符的时候
- 或者进行属性访问的时候
关于标识符的处理会在第四章——所用域链中作介绍,这里我们只要注意的是,此算法总返回一个引用类型的值(这对this的值是至关重要的)。
标识符其实就是变量名,函数名,函数参数名以及全局对象的未受限的属性。如下所示:
var foo = 10;
function bar() {}
中间过程中,对应的引用类型的值如下所示:
var fooReference = {
base: global,
propertyName: 'foo'
};
var barReference = {
base: global,
propertyName: 'bar'
};
要从引用类型的值中获取一个对象实际的值需要GetValue方法,该方法用伪代码可以描述成如下形式:
function GetValue(value) {
if (Type(value) != Reference) {
return value;
}
var base = GetBase(value);
if (base === null) {
throw new ReferenceError;
}
return base.[[Get]](GetPropertyName(value));
}
上述代码中的[[Get]]方法返回了对象属性实际的值,包括从原型链中继承的属性:
GetValue(fooReference); // 10
GetValue(barReference); // function object "bar"
对于属性访问来说,有两种方式: 点符号(这时属性名是正确的标识符并且提前已经知道了)或者中括号符号:
foo.bar();
foo['bar']();
中间过程中,得到如下的引用类型的值:
var fooBarReference = {
base: foo,
propertyName: 'bar'
};
GetValue(fooBarReference); // function object "bar"
问题又来了,引用类型的值又是如何影响函数上下文中this的值的呢?——非常重要。这也是本文的重点。总的来说,决定函数上下文中this的值的规则如下所示:
函数上下文中this的值是函数调用者提供并且由当前调用表达式的形式而定的。 如果在调用括号()的左边,有引用类型的值,那么this的值就会设置为该引用类型值的base对象。 所有其他情况下(非引用类型),this的值总是null。然而,由于null对于this来说没有任何意义,因此会隐式转换为全局对象。
如下所示:
function foo() {
return this;
}
foo(); // global
上述代码中,调用括号的左侧是引用类型的值(因为foo是标识符):
var fooReference = {
base: global,
propertyName: 'foo'
};
相应的,this的值会设置为引用类型值的base对象,这里就是全局对象。
属性访问也是类似的:
var foo = {
bar: function () {
return this;
}
};
foo.bar(); // foo
同样的,也是引用类型的值,它的base对象是foo对象,激活bar函数的时候,this的值就设置为foo对象了:
var fooBarReference = {
base: foo,
propertyName: 'bar'
};
然而,同样的函数以不同的激活方式的话,this的值就完全不同了:
var test = foo.bar;
test(); // global
因为test也是标识符,这样就产生了另外的引用类型的值,其中base对象(全局对象)就是this的值:
var testReference = {
base: global,
propertyName: 'test'
};
至此,我们就可以精确的解释,为什么同样的函数,以不同的调用方式激活,this的值也会不同了——答案就是处理过程中,是不同的引用类型的值:
function foo() {
alert(this);
}
foo(); // global, 因为
var fooReference = {
base: global,
propertyName: 'foo'
};
alert(foo === foo.prototype.constructor); // true
// 另一种调用方式
foo.prototype.constructor(); // foo.prototype, 因为
var fooPrototypeConstructorReference = {
base: foo.prototype,
propertyName: 'constructor'
};
如下是另外一种(典型的)利用调用表达式来动态决定this值的例子:
function foo() {
alert(this.bar);
}
var x = {bar: 10};
var y = {bar: 20};
x.test = foo;
y.test = foo;
x.test(); // 10
y.test(); // 20
函数调用以及非引用类型
正如此前提到过的,当调用括号左侧为非引用类型的时候,this的值会设置为null,并最终变成全局对象。
我们来考虑下如下表达式:
(function () {
alert(this); // null => global
})();
上述例子中,有函数对象,但非引用类型对象(因为它不既不是标识符也不属于属性访问),因此,this的值最终设置为全局对象。
如下是更为复杂的例子:
var foo = {
bar: function () {
alert(this);
}
};
foo.bar(); // Reference, OK => foo
(foo.bar)(); // Reference, OK => foo
(foo.bar = foo.bar)(); // global?
(false || foo.bar)(); // global?
(foo.bar, foo.bar)(); // global?
看了上述代码,你可能又有疑问了:为什么明明是属性访问,但是最终this的值不是base对象而是全局对象呢?
这里主要疑问在最后三个表达式,这三个表达式添加了特定的操作之后,调用括号左侧就不再是引用类型的值了。
第一种情况——非常明确,是引用类型,最终this的值设置为base对象,foo。
第二种情况有一个组操作符(grouping operator),该操作符不会触发调用获取引用类型实际值的方法,比如:GetValue方法。 相应的,处理组操作符中间过程中——获得的仍然是一个引用类型的值,这也就解释了为什么this的值设置成了base对象,foo。
第三种情况是一个赋值操作符(assignment operator),与组操作符不同的是,它会触发调用GetValue方法(参见11.13.1中的第三步)。 最后返回的时候就是一个函数对象了(而不是引用类型的值了),这就意味着this的值会设置为null,最终会变成全局对象。
第四和第五种情况也是类似的——逗号操作符和OR逻辑表达式都会触发调用GetValue方法,于是相应地就会丢失原先的引用类型值,变成了函数类型,this的值就变成了全局对象了。
引用类型以及null(this的值)
有这么一种情况下,当调用表达式左侧是引用类型的值,但是this的值却是null,最终变为全局对象。 发生这种情况的条件是当引用类型值的base对象恰好为活跃对象。
当内部子函数在父函数中被调用的时候就会发生这种情况。正如第二章介绍的, 局部变量,内部函数以及函数的形参都会存储在指定函数的活跃对象中:
function foo() {
function bar() {
alert(this); // global
}
bar(); // 和AO.bar()是一样的
}
活跃对象总是会返回this值为——null(用伪代码来表示,AO.bar()就相当于null.bar())。然后,如此前描述的,this的值最终会由null变为全局对象。
当函数调用包含在with语句的代码块中,并且with对象包含一个函数属性的时候,就会出现例外的情况。with语句会将该对象添加到作用域链的最前面,在活跃对象的之前。 相应地,在引用类型的值(标识符或者属性访问)的情况下,base对象就不再是活跃对象了,而是with语句的对象。另外,值得一提的是,它不仅仅只针对内部函数,全局函数也是如此, 原因就是with对象掩盖了作用域链中更高层的对象(全局对象或者活跃对象):
var x = 10;
with ({
foo: function () {
alert(this.x);
},
x: 20
}) {
foo(); // 20
}
// because
var fooReference = {
base: __withObject,
propertyName: 'foo'
};
当调用的函数恰好是catch从句的参数时,情况也是类似的:在这种情况下,catch对象也会添加到作用域链的最前面,在活跃对象和全局对象之前。 然而,这个行为在ECMA-262-3中被指出是个bug,并且已经在ECMA-262-5中修正了;因此,在这种情况下,this的值应该设置为全局对象,而不是catch对象。
try {
throw function () {
alert(this);
};
} catch (e) {
e(); // __catchObject - in ES3, global - fixed in ES5
}
// on idea
var eReference = {
base: __catchObject,
propertyName: 'e'
};
// 然而,既然这是个bug
// 那就应该强制设置为全局对象
// null => global
var eReference = {
base: global,
propertyName: 'e'
};
同样的情况还会在递归调用一个非匿名函数的时候发生(函数相关的内容会在第五章作相应的介绍)。在第一次函数调用的时候,base对象是外层的活跃对象(或者全局对象), 在接下来的递归调用的时候——base对象应当是一个存储了可选的函数表达式名字的特殊对象,然而,事实却是,在这种情况下,this的值永远都是全局对象:
(function foo(bar) {
alert(this);
!bar && foo(1); // "should" be special object, but always (correct) global
})(); // global
但函数作为构造器被调用时this的值
这里要介绍的是函数上下文中关于this值的另外一种情况——当函数作为构造器被调用的时候:
function A() {
alert(this); // newly created object, below - "a" object
this.x = 10;
}
var a = new A();
alert(a.x); // 10
在这种情况下,new操作符会调用“A”函数的内部[[Construct]]。 在对象创建之后,会调用内部的[[Call]]函数,然后所有“A”函数中this的值会设置为新创建的对象。
手动设置函数调用时this的值
Function.prototype上定义了两个方法(因此,它们对所有函数而言都是可访问的),允许手动指定函数调用时this的值。这两个方法是:.apply和.call; 它们都接受第一个参数作为调用上下文中this的值。而它们的不同点其实无关紧要:对于.apply来说,第二个参数接受数组类型(或者是类数组的对象,比如arguments), 而.call方法接受任意多的参数。这两个方法只有第一个参数是必要的——this的值。
如下所示:
var b = 10;
function a(c) {
alert(this.b);
alert(c);
}
a(20); // this === global, this.b == 10, c == 20
a.call({b: 20}, 30); // this === {b: 20}, this.b == 20, c == 30
a.apply({b: 30}, [40]) // this === {b: 30}, this.b == 30, c == 40
总结
本文我们讨论了ECMAScript中this关键字的特性(相对C++或者Java而言,真的可以说是特性)。洗完此文对大家理解this关键字在ECMAScript中的工作原理有所帮助。
扩展阅读
变量对象(Variable object)
说明
此文译自Dmitry A.Soshnikov 的文章Variable object
另,此文还有另外一位同事(宋珍珍)共同参译
概要
我们总是会在程序中定义一些函数和变量,之后会使用这些函数和变量来构建我们的系统。
然而,对于解释器来说,它又是如何以及从哪里找到这些数据的(函数,变量)?当引用一个对象的时候,在解释器内部又发生了什么?
许多ECMA脚本程序员都知道,变量和执行上下文是密切相关的:
var a = 10; // 全局上下文中的变量
(function () {
var b = 20; // 函数上下文中的局部变量
})();
alert(a); // 10
alert(b); // "b" is not defined
不仅如此,许多程序员也都知道,ECMAScript标准中指出独立的作用域只有通过“函数代码”(可执行代码类型中的一种)才能创建出来。比方说,与C/C++不同的是,在ECMAScript中for循环的代码块是无法创建本地上下文的:
for (var k in {a: 1, b: 2}) {
alert(k);
}
alert(k); // 尽管循环已经结束,但是变量“k”仍然在作用域中
下面就来详细介绍下,当声明变量和函数的时候,究竟发生了什么。
数据声明
既然变量和执行上下文有关,那它就该知道数据存储在哪里以及如何获取。这种机制就称作变量对象:
A variable object (in abbreviated form — VO) is a special object related with an execution context and which stores:
- variables (var, VariableDeclaration);
- function declarations (FunctionDeclaration, in abbreviated form FD);
- and function formal parameters
declared in the context.
举个例子,可以用ECMAScript的对象来表示变量对象:
VO = {};
VO同时也是一个执行上下文的属性:
activeExecutionContext = {
VO: {
// 上下文中的数据 (变量声明(var), 函数声明(FD), 函数形参(function arguments))
}
};
对变量的间接引用(通过VO的属性名)只允许发生在全局上下文中的变量对象上(全局对象本身就是变量对象,这部分会在后续作相应的介绍)。 对于其他的上下文而言,是无法直接引用VO的,因为VO是实现层的。
声明新的变量和函数的过程其实就是在VO中创建新的和变量以及函数名对应的属性和属性值的过程。
如下所示:
var a = 10;
function test(x) {
var b = 20;
};
test(30);
上述代码对应的变量对象则如下所示:
// 全局上下文中的变量对象
VO(globalContext) = {
a: 10,
test:
};
// “test”函数上下文中的变量对象
VO(test functionContext) = {
x: 30,
b: 20
};
但是,在实现层(标准中定义的),变量对象只是一个抽象的概念。在实际执行上下文中,VO可能完全不叫VO,并且初始的结构也可能完全不同。
不同执行上下文中的变量对象
变量对象上的一些操作(比如:变量的初始化)和行为对于所有的执行上下文类型来说都已一样的。从这一点来说,将变量对象表示成抽象的概念更加合适。 函数上下文还能定义额外的与变量对象相关的信息。
AbstractVO (generic behavior of the variable instantiation process)
║
╠══> GlobalContextVO
║ (VO === this === global)
║
╚══> FunctionContextVO
(VO === AO, object and are added)
接下来对这块内容进行详细介绍。
全局上下文中的变量对象
首先,有必要对全局对象(Global object)作个定义。
全局对象是一个在进入任何执行上下文前就创建出来的对象;此对象以单例形式存在;它的属性在程序任何地方都可以直接访问,其生命周期随着程序的结束而终止。
全局对象在创建的时候,诸如Math,String,Date,parseInt等等属性也会被初始化,同时,其中一些对象会指向全局对象本身——比如,DOM中,全局对象上的window属性就指向了全局对象(但是,并非所有的实现都是如此):
global = {
Math: ,
String:
...
...
window: global
};
在引用全局对象的属性时,前缀通常可以省略,因为全局对象是不能通过名字直接访问的。然而,通过全局对象上的this值,以及通过如DOM中的window对象这样递归引用的方式都可以访问到全局对象:
String(10); // 等同于 global.String(10);
// 带前缀
window.a = 10; // === global.window.a = 10 === global.a = 10;
this.b = 20; // global.b = 20;
回到全局上下文的变量对象上——这里变量对象就是全局对象本身:
VO(globalContext) === global;
准确地理解这个事实是非常必要的:正是由于这个原因,当在全局上下文中声明一个变量时,可以通过全局对象上的属性来间地引用该变量(比方说,当变量名提前未知的情况下)
var a = new String('test');
alert(a); // directly, is found in VO(globalContext): "test"
alert(window['a']); // indirectly via global === VO(globalContext): "test"
alert(a === this.a); // true
var aKey = 'a';
alert(window[aKey]); // indirectly, with dynamic property name: "test"
函数上下文中的变量对象
在函数的执行上下文中,VO是不能直接访问的。它主要扮演被称作活跃对象(activation object)(简称:AO)的角色。
VO(functionContext) === AO;
活跃对象会在进入函数上下文的时候创建出来,初始化的时候会创建一个arguments属性,其值就是Arguments对象:
AO = {
arguments:
};
Arguments对象是活跃对象上的属性,它包含了如下属性:
- callee —— 对当前函数的引用
- length —— 实参的个数
- properties-indexes(数字,转换成字符串)其值是函数参数的值(参数列表中,从左到右)。properties-indexes的个数 == arguments.length;
arguments对象的properties-indexes的值和当前(实际传递的)形参是共享的。
如下所示:
function foo(x, y, z) {
// 定义的函数参数(x,y,z)的个数
alert(foo.length); // 3
// 实际传递的参数个数
alert(arguments.length); // 2
// 引用函数自身
alert(arguments.callee === foo); // true
// 参数互相共享
alert(x === arguments[0]); // true
alert(x); // 10
arguments[0] = 20;
alert(x); // 20
x = 30;
alert(arguments[0]); // 30
// 然而,对于没有传递的参数z,
// 相关的arguments对象的index-property是不共享的
z = 40;
alert(arguments[2]); // undefined
arguments[2] = 50;
alert(z); // 40
}
foo(10, 20);
上述例子,在当前的Google Chrome浏览器中有个bug——参数z和arguments[2]也是互相共享的。
处理上下文代码的几个阶段
至此,也就到了本文最核心的部分了。处理执行上下文代码分为两个阶段:
- 进入执行上下文
- 执行代码
对变量对象的修改和这两个阶段密切相关。
要注意的是,这两个处理阶段是通用的行为,与上下文类型无关(不管是全局上下文还是函数上下文都是一致的)。
进入执行上下文
一旦进入执行上下文(在执行代码之前),VO就会被一些属性填充(在此前已经描述过了):
- 函数的形参(当进入函数执行上下文时)
—— 变量对象的一个属性,其属性名就是形参的名字,其值就是实参的值;对于没有传递的参数,其值为undefined - 函数声明(FunctionDeclaration, FD) —— 变量对象的一个属性,其属性名和值都是函数对象创建出来的;如果变量对象已经包含了相同名字的属性,则替换它的值
- 变量声明(var,VariableDeclaration) —— 变量对象的一个属性,其属性名即为变量名,其值为undefined;如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在的属性。
看下面这个例子:
function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
}
test(10); // call
当以10为参数进入“test”函数上下文的时候,对应的AO如下所示:
AO(test) = {
a: 10,
b: undefined,
c: undefined,
d: <reference to FunctionDeclaration "d">
e: undefined
};
注意了,上面的AO并不包含函数“x”。这是因为这里的“x”并不是函数声明而是函数表达式(FunctionExpression,简称FE),函数表达式不会对VO造成影响。 尽管函数“_e”也是函数表达式,然而,正如我们所看到的,由于它被赋值给了变量“e”,因此它可以通过“e”来访问到。关于函数声明和函数表达式的区别会在第五章——函数作具体介绍。
至此,处理上下文代码的第一阶段介绍完了,接下来介绍第二阶段——执行代码阶段。
执行代码
此时,AO/VO的属性已经填充好了。(尽管,大部分属性都还没有赋予真正的值,都只是初始化时候的undefined值)。
继续以上一例子为例,到了执行代码阶段,AO/VO就会修改成为如下形式:
AO['c'] = 10;
AO['e'] = ;
再次注意到,这里函数表达式“_e”仍在内存中,这是因为它被保存在声明的变量“e”中,而同样是函数表达式的“x”却不在AO/VO中: 如果尝试在定义前或者定义后调用“x”函数,这时会发生“x为定义”的错误。未保存的函数表达式只有在定义或者递归时才能调用。
如下是更加典型的例子:
alert(x); // function
var x = 10;
alert(x); // 10
x = 20;
function x() {};
alert(x); // 20
上述例子中,为何“x”打印出来是函数呢?为何在声明前就可以访问到?又为何不是10或者20呢?原因在于,根据规则——在进入上下文的时候,VO会被填充函数声明; 同一阶段,还有变量声明“x”,但是,正如此前提到的,变量声明是在函数声明和函数形参之后,并且,变量声明不会对已经存在的同样名字的函数声明和函数形参发生冲突, 因此,在进入上下文的阶段,VO填充为如下形式:
VO = {};
VO['x'] = <引用了函数声明“x”>
// 发现var x = 10;
// 如果函数“x”还未定义
// 则 "x" 为undefined, 但是,在我们的例子中
// 变量声明并不会影响同名的函数值
VO['x'] = <值不受影响,仍是函数>
随后,在执行代码阶段,VO被修改为如下所示:
VO['x'] = 10;
VO['x'] = 20;
正如在第二个和第三个alert显示的那样。
如下例子再次看到在进入上下文阶段,变量存储在VO中(因此,尽管else的代码块永远都不会执行到,而“b”却仍然在VO中):
if (true) {
var a = 1;
} else {
var b = 2;
}
alert(a); // 1
alert(b); // undefined, but not "b is not defined"
关于变量
大多数讲JavaScript的文章甚至是JavaScript的书通常都会这么说:“声明全局变量的方式有两种,一种是使用var关键字(在全局上下文中),另外一种是不用var关键字(在任何位置)”。
而这样的描述是错误的。要记住的是:
使用var关键字是声明变量的唯一方式
如下赋值语句:
a = 10;
仅仅是在全局对象上创建了新的属性(而不是变量)。“不是变量”并不意味着它无法改变,它是ECMAScript中变量的概念(它之后可以变为全局对象的属性,因为VO(globalContext) === global,还记得吧?)
不同点如下所示:
alert(a); // undefined
alert(b); // "b" is not defined
b = 10;
var a = 20;
接下来还是要谈到VO和在不同阶段对VO的修改(进入上下文阶段和执行代码阶段):
进入上下文:
VO = {
a: undefined
};
我们看到,这个阶段并没有任何“b”,因为它不是变量,“b”在执行代码阶段才出现。(但是,在我们这个例子中也不会出现,因为在“b”出现前就发生了错误)
将上述代码稍作改动:
alert(a); // undefined, we know why
b = 10;
alert(b); // 10, created at code execution
var a = 20;
alert(a); // 20, modified at code execution
这里关于变量还有非常重要的一点:与简单属性不同的是,变量是不能删除的{DontDelete},这意味着要想通过delete操作符来删除一个变量是不可能的。
a = 10;
alert(window.a); // 10
alert(delete a); // true
alert(window.a); // undefined
var b = 20;
alert(window.b); // 20
alert(delete b); // false
alert(window.b); // still 20
但是,这里有个例外,就是“eval”执行上下文中,是可以删除变量的:
eval('var a = 10;');
alert(window.a); // 10
alert(delete a); // true
alert(window.a); // undefined
利用某些debug工具,在终端测试过这些例子的童鞋要注意了:其中Firebug也是使用了eval来执行终端的代码。因此,这个时候var也是可以删除的。
实现层的特性:__parent__属性
正如此前介绍的,标准情况下,是无法直接访问活跃对象的。然而,在某些实现中,比如知名的SpiderMonkey和Rhino,函数有个特殊的属性__parent__, 该属性是对该函数创建所在的活跃对象的引用(或者全局变量对象)。
如下所示(SpiderMonkey,Rhino):
var global = this;
var a = 10;
function foo() {}
alert(foo.__parent__); // global
var VO = foo.__parent__;
alert(VO.a); // 10
alert(VO === global); // true
上述例子中,可以看到函数foo是在全局上下文中创建的,相应的,它的__parent__属性设置为全局上下文的变量对象,比如说:全局对象。
然而,在SpiderMonkey中以相同的方式获取活跃对象是不可能的:不同的版本表现都不同,内部函数的__parent__属性会返回null或者全局对象。
在Rhino中,以相同的方式获取活跃对象是允许的:
如下所示(Rhino):
var global = this;
var x = 10;
(function foo() {
var y = 20;
// the activation object of the "foo" context
var AO = (function () {}).__parent__;
print(AO.y); // 20
// __parent__ of the current activation
// object is already the global object,
// i.e. the special chain of variable objects is formed,
// so-called, a scope chain
print(AO.__parent__ === global); // true
print(AO.__parent__.x); // 10
})();
总结
本文,我们介绍了与执行上下文相关的对象。希望,本文能够对大家有所帮助,同时也希望本文能够起到解惑的作用。
扩展阅读
执行上下文(Execution Context)
说明
此文译自Dmitry A.Soshnikov 的文章Execution Context
概要
本文将向大家介绍ECMAScript的执行上下文以及相关的可执行代码类型。
定义
每当控制器到达ECMAScript可执行代码的时候,控制器就进入了一个执行上下文。
执行上下文(简称:EC)是个抽象的概念,ECMA-262标准中用它来区分不同类型的可执行代码。
标准中并没有从技术实现的角度来定义执行上下文的具体结构和类型;这是实现标准的ECMAScript引擎所要考虑的问题。
一系列活动的执行上下文从逻辑上形成一个栈。栈底总是全局上下文,栈顶是当前(活动的)执行上下文。当在不同的执行上下文间切换(退出的而进入新的执行上下文)的时候,栈会被修改(通过压栈或者退栈的形式)。
可执行代码类型
可执行代码类型和执行上下文相关。有的时候,当提到代码类型的时候,其实就是在说执行上下文。
举个例子,我们将执行上下文的栈以数组的形式来表示:
ECStask = [ ];
每次控制器进入一个函数(哪怕该函数被递归调用或者作为构造器),都会发生压栈的操作。内置eval函数工作的时候也不例外。
全局代码
这类代码是在“程序”级别上被处理的:比如,加载一个外部的js文件或者内联的js代码(被包含在<script></script>标签内)。全局代码不包含任何函数体内的代码。
在初始化的时候(程序开始),ECStack如下所示:
ECStack = [
globalContext
];
函数代码
一旦控制器进入函数代码(各类函数),就会有新的元素会被压栈到ECStack。要注意的是:实体函数代码并不包括内部函数的代码。如下所示,我们调用一个函数,该函数递归调用自己一次:
(function foo(bar){
if (bar){
return;
}
foo(true);
})();
之后,ECStack就被修改成如下所示:
//首先激活foo函数
ECStack = [
functionContext
globalContext
];
//递归激活foo函数
ECStack = [
functionContext - recursively
functionContext
globalContext
];
每次函数返回,退出当前活动的执行上下文时,ECStack就会被执行对应的退栈操作——先进后出——和传统的栈实现一致。同样的,当抛出未捕获的异常时,也会退出一个或者多个执行上下文,ECStack也会做相应的退栈操作。待这些代码完成之后,ECStack中就只剩下一个执行上下文(globalContext)——直到整个程序结束。
Eval代码
说到eval代码就比较有意思了。这里要提到一个叫做调用上下文的概念,比如:调用eval函数时候的上下文,就是一个调用上下文,eval函数中执行的动作(例如:变量声明或者函数声明)会影响整个调用上下文:
eval(‘var x = 10’);
(function foo(){
eval(‘ var y = 20’);
})();
alert(x); // 10
alert(y); // ”y” is not defined
ECStack会被修改为:
ECStack = [
globalContext
];
//eval(‘var x = 10’);
ECStack.push(
evalContext,
callingContext: globalContext
);
// eval exited context
ECStack.pop();
//foo function call
ECStack.push( functionContext);
//eval(‘ var y = 20’);
ECStack.push(
evalContext,
callingContext: functionContext
);
//return from eval
ECStack.pop();
//return from foo
ECStack.pop();
在1.7以上版本SpiderMonkey的实现中(Firefox,Thunderbird浏览器内置的JS引擎),允许在调用eval函数的时候,将调用上下文作为第二个参数传递给eval函数。因此,如果传入的调用上下文存在的话,就有可能会影响该上下文中原有的私有变量(在该上下文中声明的变量):
function foo(){
var x = 1;
return function() { alert(x); }
};
var bar = foo();
bar(); // 1
eval(‘x = 2’, bar); //传递上下文,影响了内部变量“var x”
bar(); // 2
总结
这些基本理论对于后面执行上下文相关的细节(诸如变量对象、作用域链等等)分析是非常必要的。
扩展阅读
ECMA-363-3标准文档的对应的章节—— 10. 执行上下文
如何在Eclipse中自定义Bundle
写代码的时候,我经常有这样的需求: 希望把经常要写的代码做成模板,然后,像linux那样,输入部分字符,按下Tab就帮忙自动补齐
以前用Netbeans和TextMate(Bundle)的时候,就很喜欢这样。
现在,在Eclipse + Aptana的环境下,也希望能够有这样的感受。于是,摸索出了自定义Bundle的方法,如下所示:
说明: “Eclipse3.7环境下,”
场景: 我经常在文件最上面要写上作者信息比如:goddyzhao<[email protected]>
所以,我打算把这个作为一个snippet,下次直接输入”auinfo”,然后tab就帮我自动补齐。
具体步骤如下:
1. 点击”Commands” -> “Text” -> “Edit this bundle”
这个时候,在Eclipse的Project View中就能够看到一个项目,名字就叫“Text”
2. 打开项目中snippets目录下的snippets.rb文件,文件内容都描述了一条条的snippet
其格式如下所示:
snippet 'Lorem ipsum' do |s|
s.trigger = 'lorem'
s.expansion = 'Lorem ipsum dolor sit '
end
- 写法非常简单,一看就明白了。于是,跟着写上我自己的:
snippet 'Author Information' do |s|
s.trigger = 'auinfo'
s.expansion = 'goddyzhao'
end
- 重启Eclipse,就OK了!