JavaScript欲速则不达—通过解析过程了解JavaScript

接触javascript一段时间后,看到网上很多关于很多什么高效javascript准则、高性能javascript,最后发现也就那么几条而且总是被搬出来,譬如:申明变量用var;用json格式来创建对象比调用构造器的性能高;避免对象的嵌套,尽量利用局部变量,避免嵌套查询,等等(很多就不列举了,google“加快js的X个技巧”搜索结果一大堆)。

本文从javascript的解析引擎怎么解析javascript这角度来让我们明白为什么会有以上那些javascript高效准则的。

javascript解析引擎是啥?javascript解析引擎也是程序,而且每个浏览器的编写解析引擎的语言(C或者C++)以及解析原理都不相同,标准的javascript解析引擎会按照 ECMAScript文档来实现,但是因为浏览器的份额,以至于每个浏览器都有自己的一套标准。但是最后某些浏览器使用的人多了,而且W3C组织觉得某条标准不错,最后就将这条本来不是标准的标准给打上标准的标签了(IE就是个例子,谁叫微软是如此强大)。好比W3C组织是联合国,而控制浏览器大份额的公司好比成员国中的帝国。强大的帝国可以不用理会联合国,而联合国可能会跟着帝国的意思走。

不过截至现在还活跃着的javascript引擎还是挺多,还没到垄断的地步:chrome的 V8,IE的JSscript,Chakra (IE9),firefox的 JaegerMonkey,opera的 Carakan,Safair的NItro、IE9以上则采用Chakra。

虽然每个浏览器的引擎都不相同,但是javascript的语言性质决定了javascript关键的渲染原析还是仍然是动态执行javascript字符串,只是词法分析、语法分析、变量赋值、字符串拼接的实现方式有所不同。javascript引擎是浏览器引擎的其中一部分,我想至少这几年不会出现如wibkit一样的浏览器引擎,因为现在很多浏览器开发商都基于wibkit二次开发(360、猎豹、淘宝等),更注重用户体验,也不用担心没有竞争浏览器引擎开发会停滞不前,因为还有毕竟还有IE依然在那坚挺着。

好了,进入正题:JavaScript解析引擎到底是干什么的?JavaScript解析引擎就是根据ECMAScript定义的语言的标准来动态执行javascript字符串,虽然之前说现在很多浏览器不全是按照标准来的,解释机制也不尽相同,但动态解析js的过程还是分两个阶段:语法检查和运行阶段,再怎么变他还是动态执行的脚本语言吧,除非它不是javascript。而语法检查包括词法分析和语法分析,运行阶段又包括预解析和运行阶段(像V8引擎会将javascript字符串编译成二进制代码,此过程应该归到语法检查过程中)。

了解JavaScript的解析过程能帮助我们理解为什么用 var 声明 和 用 function 声明的函数变量名预先解析、理解作用域和作用域链、闭包以及那些编写高性能js技巧的原因。

JavaScript解析过程

在JavaScript解析过程中,如有错误就直接跳出执行下一个 script 代码段,所以在同一个 script 内的代码段有错误的话就不会执行下去,但是不会影响下一个 script 内的代码段。

语法检查

语法检查也是JavaScript解析器的工作之一,包括 词法分析 和 语法分析,过程大致如下:

词法分析

词法分析,JavaScript解释器先把javascript代码(字符串)的字符流按照ECMAScript标准转换为记号流。

例如:把字符流:

a = (b - c);

转换为记号流:

NAME "a"
EQUALS
OPEN_PARENTHESIS
NAME "b"
MINUS
NAME "c"
CLOSE_PARENTHESIS
SEMICOLON

语法分析

语法分析:JavaScript语法分析器在经过词法分析后将记号流,按照ECMAScript标准把词法分析所产生的记号生成语法树

通俗地说就是,把从程序中收集的的信息存储到数据结构中,每取一个词法记号,就送入语法分析器进行分析。

语法分析不要做的事:去掉注释,自动生成文档;提供错误位置(可以通过记录行号来提供)。而ECMAScript标准如下:

  1. var,if,else,break,continue等是JavaScript的关键词
  2. abstract,int,long等是JavaScript保留词
  3. 怎么样算是数字、怎么样算是字符串等等
  4. 定义了操作符(+,-,=)等操作符
  5. 定义了JavaScript的语法
  6. 定义了对表达式,语句等标准的处理算法,比如遇到==该如何处理
  7. ……

然后语法检查正确无误之后,就可以进入运行阶段了。

运行阶段

“预解析”

在javascript引擎在将语法检查后正确后生成的语法树复制进来之后,javascript引擎会对语法树当中的变量声明、函数声明以及函数的形参进行属性填充,这过程是发生在执行代码之前,也就是为真正的解析阶段做好准备,这既是为什么此阶段叫“预解析”的原因了。而“预解析”从语法检查阶段复制过来的包括以下信息:

  1. 内部变量表varDecls

    将varDecls保存的用var进行显示声明的局部变量。

  2. 内嵌函数表funDecls

    函数声明在语法分析阶段工作就跟变量声明不同。在“预解析”阶段,发现有函数定义的时候,除了记录函数的声明外,还会创建一个原型链对象(prototype)。

  3. …其他的信息还没了解到

火狐的javascript引擎SpiderMonkey可以访问属性__parent__,现在火狐的采用的是JaegerMonkey引擎访问不到这个属性。此属性访问到的是全局变量对象(不同版本的SpiderMonkey访问到的对象可以不同,我在火狐2.0测试访问到的是全局变量对象)。如下测试代码:

var global = window;
var a = 10;
var e;
function foo(i) {
    console.log(i);// 1
}
var b = {
    c:function(){},
    d:function(){}
};
var VO = foo.__parent__;
console.log(a.__parent__); // window
console.log(VO.e); // undefined
console.log(VO === global); // true
console.log(VO.a); // 10
foo(1);
console.log(p); // error p is not defined

JavaScript欲速则不达—通过解析过程了解JavaScript

测试环境是firefox2.0

创建函数foo是为了能通过函数来访问__parent__属性并且赋值给变量VO,__parent__属性也可以通过变量以及对象来访问。函数foo是在全局上下文中创建的,所以__parent__属性就是全局上下文的变量对象。

通过以上测试结果,我们可以大概明白下“预解析”所做的事,再此之前我们得先清楚执行上下文、变量对象、活动对象的概念。

执行上下文

执行上下文可以可以理解为object

在进入任何执行上下文前就创建出来的就是在全局对象,所以上例中通过__parent__得到的全局对象window就等于当前执行上下文。

执行上下文结构(exection context,也有称之为执行上下文)包括:

  • 变量对象(Variable Object):vars、function declaration(变量、函数声明),arguments(参数)构成,变量对象是以单例形式存在
  • 作用域链(Scope Chain):variable object + all parent scopes(变量对象以及所有父级作用域)构成
  • this值:(thisValue):content object

在进入任何一个执行上下文之前或者进入代码之前首先被创建出来的是全局下的执行上下文,此对象的属性可以在程序任何地方访问到。不同的执行上下文中的存储的值也会不同,而在javascript中能创建不同上下文的就只有函数,而函数有个特殊的变量对象 活动对象

活动对象包含函数内的变量对象之外,还包含特殊对象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是不共享的,就算通过arguments对象的index-property修改也不会影响到函数形参z,依然是“预解析”填充的undefined值
    z = 40;
    alert(arguments[2]);// undefined
    arguments[2] = 50;
    alert(z);// 40
}
foo(10, 20);

以上代码我们知道“预解析”阶段创建执行上下文之后,还会对变量对象/活动对象(VO/AO)的一些属性填充数值(这个过程导致的结果在 《关于变量以及“预解析”对变量的影响》说的很详细了):

  • 函数的形参

    执行上下文中的变量对象的一个属性,其属性名就是形参的名字,其值就是实参的值;对于没有传递的参数,其值为undefined

  • 函数声明

    执行上下文中的变量对象的一个属性,属性名和值都是函数对象创建出来的;如果变量对象已经包含了相同名字的属性,则会替换它的值

  • 变量声明

    执行上下文中的变量对象的一个属性,其属性名即为变量名,其值为undefined;如果变量名和已经声明的函数名或者函数的参数名相同,则不会影响已经存在函数声明的属性。

所以我们想到几条准则:

  1. 为什么说采用json格式来创建对象比调用构造器的性能高了?因为执行上下文中的变量对象是采用用json格式存储的;
  2. 为什么给函数参数最好给变量或者形参定义一个初始值?虽然不赋值其值为undefined,但是有时会影响代码可读性

变量对象/活动对象(VO/AO)填充的顺序也是按照以上顺序:函数的形参->函数申明->变量申明;在变量对象/活动对象(VO/AO)中权重高低也按照函数的形参->函数申明->变量申明顺序来。如下代码:

var a=1;
function b(a) {
    alert(a);
}
var b;
alert(b);//  function b( a) { alert(a); }
b(); //undefined
                
变量对象/活动对象(VO/AO)填充及优先顺序

以上代码在进入执行上下文时,按照函数的形参->函数申明->变量申明顺序来填充,并且优先权永远都是函数的形参>函数申明>变量申明,所以只要alert(a)中的a是函数中的形参,就永远不会被函数和变量申明覆盖。就算没有赋值也是默认填充的undefined值。

执行代码

经过“预解析”创建执行上下文之后,就进入执行代码阶段,VO/AO就会重新赋予真实的值,“预解析”阶段赋予的undefined值会被覆盖。

此阶段才是程序真正进入执行阶段,javascript引擎会一行一行的读取并运行代码了。此时那些变量都会重新赋值,就算不是用 var 关键字声明的变量,在此阶段也会被当作变量赋值,但是此变量不管是不是在全局环境内声明,最后都会被当作全局变量。

假如变量是定义在函数内的,代码段从头到尾该函数都没被激活(调用)的话,则变量值永远都是undefined值;如果不是用 var 关键字声明,该函数永远没被调用,则该变量怎永远不会出现。这样似乎能让javascript执行速度更快,所以我们声明变量就一定得用var申明,不然它就不是真正意义上的变量。

进入了执行代码阶段,在“预解析”阶段所创建的任何东西可能都会改变,不仅仅是VO/AO,this和作用域链也会因为某些语句而改变,下面我们会讲到。

了解完javascript的解析过程最后我们再来了解下firebug的控制台对javascript的报错提示吧。

其实firebug的控制台也算是JavaScript的解释器,而且他们会提示我们看到哪行错误了或者错误发生在哪个时期。语法检查阶段错误了还是运行期错误了,如下:

alert(var);// SyntaxError: syntax error 语法分析阶段错误 :语法错误
var=1;;    // SyntaxError:  missing variable name 语法分析阶段错误 :var是保留字符,导致变量名丢失
a=b=v      // ReferenceError: v is not defined    运行期错误: v 是未定义的
JavaScript错误信息)

有如此详细的错误提示,是不是就很快就知道代码中到底是哪里错了呢!

接下来我们详细来介绍与执行上下文中的一个重要的概念——作用域链:

作用域链(Scope Chain)

作用域链是一条用于处理标识符进行变量查询的变量对象列表,每个上下文都有自己的变量对象:对于全局上下文而言,其变量对象就是全局对象本身;对于函数而言,其变量对象就是活跃对象。

作用域链和执行上下文有关,用于在处理标识符时进行变量查询,而且它是一条变量对象的链,所以用数组来表示比较形象。作用域链包含了活跃对象和该函数的内部属性[[Scope]](因为该属性是上下文中Scope中的Scope,所以用暂且用两个中括号[[]]表示),大致如下:

content = {
    VO:{...},//或者 AO
    this:thisValue,
    Scope:[
        AO||Vo
        [Scope]
    ]
}
作用域链以及执行上下文的关系

在javascript中只有函数能规定作用域,全局执行上下文中的 Scope 是全局上下文中的属性,也是最外层的作用域链。

函数的属性[[Scope]]是在“预解析”的时候就已经存在的了,它包含了所有上层变量对象,并一直保存在函数中。就算函数永远都没被激活(调用),[[Scope]]也都还是存在函数对象上。

在创建执行上下文的 Scope 属性和进入执行上下文过程如下:

            Scope = AO + [[Scope]] //预解析时的 Scope 属性
            Scope = [AO].concat([[Scope]]); //执行阶段,将AO添加到作用域链的最前端
        
执行上下文定义的 Scope 属性变化过程

执行上下文中的[AO]是函数的活动对象,而[[Scope]]则是该函数属性作用域。当前函数的AO永远是在最前面的,保存在堆栈上,而每当函数激活的时候,这些AO都会压栈到该堆栈上,查询变量是先从栈顶开始查找,也就是说作用域链的栈顶永远是当前正在执行的代码所在环境的VO/AO(当函数调用结束后,则会从栈顶移除)。

通俗点讲就是:JavaScript解释器通过作用域链将不同执行位置上的变量对象串连成列表,并借助这个列表帮助JavaScript解释器检索变量的值。作用域链相当于一个索引表,并通过编号来存储它们的嵌套关系。当JavaScript解释器检索变量的值,会按着这个索引编号进行快速查找,直到找到全局对象为止,如果没有找到值,则传递一个特殊的 undefined值。

是不是又想到了一条javascript高效准则:为什么说在该函数内定义的变量,能减少函数嵌套能提高javascript的效率?因为函数定义的变量,此变量永远在栈顶,这样子查询变量的时间变短了。

作用域的特性

保证查询有序的访问所有变量和函数

作用域链感觉就是一个VO链表,当访问一个变量时,先在链表的第一个VO上查找,如果没有找到则继续在第二个VO上查找,直到搜索结束,也就是搜索到全局执行环境的VO中。这也就形成了作用域链的概念。

var color="blue";
function changecolor(){
    var anothercolor="red";
    function swapcolors(){
        var tempcolor=anothercolor;
        anothercolor=color;
        color=tempcolor;
        // Todo something
    }
    swapcolors();
}
changecolor();//这里不能访问tempcolor和anocolor;但是可以访问color;
alert("Color is now  "+color);
作用域链保护变量安全

分析代码如下:

  • 全局环境有1个变量color和1个函数changecolor()。
  • changecolor()函数的局部环境中具有1个anothercolor属性和1个swapcolors函数,当然,changecolor函数中可以访问自身以及它外围(也就是全局环境)中的变量。
  • swapcolor()函数的局部环境中具有1个变量tempcolor。在该函数内部可以访问上面的2个环境(changecolor和window)中的所有变量,因为那2个环境都是它的父执行环境。

通过上例以及分析,我们可以得知内部环境可以通过作用域链访问所有外部环境的变量对象,但外部环境不能访问内部环境中的的变量对象。这些环境之间是线性、有次序的。每个环境都可以向上搜索作用域链,以便查询变量和函数名;但任何环境不能通过向下搜索作用域链条进入另一个环境。对于上述例子的swapcolor()函数而言,其作用域链包括:swapcolor()的变量对象、changecolor()变量对象和全局对象。swapcolor()的局部环境开始先在自己的Variable Object中搜索变量和函数名,找不到,则向上搜索changecolor作用域链。。。。。以此类推。但是,changecolor()函数是无法访问swapcolor中的变量。

闭包

闭包:所有的函数都是闭包,因为它们都是在创建的时候就保存了上层上下文的作用域链。函数内能看出作用域的那些特性呢?如下:

var x = 10;
function run() {
    var x = 20;
    foo(); // 10, but not 20
}
function foo() {
    console.log(x);
}
run();
作用域闭包举例一

为什么是10而不是20?之前不是说闭包函数会将自身的AO添加到上一级函数的作用域链上,而函数foo()是定义在全局上下文中的,所以函数foo()的作用域链上只有全局对象中的var x =10;

function foo() {
    var x = 10;
    var y = 20;
    return function () {
        console.log([x, y]);
    };
}
var x = 30;
var bar = foo();
bar(); // [10, 20]
作用域闭包举例二

上例再一次告诉我们:闭包中确认变量的值得先明确它的作用域链以及变量对象在作用域链上的前后顺序。上例中的匿名函数是创建在函数foo内的,所以foo()函数中定义的变量x在作用域链中的位置比全局上下文中的x靠前,前者比后者先访问到。以上两个例子同时也说明了函数的作用域是在函数创建既“预解析”阶段就已经就已经定义了,而在代码执行阶段则是将函数的作用域添加到作用域链上。

不过有个例外,就是当函数是通过构造函数创建的时候。如下:

var x = 10;
function foo() {
    var y = 20;
    function barFD() {
        console.log(x);
        console.log(y);
    }
    var barFE = function () {
        console.log(x);
        console.log(y);
    };
    var barFn = new Function('console.log(x); console.log(y);');

    barFD(); // 10, 20
    barFE(); // 10, 20
    barFn(); // 10, "y" is not defined
}
foo();
                
构造函数中的作用域链

上例中,函数barFn是通过Function构造器来构建的,当函数通过Function构造器来创建的时候,其[[Scope]]属性永远都是全局上下文对象上的Scope属性。 哪怕是在非全局上下文创建闭包都只能访问到全局上下文对象中的Scope属性。

原型链查询

在介绍“预解析”阶段时,我们有提到当创建函数时,同时也会创建原型链对象(prototype)函数天生的。原型链对象在作用域链中没有找到变量对象时,那么就会通过原型链来查找。

function Foo() {
    function bar() {
        alert(x);
    }
    bar();
}
Object.prototype.x = 10;
Foo(); // 10
原型链查询

上例中在作用域链中遍历查询,到了全局对象了,该对象继承自Object.prototype,因此,最终变量“x”的值就变成了10。

不过,在原型链上定义变量对象有些浏览器不支持,譬如IE6,而且这样增加了变量对象的查询时间。

所以变量声明尽量在调用函数AO里,既在用到该变量的函数内声明变量对象。

最后再提下eval这个函数,这个函数能改变作用域链(文章接下来还会说能改变作用域链的方法)。但是它在各个浏览器中有差异:

  • IE(低版本):window.eval()和eval()一样只在当前作用域生效。
  • 现代浏览器:直接调用eval()为当前作用域,window.eval()调用为全局作用域。

是不是又想起一条法则了?在写js中别使用eval()函数。

作用域是在“预解析”时就已经决定的,所以作用域被叫做静态作用域,而在执行阶段的则被叫做动态链,因为在执行阶段会改变作用域链中填充的值。

代码执行阶段对“预解析”的改变

前面我们说到预解析中创建的执行上下文包括:变量对象、this、作用域链。关于执行阶段对变量对象影响,接下来就不再说了,有兴趣可以阅读了解更多。

以上我们知道了什么是作用域链以及作用域链的作用,但是之前说的作用域链接大部分是“预解析”就已经确定下来的,接下来我们说说执行代码后的作用域链。

执行改变作用域链

上面我们有说到:函数都是闭包这个概念。

创建了函数就有一个闭包,而变量是在函数的执行上下文保存起来的静态作用域链上查询的,而当前函数内创建的的变量会在函数结束后就被销毁。而闭包就能函数结束之后还能让这些变量一直保存在作用域链上。闭包概念如下:

  • 理论角度:所有函数都是闭包。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量。
  • 应用角度:当在代码中引用了自由变量,即使创建它的上下文已经销毁,此变量还能访问。

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量

关于闭包的成因阅读。ECMAScript标准中,同一个上下文创建的闭包(理论上的闭包)是共用一个作用域的,也就是说闭包中对其中变量修改会影响到其他闭包对其闭包的读取。如闭包的使用的例子,自由变量i在两个闭包的作用域中,所以i值产生了变化而影响另外一个闭包的i的取值。

在介绍闭包成因的文章中有说举过一个在循环中找到对应元素的索引的例子,当时的解决方案:利用创建额外的闭包(理论上的闭包)来解决同一个上下文创建的闭包共用一个作用域的问题。将i值传进去,每遍历一次就创建一个新的变量对象,也就是一个新的作用域,所以就不会影响其他作用于下的变量对象。

所谓创建额外的闭包就是创建函数,不管是匿名函数、函数表达式、函数声明(除了构造函数),只要能创建作用域链就行,与函数类型无关,然而创建额外的函数不是唯一的方法。

遍历最外层代码:

for (i = 0; i < len; i++) {
...//①
}
            
遍历获取索引值最外层代码
  • 使用函数创建闭包:

    方法一:

    (function(i){
        lists[i].onmouseover = function () {
           alert(i);
        };
    })(i);
                        
    使用函数闭包获取索引值方法一

    直接将匿名函数赋值给事件,创建额外的函数来创建多个作用域。

    方法二:

    lists[i].onmouseover = (function (x) {
        return function (){
            alert(i);
        };
    })(i);
                        
    使用函数闭包获取索引值方法二

    而方法二原理跟方法一类似。它是利用return在闭包中返回,而闭包中返回的语句会将控制流返回给调用上下文,也就是是返回几个就有几个执行上下文,相应的作用域链也有相同的个数。

  • 使用with改变作用域链:

    with({i:i})
        lists[i].onmouseover = function(){alert(i)};
                        
    使用with创建对象闭包

    对于with内的对象变量。当代码运行到with语句,with对象将会被推入作用域链顶端,如下例子:

    var x = 10, y = 10;
    with ({x:20,y:20}) {
        var y = 30;
        alert(x); // 20
        alert(y); // 30
    }
    alert(x); // 10
    alert(y); // 30
                        
    with语句实例

    发生了什么?怎么最外层的y成了30,里层的x变成了20?

    在进入上下文的时候(“预解析阶段”),变量x、y已经被添加到了执行上下文中的变量对象中,并且位于作用域链的顶端。之后,到了执行代码阶段,发生了如下的改动:

    1. x、y被赋值:x=10, y=10
    2. 对象{x:20,y:20}添加到了作用域链的顶端,修改了“x”和“y”的值:x=20, y=20
    3. 在with中遇到了var语句,修改变量y的值:y=30
    4. 在with语句结束之后,其特殊对象从作用域链中移除(with对象以及语句中修改的也将被移除),作用域链又恢复到了with语句前的状态

    是不是很不错?但是有些人马山就想起了一条javascript性能准则:尽可能的避免使用with语句。

    因为当代码运行到with语句时,运行期上下文的作用域链临时被改变了。一个新的变量对象被创建,这个对象被推入作用域链的顶端,这就意味着with以外的变量对象处于第二个作用域链中,因此访问代价肯定会提高,所以访问要避免使用with语句。

  • 使用try { ... } catch (ex) { ... }改变作用域链:

    try-catch改变作用域链的原理跟with一样,try 部分包含需要运行的代码,而 catch 部分包含错误发生时运行的代码。如下:

    var array = null;
    var x=10;
    try {
        document.write(array[0]);
    } catch(x) {
    x =20;
        document.writeln("catch内的x值"+x); //20
    }
    document.writeln("catch外的x值"+x);   //10
                        
    try..catch事例

    try-catch语句在代码调试和异常处理中非常有用,因此不建议完全避免。

this

跟作用域链不同,this值在进入上下文阶段就确定了。一旦进入执行代码阶段,this值就不会变了。如下:

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的值是函数调用者提供并且由当前调用表达式的形式而定的。 如果调用括号()的左边的是值引用类型值,那么该值就会设置为该对象本身。其他情况下,this的值总是null,由于null对于this来说没有任何意义,因此会隐式转换为全局对象。
引用类型的值有两种情况:当我们处理一个标示符时或一个属性访问器。标识符是变量名,函数名,函数参数名和全局对象中未识别的属性名。

以上规则是我们判断this值的口诀,很重要。然后我们利用口诀来分析一下例子。

全局代码中的this值

这种情况是最简单的,this值总是全局对象,因此可以利用this来访问全局对象上的属性:

var a = 10;
alert(this.a); // 10
alert(this);  // null =>object window
                
全局代码中的this值

第一个例子:a是在全局对象上的属性,所以弹出是10。

第二个例子:因为它不既不是标识符也不属于属性访问,因此this值会被设置为null,最终隐式转换成全局对象。

函数内的this的值

此函数是指用function声明的函数。然后我们来讲讲特殊对象——函数,如下实例

(function(){
function bar() {
    alert(this); // object window
}
bar();
function New(){
    alert(this); // object Object
}
var n = new New();
})();
                
函数内的this值事例

参考口诀,首先我们解释以上例子

第一个例子:bar()调用当前表达式,小括号前面不是函数名么?怎么this指向还是全局对象?这是因为当调用括号()的左边的值是函数,局部变量、内部函数以及函数的形参都存储在该函数的活跃对象上时,this的值是null,最终隐式转换为全局对象。

第二个例子:不解释,记住使用构造函数创建出来的函数this指向就是该函数对象本省。

函数对象但不是引用类型的对象(它不是标示符,也不是属性访问器),相应地,this值最终设为全局对象。函数就是这么特殊,以上两个例子就可以说明这个特殊的对象特殊性了,而且我们还可以利用Function.prototype上定义的两个方法:.apply和.call来手动设置函数调用时的this值。这两个方法都接受第一个参数作为调用上下文中this的值。第二个参数有点不同:对于.apply来说,第二个参数接受数组类型(或者是类数组的对象,比如arguments); 而.call方法接受任意多的参数。这两个方法只有第一个参数是必要的——this的值。如下:

var b = 10;
function a(c) {
    alert(this.b);
    alert(c);
}
a(10); // this === global, this.b == 10, c == 10
a.call({b: 20}, 20); // this === {b: 20}, this.b == 20, c == 20
a.apply({b: 30}, [30]); // this === {b: 30}, this.b == 30, c == 30
                
手动设置函数调用时this的值

当函数调用包含在with语句中,并且with对象包含一个函数属性的时候,with语句会将该对象添加到作用域链的最顶端,在活跃对象之前,所以就不会出现调用括号()的左边的值是活跃对象的情况了,原因跟执行改变作用域链中的with语句一样:with语句会将该对对象添加到作用域链的最顶端。如下:

var x = 10;
with ({
    foo: function () {
        alert(this.x);
    },
    x: 20
}) {
    foo(); // 20
}
                
with语句改变this值

with语句对JavaScript高效有影响,所以还是一如既往的不建议使用。try-catch也能完成相同的任务。

结束语:文章有点长,希望对大家有所帮助。哪里有错误,请务必留言转告小北我,谢谢!

本文源链接:http://www.html5jscss.com/js-data-scope.html

转载请注明《JavaScript欲速则不达—通过解析过程了解JavaScript》| html5jscss

评论关闭了.