这是学习总结形式的文章,对读者无意义,请忽略
一. 引子
作用域链、原型链、THIS指针是 JAVASCRIPT 入门级的三个重要概念。作为初学者经常容易混淆作用域链和原型链,导致使用 this 指针时诸多混乱,比方企图使用 this 指针引用一个私有变量;也可能导致在原型函数中使用类的私有变量或者调用类的私有函数的错误。举两个例子,不加解释:
function Person(sIname)
{
var sName = sIname;
this.showName = function(){
console.log(this.sName);
};
}
var oPerson = new Person('yao');
oPerson.showName();//输出 undefined
function Person(sInName)
{
var sName = sInName;
function privateFunc()
{
console.log("正在执行私有函数");
}
}
Person.prototype.publicFunc = function(){
privateFunc();//错误,privateFunc is not defined
};
var oPerson = new Person('yao');
oPerson.publicFunc();
二. 作用域链初步
在程序设计领域,作用域也是一个基础的概念,我的理解是,作用域是针对某个变量而言的,作用域指出了能够访问到这个变量的最大区域。作用域也是可以嵌套的,内嵌的作用域可以访问到外层作用域定义的变量,反之不行。在JS中,形成一个内嵌的定义域没有别的方法,就是通过使用函数定义。
如果把作用域看做一个节点,则内嵌的作用域则可以看成是子节点,于是,所有的作用域整合起来,就是一颗树。而对于这棵树的根节点和任一个子孙节点,使用深度优先搜索,必然能找到一个路径,这个路径就是作用域链。
对于JS而言,作用域链实际上描述的就是我这里说的这种路径。只不过,作用域链中存放的是一个个的指针,指向了一个个的作用域,JS 使用变量对象来存储作用域中的各种变量。作用域链的生成和变量对象的生成有它们各自不同的时机,把握住这一点,是理解作用域链的第一步。
1. 从作用域链和变量对象的生成和销毁讲起
定义一个函数的时候,JS解析器会为这个函数生成一个作用域链,此作用域链视外围的作用域的作用域链加上一个指针,该指针用于将来指向这个函数的变量对象(存储内部作用域内容的地方)。也就是说,在定义函数的时候,函数的作用域链就已经产生了,只不过此时变量对象还没有生成。
例如在全局作用域中定义函数 fOuter ,那么此时(定义fOuter时)函数fOuter作用域链以及作用域链中各个指针所指向的变量对象如下:
作用域链 : 全局作用域指针 ---> 指向 fOuter 函数变量对象的指针(null)
全局变量对象 ---> null
假如 fOuter 函数的定义是这样的:
//fOuter 的外部是全局作用域
function fOuter()
{
function fInner()
{
}
}
那么,当 fOuter 定义的时候,fInner 是否有自己的作用域呢,当然没有,因为 定义 fOuter 的时候根本没有定义 fInner 函数。因为 fInner 函数只会在 fOuter 函数执行的时候才会定义,所以,只有到那时, fInner 函数才拥有了自己的作用域链。比方说,我们再进一步完善代码:
//fOuter 的外部是全局作用域
function fOuter()
{
var sVar = "yao";//字符串变量
function fInner()
{
console.log(sVar);
}
fInner();
}
fOuter();
在这一小段代码中,在全局作用域里发生了两件事情:fOuter 函数的定义和fOuter函数的调用。按照时间的发展顺序,我们来跟踪作用域链和变量对象的各种变化:
1. 定义函数 fOuter ,此时定义了 fOuter 函数的作用域链,类似如下:
fOuter 函数的作用域链: 指向全局作用域(变量对象)的指针 ---> 指向 fOuter 的作用域(变量对象)的指针
全局作用域的变量对象 不存在的fOuter 的变量对象
2. 执行 fOuter 函数,执行 fOuter 函数实际上发生了很多事情,在不考虑 arguments this 等问题的情况下,加上我的推测,含如下步骤:
1)创建 fOuter 函数的变量对象,同时,使得 fOuter 函数的作用域链的最后一项指向这个变量对象,表现为:
fOuter 函数的作用域链: 指向全局作用域(变量对象)的指针 ---> 指向 fOuter 的作用域(变量对象)的指针
全局作用域的变量对象 fOuter 的变量对象(已经存在)
2)执行 var sVar = "yao";语句,在 这个变量对象中添加 sVar 变量并赋值 "yao"
3)定义fInner 函数,于是fInner函数获得了自己的作用域链,如下:
fInner 函数的作用域链:指向全局作用域变量对象的指针--> 指向 fOuter 变量对象的指针-->指向fInner的变量对象的指针
全局作用域的变量对象 fOuter 的变量对象 不存在的 fOuter 的变量对象
4)执行 fInner 函数,于是又发生了与执行 fOuter 类似的事情,大致如下:
4.1)创建 fInner 函数的变量对象,同时,使得 fInner 函数的作用域链的最后一项指向这个变量对象
4.2)执行 console.log(sVar);于是把 sVar 的值"yao"输出到控制台日志中了。fInner 函数通过作用域链在 fOuter 的变量对象中找到了 sVar 。
5)随着fInner 函数执行完,fInner 的变量对象也被删除,把 fInner 作用域链中相应的指针重新设置为无效的值(可能是undefined或null吧,没有查证)
3. 此时,随着 fOuter 函数执行结束,JS解析器决定删除 fOuter 的变量对象,并恢复 fOuter 的作用域链为调用 fOuter 之前的样子:
fOuter 函数的作用域链: 指向全局作用域(变量对象)的指针 ---> 指向 fOuter 的作用域(变量对象)的指针
全局作用域的变量对象 不存在的fOuter 的变量对象
而与此同时,解析器注意到 fInner 的作用域链已经没有存在的意义,于是 fInner 的作用域链被删除4. 最终,伴随着全部脚本执行完,解析器发现 fOuter 的作用域链也不再有用了,于是删除
2. 再补充变量对象和this指针的知识
在上面的例子中,我有意回避了变量对象中 arguments ,this 指针的问题,因为对于讲解上一个问题没有帮助。作为与作用域链相关的重要问题之一,必须指出,arguments,this指针存在于作用域(变量对象)中,所以当你刚刚定义一个函数的时候,不要奢谈arguments和this,因为,此时,这个函数甚至没有自己的变量对象。那么,arguments 和 this 分别是什么呢?
1)先简单谈谈arguments,其实没啥好谈
全局作用域的变量对象中是没有arguments这个属性的,这一点,大家可以自己试试在全局作用域中执行 alert(typeof arguments == 'undefined'); 。
而当你运行一个函数的时候,在函数的变量对象中会自动增加这个属性,这是一个类数组对象。存放这传入函数的参数,此外arguments.callee 很常用,指向函数对象。
2)关于 this 对象,见第三部分,马上开始
三. this 指针初步
this 指针是与作用域链和变量对象密不可分的一个概念。正如之前所述,this指针只存在于变量对象中。
1. 全局作用域中 this 指向 window
对于全局作用域的变量对象,其this指向Global对象(来自网络)。ECMAScript没有指出如何访问Global对象,但是所有浏览器实现中都把这个全局变量(Global)作为window对象的一部分来实现(参考尼古拉斯《js高程》)。因此实际上,如果想范文Glocal对象的属性或方法,直接访问window即可。在全局作用域中,this实际上指向window。
当函数运行时,它的变量对象中也会保存一个this指针,这个指针指向了调用这个函数的对象。这里分几种情况来讨论:
2. 函数作用域中,this指针指向调用函数的对象
1)函数在全局作用域中被调用
假如函数是在全局作用域中被调用,那么实际上是window对象调用了它,那么在这个函数的变量对象中,this实际上指向了window。读者可以使用以下代码检测:
//fOuter 的外部是全局作用域
function fOuter()
{
console.log(this === window);
}
fOuter();//打日志 true
2)使用new生成对象时
使用new操作符生成对象时,实际上发生了以下几个步骤
i. 生成一个对象
ii. 函数的变量对象的 this 指向这个对象
iii. 执行构造函数
iv. 返回这个对象(假如构造函数中没有执行 return 语句的话,默认返回步骤 i 生成的对象)
下面给出例子,增进理解。
//fOuter 的外部是全局作用域
function Person(sInName)
{
this.sName = sInName;//这里的 this 在此例中实际就是 oPerson
console.log(this === window);//false
}
var oPerson = new Person('yaozhiyi');
console.log(oPerson.sName);//将 'yaozhiyi' 打入日志
console.log(window.sName);//打日志 undefined
如果我们不使用 new 而是直接调用 Person 那么,在 Person 函数执行时,其中出现的 this 都指向 window ,参照下方代码:
//fOuter 的外部是全局作用域
function Person(sInName)
{
this.sName = sInName;//这里的 this 在此例中实际就是 window
console.log(this === window);//true
}
Person('yaozhiyi');
console.log(window.sName);//打日志 'yaozhiyi'
3. 实践注意事项
上面基本把 this 指针的两种指向讲完了。在实践过程中个,有一些重要的注意事项。
1)设置超时函数时
以类似于 setTimeout(fHandler, 1000)的方式设置超时函数时,fHandler中出现的所有 this 都指向 window ,因为是在全局作用于中调用 fHandler的。例如:
//fOuter 的外部是全局作用域
window.sName = "不好意思,呵呵";
function Person(sInName)
{
this.sName = sInName;
function fHandler()
{
console.log(window === this);//true
console.log(this.sName);
}
setTimeout(fHandler,1000);//一秒之后,输出"不好意思,呵呵"
}
var oPerson = new Person('yaozhiyi');
不过,这种情况可以使用之前讲解的作用域链原理轻松解决,在fHandler的父级作用域中设置变量为 this 的值,并在fHandler中通过作用域链找到这个变量即可。这里提供一小段代码并做注解:
//fOuter 的外部是全局作用域
window.sName = "不好意思,呵呵";
function Person(sInName)
{
this.sName = sInName;
var _this = this;
function fHandler()
{
console.log(window === this);//true
console.log(this.sName);//一秒之后,输出"不好意思,呵呵"
console.log(_this.sName);//一秒之后,输出 'yaozhiyi'
}
setTimeout(fHandler,1000);
}
var oPerson = new Person('yaozhiyi');
2)使用 apply 和 call
二是使用函数对象的 apply 和 call 方法可以改变调用函数的对象。
3)通过 this 访问私有变量和私有函数必然失败
因为 this 指针指向对象,因此能够通过 this 访问对象中定义的属性和方法,但是不要试图通过 this 指针访问私有变量和私有方法,因为,它们根本不是那个对象的属性。同样,给出代码以及良好的注释:
//fOuter 的外部是全局作用域
function Person(sInName)
{
var sName = sInName;//sName是私有变量
//logName 意思是把名字打到浏览器日志里
this.logName = function(){
//通过作用域链找到了父作用域的 sName
console.log(sName);//输出 yaozhiyi
//oPerson对象根本没有 sName 这个属性
console.log(this.sName);//输出 undefined
};
}
var oPerson = new Person('yaozhiyi');
//调用 logName 时,对于 logName 函数的变量对象的 this 指向了 oPerson
oPerson.logName();
上面这段代码一方面表示,通过 this 指针是无法访问私有变量的,另一方面,也提供了一个访问私有变量的方法。那就是利用属性函数(见上述代码的this.logName)。不要试图通过圆形函数访问私有变量和私有函数,因为,它的作用域链根本和Person函数的作用域没有交集,无法访问到Person函数中的私有变量。下面的代码中,原型函数logName的父作用域是 全局作用域,logName函数通过作用域链访问到了 sName 。
//fOuter 的外部是全局作用域
var sName = "呵呵,其实我是全局作用域变量";
function Person(sInName)
{
var sName = sInName;//sName是私有变量
}
Person.prototype.logName = function()
{
console.log(sName);//输出 "呵呵,其实我是全局作用域变量"
console.log(this.sName);//undefined
};
var oPerson = new Person('yaozhiyi');
//调用 logName 时,对于 logName 函数的变量对象的 this 指向了 oPerson
oPerson.logName();
三. 原型链
其实,写作本文之前,我的初衷是总结this指针,如果忠实于这个初衷,那么“原型链”这一部分我只有一句话讲,那就是:“使用this访问属性或者函数的时候,会沿着this所指向的对象的原型链一路往上查找,直至找到”。还有必须知道,任何函数的原型对象默认都是 Object 的实例。这里我提供一些简单的例子:
下面这个例子中,使用 isPrototypeOf 函数判断 Person.prototype 是 Object 的实例,因为 Object 的实例对象会有一个指向 Object.prototype 的指针。
function Person()
{
}
console.log(Object.prototype.isPrototypeOf(Person.prototype));//true
下面这两个例子说明,如果在当前 this 所指向的对象的属性中没有找到所需的属性,会沿着原型链一路找,直到 Object.prototype 。
Object.prototype.sName = "呵呵,我是Object.prototype的属性"; function Person() { //this 实际上指向 oPerson console.log(this === window);//输出 false console.log(this.sName);//输出 "呵呵,我是Object.prototype的属性" } var oPerson = new Person(); console.log(oPerson.sName);//输出 "呵呵,我是Object.prototype的属性"
Object.prototype.sName = "呵呵,我是Object.prototype的属性"; function Person(sInName) { this.sName = sInName; console.log(this.sName);//输出 'yaozhiyi' } var oPerson = new Person('yaozhiyi');
总结:本文的初衷是分析 this 指针与作用域链和原型链的关系问题,其中最有价值的就是大量的实例以及丰富的注释。虽然大量的篇幅用于讲解作用域链和this指针,但是原型链却没有过多描述,原型链与this的关系实在一言以蔽之足以。其实真正的难处在于原型链本身的灵活应用,原型链与面向对象,将是我之后可能讨论的主题。
感谢尼古拉斯,我的这些总结或者猜测,基本上都得益于他在《javascript 高级程序设计》中详细、经得起推敲的讲解。不过,因为只看过这本好书,所以没有更有深度的论述,以后抽空再研究了。