javascript语法里一些难点问题

谈谈javascript语法里一些难点问题 

1)    引子

  前不久我建立的技术群里一位MM问了一个这样的问题,她贴出的代码如下所示:

复制代码
var a = 1;

function hehe()

{

         window.alert(a);

         var a = 2;

         window.alert(a);

}

hehe();
复制代码

  执行结果如下所示:

  第一个alert:

 

  第二个alert:

 

  这是一个令人诧异的结果,为什么第一个弹出框显示的是undefined,而不是1呢?这种疑惑的原理我描述如下:

  一个页面里直接定义在script标签下的变量是全局变量即属于window对象的变量,按照javascript作用域链的原理,当一个变量在当前作用域下找不到该变量的定义,那么javascript引擎就会沿着作用域链往上找直到在全局作用域里查找,按上面的代码所示,虽然函数内部重新定义了变量的值,但是内部定义之前函数使用了该变量,那么按照作用域链的原理在函数内部变量定义之前使用该变量,javascript引擎应该会在全局作用域里找到变量定义,而实际情况却是变量未定义,这到底是怎么回事呢?

  当时群里很多人都给出了问题的解答,我也给出了我自己的解答,其实这个问题很久之前我的确研究过,但是刚被问起了我居然还是有个卡壳期,在加上最近研究javascriptMVC的写法,发现自己读代码时候对new 、prototype、apply以及call的用法任然要体味半天,所以我觉得有必要对javascript基础语法里比较难理解的问题做个梳理,其实写博客的一个很大的好处就是写出来的知识逻辑会比你在脑子里反复梳理的逻辑映像更加的深刻。

       下面开始本文的主要内容,我会从基础知识一步步讲起。

2)    Javascript的变量

  Java语言里有一句很经典的话:在java的世界里,一切皆是对象

  Javascript虽然跟java没有半点毛关系,但是很多会使用javascript的朋友同样认为:在javascript的世界里,一切也皆是对象

  其实javascript语言和java语言一样变量是分为两种类型:基本数据类型和引用类型。

  基本类型是指:Undefined、Null、Boolean、Number和String;而引用类型是指多个指构成的对象,所以javascript的对象指的是引用类型。在java里能说一切是对象,是因为java语言里对所有基本类型都做了对象封装,而这点在javascript语言里也是一样的,所以提在javascript世界里一切皆为对象也不为过。

  但是实际开发里如果我们对基本类型和引用类型的区别不是很清晰,就会碰到我们很多不能理解的问题,下面我们来看看下面的代码:

复制代码
    var str = "sharpxiajun";

    str.attr01 = "hello world";

    console.log(str);//  运行结果:sharpxiajun

    console.log(str.attr01);// 运行结果:undefined
复制代码

 

  运行之,我们发现作为基本数据类型,我们没法为这个变量添加属性,当然方法也同样不可以,例如下面的代码:

复制代码
    str.ftn = function(){

        console.log("str ftn");

    }

    str.ftn();
复制代码

  运行之,结果如下图所示:

 

 当我们使用引用类型时候,结果就和上面完全不同了,大家请看下面的代码:

    var obj1 = new Object();

    obj1.name = "obj1 name";

    console.log(obj1.name);// 运行结果:obj1 name

  javascript里的基本类型和引用类型的区别和其他语言类似,这是一个老调长谈的问题,但是在现实中很多人都理解它,但是却很难应用它去理解问题。

   Javascript里的基本变量是存放在栈区的(栈区指内存里的栈内存),它的存储结构如下图所示:


   
javascript里引用变量的存储就比基本类型存储要复杂多,引用类型的存储需要内存的栈区和堆区(堆区是指内存里的堆内存)共同完成,如下图所示:

  在javascript里变量的存储包含三个部分:

    部分一:栈区的变量标示符;

    部分二:栈区变量的值;

    部分三:堆区存储的对象。

  变量不同的定义,这三个部分也会随之发生变化,下面我来列举一些典型的场景:

  场景一:如下代码所示

    var qqq;

    console.log(qqq);// 运行结果:undefined

 

  运行结果是undefined,上面的代码的标准解释就是变量被命名了,但是还未初始化,此时在变量存储的内存里只拥有栈区的变量标示符而没有栈区的变量值,当然更没有堆区存储的对象。

  场景二:如下代码所示

    var qqq;

    console.log(qqq);// 运行结果:undefined

    console.log(xxx);

 

  运行之,结果如下图所示:

 

  会提示变量未定义。在任何语言里变量未定义就使用都是违法的,我们看到javascript里也是如此,但是我们做javascript开发时候,经常有人会说变量未定义也是可以使用,怎么我的例子里却不能使用了?那么我们看看下面的代码:

复制代码
    xxx = "outer xxx";

    console.log(xxx);// 运行结果:outer xxx

    function testFtn(){

        sss = "inner sss";

        console.log(sss);// 运行结果:outer sss

    }

    testFtn();

    console.log(sss);//运行结果:outer sss

    console.log(window.sss);//运行结果:outer sss
复制代码

  在javascript定义变量需要使用var关键字,但是javascript可以不使用var预先定义好变量,在javascript我们可以直接赋值给没有被var定义的变量,不过此时你这么操作变量,不管这个操作是在全局作用域里还是在局部作用域里,变量最终都是属于window对象,我们看看window对象的结构,如下图所示:

 

  由这两个场景我们可以知道在javascript里的变量不能正常使用即报出“xxx is not defined”错误(这个错误下,后续的javascript代码将不能正常运行)只有当这个变量既没有被var定义同时也没有进行赋值操作才会发生,而只有赋值操作的变量不管这个变量在那个作用域里进行的赋值,这个变量最终都是属于全局变量即window对象

  由上面我列举的两个场景我们来理解下引子里网友提出的问题,下面我修改一下代码,如下所示:

复制代码
    //var a = 1;

    function hehe()

    {

        console.log(a);

        var a = 2;

        console.log(a);

    }

    hehe();
复制代码

  结果如下图所示:

 

  我再改下代码:

复制代码
    //var a = 1;

    function hehe()

    {

        console.log(a);

       // var a = 2;

        console.log(a);

    }

    hehe();
复制代码

  运行之,结果如下所示:

 

  对比二者代码以及引子里的代码,我们发现问题的关键是var a=2所引起的。在代码一里我注释了全局变量的定义,结果和引子里代码的结果一致,这说明函数内部a变量的使用和全局环境是无关的,代码二里我注释了关键代码var a = 2,代码运行结果发生了变化,程序报错了,的确很让人困惑,困惑之处在于局部作用域里变量定义的位置在变量第一次使用之后,但是程序没有报错,这不符合javascript变量未定义既要报错的原理。

  其实这个变量任然被定义即内存存储里有了标示符,只不过没有被赋值,代码一则说明,内部变量a已经和外部环境无关,怎么回事?如果我们按照代码运行是按照顺序执行的逻辑来理解,这个代码也就没法理解。

  其实javascript里的变量和其他语言有很大的不同,javascript的变量是一个松散的类型,松散类型变量的特点是变量定义时候不需要指定变量的类型,变量在运行时候可以随便改变数据的类型,但是这种特性并不代表javascript变量没有类型,当变量类型被确定后javascript的变量也是有类型的。但是在现实中,很多程序员把javascript松散类型理解为了javascript变量是可以随意定义即你可以不用var定义,也可以使用var定义,其实在javascript语言里变量定义没有使用var,变量必须有赋值操作,只有赋值操作的变量是赋予给window,这其实是javascript语言设计者提升javascript安全性的一个做法。

  此外javascript语言的松散类型的特点以及运行时候随时更改变量类型的特点,很多程序员会认为javascript变量的定义是在运行期进行的,更有甚者有些人认为javascript代码只有运行期,其实这种理解是错误的,javascript代码在运行前还有一个过程就是:预加载,预加载的目的是要事先构造运行环境例如全局环境,函数运行环境,还要构造作用域链(关于作用域链和环境,本文后续会做详细的讲解),而环境和作用域的构造的核心内容就是指定好变量属于哪个范畴,因此在javascript语言里变量的定义是在预加载完成而非在运行时期。

  所以,引子里的代码在函数的局部作用域下变量a被重新定义了,在预加载时候a的作用域范围也就被框定了,a变量不再属于全局变量,而是属于函数作用域,只不过赋值操作是在运行期执行(这就是为什么javascript语言在运行时候会改变变量的类型,因为赋值操作是在运行期进行的),所以第一次使用a变量时候,a变量在局部作用域里没有被赋值,只有栈区的标示名称,因此结果就是undefined了。

  不过赋值操作也不是完全不对预加载产生影响,预加载时候javascript引擎会扫描所有代码,但不会运行它,当预加载扫描到了赋值操作,但是赋值操作的变量有没有被var定义,那么该变量就会被赋予全局变量即window对象。

  根据上面的内容我们还可以理解下javascript两个特别的类型:undefined和null,从javascript变量存储的三部分角度思考,当变量的值为undefined时候,那么该变量只有栈区的标示符,如果我们对undefined的变量进行赋值操作,如果值是基本类型,那么栈区的值就有值了,如果栈区是对象那么堆区会有一个对象,而栈区的值则是堆区对象的地址,如果变量值是null的话,我们很自然认为这个变量是对象,而且是个空对象,按照我前面讲到的变量存储的三部分考虑:当变量为null时候,栈区的标示符和值都会有值,堆区应该也有,只不过堆区是个空对象,这么说来null其实比undefined更耗内存了,那么我们看看下面的代码:

复制代码
    var ooo = null;

    console.log(ooo);// 运行结果:null

    console.log(ooo == undefined);// 运行结果:true

    console.log(ooo == null);// 运行结果:true

    console.log(ooo === undefined);// 运行结果:false

    console.log(ooo === null);// 运行结果:true
复制代码

 

  运行之,结果很震惊啊,null居然可以和undefined相等,但是使用更加精确的三等号“===”,发现二者还是有点不同,其实javascript里undefined类型源自于null即null是undefined的父类,本质上null和undefined除了名字这个马甲不同,其他都是一样的,不过要让一个变量是null时候必须使用等号“=”进行赋值了。

  当变量为undefined和null时候我们如果滥用它javascript语言可能就会报错,后续代码会无法正常运行,所以javascript开发规范里要求变量定义时候最好马上赋值,赋值好处就是我们后面不管怎么使用该变量,程序都很难因为变量未定义而报错从而终止程序的运行,例如上文里就算变量是string基本类型,在变量定义属性程序还是不会报错,这是提升程序健壮性的一个重要手段,由引子的例子我们还知道,变量定义最好放在变量所述作用域的最前端,这么做也是保证代码健壮性的一个重要手段。

  下面我们再看一段代码:

复制代码
    var str;

    if (undefined != str && null != str && "" != str){

        console.log("true");

    }else{

        console.log("false");

    }

    if (undefined != str && "" != str){

        console.log("true");

    }else{

        console.log("false");

    }

    if (null != str && "" != str){

        console.log("true");

    }else{

        console.log("false");

    }

    if (!!str){

        console.log("true");

    }else{

        console.log("false");

    }

    str = "";

    if (!!str){

        console.log("true");

    }else{

        console.log("false");

    }
复制代码

 

  运行之,结果都是打印出false。

  使用双等号“==”,undefined和null是一回事,所以第一个if语句的写法完全多余,增加了不少代码量,而第二种和第三种写法是等价,究其本质前三种写法本质都是一致的,但是现实中很多程序员会选用写法一,原因就是他们还没理解undefined和null的不同,第四种写法是更加完美的写法,在javascript里如果if语句的条件是undefined和null,那么if判断的结果就是false,使用!运算符if计算结果就是true了,再加一个就是false,所以这里我建议在书写javascript代码时候判断代码是否为未定义和null时候最好使用!运算符。

  代码四里我们看到当字符串被赋值了,但是赋值是个空字符串时候,if的条件判断也是false,javascript里有五种基本类型,undefined、null、boolean、Number和string,现在我们发现除了Number都可以使用!来判断if的ture和false,那么基本类型Number呢?

复制代码
    var num = 0;

    if (!!num){

        console.log("true");

    }else{

        console.log("false");

    }
复制代码

 

  运行之,结果是false。

  如果我们把num改为负数或正数,那么运行之的结果就是true了。

  这说明了一个道理:我们定义变量初始化值的时候,如果基本类型是string,我们赋值空字符串,如果基本类型是number我们赋值为0,这样使用if语句我们就可以判断该变量是否是被使用过了。

  但是当变量是对象时候,结果却不一样了,如下代码:

复制代码
    var obj = {};

    if (!!obj){

        console.log("true");

    }else{

        console.log("false");

    }
复制代码

 

  运行之,代码是true。

  所以在定义对象变量时候,初始化时候我们要给变量赋予null,这样if语句就可以判断变量是否初始化过。

  其实if加上!运算判断对象的现象还有玄机,这个玄机要等我把场景三讲完才能说清楚哦。

  场景三:复制变量的值和函数传递参数

  首先看看这个场景的代码:

复制代码
    var s1 = "sharpxiajun";

    var s2 = s1;

    console.log(s1);//// 运行结果:sharpxiajun

    console.log(s2);//// 运行结果:sharpxiajun

    s2 = "xtq";

    console.log(s1);//// 运行结果:sharpxiajun

    console.log(s2);//// 运行结果:xtq
复制代码

 

  上面是基本类型变量的赋值,我们再看看下面的代码:

复制代码
    var obj1 = new Object();

    obj1.name = "obj1 name";

    console.log(obj1.name);// 运行结果:obj1 name

    var obj2 = obj1;

    console.log(obj2.name);// 运行结果:obj1 name

    obj1.name = "sharpxiajun";

    console.log(obj2.name);// 运行结果:sharpxiajun
复制代码

 

  我们发现当复制的是对象,那么obj1和obj2两个对象被串联起来了,obj1变量里的属性被改变时候,obj2的属性也被修改。

  函数传递参数的本质就是外部的变量复制到函数参数的变量里,我们看看下面的代码:

复制代码
    function testFtn(sNm,pObj){

        console.log(sNm);// 运行结果:new Name

        console.log(pObj.oName);// 运行结果:new obj

        sNm = "change name";

        pObj.oName = "change obj";

    }

    var sNm = "new Name";

    var pObj = {oName:"new obj"};

    testFtn(sNm,pObj);

    console.log(sNm);// 运行结果:new Name

    console.log(pObj.oName);// 运行结果:change obj
复制代码

 

  这个结果和变量赋值的结果是一致的。

  在javascript里传递参数是按值传递的

  上面函数传参的问题是很多公司都爱面试的问题,其实很多人都不知道javascript传参的本质是怎样的,如果把上面传参的例子改的复杂点,很多朋友都会栽倒到这个面试题下。

为了说明这个问题的原理,就得把上面讲到的变量存储原理综合运用了,这里我把前文的内容再复述一遍,两张图,如下所示:

 

  这是基本类型存储的内存结构。

 

  这是引用类型存储的内存结构。

  还有个知识,如下:

  在javascript里变量的存储包含三个部分:

  部分一:栈区的变量标示符;

  部分二:栈区变量的值;

  部分三:堆区存储的对象。

  在javascript里变量的复制(函数传参也是变量赋值)本质是传值,这个值就是栈区的值,而基本类型的内容是存放在栈区的值里,所以复制基本变量后,两个变量是独立的互不影响,但是当复制的是引用类型时候,复制操作还是复制栈区的值,但是这个时候值是堆区对象的地址,因为javascript语言是不允许操作堆内存,因此堆内存的变量并没有被复制,所以复制引用对象复制的值就是堆内存的地址,而复制双方的两个变量使用的对象是相同的,因此复制的变量其中一个修改了对象,另一个变量也会受到影响。

  原理讲完了,下面我列举一个拔高的例子,代码如下:

复制代码
    var ftn1 = function(){

        console.log("test:ftn1");

    };

    var ftn2 = function(){

        console.log("test:ftn2");

    };

    function ftn(f){

       f();

       f = ftn2;

    }

    ftn(ftn1);// 运行结果:test:ftn1

    console.log("====================华丽的分割线======================");

    ftn1();// 运行结果:test:ftn1
复制代码

 

  这个代码是很早之前有位朋友考我的,我当时答对了,但是我是蒙的,问我的朋友答错了,其实当时我们两个都没搞懂其中缘由,我朋友是这么分析的他认为f是函数的参数,属于函数的局部作用域,因此更改f的值,是没法改变ftn1的值,因为到了外部作用域f就失效了,但是这种解释很难说明我上文里给出的函数传参的实例,其实这个问题答案就是函数传参的原理,只不过这里加入了个混淆因素函数,在javascript函数也是对象,局部作用域里f = ftn2操作是将f在栈区的地址改为了ftn2的地址,对外部的ftn1和ftn2没有任何改变。

  记住:javascript里变量复制和函数传参都是在传递栈区的值

  栈区的值除了变量复制起作用,它在if语句里也会起到作用,当栈区的值为undefined、null、“”(空字符串)、0、false时候,if的条件判断则是为false,我们可以通过!运算符计算,因此当我们的代码如下:

复制代码
    var obj = {};

    if (!!obj){

        console.log("true");

    }else{

        console.log("false");

    }
复制代码

3)    作用域链相关的问题

  作用域链是javascript语言里非常红的概念,很多学习和使用javascript语言的程序员都知道作用域链是理解javascript里很重要的一些概念的关键,这些概念包括this指针,闭包等等,它非常红的另一个重要原因就是作用域链理解起来太难,就算有人真的感觉理解了它,但是碰到很多实际问题时候任然会是丈二和尚摸不到头脑,例如上篇引子里讲到的例子,本篇要讲的主题就是作用域链,再无别的内容,希望看完本文的朋友能有所收获。

  讲作用域链首先要从作用域讲起,下面是百度百科里对作用域的定义:

作用域在许多程序设计语言中非常重要。

通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

作用域的使用提高了程序逻辑的局部性,增强程序的可靠性,减少名字冲突。

 

  在我最擅长的服务端语言java里也有作用域的概念,java里作用域是以{}作为边界,不过在纯种的面向对象语言里我们没必要把作用域研究的那么深,也没必要思考复杂的作用域嵌套问题,因为这些语言关于作用域的深度运用并不会给我们编写的代码带来多大好处。但是在javascript里却大不相同,如果我们不能很好的理解javascript的作用域我们就没办法使用javascript编写出复杂的或者规模宏大的程序。

  由百度百科里的定义,我们知道作用域的作用是保证变量的名字不发生冲突,用现实的场景来理解有个人叫做张三,张三虽然只是一个名字,但是认识张三的人根据名字就能唯一确认这个人到底是谁,但是这个世界上叫做张三的人可不止一个,特别是两个叫张三的人有交集的时候我们就要有个办法明确指定这个张三绝不是另外一个张三,这时我们可能会根据两大张三年龄的差异来区分:例如一个张三叫大张三,相对的另外一个张三叫小张三了。编程语言里的作用域其实就是为了做类似的标记,作用域会设定一个范围,在这个范围里我们是不会弄错变量的真实含义。

  前面我讲到在java里通过{}来设置作用域,在{}里面的变量会得到保护,这种保护就是不让{}里的变量被外部变量混淆和污染。那么{}的方式适合于javascript吗?我们看看下面的例子:

复制代码
    var s1 = "sharpxiajun";

    function ftn(){

        var s2 = "xtq";

        console.log(this);// 运行结果: window

        console.log("s1:" + this.s1 + ";s2:" + this.s2);//运行结果:s1:sharpxiajun;s2:undefined

        console.log("s1:" + this.s1 + ";s2:" + s2);// 运行结果:s1:sharpxiajun;s2:xtq

    }

    ftn();
复制代码

 

  在javascript世界里有一个大的作用域环境,这个环境就是window,window环境不需要我们自己使用什么方式构建,页面加载时候页面会自动构造的,上面代码里有一个大括号,这个大括号是对函数的定义,运行之,我们发现函数作用域内部定义的s2变量是不能被window对象访问的,因此s2变量是被{}保护起来了,它的生命周期和这个函数的生命周期有关。

  由这个例子是不是说明在javascript里,变量也是被{}保护起来了,在javascript语言里还有非函数的{},我们再看看下面的例子:

复制代码
    if (true){

        var a = "aaaa";

    }

    console.log(a);// 运行结果:aaaa
复制代码

 

  我们发现javascript里{}有时是起不到定义作用域的功能。这也说明javascript里的作用域定义是和其他语言例如java不同的。

  在javascript里作用域有一个专门的定义execution context,有的书里把这个名字翻译成执行上下文,有的书籍里把它翻译成执行环境,我更倾向于后者执行环境,下文我提到的执行环境就是execution context。这个命名非常形象,这个形象体现在execution这个单词,execution含义就是执行,我们来想想javascript里那些情况是执行:

  情况一:当页面加载时候在script标签下的javascript代码会按顺序执行,而这些能被执行的代码都是属于window的变量或函数;

  情况二:当函数的名字后面加上小括号(),例如ftn(),这也是在执行,不过它执行的是函数。

  如此说来,javascript里的执行环境有两类一类是全局执行环境,即window代表的全局环境,一类是函数代表的函数执行环境,这也就是我们常说的局部作用域

执行环境在javascript语言里并非是一个抽象的概念,而是有具体的实现,这个实现其实是个对象,这个对象也有个名字叫做variable object,这个变量有的书里翻译为变量对象,这是直译,有的书里把它称为上下文变量,这里我还是倾向于后者上下文变量,下文里提到的上下文变量就是指代variable object。上下文变量存储的是上下文变量所处执行环境里定义的所有的变量和函数。

  全局执行环境的上下文变量是可以访问到的,它就是window对象,所以我们说window能代表全局作用域是有道理的,但是局部作用域即函数的执行环境里的上下文变量是代码不能访问到的,不过javascript引擎在处理数据时候会使用到它。

  在javascript语言里还有一个概念,它的名字叫做execution context stack,翻译成中文就是执行环境栈,每个要被执行的函数都会先把函数的执行环境压入到执行环境栈里,函数执行完毕后,这个函数的执行环境就会被执行环境栈弹出,例如上面的例子:函数执行时候函数的执行环境会被压入到执行环境栈里,函数执行完毕,执行环境栈会把这个环境弹出,执行环境栈的控制权就会交由全局环境,如果函数后面还有代码,那么代码就是接着执行。如果函数里嵌套了函数,那么嵌套函数执行完毕后,执行环境栈的控制权就交由了外部函数,然后依次类推,最后就是全局执行环境了。

  讲到这里我们大名鼎鼎的作用域链要登场了,函数的执行环境被压入到执行环境栈里后,函数就要执行了,函数执行的第一步不是执行函数里的第一行代码而是在上下文变量里构造一个作用域链,作用域链的英文名字叫做scope chain,作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,这个概念里有两个关键意思:有权访问和有序,我们看看下面的代码:

  

复制代码
 var b1 = "b1";

    function ftn1(){

        var b2 = "b2";

        var b1 = "bbb";

        function ftn2(){

            var b3 = "b3";

            b2 = b1;

            b1 = b3;

            console.log("b1:" + b1 + ";b2:" + b2 + ";b3:" + b3);// 运行结果:b1:b3;b2:bbb;b3:b3

        }

        ftn2();

    }

    ftn1();

console.log(b1);// 运行结果:b1
复制代码

 

 

  有这个例子我们发现,ftn2函数可以访问变量b1,b2,这个体现了有权访问的概念,当ftn1作用域里改变了b1的值并且把b1变量重新定义为ftn1的局部变量,那么ftn2访问到的b1就是ftn1的,ftn2访问到b1后就不会在全局作用域里查找b1了,这个体现了有序性。

  下面我要总结下上面讲述的知识:

  本篇的小标题是:作用域链的相关问题,这个标题定义的含义是指作用域链是大名鼎鼎了,但是作用域链在广大程序员的理解里其实包含的意义已经超越了作用域链在javascript语言本身的定义。广大程序员对作用域链的理解有两块一块是作用域,而作用域在javascript语言里指的是执行环境execution context,执行环境在javascript引擎里是通过上下文变量体现的variable object,javascript引擎里还有一个概念就是执行环境栈execution context stack,当某一个函数的执行环境压入到了执行环境栈里,这个时候就会在上下文变量里构造一个对象,这个对象就是作用域链scope chain,而这个作用域链就是广大程序员理解的第二块知识,作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到window对象即被终止,作用域链向下访问变量是不被允许的。

  很多人常常认为作用域链是理解this指针的关键,这个理解是不正确的的,this指针构造是和作用域链同时发生的,也就是说在上文变量构建作用域链的同时还会构造一个this对象,this对象也是属于上下文变量,而this变量的值就是当前执行环境外部的上下文变量的一份拷贝,这个拷贝里是没有作用域链变量的,例如代码:

复制代码
    var b1 = "b1";

    function ftn1(){

        console.log(this);// 运行结果: window

        var b2 = "b2";

        var b1 = "bbb";

        function ftn2(){

            console.log(this);// 运行结果: window

            var b3 = "b3";

            b2 = b1;

            b1 = b3;

            console.log("b1:" + b1 + ";b2:" + b2 + ";b3:" + b3);// 运行结果:b1:b3;b2:bbb;b3:b3

        }

        ftn2();

    }

    ftn1();
复制代码

 

  我们看到函数ftn1和ftn2里的this指针都是指向window,这是为什么了?因为在javascript我们定义函数方式是通过function xxx(){}形式,那么这个函数不管定义在哪里,它都属于全局对象window,所以他们的执行环境的外部的执行上下文都是指向window。

  但是我们都知道现实代码很多this指针都不是指向window,例如下面的代码:

复制代码
var obj = {

    name:"sharpxiajun",

    ftn:function(){

        console.log(this);// 运行结果: Object { name="sharpxiajun", ftn=function()}

        console.log(this.name);//运行结果: sharpxiajun

    }

}

obj.ftn();//
复制代码

 

  运行之,我们发现这里this指针指向了Object,这就怪了我前文不是说javascript里作用域只有两种类型:一个是全局的一个是函数,为什么这里Object也是可以制造出作用域了,那么我的理论是不是有问题啊?那我们看看下面的代码:

复制代码
var obj1 = new Object();

obj1.name = "xtq";

obj1.ftn = function(){

    console.log(this);// 运行结果: Object { name="xtq", ftn=function()}

    console.log(this.name);//运行结果: xtq

}

obj1.ftn();
复制代码

   这两种写法是等价的,第一种对象的定义方法叫做字面量定义,而第二种写法则是标准写法,Object对象的本质也是个function,所以当我们调用对象里的函数时候,函数的外部执行环境就是obj1本身,即外部执行环境上下文变量代表的就是obj1,那么this指针也是指向了obj1。

 

  结果则是true,因为var obj = {}相当于var obj = new Object(),虽然对象里没什么内容,但是在堆区里,对象的内存已经分配了,而变量栈区的值已经是内存地址了,所以if语句判断就是true了。

原文地址:https://www.cnblogs.com/sunscheung/p/4839424.html