javascript打破作用域的牢笼
javascript作为一种松散型语言,有着很多令人瞠目结舌的特性(往往是一些令人捉摸不透的奇怪特性),本文我们将介绍如何使用javascript的一些特性来打破常规编程语言“作用域的牢笼”。
1.javascript声明提升很多人应该知道,js有变量声明提升、函数声明提升的特性。不管你之前是否了解,看下面的代码运行的结果是否符合你的预期:
var a=123;
//可以运行
abc();
//报错:def is not a function
def();
function abc(){
//undefined
console.log(a);
var a="hello";
//hello
console.log(a);
}
var def=function(){
console.log("def");
}
实际上js在运行时会对代码进行两轮扫描。第一轮,初始化变量;第二轮,执行代码。第二轮执行代码很好理解,不过第一轮过程比较模糊。具体来说,第一轮会做下面三件事:
(1)声明并初始化函数参数
(2)声明局部变量,包括将匿名函数赋给一个局部变量,但并不初始化他们
(3)声明并初始化函数
明白了这些理论基础之后,上面那段代码在第一轮扫描之后实际上被js编译器“翻译”成了如下代码:
var a;
a=123;
function abc(){
//局部变量,将会取代外部的a
var a;
//undefined
console.log(a);
var a="hello";
//hello
console.log(a);
}
var def;
//可以运行
abc();
//报错:def is not a function
def();
var def=function(){
console.log("def");
}
现在再来看注释里展示的程序运行时输出,是不是觉得顺理成章了。这就是js声明提升在当中起到的作用。
知道了js声明提升的作用机制之后,我们来看下面这段代码:
var obj={};
function start(){
//undefined
//this is obj.a
console.log(obj.a);
//undefined
//this is a
console.log(a);
//成功输出
//成功输出
console.log("页面执行完成");
}
start();
var a="this is a";
obj.a="this is obj.a";
start();
上述注释第一行表示第一次执行start()方法时的输出,第二行表示第二次执行start()方法的输出。可以看到,由于js声明提升的存在,两次执行start()方法都没有报错。下面来看对这个例子进行小小的修改:
var obj={};
function start(){
//undefined
//this is obj.a
console.log(obj.a);
//报错
//this is a
console.log(a);
//因为上一行的报错导致后续代码不执行
//成功输出
console.log("页面执行完成");
}
start();
/*---------------另一个js文件----------------*/
var a="this is a";
obj.a="this is obj.a";
start();
此时,由于将a变量的声明推迟到另一个js文件中,导致第一次执行的时候console.log(a)代码报错,从而后续的js代码不再执行。不过第二次执行start()方法仍然正常执行。这就是为什么几乎所有地方都推荐大家使用“js命名空间”来部署不同的js文件。下面我们用一段代码来总结声明提升+命名空间如何巧妙的“打破作用于的牢笼”:
/*-----------------第一个js文件----------------*/
var app={};
app.first=(function(){
function a(){
app.second.b();
}
return {
a:a
};
})();
/*-----------------另一个js文件----------------*/
app.second=(function(){
function b(){
console.log("this is second.b");
}
return {
b:b
};
})();
//程序起点,输出this is second.b
app.first.a();
这段程序将不会有任何报错,我们可以在第一个js文件内访问任何app命名空间后续的属性,只要程序起点在所有必要的赋值工作之后执行,就不会有任何问题。这个例子成功的展示了如何通过合理的设计代码结构来充分利用js语言的动态特性。
看到这里读者可能会觉得这文章有点标题党,上面的技巧只是通过代码布局来做出的一种“假象”:看上去前面的代码在访问不存在的属性,实际上真正执行时的顺序都是合理正确的。那下面本文将介绍真正的“跨作用于访问”技巧。
2.js执行时代码大家都知道js语言有一个“eval()”方法,他就是一个典型的“真正打破作用于牢笼”的方法。看下面这段代码:
(function(){
var code="console.log(a)";
//this is a bird
test(code);
function test(code){
console.log=function(arg){
console.info("this is a "+arg);
};
var a="bird";
eval(code);
}
})();
看了这段代码,相信很多人可能会不禁感叹js的奇葩:“这也能行?!”。是的。test()方法由于声明提升的机制,因此能够被提前调用,正常执行。test()方法接受一个code参数,在test()方法内部我们重写了console.log方法,修改了一下输出格式,并且在test内部定义了一个私有变量var a=”bird”。在test方法最后我们使用eval来动态执行code的代码,打印结果非常神奇:浏览器使用了我们重写的console.log方法打印出了test方法内部的私有变量a。这是完全的作用域隔离。
类似的方法在js中还有很多,例如:eval(),settimeout(),setinterval()以及部分原生对象的构造方法。但是有两点要提醒:
(1)这种方式会大大降低程序的执行效率。大家都知道js本身是解释性语言,其本身性能已经比编译型语言慢了好多个级别。在这基础之上如果我们再使用eval这样的方法去“再编译”一段字符串代码,程序的性能将会慢很多。
(2)使用这种方式编程会剧增代码的复杂度,分分钟你就会看不懂自己写的代码。本文介绍这种方法是希望能让读者全面的了解js语法特性从而能更好的修正、排错。本文完全不推荐在生产级别的代码中使用第二种方式。
以上就是javascript打破作用域的牢笼的内容。