Javascript的脑洞(一)

无穷大的数

var a = 12 / 0

if (a== Infinity){
    console.log(typeof a,a)  //number Infinity
}

既然JS的变量实际都是某种引用(存放对象的地址),那么a这个名字的背后可以存放任意东西,包括无穷大这个值。但是JS在这里直接违背了数学规律,让0除的结果变成无穷大,真实脑洞大开!如果设计成NaN值,也更好理解吧?

undefined被定义

undefined是个特殊对象,这个对象未定义类型和值。我们通过undefined这个句柄来抓住和引用它。

var a;
if (a==undefined){
    a = "hello world"
}

 JS似乎忘记把undefined放进关键字列表,因此我们重新定义undefined这个句柄。

function joke() {
    var undefined = "hello world";
    var c;
    if (c==undefined){
        console.log("haha"); //无法走到这一行,因为undefined被指向了“hello world”
    }
}
joke()

解决办法是使用void expr。void是个运算符,会计算expr表达式的值,但是void表达式返回的是真正undefined对应的对象引用。

function joke() {
    var undefined = "hello world";
    var c;
    if (c==void(0)){ //c指向真的undefined对象,void(0)也指向真的undefined对象
                     //他们都不是局部var undefined
        console.log("haha");  //打印成功
    }
}
joke()

函数内定义函数

var x = 10;
 
function foo() {
  console.log(x);
}
 
function out() {
  var x = 20;
  foo(); 
}

out()

这一段代码打印出10,而不是20。根据C语言经验,虽然var x=20干扰了一下视线,但那时局部变量,foo函数看不到。这非常符合C的常识。但是下面代码,让熟悉C语言的人挠头了:

var x = 10;
 
function out() {
  var x = 20;
  function foo() {
      console.log(x);
  }  
  foo(); 
}

out()

代码输出20,正好是局部变量var x=20. 这涉及到作用域链(Scope Chain)这个概念。foo函数跑在out这个大环境下,out跑在全局这个最大环境下。局部环境自然会受到大环境影响。正如村里的环境受国内环境影响,国内环境受国际环境影响一样。JS的一大特点就是函数内定义函数这一个脑洞大开的想法。因为万物皆对象,函数本身也是一种特殊的对象。所以,foo就是函数背后对象的标识符。foo对象有一个属性,名字是[[Scope]]。foo.[[Scope]] = [ VO数组 ];那么VO(变量对象Variable Object)是什么?VO是一个对象,存放变量的标识符有关的信息。foo.[[Scope]] = [  out的VO,  global的VO ]。因为foo的外层环境是out,再外层环境是global,所以,这个数组就反应了这个关系。而且,一旦解析器分析了代码,这个环境关系就是确定的,与调用堆栈(函数之间调用关系)毫无关系。所以,foo.[[Scope]] 是静态的不变的。当真正调用函数foo时,需要一个辅助性的对象(名字叫做Execution Context执行上下文,简称EC对象)用于帮助执行代码。fooEC对象的伪代码看起来像这样:

fooEC = {
    fooVO: {...},
    this: ...
    Scope: [ fooVO, outVO, globleVO ]
}

outVO = {
    x : ...
    foo : pointer to function
}

globleVO = {
    x : ...
    out : pointer to function
}

当foo函数执行时遇到x,就在fooEC.Scope中查找x,显然fooVO中没有,幸运的是outVO中存在x,因此就是20了。

全局变量随意创建

function foo() {
  y = 30;
}

foo()

console.log(this.y);

如果未指定var,y=30创建了一个全局变量Golbal的属性y。this指向全局变量Global, 因此this.y==Global.y==30

准备和执行,请扫描两遍

阅读JS代码时,为了稳妥起见,请人肉扫描两遍代码。第一遍,建立一些神秘的概念对象,帮助思考第二遍的扫描。

function f() {
  var b = 20; // 函数上下文中的局部变量
  console.log(a); // undefined
};
f();

var a = 10; // 全局上下文中的变量
console.log(b); // ReferenceError: b is not defined

执行这段简单的代码前,要先准备执行环境,相关信息放到叫做执行环境对象的地方。假设此对象为:
globalEC = {
    globalVO={ f, a },  //存放自定义的函数,var变量等
    this = global,//存放的是this指向的对象
    Scope =  [globalVO] //存放作用域链表(用数组模拟),用于依次在里面搜索变量
}
万物皆对象,实际上f,a都是全局对象global的属性,而不是自由的存在。

然后看到函数调用f(), 当进入此函数前,要先准备执行环境,相关信息放到叫做执行环境对象的地方。假设此对象为:
fEC = {
    myVO = { b },//存放函数形参,定义的var变量,定义的内部函数标识符等
    this = global,  //存放this指向的对象。f这个符号归属global对象。
    Scope = [myVO, globalVO], //存放作用域链表(用数组模拟),用于依次在里面搜索变量
}
好了,准备结束,开始执行函数f(), 此时要心中有fEC:
第一句话var b=20; b这个标识符在myVO中找到了,它就是函数f中的局部变量。
第二句话console.log(a); a是谁?myVO中没有!顺着Scope链找到globalVO,这里面有个a,就是它了。
但是此时a还未赋值,其初始值是undefined。
函数f()调用返回后,回到globalEC环境,这不就是函数的调用栈的概念吗?此时心中要想着globalEC是当家人。
执行var a=10;a在globalVO中找到了,初始值是undefined,然后赋值为10,但是对console.log(a)而言赋值太晚了。
执行console.log(b), b在globalEC.Scope中搜索不到,因此报变量not defined。
记住JS变量是可以先引用,后定义的(对比其它语言而言是脑洞大开),原因在于EC准备阶段,这个隐藏的执行流程。
程序员看代码时,总不能这么麻烦反复人肉扫描代码吧?简单的办法,就是把var变量都人肉提升到头部,就像传统的C那样

//begin VO  
var a ; 
//end VO  

function f() {
  
  //begin VO  
  var b ; 
  //end VO  
    
  b = 20;  
  console.log(a); // undefined
};
f();

a = 10; 
console.log(b); // ReferenceError: b is not defined

这个方法叫做变量提升hoisting。也许是有益的一种编程风格,避免了每次阅读代码人肉提升变量,或者幻想EC对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值