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

1) 引子

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

var a = 1;

function hehe()

{

         window.alert(a);

         var a = 2;

         window.alert(a);

}

hehe();

执行结果以下所示:html

第一个alert:前端

clipboard.png

第二个alert:java

clipboard.png

这是一个使人诧异的结果,为何第一个弹出框显示的是undefined,而不是1呢?这种疑惑的原理我描述以下:程序员

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

当时群里不少人都给出了问题的解答,我也给出了我本身的解答,其实这个问题好久以前个人确研究过,可是刚被问起了我竟然仍是有个卡壳期,在加上最近研究javascriptMVC的写法,发现本身读代码时候对new 、prototype、apply以及call的用法任然要体味半天,因此我以为有必要对javascript基础语法里比较难理解的问题作个梳理,其实写博客的一个很大的好处就是写出来的知识逻辑会比你在脑子里反复梳理的逻辑映像更加的深入。安全

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

2) Javascript的变量

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

Javascript虽然跟java没有半点毛关系,可是不少会使用javascript的朋友一样认为:在javascript的世界里,一切也皆是对象函数

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

基本类型是指: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();

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

clipboard.png

当咱们使用引用类型时候,结果就和上面彻底不一样了,你们请看下面的代码:

var obj1 = new Object();

obj1.name = "obj1 name";

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

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

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

clipboard.png

clipboard.png

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

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

  • 部分二:栈区变量的值;部分一:栈区的变量标示符;
  • 部分二:栈区变量的值;
  • 部分三:堆区存储的对象。

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

场景一:以下代码所示:

var qqq;

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

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

场景二:以下代码所示:

var qqq;

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

console.log(xxx);

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

clipboard.png

会提示变量未定义。在任何语言里变量未定义就使用都是违法的,咱们看到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对象的结构,以下图所示:

clipboard.png

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

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

//var a = 1;

function hehe()

{

    console.log(a);

    var a = 2;

    console.log(a);

}

hehe();

结果以下图所示:

clipboard.png

我再改下代码:

//var a = 1;

function hehe()

{

    console.log(a);

   // var a = 2;

    console.log(a);

}

hehe();

运行之,结果以下所示:

clipboard.png

对比两者代码以及引子里的代码,咱们发现问题的关键是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传参的本质是怎样的,若是把上面传参的例子改的复杂点,不少朋友都会栽倒到这个面试题下。

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

clipboard.png

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

clipboard.png

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

还有个知识,以下:

在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语法里一些难点问题(一)

相关文章
相关标签/搜索