谈谈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");
    }

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

  看来本主题又没法写完,其实本来我写本文是想讲new,prototype,call(apply)以及this,没想讲变量定义就讲了这么多,算了,先发表出来吧,吃了晚饭接着写,希望今天写完。

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索javascript
, 变量
, 赋值问题
, 运行
, 代码
, 地址是传参
, 运行结果
, 结果
, javascript存储
, console
, 堆区
, 栈区
, javascript定义变量
传地址赋值
javascript难点、javascript重难点、javascript的难点、英语语法难点、高中英语语法难点,以便于您获取更多的相关知识。

时间: 2025-01-21 00:27:02

谈谈javascript语法里一些难点问题(一)的相关文章

谈谈javascript语法里一些难点问题(二)

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

Javascript 6里的4个新语法_javascript技巧

JS 的 ES6版本已经被各大浏览器广泛支持,很多前端框架也已经使用 ES6,并且还有 Babel 可以做兼容处理,所以ES6已经进入了应用阶段 如果您对 ES6 还不太熟悉,下面4个简单的基础用法可以帮助您快速了解ES6 1.使用 let 和 const 声明变量在传统的 ES5 代码中,变量的声明有两个主要问题 (1)缺少块儿作用域的支持 (2)不能声明常量 ES6中,这两个问题被解决了,增加了两个新的关键字:let 和 const 块儿作用域使用 let var a = 1; if (tr

12种不宜使用的Javascript语法

这几天,我在读<Javascript语言精粹>. 这本书很薄,100多页,正好假日里翻翻. 该书的作者是Douglas Crockford,他是目前世界上最精通Javascript的人之一,也是Json格式的创造者. 他认为Javascript有很多糟粕.因为1995年Brendan Eich设计这种语言的时候,只用了三个月,很多语言特性没有经过深思熟虑,就推向了市场.结果等到人们意识到这些问题的时候,已经有100万程序员在使用它了,不可能再大幅修改语言本身了.所以,Douglas Crock

浅谈javascript语法和定时函数

  初学者可能对Javascript的定时器有误解,认为它们是线程,其实Javascript是运行于单线程中的,而定时器仅仅是计划在未来的某个时间执行,而具体的执行时间是不能保证的,因为在页面的生命周期中,不同的时间可能有其它代码在控制Javascript的里进程. 一.JavaScript基本语法. (一)数据类型与变量类型. 整数,小数,布局,字符串,日期时间,数组 强制转换: parseInt() parseFloat() isNaN() (二)数组 var 数组名 = new Array

在JavaScript SDK里使用SoundCloud API

SoundCloud开发出了一款可被开发者使用的API,这款API能使开发者获得他们想要的几乎任何数据.但是该API的用法有些混乱,特别是对初学者来说,因为此时的SoundCloud API开发文档和文档示例使用的都是SDK(软件开发工具箱)的不同版本. SoundCloud介绍链接地址: http://baike.sogou.com/v128528573.htm SoundCloud API和SoundCloud SDK之间有什么区别呢?从根本上说,SoundCloud API是一个URL的集

JavaScript语法树与代码转化实践

JavaScript 语法树与代码转化实践 归纳于笔者的现代 JavaScript 开发:语法基础与实践技巧系列文章中.本文引用的参考资料声明于 JavaScript 学习与实践资料索引中,特别需要声明是部分代码片引用自 Babel Handbook 开源手册;也欢迎关注前端每周清单系列获得一手资讯. JavaScript 语法树与代码转化 浏览器的兼容性问题一直是前端项目开发中的难点之一,往往客户端浏览器的升级无法与语法特性的迭代保持一致;因此我们需要使用大量的垫片(Polyfill),以保证

《JavaScript DOM 编程艺术》读书笔记之JavaScript 语法_javascript技巧

注释         单行注释://         多行注释:/* */         "<!--"可以用作单行注释,由于和HTML的"<!--  -->"多行注释类似,容易混淆,所以不建议这种注释方法 变量        在JavaScript 语言里,变量和其他语法元素的名字都是区分字母大小写的.名字mood的变量与名字是Mood.MOOD或mOOd的变量没有任何关系,它们不是同一个变量.        JavaScript 语法不允许变量

IE和Firefox之间在JavaScript语法上的差异_javascript技巧

尽管 JavaScript 历史上使用冗长而令人生厌的代码块来标的特定浏览器的时期已经结束了,但是偶尔使用一些简单的代码块和对象检测来确保一些代码在用户机器上正常工作依然是必要的. 这篇文章中,我会略述一下 Internet Explorer 和 Firefox 在 JavaScript 语法上不同的 7 个方面. 1. CSS "float" 属性 获取给定对象的特定 CSS 属性的基本语法是 object.style 属性,而且有连字符的属性要用骆驼命名法来代替.例如,获取一个 I

JavaScript语法着色引擎(demo及打包文件下载)_prototype

应 得意小蛇 的建议,我整理了一下去年写的JavaScript语法着色引擎,并提供下载,喜欢的尽管拿去,嘿嘿 总的来说是很简单的东西,只是提供了关键字的着色以及一些基本的语法(例如注释,字符串,正则等等),从demo中应该很容易看到其用法,这里简单介绍下: 类名:Lighter 通过new Lighter()可以得到一个着色引擎实例,假设为lighter,有以下属性和方法: 语言属性:lighter.language 这个属性的范围是可以根据语法文件的数量自己添加的,提供的demo中有'cpp'