视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
JavaScript打破作用域的牢笼
2020-11-27 20:24:45 责编:小采
文档

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语法特性从而能更好的修正、排错。本文完全不推荐在生产级别的代码中使用第二种方式。

下载本文
显示全文
专题