无穷大的数
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对象。