向高级Javascript程序员阵营迈进:Javascript一些概念研究总结
习惯于OOP语言编程后,会发现Javascript世界有很多匪夷所思的奇奇怪怪的现象(比如闭包),我花了大量的精力研究这些奇怪现象的根源,最后发现:源自于javascript的作用域不是块级作用域,同时它有一套基于作用域链的标识查找机制。本文大部分内容来自互联网,经过整理、改进而成。
- Javascript引擎和DOM采用的垃圾回收算法:引用计数
javascript和DOM有各自的垃圾回收器,单独运作良好,合作时一不小心会出问题。引用计数这个算法的缺陷就是:Javascript 对象和DOM对象彼此循环引用,造成彼此的引用计数永远不能为0,垃圾回收器无法正确回收这些参与循环引用的对象,最终造成内存泄漏(Memory Leak)。闭包是循环引用“大户”。如果对垃圾回收感兴趣,可以看看 垃圾收集趣史 - 词法作用域(lexical scope,一般简称作用域)、with/eval
简单来说javascript的作用域是由function划分的。读完这篇文章你会了解词法作用域 Javascript运行机制浅探,with/eval这 两个特例会扰乱作用域,即所谓动态作用域(dynamic scope) - 作用域链(Scope Chain) 和 标识查找机制
作用域链是一个链表(数据结构),它是Javascript的灵魂,只有理解了它才能理解Javascript世界奇奇怪怪的现象。作用域链由活动对象链成。
标识查找机制稍后结合函数执行的原理加以说明。 - 活动对象(call object)
国内很多人称之为调用对象(call object),本文用英文call obejct(但我私下认为翻译为"活动对象"更好,不至于和this所指的对象混淆。)
非常特殊的javascript引擎内的对象,ECMAScript规范术语称之为activation object(活动对象)。多个call object和全局对象组成作用域链(scope chain ) - 函数的本质(有名函数、匿名函数)、函数的[[scope]]属性 函数在javascript里面是一个特殊的引用类型 ,它继承于位于javascript世界最顶端的object,类型是Function,是其他常见引用类型的构造函数的所属类型。
在定义函数的时候,Javascript引擎会为function对象的一个私有[[scope]]属性赋值,理论上只有js引擎自己才能访问(也即:一般情况下无法通过语法来访问,但Firefox下有一个__parent___可以访问到)。匿名函数的[[scope]]属性指向匿名函数定义时的上下文对象;有名函数除了和匿名函数一样,还会在[[scope]]属性的顶端再指向一个Javascript对象(继承自obejct.prototype),这个对象被链接到函数定义时的Scope Chain,他本身带有一个属性就是函数的名字,这确保函数内部的代码可以无误地访问到自己的函数名以便进行递归。
当定义函数的时候,javascript解析器会将函数的作用域链(scope chain)设置为定义函数时函数所在的“环境”,如果函数是一个全局函数,则scope chain中只有window对象。
当执行函数时的微观世界,请看稍后的说明。 - 闭包(closure)
javascript所有的函数都是闭包,但是只有嵌套形式的闭包(也是我们经常讨论的形式)才能体现这个javascript 特性的强大。推荐阅读这篇文章: 深入理解JavaScript闭包(closure) - 函数执行时的作用域链和活动对象是如何形成的及与闭包的关系
1、javascript解析器启动时就会初始化建立一个全局对象global object,这个全局对象就 拥有了一些预定义的全局变量和全局方法,如Infinity, parseInt, Math,所有程序中定义的全局变量都是这个全局对象的属性。在客户端javascript中,Window就是这个javascript的全局对象。
2、当javascript执行一个function时,会生成一个对象,称之为call object,function中的局部变量和function的参数都成为这个call object的属性,以免覆写同名的全局变量。
3、javascript解析器每次执行function时,都会为此function创建一个execution context执行环境,在此function执行环境中最重要的一点就是function的作用域链scope chain,这是一个对象链,由全局对象和活动对象构成,对象链具体构成过程见下面说明。
4、标识的查找机制:当javascript查询变量x的值时,就会检查此作用域链中第一个对象,可能是function的call object或全局对象(比如window),如果对象中有定义此x属性,则返回值,不然检查作用域链中的下一个对象是否定义x属性,在作用域链中没有找到,最后返回undefined。
5、当javascript执行一个function时,它会先将此function定义时的作用域作为其作用域链,然后创建一个活动对象(call object),置于作用域链的顶部,function的参数及内部var声明的所有局部变量都会成为此调用对象的属性。
6、this关键词指向方法的调用者,而不是以调用对象的属性存在,同一个方法中的this在不同的function调用中,可能指向不同的对象。
7、The Call Object as a Namespace。将活动对象当作命名空间使用,避免命名污染。
(function() {
// 在方法体内用var声明的所有局部变量,都是以方法调用时创建的活对象的属性形式 存在。
// 这样就避免与全局变量发生命名冲突。
})();
8、javascript中所有的function都是一个闭包,但只有当一个嵌套函数被导出到它所定义的作用域外时,这种闭包才强大。如果理解了闭包,就会理解function执行时的作用域链和活动对象,才能真正掌握javascript。
9、嵌套闭包的微观世界:在嵌套闭包时,当内部函数的引用被保存到嵌套闭包之外一个全局变量或者一个对象的属性时,在这种情况下,此内部函数有一个外部引用,并且在其外围调用函数的活动对象中有一个属性指向此内部函数。因为有其他对象引用此内部函数,所以在外围函数被调用一次后,其创建的活动对象会继续存在,并不会被垃圾回收器回收(因为引用计数不为0),内部函数的参数和局部变量都会在这个活动对象中得以维持,javascript代码任何形式都不能直接访问此活动对象,但是此活动对象是内部函数被调用时创建的作用域链的一部分,可以被内部函数访问并修改。
最后介绍一个奇怪现象:下面的代码,为什么鼠标移动到li上,title总是6,而不是我们所预想的数字呢?看你能不能根据以上的知识,解释这种现象的原因。提示:变量查找机制。
<html>
<head>
<title>循环内的闭包 应该谨慎</title>
</head>
<body>
<ul id="list">
<li>第1条记录</li>
<li>第2条记录</li>
<li>第3条记录</li>
<li>第4条记录</li>
<li>第5条记录</li>
<li>第6条记录</li>
</ul>
<script type="text/javascript">
var list_obj = document.getElementById("list").getElementsByTagName("li"); //获取list下面的所有li的对象数组
for (var i = 0; i <= list_obj.length; i++) {
list_obj[i].onmousemove = function() {
this.style.backgroundColor = "#eee";
document.title=i
};
list_obj[i].onmouseout = function() {
this.style.backgroundColor = "#fff";
}
}
</script>
</body>
</html>
<head>
<title>循环内的闭包 应该谨慎</title>
</head>
<body>
<ul id="list">
<li>第1条记录</li>
<li>第2条记录</li>
<li>第3条记录</li>
<li>第4条记录</li>
<li>第5条记录</li>
<li>第6条记录</li>
</ul>
<script type="text/javascript">
var list_obj = document.getElementById("list").getElementsByTagName("li"); //获取list下面的所有li的对象数组
for (var i = 0; i <= list_obj.length; i++) {
list_obj[i].onmousemove = function() {
this.style.backgroundColor = "#eee";
document.title=i
};
list_obj[i].onmouseout = function() {
this.style.backgroundColor = "#fff";
}
}
</script>
</body>
</html>
为什么上面的代码改成下面的就好了呢?
<html>
<head>
<title>循环内的闭包 应该谨慎</title>
</head>
<body>
<ul id="list">
<li>第1条记录</li>
<li>第2条记录</li>
<li>第3条记录</li>
<li>第4条记录</li>
<li>第5条记录</li>
<li>第6条记录</li>
</ul>
<script type="text/javascript">
var list_obj = document.getElementById("list").getElementsByTagName("li"); //获取list下面的所有li的对象数组
for (var i = 0; i <= list_obj.length; i++) {
(function(i){
list_obj[i].onmousemove = function() {
this.style.backgroundColor = "#eee";
document.title=i
};
list_obj[i].onmouseout = function() {
this.style.backgroundColor = "#fff";
}
})(i);
}
</script>
</body>
</html>
<head>
<title>循环内的闭包 应该谨慎</title>
</head>
<body>
<ul id="list">
<li>第1条记录</li>
<li>第2条记录</li>
<li>第3条记录</li>
<li>第4条记录</li>
<li>第5条记录</li>
<li>第6条记录</li>
</ul>
<script type="text/javascript">
var list_obj = document.getElementById("list").getElementsByTagName("li"); //获取list下面的所有li的对象数组
for (var i = 0; i <= list_obj.length; i++) {
(function(i){
list_obj[i].onmousemove = function() {
this.style.backgroundColor = "#eee";
document.title=i
};
list_obj[i].onmouseout = function() {
this.style.backgroundColor = "#fff";
}
})(i);
}
</script>
</body>
</html>